Gitの応用操作

目標

課題1: git amendによるコミット修正

コミットメッセージでうっかりタイポしてしまった。恥ずかしいので修正しよう。

Step 1: リポジトリのクローン

最初に作業用のディレクトリgithubに移動しよう。ターミナルで、以下を実行せよ。

cd
cd github

以下の演習は、全てのこのgithubディレクトリ以下で作業する。

次にサンプル用のリポジトリをクローンせよ。

git clone https://github.com/cpss2026-git/amend-sample.git
cd amend-sample

なお、git clone実行時にGitHubへのアクセス権を求められたら、URLの入力を間違えている。処理を中断し、もう一度正しいURLを入力せよ。

Step 2: 歴史の確認

履歴を確認し、最新のコミットメッセージに打ち間違いがあることを確認せよ。

git log --oneline

Step 3: コミットの保存

修正する前に、現在の最新のコミットに別名をつけておこう。

git branch original_main

Step 4: コミットの修正

コミットメッセージを修正しよう。

git commit --amend -m "updates README.md"

Step 5: 歴史の修正を確認

歴史が修正されたことを確認しよう。

git log --oneline

Step 6: 歴史の分岐を確認

git commit --amendはコミットハッシュを変更するため、歴史が分岐する。以下のコマンドを実行し、歴史が確かに分岐したことを確認してみよう。

git log --all --graph --oneline

課題2: git mergeによる衝突の解決

ある詩人が、ロバの上で詩を作っていたが、「僧は推す、月下の門」とするか、「僧は敲く、月下の門」とするか迷って、都の長官、韓愈の列に突っ込んでしまった。git mergeでどちらにするか決断して上げよう。

Step 1: リポジトリのクローン

サンプル用のリポジトリをクローンせよ。

cd
cd github
git clone https://github.com/cpss2026-git/merge-sample.git
cd merge-sample

Step 2: ブランチの準備

origin/knockが存在することを確認せよ。

git branch -vva

origin/knockからknockブランチを作成せよ。

git branch knock origin/knock

Step 3: 差分確認

mainブランチと、knockブランチの差分を確認せよ。

git diff knock

Step 4: マージと、マージの中止

mainブランチから、knockブランチをマージせよ。

git merge knock

poetry.txtで衝突が起きたはずだ。中身を確認せよ。

cat poetry.txt

一度マージを中止して元に戻ることを確認しよう。

git merge --abort
cat poetry.txt

Step 5: マージと衝突の解決

次は衝突を解決し、マージを実行しよう。

git merge knock

衝突がおきるはずなので、韓愈のアドバイス通り「推」ではなく「敲」の方を残して保存せよ。VS Codeの「フォルダーを開く」でこのディレクトリ(~/github/merge-sample)を開き、その上でpoetry.txtを開いて修正せよ。手で修正してもよいが、VS Codeのマージエディタが開いているはずなので、そこで「入力側の変更を取り込む」をクリックするだけでも良い。

その後、

git add poetry.txt
git commit -m "knock"

を実行し、マージを完了させよ。

Step 6: 最終的な歴史の確認

以下のコマンドでマージが完了した状態の歴史を確認しよう。

git log --all --graph --oneline

課題3: git rebaseによる歴史改変

Bobは、姉であるAliceの大事なアイスを食べてしまった。このままでは大目玉だ。git rebaseにより歴史を改変し、Bobにアリバイを作ってあげよう。

Step 1: リポジトリのクローン

サンプル用のリポジトリをクローンせよ。

cd 
cd github
git clone https://github.com/cpss2026-git/rebase-history-sample.git
cd rebase-history-sample

Step 2: 歴史の確認

現在の歴史を確認しよう。

$ git log --oneline
b6f729a (HEAD -> main, origin/main, origin/HEAD) Bob headed off to school.
f1ecd8d Ice cream was gone.
de96bad The ice cream was still there.
9e6dca2 (origin/start) The two woke up.

時間は「下から上」に流れている。したがって、現在の歴史は

  1. AliceとBobが目を覚ます
  2. Aliceがアイスを確認する
  3. Aliceがアイスが無くなっていることに気づく
  4. Bobが学校へ行く

となっている。alice.txtbob.txtには、それぞれの行動が記されている。差分を見てみよう。

git diff HEAD^
git diff HEAD^^ HEAD^
git diff HEAD^^^ HEAD^^

上記は歴史を一つずつさかのぼっている。「ボブが学校へ行く」「Aliceがアイスが無くなっていることに気づく」「Aliceがアイスを確認する」という順番になっている。

さて、このままではBobがAliceのアイスを食べたことがバレてしまい、大目玉をくらう。git rebaseで歴史を改変してアリバイを作ってあげよう。

Step 3: ブランチの作成

二人が起きた時点にブランチを作る。

