2017 年度 OSS リテラシー 3 : 第 8 回 Git, GitHub 入門 (2)

ブランチの操作

ブランチを活用することで複数の開発者が効率的に同時並行で開発を行うことができる. 開発者は Git のデフォルトのブランチである master ブランチから新たにブランチを作成し, その新規作成したブランチで開発作業を行う. 開発者が複数いる場合はブランチも複数存在することになる. 開発者は開発作業を一区切りさせたら, それぞれのブランチを master ブランチにマージする.

git branch

git branch は, ブランチ名の一覧を表示するとともに, 現在のブランチを 確認するためのコマンドである.

$ git branch

  * master

出力の中で, "*" (アスタリスク) の付いているのが現在のブランチである. 上記出力は, 現在は master ブランチにいること, ブランチは master のみ であることを意味する.

git checkout -b

feature-A ブランチの切り替えとコミット

現在の master ブランチから新しいブランチを作成するには, git checkout -b コマンドを用いる.

feature-A ブランチを作るには, 以下のようにコマンドを実行する.

$ git checkout -b feature-A

  Switched to a new branch 'feature-A'

このコマンドは, ブランチを作成し, 現在のブランチを feature-A に 移動するという 2 つの操作を同時に行うものである. 再び git branch コマンドを 実行すると, 現在は feature-A ブランチにいることがわかる.

$ git branch

  * feature-A
  master

それでは前回作成した README.md に 1 行追加してみる.

$ vi README.md

  test (feature-A)    [最終行に追加]

この変更をコミットする.

$ git status

  On branch feature-A
  Changes to be committed:
   (use "git reset HEAD <file>..." to unstage)

  modified:   README.md

$ git add README.md 

$ git commit -m "Add feature-A"

  [feature-A 8d8cf00] Add feature-A
   1 files changed, 1 insertion(+)

現在の状況を絵に表すと以下のようになる. 赤が master ブランチ, オレンジが 今作成した feature-A ブランチである. README.md を編集してコミットしたので, feature-A ブランチが master ブランチから分岐した.

master ブランチへの切り替え

次に, feature-A ブランチの変更が master ブランチに「影響しない」ことを 確認する. ブランチの切り替えは git checkout コマンドを用いる. 切り替えるだけなら -b オプションは必要ない.

$ git checkout master

先ほど feature-A ブランチで変更を加えた README.md を表示してみる. cat コマンドで確認すると, 先ほど加えた test (feature-A) が存在しないことがわかる.

$ cat README.md 

  Git tutorial
  It's Git tutorial
                        <-- 先ほど加えた "test (feature-A)" がない.
1 つ前のブランチへ切り替える

1 つ前のブランチ (feature-A) に戻す. 1 つ前のブランチに戻すには引数に "-" をつければよい. 表示されるメッセージから feature-A に切り替わったことがわかる. もちろん, "-" の代わりに feature-A と書いても良い.

$ git checkout -

  Switched to branch 'feature-A'

$ cat README.md 

  Git tutorial
  Git tutorial desu
  test (feature-A)

git merge

feature-A で行った作業 (例えば, バグ修正) が終わったあとは, feature-A の変更を master ブランチに統合する. まずは統合先のブランチ (master) へ移動する.

$ git checkout master

  Switched to branch 'master'

git merge コマンドを実行する. git merge コマンドを実行するとエディターが 起動する. エディターに vi を使いたい場合は予め環境変数 EDIOR を vi にしておくこと.

$ export EDITOR=vi

$ git merge --no-ff feature-A

  Merge made by the 'recursive' strategy.
  README.md | 1 +
  1 files changed, 1 insertion(+)

README.md ファイルの中を確認すると, feature-A ブランチに加えた変更が master ブランチに反映されていることがわかる.

$ cat README.md 

  Git tutorial
  It's Git tutorial
  test (feature-A)    <-- この行が増えた. 

現在の状況を図に表すと以下のようになる. feature-A ブランチで行った変更が master ブランチに取り込まれた.

git log --graph

git log --graph コマンドを用いると, feature-A ブランチが分岐し 統合されたことをグラフィカルに表示することができる.

