Git のコマンド色々たたいてみる


## はじめに

Git を使い始めて早5年ほど。操作系のコマンドだと未だ add, commit, push, merge 程度のコマンドしか使ったことがない。他にも色々とあるのは知っているが、どのような挙動を起こすのか把握しておらず、怖くて使っていない。この記事では色々試してみようと思う。

## 今回使う Git のバージョン

```
$ git --version
git version 2.29.2
```

## 試す

test 用のリポジトリ。
https://github.com/firedial/git_practice/tree

### マージ時にコンフリクト起こし、マージを取り消したい時

関係するブランチ

* conflict-master
* conflict-development

元々 conflict-master に `人のお金で焼肉食べたい。` という文書が書かれた[テキストファイル](https://github.com/firedial/git_practice/commit/a0e0f1280a9be70de1dfe96173108ba5e3eee340)があり、そこから conflict-development ブランチを切って `人のお金で寿司食べたい。` という[テキストファイル](https://github.com/firedial/git_practice/commit/4f5c6705855a32aa6694a133ababd60e3305556d)に変更した。その後 conflict-master で `人のお金で蟹食べたい。` という[テキストアフィル](https://github.com/firedial/git_practice/commit/793c12faf59d7e9d3b6d46f005d30cc3d39f5ce3)に書き換わった。

ここで、 conflict-master と conflict-development をマージするとコンフリクトを起こす。

```
$ git merge conflict-development
Auto-merging command_test/conflict.txt
CONFLICT (content): Merge conflict in command_test/conflict.txt
Automatic merge failed; fix conflicts and then commit the result.
```

その時のファイル状態は下記の通りである。

```
$ git status
On branch conflict-master
Your branch is based on 'origin/command-test', but the upstream is gone.
  (use "git branch --unset-upstream" to fixup)

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:   conflict.txt

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

その状態からマージを取り消す。

```
$ git merge --abort
```

そうすると、マージする前に戻る。

```
$ git status
On branch conflict-master
Your branch is based on 'origin/command-test', but the upstream is gone.
  (use "git branch --unset-upstream" to fixup)

nothing to commit, working tree clean
```

### 作業用ツリーの修正を元に戻したい時

関係するブランチ

* reset-master

`焼肉食べたい。` と書かれた[テキストファイル](https://github.com/firedial/git_practice/commit/6f798f4434ca7edc7f34715e37bfea49d7df83fc)を編集することを考える。

```
$ git diff
diff --git a/reset_test/reset.txt b/reset_test/reset.txt
index 393fada..3b153da 100644
--- a/reset_test/reset.txt
+++ b/reset_test/reset.txt
@@ -1,4 +1,4 @@
 
-焼肉食べたい。
+寿司食べたい。
```

作業ツリーの変更を全て取り消す。

```
$ git checkout .
Updated 1 path from the index
```

そうすると元に戻っていることが確認できる。

```
$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

nothing to commit, working tree clean
```

---

ただそれだけだと、新規に作成したファイルが残ったままになる。試しに `add.txt` というファイルと、 `add` というディレクトリを作成する。

```
$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

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

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

$ git checkout .
Updated 0 paths from the index

$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

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

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

このように元に戻せていない。この場合は `git clean` を使う。そうすることで `add.txt` が削除される(この時、完全にファイルが削除されることに注意)。

```
$ git clean -f
Removing add.txt

$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

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

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

ディレクトリを削除するには `-d` オプションもつける(これもディレクトリ配下全て削除されるので注意)。

```
$ git clean -df
Removing add/

$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

nothing to commit, working tree clean
```

### add 操作を取り消したい時

関係するブランチ

* reset-master

ファイル編集して add してみる。

```
$ git diff
diff --git a/reset_test/reset.txt b/reset_test/reset.txt
index 393fada..3b153da 100644
--- a/reset_test/reset.txt
+++ b/reset_test/reset.txt
@@ -1,4 +1,4 @@
 
-焼肉食べたい。
+寿司食べたい。
 
$ git add reset.txt 

$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

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

取り消しコマンドを叩く。

```
$ git reset HEAD .
Unstaged changes after reset:
M	reset_test/reset.txt
```

そうすると add する前に戻っていて、ファイル変更はそのまま保持されている。

```
$ git status
On branch reset-master
Your branch is up to date with 'origin/reset-master'.

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:   reset.txt

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

$ git diff
diff --git a/reset_test/reset.txt b/reset_test/reset.txt
index 393fada..3b153da 100644
--- a/reset_test/reset.txt
+++ b/reset_test/reset.txt
@@ -1,4 +1,4 @@
 
-焼肉食べたい。
+寿司食べたい。

```

### commit 操作を取り消したい時(履歴は残らない)

関係するブランチ

* reset-master

コミットをしてしまったが、そのコミットを取り消したいことを考える。これはリモートにプッシュする前に留めておくべきである(そのことについては後で試してみる)。

コミットを追加。 push する前なので origin より一つ進んでいる状態。

```
$ git commit -m "寿司に変更"
[reset-master ba77535] 寿司に変更
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
ba77535 (HEAD -> reset-master) 寿司に変更
6f798f4 (origin/reset-master) 焼肉
```

そのコミットを取り消す。 `HEAD~` は `HEAD` の一つ前のコミットを指す。今回で言うと `6f798f4` のコミットである。

```
$ git reset --hard HEAD~
HEAD is now at 6f798f4 焼肉
```

---

上記のことはローカル内のことだったが、リモートにプッシュしてしまった場合どうなるのか試してみる。

コミットを取り消す作業者の作業: <font color="red">赤色</font>
共同作業者の作業: <font color="blue">青色</font>

<font color="red">まずは変更をリモートにプッシュする。</font>

```
$ git commit -m "寿司に変更"
[reset-master 9d698c9] 寿司に変更
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 414 bytes | 414.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/firedial/git_practice.git
   6f798f4..9d698c9  reset-master -> reset-master

$ git log --oneline
9d698c9 (HEAD -> reset-master, origin/reset-master) 寿司に変更
6f798f4 焼肉
```

<font color="blue">共同開発者がその変更をプルする。</font>

```
$ git log --oneline
9d698c9 (HEAD -> reset-master, origin/reset-master) 寿司に変更
6f798f4 焼肉
```

<font color="red">コミットを取り消す。</font>

```
$ git reset --hard HEAD~
HEAD is now at 6f798f4 焼肉

$ git log --oneline
6f798f4 (HEAD -> reset-master) 焼肉
```

<font color="red">その変更をリモートにプッシュする。この時 `-f` オプションを使って強制的にプッシュする必要がある。</font>

```
$ git push -f origin reset-master
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/firedial/git_practice.git
 + 9d698c9...6f798f4 reset-master -> reset-master (forced update)
```

そのコミットがなかったことになりました。めでたしめでたし？？？？

```
$ git log --oneline
6f798f4 (HEAD -> reset-master, origin/reset-master) 焼肉
```

<font color="red">取り消した後から別に変更を加える。</font>

```
$ git commit -m "パスタに変更"
[reset-master 44ebdc8] パスタに変更
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 420 bytes | 210.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/firedial/git_practice.git
   6f798f4..44ebdc8  reset-master -> reset-master

$ git log --oneline
44ebdc8 (HEAD -> reset-master, origin/reset-master) パスタに変更
6f798f4 焼肉
```

<font color="blue">取り消されたコミットから変更を加える。</font>

```
$ git commit -m "蟹に変更"
[reset-master 5b60085] 蟹に変更
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
5b60085 (HEAD -> reset-master) 蟹に変更
9d698c9 (origin/reset-master) 寿司に変更
6f798f4 焼肉
```

<font color="blue">そしてその変更をリモートにプッシュする。</font>

```
$ git push
To https://github.com/firedial/git_practice.git
 ! [rejected]        reset-master -> reset-master (fetch first)
error: failed to push some refs to 'https://github.com/firedial/git_practice.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
```

このように `error: failed to push some refs` と出て変更をリモートに反映できなくなる。

### commit 操作を取り消したい時(履歴は残る)

上記で述べた方法でコミットを取り消すと共同開発者に影響が出てしまう。そのため、リモートにプッシュ後の取り消しは `revert` を使うべきである。

パスタに変更した(44ebdc8)のを取り消すことを考える。

```
$ git log --oneline
44ebdc8 (HEAD -> reset-master, origin/reset-master) パスタに変更
6f798f4 焼肉
```

revert コマンドを叩く。

```
$ git revert 44ebdc8
[reset-master 971602a] Revert "パスタに変更"
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
971602a (HEAD -> reset-master, origin/reset-master) Revert "パスタに変更"
44ebdc8 パスタに変更
6f798f4 焼肉
```

焼肉食べたいに戻っていることを確認。

```
$ cat reset.txt 

焼肉食べたい。

```

### 他のブランチのコミットを取り込む

関係するブランチ

* cherry-master
* cherry-development
* cherry-test

cherry-master から cherry-development で開発をし、その修正を cherry-test に取り込んでテストを行う。
テスト中に修正不備を発覚し修正を行うが、間違って cherry-test の方に修正をコミットしてしまった。
その修正を cherry-development に取り込むことをやってみる。

元々 [pick.txt](https://github.com/firedial/git_practice/blob/cherry-master/cherry/pick.txt) には下記の内容が入っている。

```
$ cat pick.txt 

食べたいものリスト
* 焼肉(人のお金で)
* 寿司(人のお金で)
```

それを cherry-development で下記のように[修正](https://github.com/firedial/git_practice/commit/d41bd9c9ef2037ec63a7d34a93a461a635d2793f#diff-31cf2774f9fa403f36ec85c5f90192cd790c9d361d1a1e480f3171a6978b62ad)。

```
$ cat pick.txt 

食べたいものリスト
* 焼肉(人のお金で)
* 寿司(人のお金で)
* 蟹
```

その変更を cherry-test にマージ。

```
 $ git merge cherry-development
Updating fea6cfd..d41bd9c
Fast-forward
 cherry/pick.txt | 1 +
 1 file changed, 1 insertion(+)
```

その際 `人のお金で` という非常に大事な文言が抜けていることに気付き、修正するが cherry-test にコミットしてしまう。

```
$ git commit -m "大事な文言追加"
[cherry-test 806b5c2] 大事な文言追加
 1 file changed, 1 insertion(+), 1 deletion(-)
```

その修正を cherry-development に反映したい。そこで出てくるのが cherry-pick である。

修正したコミットのハッシュ値 806b5c2 を控える。

```
$ git log --oneline
806b5c2 (HEAD -> cherry-test, origin/cherry-test) 大事な文言追加
d41bd9c (origin/cherry-development, cherry-development) 蟹を追加
fea6cfd (origin/cherry-master, cherry-master) 食べたいものリスト
```

ブランチを切り替えて、今の状況を確認。

```
$ git checkout cherry-development
Switched to branch 'cherry-development'
Your branch is up to date with 'origin/cherry-development'.

$ git log --oneline
d41bd9c (HEAD -> cherry-development, origin/cherry-development) 蟹を追加
fea6cfd (origin/cherry-master, cherry-master) 食べたいものリスト
```

そこで cherry-pick コマンドを叩く。

```
$ git cherry-pick 806b5c2
[cherry-development 8ca6349] 大事な文言追加
 Date: Fri Jan 1 22:14:11 2021 +0900
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
8ca6349 (HEAD -> cherry-development) 大事な文言追加
d41bd9c (origin/cherry-development) 蟹を追加
fea6cfd (origin/cherry-master, cherry-master) 食べたいものリスト
```

コミットが適用され、無事ファイルも修正されている。

```
$ cat pick.txt 

食べたいものリスト
* 焼肉(人のお金で)
* 寿司(人のお金で)
* 蟹(人のお金で)
```

## 終わりに

ちょっと駆け足だったが、細かく解説できた方かなと思う。特に reset でコミットを取り消すとどうなるかを、実際試すことができてよかった。

今年もいくつかの記事がかければなぁと思いますので、暖かく見守ってもらえると嬉しいです。