git branch start origin/start

Step 4: コミットの入れ替え

mainブランチからstartブランチに対してリベースをする。

git rebase -i start

こんな画面が表示されたはずだ。

pick de96bad The ice cream was still there.
pick f1ecd8d Ice cream was gone.
pick b6f729a Bob headed off to school.

これを順序を入れ替えて以下の状態にせよ。

pick b6f729a Bob headed off to school.
pick de96bad The ice cream was still there.
pick f1ecd8d Ice cream was gone.

Vimで入れ替えるには、以下の手順を取る。

  1. 「j」と「k」でカーソルを上下に移動し、「Bob headed off to school.」の行に合わせる
  2. 「dd」と入力し、3行目を切り取る
  3. 「k」を数回入力し、カーソルを一番上に移動する
  4. 「P (シフトキーを押しながらp)」を入力し、行の一番上に先ほど切り取った行を貼り付ける
  5. 「ZZ (シフトキーを押しながらZを二回)」を入力し、歴史改変を終了する。

Step 5: 改変された歴史の確認

歴史が無事に改変されたか確認しよう。

$ git log --oneline
469ce60 (HEAD -> main) Ice cream was gone.
65552f7 The ice cream was still there.
650e6bd Bob headed off to school.
9e6dca2 (origin/start, start) The two woke up.

歴史は以下のように改変された。

  1. AliceとBobが目を覚ます
  2. Bobが学校へ行く
  3. Aliceがアイスを確認する
  4. Aliceがアイスが無くなっていることに気づく

Bobが学校に行った後にアイスがあることが確認されているのだから、Bobはアイスを食べることができない。すなわちアリバイが成立し、Aliceに怒られることは無くなった。

alice.txtbob.txtには、それぞれの行動が記されている。差分を見てみよう。

git diff HEAD^
git diff HEAD^^ HEAD^
git diff HEAD^^^ HEAD^^

これで一つずつ歴史をさかのぼることができる。「アリスがアイスがないことに気づく」「アリスがアイスの存在を確認する」「ボブが学校に行く」という歴史になっていることがわかるだろう。

課題4: git rebaseによる衝突の解決

Step 1: リポジトリのクローン

サンプル用のリポジトリをクローンせよ。

cd
cd github
git clone https://github.com/cpss2026-git/rebase-conflict-sample.git
cd rebase-conflict-sample

Step 2: ブランチの準備

origin/branchからbranchを作成せよ。

git switch branch

プロンプトのカレントブランチ表示がbranchとなっていることを確認すること。

本来、git switch branchというコマンドは、すでに存在しているbranchというブランチをカレントブランチにする、という命令だが、Gitはもしbranchが存在せず、origin/branchが存在する場合、自動的にorigin/branchからbranchを作成し、branchをカレントブランチとする。

明示的にorigin/branchからbranchを作成し、branchをカレントブランチとするコマンドは

git switch -c branch origin/branch

であり、先程のgit switch branchはその省略形になっている。

Step 3: 歴史の確認

現在の歴史が分岐していることを確認せよ。

git log --all --graph --oneline

Step 4: リベースの実行

branchからmainに対してリベースを実行し、衝突が発生することを確認せよ。

git rebase main

Step 5: 状態の確認

現在の状態を確認せよ。

git status

特に、いまリベース中であること、どのコミットを処理中に衝突が起きたのか、衝突が起きたのはどのファイルかを確認すること。

Step 6: 衝突の解決

VSCodeで衝突状態にあるファイル(text1.txt)を修正し、衝突を解決せよ。VS Codeの「フォルダーを開く」から~github/rebase-conflict-sampleを開き、その上でtext1.txtを開くと衝突箇所が表示されている。このような表示になっているはずだ。

Text1:
<<<<<<< HEAD
The way to get started is to quit talking and begin doing.
It's kind of fun to do the impossible.
The flower that blooms in adversity is the rarest and most beautiful of all.
=======
If you can dream it, you can do it.
>>>>>>> 8f1d6d2 (f2)

この<<<<<<< HEAD=======>>>>>>> 8f1d6d2 (f2)を削除して、以下のような文章を完成させよう。

Text1:
The way to get started is to quit talking and begin doing.
It's kind of fun to do the impossible.
The flower that blooms in adversity is the rarest and most beautiful of all.
If you can dream it, you can do it.

もし、「Accept Current Change | Accept Incoming Change | Accept Both Changes | Compare Changes」という表示がされていた場合は、「Accept Both Changes」をクリックすると自動的に両方の修正を取り込むことができる。

修正が終わったらファイルを保存すること。

Step 7: 解決をGitに伝える

解決が終わったらgit addgit commitを実行し、Gitに衝突の解決を伝えよう。

git add text1.txt
git commit -m "f2"