$ git log --graph

  *   commit dc415cb8fa8bcb58cef82270cf885364f127ce9f
  |\  Merge: fe38f9a 8d8cf00
  | | Author: sugiyama <[email protected]>
  | | Date:   Sat Nov 25 06:03:35 2017 +0900
  | | 
  | |     Merge branch 'feature-A'
  | | 
  | * commit 8d8cf007d422a92f4616648c3583fd8c01f0176f
  |/  Author: sugiyama <[email protected]>
  |   Date:   Sat Nov 25 05:44:56 2017 +0900
  |   
  |       Add feature-A
  | 
  * commit 322bfbf4f15e9e75fe1a8f6c6ebdb3d9590c79c0
  | Author: sugiyama <[email protected]>
  | Date:   Tue Nov 21 02:37:07 2017 +0900
  | 
  |     remove test.txt
  | 

コミットを変更する操作

git reset

バージョン管理のありがたいところは, 過去の情報に簡単に戻れることである. ここでは feature-A ブランチを分岐する前に戻って fix-B というブランチを作成する.

リポジトリの HEAD, ステージ, 現在のワーキングツリーを指定した状態まで 戻すには, git reset --hard コマンドを用いる. 引数に戻りたい場所のハッシュを 与えることで, そのときの状態を完全に復元することができる.

まずは戻す場所のハッシュを確認する. ブランチ分岐の前の commit の 横の文字列がハッシュである.

$ git log --graph

  ...(中略)....

  * commit 322bfbf4f15e9e75fe1a8f6c6ebdb3d9590c79c0   <-- ハッシュ
  | Author: sugiyama <[email protected]>
  | Date:   Tue Nov 21 02:37:07 2017 +0900
  | 
  |     remove test.txt

  ...(以下, 略)....

上記で得たハッシュを用いて git reset する.

$ git reset --hard 322bfbf4f15e9e75fe1a8f6c6ebdb3d9590c79c0

  HEAD is now at 322bfbf remove test.txt

リセットした後に README.md を確認すると, 最終行が消えていることがわかる.

$ cat README.md 

  Git tutorial
  It's Git tutorial
                        <-- 先ほど加えた "test (feature-A)" がない.

ここで fix-B ブランチを作成する. README.md ファイルを編集したあと, add と commit を行う.

$ git checkout -b fix-B

  Switched to a new branch 'fix-B'

$ vi README.md

  test (fix-B)    [末尾に追加] 

$ git add README.md 

$ git commit -m "ADD fix-B"

  [fix-B 1ba6722] ADD fix-B
   1 file changed, 1 insertion(+)

また, git log コマンドを実行すると, master ブランチからの続きとして "fix-B" の変更が存在することがわかる.

$ git log --graph

  * commit 1ba6722e40e23def1d14d86938dfef42d7b0d775
  | Author: sugiyama <[email protected]>
  | Date:   Sat Nov 25 06:33:15 2017 +0900
  | 
  |     ADD fix-B
  | 
  * commit 322bfbf4f15e9e75fe1a8f6c6ebdb3d9590c79c0
  | Author: sugiyama <[email protected]>
  | Date:   Tue Nov 21 02:37:07 2017 +0900
  | 
  |     remove test.txt
  | 
  * commit 1a061f5214eba2ed12075a7d299c08a193c84212
  | Author: sugiyama <[email protected]>
  | Date:   Tue Nov 21 02:35:40 2017 +0900
  | 
  |     add test.txt
  | 
  ... (略)...

現在の状況を図に表すと以下のようになる. feature-A を作ったところまで 戻ってから fix-B を作ったので, feature-A と fix-B の分岐点は同じである.

feature-A ブランチをマージしたあとの状態に進む

git reflog を実行すると, このレポジトリで行われた操作が全て確認できる. git reflog の出力を見ると, feature-A ブランチの作成とマージ, fix-B ブランチの 作成などを確認することができる.

