1
1

Gitコマンドを開発の流れに沿って紹介してみる

Last updated at Posted at 2024-07-10

リモートリポジトリをクローン

リモートリポジトリのソースを取得

リモートリポジトリ上のCodeからurlを取得し、
image.png

git clone urlでリポジトリをローカルにおとす。

$ git clone git@github.com:hoge/huga.git
Cloning into 'test'...
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 12 (delta 1), reused 5 (delta 0), pack-reused 0
Receiving objects: 100% (12/12), done.
Resolving deltas: 100% (1/1), done.

リモートリポジトリを追跡するローカルのブランチを確認してみる

現状、リモートリポジトリには3つブランチが存在している。
image.png

git cloneしてきた場合、ローカルのブランチの状態はどうなっているんだろうか。

##ローカルブランチを表示
$ git branch
* main

##リモート追跡ブランチを表示
$ git branch -r
  origin/HEAD -> origin/main
  origin/main
  origin/test1
  origin/test2

上の通り、ローカルブランチとしてはmainしかないが、リモートのブランチは「リモート追跡ブランチ」なるものとして全てローカルに存在している。

※「->」に続くorigin/mainは、「リモートブランチを上流とするリモート追跡ブランチ」とのことだが。。。???

リモート追跡ブランチからローカルブランチを作成

##リモート追跡ブランチtest1からローカルブランチtest1を作成
$ git checkout -b test1 origin/test1
Switched to a new branch 'test1'
Branch 'test1' set up to track remote branch 'test1' from 'origin'.

##test1ブランチがローカルブランチとして作成されている
$ git branch
  main
* test1

ファイルの編集・追加からコミットまで

全体像

image.png

※ワークツリー⇒Git(.gitの隠しファイル)が管理しているローカルのフォルダのこと。

ワークツリーの変更を取り消す

今、以下のようにsample1.txtに修正を加えたとする。

$ git status
On branch test1
Your branch is up to date with 'origin/test1'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

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

このsample1.txtの変更をなくしたい場合は、git statusの実行結果に書いてあるように

git restore sample1.txt

を実行するか、もしくは

git checkout HEAD -- sample1.txt

を実行する。HEADは省略できるため、以下でも実行結果は同様。

git checkout -- sample1.txt

ワークツリーの変更点を確認

まずはワークツリー内のファイルの状態を表示

saple1.txtを再度修正し、sample2.txt、sample3.txtを追加、sample2.txtをインデックスに追加後以下を実行してみる

$ git status
On branch test1
Your branch is up to date with 'origin/test1'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   sample2.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample3.txt

ファイルが多い時などは-sオプションを付けると短いフォーマットで出力できる。

$ git status -s
#ファイル修正でインデックス未登録
 M sample1.txt
#ファイル追加でインデックス登録済
A  sample2.txt
#バージョン管理対象外のファイル(=新規ファイル)
?? sample3.txt

各行の1列目がインデクスの状態を、2列目がワークツリーの状態を表している。
(※M…変更、A…追加、?…バージョン管理対象外)

diffコマンドでワークツリーの変更点を確認

以下のコマンドでワークツリーと最新コミットの差分を確認できる。

$ git diff sample1.txt
diff --git a/sample1.txt b/sample1.txt
index f62562a..36524c4 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,4 +1,4 @@
-line1
+linea
 line2
 line3
 line4
@@ -6,3 +6,4 @@ line5
 line6
 line7
 line8
+line9

※引数でファイル名を指定しない場合は、ワークツリーで修正されている全てのファイルの変更点が表示される。ここでいう「全てのファイル」はgit statusChanges not staged for commit:の部分に列挙されているファイルに等しい。

diffコマンドでコミット間の差分を確認

git diff リビジョン1 リビジョン2で、リビジョン間の差分を確認できる

# HEAD~1からみたHEADの差分を確認
git diff HEAD~1..HEAD

インデックスにワークツリーの変更を追加

sample1.txtの差分をgit diffコマンドで確認してみたが、gitの差分はハンクと呼ばれる変更単位にまとめられて表示される。

上記の実行結果で言うと、line1の行のlineaへの変更のハンクと、line9の行の追加の2つのハンクとしてgitに差分が認識されている。

一部の変更単位のみをインデックスに登録する

ここでgit add sample1.txtとすると、全てのハンクがインデックスに登録されるが、-pオプションを使うと、ハンクごとにインデックスに登録するかを選択することもできる。