コミット実行時にdetached HEADと表示されることに注意。

Step 8: リベースの続行

残りのリベースプロセスを続行しよう。

git rebase --continue

最後まで実行され、リベースが完了するはずだ。

Step 9: リベース完了後の歴史の確認

以下のコマンドでリベースが完了した状態の歴史を表示せよ。

git log --oneline --graph

リベース後の歴史は期待通りとなっているか?それはどこを見るとわかるか?

課題5: git bisectの確認

数の偶奇を判定するスクリプトevenodd.shを開発していたが、いつの間にか全ての数字にevenと答えるようになってしまった。git bisectによる二分探索でどこでバグが入ったか調べよう。

Step 1: リポジトリのクローン

サンプル用のリポジトリをクローンせよ。

cd
cd github
git clone https://github.com/cpss2026-git/bisect-sample.git
cd bisect-sample

Step 2: バグの確認

evenodd.shは、本来であれば入力された数値の偶奇を判定するコードであったが、いつのまにか全ての数字にevenと答えるようになった。適当な数字を与えて実行し、確認せよ。

./evenodd.sh 1
./evenodd.sh 2

Step 3: ブランチの準備

origin/rootからrootを作成し、カレントブランチをrootにせよ。

git switch root

カレントブランチがrootになっていることを確認すること。

先に説明した通り、これは

git switch -c root origin/root

と同じ意味となる。

Step 4: バグっていないことを確認

先ほどと同様にevenodd.shを実行し、正しく実行されることを確認せよ。確認後、mainブランチに戻っておくこと。

git switch main

Step 5: git bisectの実行

少なくともrootブランチでは正常に動作し、mainブランチでは問題があることがわかった。そこで、git bisectにより「問題が初めておきたコミット」を発見しよう。以下を実行し、二分探索モードに入る。

git bisect start main root

Step 6: 状態の確認

現在の状態を確認せよ。

git status

特に、頭がとれた(detached HEAD)状態であること、二分探索モードであること、どうすればこのモードを抜けることができるか等について確認すること。

Step 7: good/bad判定

いま、Gitは適当なコミットが指すスナップショットをワーキングツリーとして展開している。この状態にバグがあるのか、それともないのかをGitに教えよう。

以下のコマンドを実行し、正しい結果が得られるか確認せよ。

./evenodd.sh 1
./evenodd.sh 2

正しい結果が帰ってきたら、

git bisect good

を実行せよ。間違っていたら

git bisect bad

を実行せよ。そのたびにGitは次の候補を持ってくるので、終了するまで上記の操作を繰り返すこと。Gitが「初めて問題が起きたらコミット」を見つけたらコミットハッシュ is the first bad commitという表示がなされるはずだ。

Step 8: ブランチの付与と二分探索モードの終了

このコミットにブランチをつけておこう。

git branch bug 先ほど見つけたコミットハッシュ

なお、「先程見つけたコミットハッシュ」のところには、初めて問題が起きたコミットのコミットハッシュを入力するが、全ての桁を入力する必要はなく、冒頭の6〜7桁を入力すれば良い。

これでバグが入ったコミットに印をつけることができた。二分探索モードを抜けよう。

git bisect reset

Step 9: 自動チェックの確認

いちいちバグの有無を人力で確認し、git bisect good/badを入力するのは面倒だ。「成功/失敗」を判定するスクリプトを使って、二分探索を自動化しよう。そのようなスクリプトtest.shが用意されている。catで見てみよう。以下のコマンドを実行せよ。

cat test.sh

以下のような表示がされるはずだ(これをコマンドとして入力する必要はない)。

#!/bin/bash

if [ `./evenodd.sh 1` != 'odd' ]; then
  exit 1
fi

if [ `./evenodd.sh 2` != 'even' ]; then
  exit 1
fi

これはevenodd.shに1と2を食わせて、oddevenが表示されるか確認し、どちらも正しければ成功(終了ステータス0)、そうでなければ失敗(終了ステータス1)を返すスクリプトだ。これを使って二分探索を自動化するには、git bisect runを用いる。

git bisect start main root
git bisect run ./test.sh

やはりコミットハッシュ is the first bad commitというメッセージが表示されるはずなので、それが先ほどbugというブランチをつけたコミットと同じものであることを確認しよう。

git branch -v

終わったら二分探索モードを抜けよう。

git bisect reset

Step 10: バグの挿入箇所の特定

いま、mainブランチにいるはずだが、先ほどバグの入ったコミットにつけたブランチに入ろう。

git switch bug

いま、「初めてバグが入ったコミット」にいるはずなのだから、「このコミット」と「一つ前のコミット」の差分を見れば、バグが入った原因がわかるはずだ。以下を実行し、どこがバグの原因であったか特定せよ。

git diff HEAD^