$ git reflog

  1ba6722 HEAD@{0}: commit: ADD fix-B
  322bfbf HEAD@{1}: checkout: moving from master to fix-B
  322bfbf HEAD@{2}: reset: moving to 322bfbf4f15e9e75fe1a8f6c6ebdb3d9590c79c0
  dc415cb HEAD@{3}: merge feature-A: Merge made by the 'recursive' strategy.  <--- ここ!
  fe38f9a HEAD@{4}: checkout: moving from feature-A to master
  8d8cf00 HEAD@{5}: checkout: moving from master to feature-A
  fe38f9a HEAD@{6}: checkout: moving from feature-A to master
  8d8cf00 HEAD@{7}: commit: Add feature-A
  fe38f9a HEAD@{8}: checkout: moving from master to feature-A
  fe38f9a HEAD@{9}: commit: add test2.txt
  322bfbf HEAD@{10}: commit: remove test.txt
  1a061f5 HEAD@{11}: commit: add test.txt
  df2a0a8 HEAD@{12}: commit: Add index
  5eb4bae HEAD@{13}: commit: 2017-11-21 modified by sugiyama
  cb505d7 HEAD@{14}: commit: 2017-11-20 README.md is modified by sugiyama
  1bab8be HEAD@{15}: commit (initial): first commit

feature-A ブランチをマージした後の状態は 4 行目の "merge feature-A: ..." であるので, そこに戻すにはハッシュ dc415cb を使えば良い. git log で 出力されるハッシュでも, git reflog で出力されるハッシュでも, どちらも使うことができる.

$ git status

  On branch fix-B
  nothing to commit, working tree clean

$ git checkout master

  Switched to branch 'master'

$ git reset --hard dc415cb

  HEAD is now at dc415cb Merge branch 'feature-A'
fix-B の修正を master に反映

現時点の master ブランチは feature-A のマージを行った直後の状態になっているので, README.md は以下のようになっている.

$ cat README.md 

  Git tutorial
  It's Git tutorial
  test (feature-A)

一方で, 先に作成した fix-B ブランチの README.md は以下のようになっている.

$ git checkout fix-B

  Switched to branch 'fix-B'

$ cat README.md 

  Git tutorial
  It's Git tutorial
  test (fix-B)

fix-B ブランチの README.md は過去の master ブランチの README.md を 元にしているので, master ブランチの最新の README.md の内容と食い違いが生じている. fix-B を feature-A にマージすると何がおこるだろうか?

$ git checkout master

$ git merge --no-ff fix-B

  Auto-merging README.md
  CONFLICT (content): Merge conflict in README.md
  ^^^^^^^^^
  Automatic merge failed; fix conflicts and then commit the result.
  ^^^^^^^^^^^^^^^^^^^^^^^            

README.md ファイルに食い違いがあるので, コンフリクトが生じ, マージが失敗する. コンフリクト (CONFICT) は競合や衝突という意味である. git status を実行すると マージに失敗していることが表示される. メッセージには, コンフリクトを手動で修正して git commit を行うか, マージを中断する場合は git merge --abort を行うことが書かれている.

$ git status

  On branch master
  You have unmerged paths.
    (fix conflicts and run "git commit")           <--- ここ!
    (use "git merge --abort" to abort the merge)   <--- ここ!

  Unmerged paths:
    (use "git add <file>..." to mark resolution)

  both modified:   README.md

  no changes added to commit (use "git add" and/or "git commit -a")

今回はコンフリクトを手動で修正してコミットすることにする. コンフリクトの生じたファイルを開くと以下のように衝突した部分が 表示される.

$ cat README.md 

  Git tutorial
  It's Git tutorial
  <<<<<<< HEAD
  test (feature-A)
  =======
  test (fix-B)
  >>>>>>> fix-B

今回は衝突した部分を両方共に生かすことにする. vi でファイルを開いて 以下のように修正する.

$ cat README.md 

  Git tutorial
  It's Git tutorial
  test (feature-A)
  test (fix-B)

修正した結果をコミットする.

$ git add README.md 

$ git commit -m "fix conflict"

  [master 195ac74] fix conflict

コンフリクトが解消されているので, 問題なくコミットされる.