$ git add sample1.txt -p
diff --git a/sample1.txt b/sample1.txt
index f62562a..36524c4 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,4 +1,4 @@
-line1
+linea
 line2
 line3
 line4
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

ハンクごとにインデックスに登録するかを操作文字y/nで選択していき、qで終了させる。

変更単位を分割する

変更行が連続していない場合

ここでいったんqで全ハンクをインデックスに登録せず、git restore sample1.txtで変更をなくした上で、再度sample1.txtを編集して差分を確認する。

$ git diff
diff --git a/sample1.txt b/sample1.txt
index f62562a..3c2e57c 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,8 +1,8 @@
 line1
 line2
-line3
+linea
 line4
-line5
+lineb
 line6
 line7
 line8

ここでは、変更行同士が近いため、複数の変更が1つのハンクとして認識されている。この場合、git add -p実行後に操作文字としてsを入力し、ハンクを分割することが出来る。

$ git add sample1.txt -p
diff --git a/sample1.txt b/sample1.txt
index f62562a..3c2e57c 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,8 +1,8 @@
 line1
 line2
-line3
+linea
 line4
-line5
+lineb
 line6
 line7
 line8
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -1,4 +1,4 @@
 line1
 line2
-line3
+linea
 line4
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

Split into 2 hunks.の通り、ハンクが2つに分割されていることが分かる。

変更行が連続している場合

ここでいったんqで全ハンクをインデックスに登録せず、git restore sample1.txtで変更をなくした上で、再度sample1.txtを編集して差分を確認する。

$ git diff
diff --git a/sample1.txt b/sample1.txt
index f62562a..5bc4a91 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line3
-line4
+linea
+lineb
 line5
 line6
 line7

先ほどと同様にハンクを分割してみる

$ git add sample1.txt -p
diff --git a/sample1.txt b/sample1.txt
index f62562a..5bc4a91 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line3
-line4
+linea
+lineb
 line5
 line6
 line7
(1/1) Stage this hunk [y,n,q,a,d,e,?]? s
Sorry, cannot split this hunk

Sorry, cannot split this hunkの通り、この例のように変更行が連続している場合、ハンクの分割に失敗してしまう。この場合は操作文字eでエディタを起動させて、変更の登録個所を手動で指定していく必要がある。操作画面は以下。

# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,7 +1,7 @@
 line1
 line2
-line3
-line4
+linea
+lineb
 line5
 line6
 line7
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.

とあるように、

  • 先頭に+がある行を除外する場合はエディタで行ごと削除する
  • 先頭に-がある行を削除する場合はエディタで行頭の- に置き換える

例えばline3からlineaへの変更のみをインデックスに登録して、line4からlinebへの変更はインデックスへの登録に含めない場合はエディタで以下を行う。

  • line4行の- に変更
  • lineb行を削除
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,7 +1,7 @@
 line1
 line2
-line3
 line4
+linea
 line5
 line6
 line7
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.

差分を確認すると、ステージングから除外された変更は依然としてワークツリーの変更として認識されていて、ステージングされた変更だけがインデックスの差分として表示されていることが分かる。
git diff --cachedコマンドについては後述

# ワークツリーと最新コミットの差分
$ git diff
diff --git a/sample1.txt b/sample1.txt
index 0f4235b..5bc4a91 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line4
 linea
+lineb
 line5
 line6
 line7

# インデックスと最新コミットの差分
$ git diff --cached
diff --git a/sample1.txt b/sample1.txt
index f62562a..0f4235b 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line3
 line4
+linea
 line5
 line6
 line7

-pオプションを使ったgit addについてはこちらの記事が非常に分かりやすいのでそちらも参照。

※ちなみに-pオプションはgit restoreコマンドなどでも使えて非常に便利なため、積極的に活用していきたい。

インデックスの変更点を確認

既に前の章でコマンドを実行していたが、最新のコミットとインデックスの差分を確認する場合はgit diff --cachedで確認できる。

ここで差分が表示されるファイルは、git statusChanges to be committed:の部分に表示されるファイルと等しい。

インデックスの変更を取り消す

$ git status
On branch test1
Your branch is up to date with 'origin/test1'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   sample1.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

上記のsample1.txtのインデックス上の変更を取り消して、ワークツリーに戻す場合は、git restore --stagedを使う。

#インデックスの変更を取り消す
$ git restore --staged sample1.txt
#ワークツリーの変更のみとなっている
$ git status
On branch test1
Your branch is up to date with 'origin/test1'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

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

