リモートリポジトリをクローン
リモートリポジトリのソースを取得
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.
リモートリポジトリを追跡するローカルのブランチを確認してみる
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
ファイルの編集・追加からコミットまで
全体像
※ワークツリー⇒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 status
でChanges 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 status
でChanges 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行目のpick
をdrop
に変え、
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を追加」を採用したい場合⇒
pick
をfixup
に変更して保存終了 - コミットメッセージを新しくつけたい場合⇒
pick
をsquash
に変更して保存終了
というように、コミットメッセージをどうするかによってどちらかの操作を行う。
今回は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
そして修正したいコミットの行のpick
をedit
に変える
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 -i
のedit
で最終的に作成された「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/ブランチ名
となっている部分が上流ブランチなので、main
とtest
1には上流ブランチが設定されていることが分かる。明示的に設定せずに設定がされているのは
- 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でリモートの変更を自動的にローカルに取り込むのではなく、リモートとローカルの差分を確認してからリモートの変更を取り込みたい場合は、以下のようにfetch
とmerge
を行う。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について深堀りしたい!」と思い買って以来、開発の際にしょっちゅう参考にしていて、今では欠かすことが出来なくなっている。。。