現在の状況を図に表すと以下のようになる. feature-A を master にマージした ところまで戻ってから fix-B をマージした. この図からもわかるように, fix-B は feature-A の修正を取り込んだ後の master の状態は知らないので, fix-B を master にマージするときにコンフリクトが生じた.

GitHub のアカウント作成

<URL:https://github.com/> から GitHub のアカウントを作る. GitHub のアカウントは一生使うことを意識して, 恥ずかしくないアカウント名にすること.

Step2 で示されるプランでは, "Unlimited public repositories for free" を選択すること. 申請するとメールで確認がやってくる.

リモートリポジトリ (GitHub) との連携

以下では GitHub 上のリモートリポジトリとの連携方法の基礎を行う.

まずは GitHub に同名のレポジトリ (git-tutorial) を用意する.

git remote

リモートリポジトリを登録する. リポジトリのパスは Web 画面に表示されたように https://github.com/<ユーザ名>/git-tutorial.git である. これをローカルリポジトリの リーモートリポジトリとして登録するために git remote add コマンドを利用する. 以下のコマンド中のユーザ名は適宜自分のものに変更すること.

$ git remote add origin https://github.com/<ユーザ名>/git-tutorial.git

以降は origin という名前 (識別子) で GitHub のリポジトリを指すことができるようになる.

git push

現在のブランチのローカルリポジトリの内容をリモートリポジトリに 送信するためには, git push コマンドを用いる.

まずは現在のブランチを確認する. master ブランチであることがわかる.

$ git branch

    feature-A
    fix-B
  * master

$ git push -u origin master

  Username for 'https://github.com': <ユーザ名>    <-- GitHub のユーザ名を入れる
  Password for 'https://[email protected]':      <-- GitHub のパスワードを入れる
  Counting objects: 27, done.
  Delta compression using up to 4 threads.
  Compressing objects: 100% (16/16), done.
  Writing objects: 100% (27/27), 2.23 KiB | 0 bytes/s, done.
  Total 27 (delta 4), reused 0 (delta 0)
  remote: Resolving deltas: 100% (4/4), done.
  To https://github.com/sugiymki/git-tutorial.git
   * [new branch]      master -> master
  Branch master set up to track remote branch master from origin.

git push に -u origin master というオプションを与えたので, orgin という名前のリモートリポジトリ (今回は GitHub の git-tutorial リポジトリ) の master ブランチに ローカルリポジトリの master ブランチの内容が送信される.

当然のことながら, リモートリポジトリに master 以外のブランチを 作成することができる. 以下の例ではローカルリポジトリで feature-D というブランチを作成し, それをリモートリポジトリに送信する.

$ git checkout -b feature-D

  Switched to a new branch 'feature-D'

$ git push -u origin feature-D

  Username for 'https://github.com': <ユーザ名>    <-- GitHub のユーザ名を入れる
  Password for 'https://[email protected]':      <-- GitHub のパスワードを入れる
  Total 0 (delta 0), reused 0 (delta 0)
  To https://github.com/sugiymki/git-tutorial.git
   * [new branch]      feature-D -> feature-D
  Branch feature-D set up to track remote branch feature-D from origin.

GitHub をブラウザで見ると, 新たに feature-D というブランチが存在することが確認できる.

リモートリポジトリから取得

今までの作業で, GitHub に作成したリポジトリをリモートリポジトリとして 登録し, feature-D ブランチを push した. 次に, 「別の開発者」として リポジトリを取得してソースの修正やマージを行ってみる (下図の「開発者 B」の立場で).

git clone

まず, テスト用のディレクトリを自分のホームディレクトリ直下に作成する.

$ mkdir ~/test-remote

$ cd test-remote

初めて GitHub 上のリモートリポジトリを取得する時には, git pull でなく git clone コマンドを用いる. GitHub 上のリポジトリは公開されているので, ユーザ名やパスワードを入力する必要はない.

$ git clone https://github.com/<ユーザ名>/git-tutorial.git

  Cloning into 'git-tutorial'...
  remote: Counting objects: 27, done.
  remote: Compressing objects: 100% (12/12), done.
  remote: Total 27 (delta 4), reused 27 (delta 4), pack-reused 0
  Unpacking objects: 100% (27/27), done.