コミットする

git addでワークツリーの変更をインデックスに登録し、git diff --cachedで、コミットする内容を確認したうえでgit commitコミットをする。
-mオプションでコミットメッセージも指定するのが一般的。

$ g cm -m'sample1.txtを編集'
[test1 cedf268] sample1.txtを編集
 1 file changed, 2 insertions(+), 2 deletions(-)

ログを確認する

git logコマンドで、現在のブランチのコミットを降順(=新しい順)で表示できる。

$ git log
commit cedf268f63b2885d586fb06bc512f0c72ed1bc5f (HEAD -> test1)
Author: taro <taro@hoge.com>
Date:   xxx xxx D HH:MM:SS 2024 +xxxx
    sample1.txtを編集

commit b58654bda715280a9b93f37baecc767fbc22bbd9 (origin/test1)
Author: taro <taro@hoge.com>
Date:   xxx xxx D HH:MM:SS 2024 +xxxx
    add sample1.txt

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <taro@hoge.com>
Date:   xxx xxx D HH:MM:SS 2024 +xxxx
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <taro@hoge.com>
Date:   xxx xxx D HH:MM:SS 2024 +xxxx
    Initial commit

ログを出力する

Git Bash上ではlinuxコマンドが使えるため、コマンドの実行結果をファイルに出力することなども出来る。

#git logの実行結果をlog.txtに出力させる。
$ git log 1> ./log.txt

#log.txtが作成されている
$ ls
README.md  log.txt  sample1.txt

作成されたファイルをエディタで開けば正規表現で検索をかけられたりするので便利。

※実はgit logコマンドのオプション(--grep)を使えば正規表現に合致するコミットログだけを表示させたり、--since--afterで日付で絞ったりすることも出来る。ただオプションの指定の仕方を覚えるのが面倒な場合はログを出力してエディタで正規表現でハイライトをかけるほうが早かったりする。

直前のコミットを修正する

コミットメッセージを変更する

git commit --amendコマンドでエディタを開き、コミットメッセージ部分を修正して保存することで変更が出来る。

もしくはコミット自体のコマンドと同様git commit --amend -m'コミットメッセージ'でコミットメッセージを直接指定することもできる

コミットの内容を変更する

例えばsample2.txtの新規作成を直前のコミットに含めるべきだった場合は以下のような流れで直前のコミットを修正する。

#インデックスに登録
$ git add sample2.txt
warning: LF will be replaced by CRLF in sample2.txt.
The file will have its original line endings in your working directory

#コミットを実行。エディタが開くため必要に応じてコミットメッセージを修正する
$ git commit --amend
[test1 c70e469] sample1.txtを変更、sample2.txtを追加
 Date: Mon Jul 8 01:03:34 2024 +0900
 2 files changed, 3 insertions(+), 2 deletions(-)
 create mode 100644 sample2.txt

#コミットの内容として、sample2.txtの追加も含まれている
$ g df HEAD~1..HEAD
diff --git a/sample1.txt b/sample1.txt
index f62562a..5bc4a91 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line3
-line4
+linea
+lineb
 line5
 line6
 line7
diff --git a/sample2.txt b/sample2.txt
new file mode 100644
index 0000000..a29bdeb
--- /dev/null
+++ b/sample2.txt
@@ -0,0 +1 @@
+line1

コミットをなかったことにする

直前のコミットをなかったことにする

今、最新コミットと1つ前のコミットの差分及び現在のコミット履歴は以下の状態とする(前章最後の例と同内容。sample1.txtの修正とsample2.txtの追加)

#最新コミットと1つ前のコミットの差分
$ git diff HEAD~..HEAD
diff --git a/sample1.txt b/sample1.txt
index f62562a..5bc4a91 100644
--- a/sample1.txt
+++ b/sample1.txt
@@ -1,7 +1,7 @@
 line1
 line2
-line3
-line4
+linea
+lineb
 line5
 line6
 line7
diff --git a/sample2.txt b/sample2.txt
new file mode 100644
index 0000000..a29bdeb
--- /dev/null
+++ b/sample2.txt
@@ -0,0 +1 @@
+line1

#現在のコミット履歴
$ git log
commit 6ff272541a0d5ab583508c4315532246262d7deb (HEAD -> test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    sample1.txtを変更、sample2.txtを追加

commit b58654bda715280a9b93f37baecc767fbc22bbd9 (origin/test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    add sample1.txt

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Initial commit

コマンド実行による影響範囲の違いで以下3種類の操作がある。

git reset --soft

コミットだけを取り消し、作業ツリーとインデックスは現在の状態のままにする。

#reset --softでコミットを1つ前の状態に戻す
$ git reset --soft HEAD~1

#reset --soft実行前の最新コミットの内容がインデックスに表示される
$ git st
On branch test1
Your branch is up to date with 'origin/test1'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   sample1.txt
        new file:   sample2.txt

git reset --mixed

コミットとインデックスの登録を取り消す。作業ツリーは現在の状態のままにする。

#reset --mixedでコミットを1つ前の状態に戻す
$ git reset --mixed HEAD~1
Unstaged changes after reset:
M       sample1.txt

#reset --mixed実行前の最新コミットの内容がワークツリーに表示される
$ git status
On branch test1
Your branch is up to date with 'origin/test1'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample2.txt

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

git reset --hard

コミット、インデックス、作業ツリーの状態を全て取り消し、指定したコミットの状態に戻す。

#reset --hardでコミットを1つ前の状態に戻す
$ git reset --hard HEAD~1
HEAD is now at b58654b add sample1.txt

#コミット履歴。最新コミットが1つ前のものに巻き戻っている。
$ git log
commit b58654bda715280a9b93f37baecc767fbc22bbd9 (origin/test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    add sample1.txt

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Initial commit

直前ではないコミットをなかったことにする

今、コミット履歴が以下のような状態で、リビジョンが1240622b9749b0c5eee511092bea5d662b33bb74のコミット(「sample1.txtを修正」とのコミットメッセージをもつコミット)をなかったことにしたいとする。

$ git log
commit 85e343d6a954caec689dd3ca6df330668c794809 (HEAD -> test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    sample2.txtを追加

commit 1240622b9749b0c5eee511092bea5d662b33bb74
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    sample1.txtを修正

commit b58654bda715280a9b93f37baecc767fbc22bbd9 (origin/test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    add sample1.txt

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Initial commit

$ git status
On branch test1
Your branch is ahead of 'origin/test1' by 2 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample3.txt

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

この場合、先ほど使ったgit resetは使えない。git reset HEAD~2とした場合、以下のようにリビジョン1240622b9749b0c5eee511092bea5d662b33bb74のコミットだけでなくリビジョン85e343d6a954caec689dd3ca6df330668c794809のコミットもなくなってしまう。

直前ではないコミットをピンポイントでなかったことにしたい場合は、rebase-iオプションを使う。

$ git rebase -i HEAD~2
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.

上記のように、ワークツリーに変更がある状態でgit rebase -iを実行するとエラーとなってしまう。
この場合、変更点をコミットしてしまう方法もあるが、スタッシュといって変更点をワークツリーとは別の領域に一時退避する機能もある。今回はこのスタッシュ機能を使って変更点を退避してから再度rebaseしていく。

ワークツリーの変更を一時退避する

#メッセージをつけて現在のワークツリーの差分をスタッシュ
#※-uをつけないと新規追加したファイルはスタッシュされないが、新規追加のファイルはワークツリーにあってもrebase出来るため今回は-uはつけない
$ git stash save "リベース用一時退避"

#スタッシュの中身を確認してみる
$ git stash list
stash@{0}: On test1: リベース用一時退避

#ワークツリーの変更が消えて新規ファイルの追加のみが差分認識されている
$ git status
On branch test1
Your branch is ahead of 'origin/test1' by 1 commit.
  (use "git push" to publish your local commits)

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample3.txt

nothing added to commit but untracked files present (use "git add" to track)

再度リベースする

$ git rebase -i HEAD~4

するとデフォルトで設定されているエディタ(通常はviと呼ばれるもので、Git Bashウィンドウズの中でviに画面が切り替わる)が開き、以下のようにコミット履歴が昇順でソートされて表示される。

pick 4ae3d06 Update README.md
pick b58654b add sample1.txt
pick 1240622 sample1.txtを修正
pick 85e343d sample2.txtを追加

# Rebase f25624d..85e343d onto f25624d (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

今回はsample1.txtを修正のコミットをなかったことにしたいので、iキー押下で挿入モードにしたうえで、3行目のpickdropに変え、

pick 4ae3d06 Update README.md
pick b58654b add sample1.txt
drop 1240622 sample1.txtを修正
pick 85e343d sample2.txtを追加

ESCキーで挿入モードを終了したうえで:wqを入力しエンターでviを保存&終了させる。これでコミットをなかったことに出来た。以下で確認。

#dropで指定したコミットが確かに消えている
$ git log
$ g lg
commit e55cb97f24e9e8aab28bd8abd563498cbb215fe9 (HEAD -> test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    sample2.txtを追加

commit b58654bda715280a9b93f37baecc767fbc22bbd9 (origin/test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    add sample1.txt

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Initial commit

最後にスタッシュに一時退避した変更をワークツリーに戻しておく。
スタッシュした差分セットをスタッシュの領域に残したままワークツリーに反映だけさせたい場合はapplyを、スタッシュした差分セットをワークツリーに反映してスタッシュの領域からは消したい場合はpopを使う。今回は差分セットを残しておく必要はないためpopを使う。

$ git stash pop stash@{0}
Auto-merging sample1.txt
On branch test1
Your branch is ahead of 'origin/test1' by 1 commit.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample3.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{0} (ed6970cba9867e020061c31bc5af51f4ee6e006c)

#popによってスタッシュのスタックから、saveした差分セットが取り出されている
$ git stash list

コミットの順番を変える

コミット履歴を時系列でみた場合に、コミットの順番を変えたほうが、そのブランチでどういうことが行われているかが分かりやすいときは、コミットの順番を変えることもできる。この場合もrebase -iが使える。今回は直近の2コミットの順番を入れ替えてみる。

git stash save "リベース用一時退避"
git rebase -i HEAD~4

viで以下のように表示されるので、

pick 4ae3d06 Update README.md
pick b58654b add sample1.txt
pick e55cb97 sample2.txtを追加

# Rebase f25624d..e55cb97 onto f25624d (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

iキーで挿入モードにして、以下のようにコミットの順番を入れ替え、:wq+Entキーで保存終了。
コミット履歴を見ると、編集した通りにコミットが入れ替えられている。

$ git log
commit 2e16090b2e6df34b89f18263625e69eb019f23b3 (HEAD -> test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    add sample1.txt

commit 820ce6d9a07819f363cfabca966f176d1165d49c
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    sample2.txtを追加

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    Initial commit

最後にスタッシュした差分セットをワークツリーに戻して終了。

$ git stash pop stash@{0}
On branch test1
Your branch and 'origin/test1' have diverged,
and have 2 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   sample1.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        sample3.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{0} (82e7b049e26c9a1e04efb624a18387b2d0e92dde)

ここで

On branch test1
Your branch and 'origin/test1' have diverged,
and have 2 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

というメッセージが出ている。

ローカルでコミットを入れ替えたことで、リモートリポジトリの同名ブランチ(厳密にいうとローカルにあるリモート追跡ブランチ)との間でコミット履歴に矛盾が生じてしまっているために表示されているメッセージと考えられる。

今作業しているtest1ブランチというのは、自分がローカルで1から作成したブランチではなく、リモートからpullしてきて、そこから作成したブランチだった。つまりtest1ブランチは他人と共有しているブランチということになるが、通常共有しているブランチのコミット履歴をrebase -iでいじってしまうのはよくないのだけども、今回は記事の進め方の都合上このような開発では不適切なブランチ戦略をとっている。

rebase後のtest1ブランチをpushする際は、-fをつけてコミット履歴の矛盾を無視してリモートのコミット履歴をローカルのもので強制的に上書きする必要がある。(ほかの人もtest1でコミットを積んでいた場合、そのコミットをpush -f後のtest1ブランチで競合なくcherry-pickでも出来ない限りそのコミットを破棄するしかなくなってしまうわけだが、これが共有ブランチでコミット履歴を書き換えるべきではない理由ということになる)

複数コミットをまとめて1つのコミットにする

現在のコミット履歴は以下の通り。

$ git log
commit 2e16090b2e6df34b89f18263625e69eb019f23b3 (HEAD -> test1)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    add sample1.txt

commit 820ce6d9a07819f363cfabca966f176d1165d49c
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    sample2.txtを追加

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900
    Initial commit

ここで「add sample1.txt」と「 sample2.txtを追加」の直近2つのコミットをまとめたいとする。

なぜコミットをまとめる必要があるか

コミットをまとめる理由としては、開発している際は、自分が加えた修正を常に把握するために出来るだけ頻繁にコミットするのが望ましいのに対して、

他人がファイルのコミット履歴を見て機能面や目的といった目線で何が行われてきたかや、Blameでソースの特定の行がどういった目的でコミットされて現在のソースになったのかを把握しやすくするために、機能単位でまとまったある程度粒度の粗いコミットにするべきだから、ということが挙げられる。下の2つのコミット履歴だと、明らかにパターン2のコミット履歴のほうが、他人からするとコミットの目的や意図が掴みやすいのが分かる。(というよりローカルはパターン1でコミットをして、pushの直前にパターン2にまとめるべき)

#コミット履歴パターン1
$ git log --oneline
xxxxxxx 注文画面で数量のバリデーションが効かないバグを解消
xxxxxxx getUserInfo関数をリファクタリング
xxxxxxx コーディング規約に違反している部分を修正
xxxxxxx getUserInfo関数を追加
xxxxxxx 注文画面のコードの土台を作成
xxxxxxx お問い合わせ画面のコードの実行時エラーを修正
xxxxxxx お問い合わせ画面を作成

#コミット履歴パターン2
$ git log --oneline
xxxxxxx 注文画面で数量のバリデーションが効かないバグを解消
xxxxxxx 注文機能を実装
xxxxxxx お問い合わせ機能を実装

実際にコミットをまとめてみる

では実際に直近2つのコミットを1つにまとめてみる。

#まずはrebase前にスタッシュ
$ git stash save "リベース用一時退避"
#直近2つのコミットの履歴を編集する
$ git rebase -i HEAD~2

コミットの順番の入れ替えと同様にviエディタが表示される。

pick 820ce6d sample2.txtを追加
pick 2e16090 add sample1.txt

ここで、「add sample1.txt」のコミットを「sample2.txtを追加」のコミットに吸収する形でコミットをまとめるのだが、

  • コミットメッセージとして、吸収する側の「sample2.txtを追加」を採用したい場合⇒pickfixupに変更して保存終了
  • コミットメッセージを新しくつけたい場合⇒picksquashに変更して保存終了
    というように、コミットメッセージをどうするかによってどちらかの操作を行う。

今回はsquashしてみると以下画面になる。

# This is a combination of 2 commits.
# This is the 1st commit message:

sample2.txtを追加

# This is the commit message #2:

add sample1.txt

ここで以下のように、吸収された側のコミットメッセージを削除し、吸収する側のコミットメッセージを、新しいコミットメッセージに差し替えて保存終了することで操作が完了する。

# This is a combination of 2 commits.
# This is the 1st commit message:

sample1.txtとsample2.txtを追加

# This is the commit message #2:


直前ではないコミットを修正する

直前ではない2つ以上前のコミットに、新規作成したsample3.txtも含めたい場合を考える。
まずは修正したいコミットから最新コミットまでをrebase -iオプションで変更するところから始まる。(当然stashしてワークツリーの変更はなくしておく)

git rebase -i HEAD~2

そして修正したいコミットの行のpickeditに変える

edit a4d71d0 Update README.md
pick 426a85a sample1.txtとsample2.txtを追加

次にaddする。

$ git add sample3.txt
$ git status
interactive rebase in progress; onto 4ae3d06
Last command done (1 command done):
   edit 426a85a sample1.txtとsample2.txtを追加
No commands remaining.
You are currently editing a commit while rebasing branch 'test1' on '4ae3d06'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   sample3.txt

add後は以下のようにcommit --amendでコミットを行い、

$ git commit --amend -m'Update README.md and add sample3.txt'
[detached HEAD 54481ef] Update README.md and add sample3.txt
 Date: xxx xxx D HH:MM:SS 2024 +0900
 3 files changed, 10 insertions(+)
 create mode 100644 sample1.txt
 create mode 100644 sample2.txt
 create mode 100644 sample3.txt

git rebase --continueでリベースを終了させる。

$ git rebase --continue
Successfully rebased and updated refs/heads/test1.

別ブランチの特定のコミットを取り込む

ここで、mainブランチから新たにtest3ブランチを作成してみる。

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git branch test3

$ git checkout test3
Switched to branch 'test3'

mainから作成したブランチのため、test1ブランチで作成したsample1.txt、sample2.txt、sample3.txtは存在しない

$ ls
README.md

コミット履歴も以下の通り。

$ git log
commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (HEAD -> test3, origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:SS 2024 +0900

    Initial commit

ここで、先ほどtest1ブランチでrebase -ieditで最終的に作成された「sample1.txtとsample2.txtとsample3.txtを追加」のコミットを、test3ブランチにも取り込みたいとする。

このような場合はcherry-pickコマンドで取り込みたいコミットのリビジョンを指定することで取り込める。

$ git cherry-pick 54481ef9452bd77f20c5cb838fa5afc9296f1a51
[test3 20665dc] sample1.txtとsample2.txtとsample3.txtを追加
 Date: xxx xxx D HH:MM:SS 2024 +0900
 3 files changed, 10 insertions(+)
 create mode 100644 sample1.txt
 create mode 100644 sample2.txt
 create mode 100644 sample3.txt

これでtest1ブランチの指定したコミットがtest3ブランチにも取り込まれてコミット履歴に追加された。

$ git log
commit 20665dc44473ecfa72f29f1f54967f1810405ee6 (HEAD -> test3)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    sample1.txtとsample2.txtとsample3.txtを追加

commit 4ae3d06daa9adeefbfd4f6c82588d56f552d3284 (origin/main, origin/HEAD, main)
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Update README.md

commit f25624d4c1259d68a5ecfb31a15b38f7666ee932
Author: taro <hoge@huga.com>
Date:   xxx xxx D HH:MM:DD 2024 +0900
    Initial commit
$ ls
README.md  sample1.txt  sample2.txt  sample3.txt

cherry-pickでは以下のように複数のコミットをまとめて取り込むこともできる

#コミット1とコミット2を取り込む
$ git cherry-pick コミット番号1 コミット番号2
#コミット1からコミット10までをまとめて取り込む
$ git cherry-pick コミット番号1..コミット番号10

コンフリクトが起きた場合は通常のコンフリクト解決手順と同じ。
チェリーピックをとりやめたい場合は以下のようにする。

git cherry-pick --abort

リモートリポジトリにプッシュする

ここでtest3ブランチをリモートにプッシュする。コマンドは以下のようになる。

git push リモートリポジトリ名(※通常origin) ローカルブランチ名

※originとしてリモートリポジトリを指定できるのは、リモートリポジトリのurlがoriginという名前で登録されているから。git remote -vで確認できる。

引数なしでgit pushだけでpushしたり、git pullだけで現在のブランチのリモートの最新をpullしたい場合は、上流ブランチの設定というものが必要になる。

上流ブランチを確認する

※上流ブランチの定義は「ブランチが変更を追跡しているブランチ」らしいけど。。。???

まずは現在の上流ブランチの設定状況をbranch -vvで確認する。

$ git branch -vv
  main  4ae3d06 [origin/main] Update README.md
  test1 54481ef [origin/test1: ahead 1, behind 1] sample1.txtとsample2.txtとsample3.txtを追加
* test3 20665dc sample1.txtとsample2.txtとsample3.txtを追加

origin/ブランチ名となっている部分が上流ブランチなので、maintest1には上流ブランチが設定されていることが分かる。明示的に設定せずに設定がされているのは

  • main⇒cloneでリポジトリを取得した際にデフォルトで設定されるから
  • test1⇒test1のリモート追跡ブランチから同名のローカルブランチを作成したから
    という理由になる。

プッシュする

test3には上流ブランチが設定されていないため、以下のようにリポジトリ名とブランチ名を明示してpushする。

$ git push origin test3
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (5/5), 428 bytes | 428.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'test3' on GitHub by visiting:
remote:      https://github.com/hoge/huga/pull/new/test3
remote:
To test:hoge/huga.git
 * [new branch]      test3 -> test3

なお、git push -u origin test3とすると上流ブランチが設定され、次回からは引数なしでpushが出来るようになる。

リモートリポジトリから最新ソースを取得する

競合を解決する

先ほどpushしたtest3ブランチで、複数の開発者が別々のコミットを作成していて競合したとする。最新のtest3ブランチをリモートからpullしたところ、sample1.txtの修正箇所がかぶり、その内容が矛盾するものであった場合を考える。

#pullするとコンフリクトに
$ git pull origin test3
From huga:hoge/test
 * branch            test3      -> FETCH_HEAD
Auto-merging sample1.txt
CONFLICT (content): Merge conflict in sample1.txt
Automatic merge failed; fix conflicts and then commit the result.

#コンフリクトが発生しているsample1.txtを開く
$ vi sample1.txt

sample1.txtは以下の通りになっている。リモート1行目はlineaなのに対して、ローカル1行目はlinebのため、どちらを採用してよいかgitは分からないためコンフリクトになっている。

<<<<<<< HEAD
lineb
=======
linea
>>>>>>> 31d8c9934b53649739418fdd1a193641c6d4c4d3
line2
line3
line4
line5
line6
line7
line8

1行目のテキストとしてlineaを採用するとする。
競合解決するためにファイルを編集する際は、最終的に残したい内容にする。なので<<<<<<< HEADとか=======は消してしまい、以下の状態で保存する。

linea
line2
line3
line4
line5
line6
line7
line8

次にaddすることで競合解決をgitに伝え、最後にcommtiして競合解決を完了する。

$ git add sample1.txt
$ git commit
[test3 f94c2ef] Merge branch 'test3' of huga:hoge/huge into test3

競合の解決に外部ツールを使う

競合の解決は通常エディタで編集することで解決するのが一般的だが、git mergetoolで競合の解決専用のツールを使うこともできるらしい。詳しくはこちら

差分を確認してから最新ソースをマージする

pullでリモートの変更を自動的にローカルに取り込むのではなく、リモートとローカルの差分を確認してからリモートの変更を取り込みたい場合は、以下のようにfetchmergeを行う。fetchによってtest3ブランチのリモート追跡ブランチが更新されるので、ローカルのtest3ブランチとリモート追跡ブランチを比較する形で差分を確認した後、リモート追跡ブランチの内容をローカルのtest3ブランチにmergeすればよい。実はpullも内部ではfetch&mergeを行っている。

git fetch
git diff HEAD..origin/test3
git merge origin/test3

rebaseでの競合解決

mergeの際と異なり、commitは必要ない点に留意

# カレントブランチをhogeブランチでrebase
git rebase hoge
# 競合解決後、コンフリクト発生対象のsamp.txtファイルをadd
git add samp.txt
# 競合解決をgitに伝えて終了
git rebase --continue

番外編

aliasでコマンドの省略形を登録しておく

頻繁に実行するgitコマンドなどは、短縮した別名ともいうべき存在であるaliasを設定しておくと便利。
通常はユーザーのルートフォルダにあるgitの設定ファイル.gitconfigに設定する。

$ git config --global alias.st "status"

$ git config --global -l
alias.st=status
hoge.fuga=hoge
・・・

aliasにはシェルも設定できるため、毎回セットで実施するgitコマンドを一種トランザクションとして登録しておくと便利。シェルで実行可能なコマンドを登録する場合は、以下のように先頭に!をつける。

alias.rbi3="!git stash && git rebase -i HEAD~3 && git pop"

git reflogで操作履歴を確認する

reflogを使うと、リポジトリ(つまり.gitファイルが管理しているディレクトリ)で行われたgitの操作履歴を確認できる。これはreset --hardでなかったことにしたコミットを復活させたい、などの時に便利。

$ git reset --hard HEAD^^
HEAD is now at 20665dc sample1.txtとsample2.txtとsample3.txtを追加

$ cat sample1.txt
line1
line2
line3
line4
line5
line6
line7
line8

以下のようにreflogで操作履歴を確認してみる。

$ git reflog
20665dc (HEAD -> test3) HEAD@{0}: reset: moving to HEAD^^
f94c2ef HEAD@{1}: commit (merge): Merge branch 'test3' of hoge:hoge/huga into test3
1d733ae HEAD@{2}: commit: line1を修正
20665dc (HEAD -> test3) HEAD@{3}: reset: moving to HEAD^
31d8c99 (origin/test3) HEAD@{4}: commit: line1修正
20665dc (HEAD -> test3) HEAD@{5}: cherry-pick: sample1.txtとsample2.txtとsample3.txtを追加
git reset HEAD@{2}
・・・

このように、HEADが度のコミットを差してきたのかが時系列で表示される。
reflogで参照できる履歴の情報は、通常30日保持される。

今やりたいのは

f94c2ef HEAD@{1}: commit (merge): Merge branch 'test3' of hoge:hoge/huga into test3

このコミットにresetしなおすことなので、以下のようにする。

git reset HEAD@{1}

これで消えてしまったコミットが復活してくれる。

おわりに

今回はこちらの書籍を頻繁に参考にした。
「Gitについて深堀りしたい!」と思い買って以来、開発の際にしょっちゅう参考にしていて、今では欠かすことが出来なくなっている。。。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1