$ ls

  git-tutorial

$ cd git-tutorial

git status や git branch を実行すると, git clone を行った直後には master ブランチにいることがわかる. また, git status のメッセージ中に 'origin/master' とあるように, clone 元のリモートリポジトリは origin という名前で参照できるように自動的に設定されている.

$ git status

  On branch master
  Your branch is up-to-date with 'origin/master'.
  nothing to commit, working tree clean

$ git branch

  * master

git branch に -a オプションをつけて実行すると, リモートリポジトリの 中に存在するブランチも表示することができる. リモートリポジトリ内に feature-D ブランチが存在することがわかる.

$ git branch -a

  * master
    remotes/origin/HEAD -> origin/master
    remotes/origin/feature-D
    remotes/origin/master

リモートリポジトリの feature-D リポジトリをローカルリポジトリに チェックアウトするためには以下のようなオプションをつけて git checkout を実行する.

$ git checkout -b feature-D origin/feature-D

  Branch feature-D set up to track remote branch feature-D from origin.
  Switched to a new branch 'feature-D'

origin はリモートリポジトリを意味するので, origin/feature-D とすることで リモートリポジトリの feature-D リポジトリをチェックアウトすることができる. -b の直後の feature-D はローカルリポジトリ内でのブランチの名前である. 通常はリモートリポジトリのブランチ名と揃えると良い (ここでは feature-D).

feature-D ブランチのファイルを編集し, git add と git commit を行う.

$ git branch

  * feature-D
    master

$ vi README.md 

  Git tutorial

  It's Git tutorial

  test (feature-A)

  test (fix-B)

  test (feature-D)

$ git add README.mod

$ git commit -m "ADD feature-D"

  [feature-D 885d038] ADD feature-D
   1 file changed, 5 insertions(+)

変更をリモートリポジトリに反映させる.

$ git push

  Username for 'https://github.com': <ユーザ名>    <-- GitHub のユーザ名を入れる
  Password for 'https://[email protected]':      <-- GitHub のパスワードを入れる
  Counting objects: 3, done.
  Delta compression using up to 4 threads.
  Compressing objects: 100% (2/2), done.
  Writing objects: 100% (3/3), 294 bytes | 0 bytes/s, done.
  Total 3 (delta 0), reused 0 (delta 0)
  To https://github.com/sugiymki/git-tutorial.git
     195ac74..885d038  feature-D -> feature-D

GitHub から確認すると, master ブランチは変更がないが, feature-D ブランチは README.md が書き換わっていることがわかる.

master ブランチは以下の通り.

feature-D ブランチは以下の通り.

現在の状況を図に表すと以下のようになる. feature-D にコミットしたので master ブランチから feature-D ブランチが分岐した.

git pull

今, 2 つ目のローカルリポジトリ (~/test-remote/git-tutorial) 以下で feature-D ブランチを修正した. しかし, 最初から利用してきた 1 つ目の ローカルリポジトリ (~/git-tutorial) 以下には先の修正は反映されていない. このような場合はどうすれば良いだろうか?

現在の状況は下図に当てはめると, 開発者 B のローカルリポジトリと GitHub のリモートリポジトリは feature-D ブランチが master ブランチから分岐したことを知っているが, 開発者 A のローカルリポジトリはそのことを知らない, ということになる. 開発者 A のローカルリポジトリにリモートリポジトリの feature-D ブランチの最新データを 持って来たいというのが現在の問題意識である.

別の開発者が commit した内容を反映させる (= ブランチを最新の 状態にする) ためのコマンドが git pull である. まず cd コマンドで 元のリポジトリに移動し, 現在のブランチを確認する.

$ cd ~/git-tutorial 

$ git branch

    feature-A
  * feature-D
    fix-B
    master

feature-D ブランチであることを確認してから, git pull を実行する. README.md の中身が変更されていること (リモートリポジトリの最新版に置き換えられたこと) を確認する.

$ cat README.md 

  Git tutorial
  It's Git tutorial
  test (feature-A)
  test (fix-B)

$ git pull origin feature-D

  remote: Counting objects: 3, done.
  remote: Compressing objects: 100% (2/2), done.
  remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
  Unpacking objects: 100% (3/3), done.
  From https://github.com/sugiymki/git-tutorial
   * branch            feature-D  -> FETCH_HEAD
     195ac74..885d038  feature-D  -> origin/feature-D
  Updating 195ac74..885d038
  Fast-forward
   README.md | 5 +++++
   1 file changed, 5 insertions(+)

$ cat README.md 

  Git tutorial

  It's Git tutorial

  test (feature-A)

  test (fix-B)

  test (feature-D)

開発フロー: GitHub flow

多人数が同じブランチで作業をすると, コンフリクトが起きやすくなる. 特に開発者全員が master ブランチを使う状況では, 常に他の開発者のことを意識せねばならず, かえって開発の効率が悪くなることがある.

このような状況が起きにくくなるように, Git ではいくつかの標準的な使い方 (開発フロー) が提案されている. ここでは GitHub 社が実践しているシンプルなワークフローである GitHub flow をごく限定的に説明する. GitHub flow の実践は来週行う. また, 全容や詳細を知りたい場合は <URL:https://gist.github.com/Gab-km/3705015> (日本語) を参照されたい.

Git flow の鉄則は,

  • 永続的なブランチは master のみ.
  • 新作業は master ブランチから新ブランチを作成してから行う.
  • 作成した新ブランチに修正/追加をコミットする.
  • 新ブランチは最終的には master ブランチにマージする (pull request する).

ことである. 新機能の追加でもバグ修正でも必ず作業用のブランチ (「feature ブランチ」や「トピックスブランチ」と呼ぶ) を作り, そのブランチで作業する. ブランチ名は行う具体的な作業名になっていることが望ましい. ここで注意すべきは, 1 つの作業を 1 つのブランチで行うことである.

例えば,

1) コードのインデントが崩れていたので修正

2) 英単語にスペルミスがあったので修正

3) 新たなメソッドを追加

という 3 つの作業を行いたい場合は, これらをまとめて 1 つの ブランチで行うのではなく, それぞれを別々のブランチで行うべきである. 他の開発者のためにも, 修正・追加の意図が伝わりやすいように, コミットの粒度に気をつけるべきである.

この開発フローではマスターブランチ以外は作業中のブランチとなるため, 気軽に作業中のブランチを push することができる. 定期的に GitHub などのリモートリポジトリに push すると良い.

課題

  • GitHub アカウントを wbt のオンラインテキストで報告せよ.
  • git-tutorial リポジトリの master ブランチに bin ディレクトリを作成し, 前回作成した fizzbuzz プログラムを移動せよ. さらに master リポジトリを GitHub に push せよ.
    • 自分の GitHub を Web ブラウザで表示しスクリーンショットを wbt に登録せよ. 課題がうまくできた場合は, ブラウザ上で bin ディレクトリの存在が確認できるはずである.
    • ヒント:
      • ディレクトリを新たに作った場合は, ディレクトリ自体を git add, git commit する必要がある.
      • ファイルの名前を変更したりディレクトリを移動する場合には git mv コマンドを用いる. 移動後に git commit する.
  • fizzbuzz プログラムの修正を行う. feature ブランチ (トピックスブランチ) を切ってから作業すること. fizzbuzz プログラムへの追加内容は「7 の倍数の時は git と言う」とする.
    • 手順
      1. feature ブランチを作成. ブランチ名は fizzbuzz プログラムの修正であることが分かりやすくなる名前とすること.
      2. feature ブランチにうつり, bin ディレクトリ内で fizzbuzz プログラムを修正.
      3. 修正後, git add, git commit を実行.
      4. その後, master ブランチに feature ブランチを git merge する.
      5. ローカルのリポジトリの master ブランチを, GitHub 上のリポジトリの master ブランチに push する.
      6. 自分の Web GitHub をブラウザで表示し, 変更点が反映されていることを確かめる.
    • 自分の GitHub を Web ブラウザで表示しスクリーンショットを wbt に登録せよ. bin ディレクトリの下に fizzbuzz プログラムをブラウザ上に表示して, 変更したことがわかるようにしておくこと.