git pull --rebase origin developを理解し使っていく

# 事の始め
> 先輩「git pull --rebaseを使ってね」
> 私「はい！(なんのことか全然知らないけど)」

rebase関連は全然知らなかったのでこれを機にしっかり見てみることにした。

# 実際に動作を見てみる

実際にチーム開発である例で確認してみる。
ローカルでは**developブランチ**から**feature/something-1**ブランチを切っている状態を想定(リモートにはプッシュしていない)。

![figure-local.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/e14266ff-64a4-c008-4cda-ab3d1bf13436.png)

一方リモートでは誰かが作業を行ったプルリクエストがマージされている状態。

![figure-remote.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/bc022f18-3212-46c0-9a47-4e5ef83bc6e0.png)

この状態からローカルに変更を適用する場合に`git pull`と`git pull --rebase`で何が違うのかを確認する。


## git pull

remoteのdevelopブランチをpullする。

```
% git pull origin develop
```

git logで確認。

```
% git log
commit 2076b38bd7801d0271485fdb3baed5b074769397 (HEAD -> feature/something-1)
Merge: a25f40f 2db3d26
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:50:20 2019 +0900

    Merge branch 'develop' of github.com:dys6/git-practice into feature/something-1

commit 2db3d260a29b2f44fcc72c716bc4d7ec972335a8 (origin/develop)
Merge: f1d0035 f4f2e6e
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:45:23 2019 +0900

    Merge pull request #1 from dys6/feature/something-2

    Implement something-2

commit f4f2e6efdad3aab6d7d5330bbdef7fd9abadf4bd
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:45:11 2019 +0900

    Implement something-2

commit a25f40faef493d2f4c0589d2ad8ed5027fb4756d
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:44:12 2019 +0900

    Implement something-1

commit f1d0035dee3a67d8f2ecd1d22bad2a31613298b2 (develop)
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:42:31 2019 +0900

    first commit
```

![figure-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/38ac5d03-3e1b-5f01-eadc-06676b61a16b.png)


## git pull --rebase

remoteのdevelopブランチをpull reabseする。

```
% git pull --rebase origin develop
```

```
% git log
commit c7d2da23a86f81db2f5c01fa445b9fc604beba8d (HEAD -> feature/something-1)
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:44:12 2019 +0900

    Implement something-1

commit 2db3d260a29b2f44fcc72c716bc4d7ec972335a8 (origin/develop)
Merge: f1d0035 f4f2e6e
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:45:23 2019 +0900

    Merge pull request #1 from dys6/feature/something-2

    Implement something-2

commit f4f2e6efdad3aab6d7d5330bbdef7fd9abadf4bd
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:45:11 2019 +0900

    Implement something-2

commit f1d0035dee3a67d8f2ecd1d22bad2a31613298b2 (develop)
Author: dys <51311958+dys6@users.noreply.github.com>
Date:   Sun Jul 14 16:42:31 2019 +0900

    first commit
```

![figure-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/9f488c38-30ed-5370-decf-77c6de293a50.png)


## 具体的に何が違ったのか
ただし`git pull`してマージの際にfast forwardが行える場合にはマージコミットは発生しない。今回の例はnon fast forwardである。


### マージのコミット
`git pull`をした際には「Merge branch 'develop' of github.com:dys6/git-practice into feature/something-1」というメッセージのコミットが発生している。一方`git pull --rebase`をした時にはそれが発生していない。
ただし`git pull`してマージの際にfast-forwardが行える場合にはマージコミットは発生しない。今回の例はnon-fast-forwardである。

### 「Implement something-1」コミットの履歴上の位置とハッシュ値
`git pull`では`git log`で表示すると実際にコミットされた順番にコミットが表示される。ハッシュ値にも変化がない。一方`git pull --rebase`は「Implement something-1」のコミットハッシュ値が**a25f40faef493d2f4c0589d2ad8ed5027fb4756d**から**c7d2da23a86f81db2f5c01fa445b9fc604beba8d**に変わっており、別のコミット扱いになっていることがわかる。また履歴上の位置も先頭になっている(コピーが作成される)。
リモートに変更がない状態で`git pull --rebase`しても履歴は変わらずコミットのハッシュ値も変わらない。

## 何が嬉しいのか
自分の作業ブランチに最新のdevelopブランチの変更を適用するために`git pull`を使っていると変更がある度にマージのコミットが発生してしまう。最終的にdevelopブランチにマージされるとdevelopの履歴が汚く?なってしまう。`git pull --rebase`を使うことで不要なマージコミット発生させずに履歴を綺麗に保つことができる。

# 実際に使っていく

## git pull --rebaseしてgit pushする
基本は自分のブランチで作業を行なってコミットしたら`git push`の前に`git pull --rebase`をしてリモートの変更をローカルに適用しながら作業を行なっていく形になる。
しかし、1回リモートにプッシュしたあと、同じブランチでまたコミットしてリモートに変更がある状態で`git pull --rebase`をしてから`git push`をしようとすると以下のように怒られる。

```
% git push origin feature/something-3
To github.com:dys6/git-practice.git
 ! [rejected]        feature/something-3 -> feature/something-3 (non-fast-forward)
error: failed to push some refs to 'git@github.com:dys6/git-practice.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
```

> 先輩「そういう時はgit push --force-with-leaseを使ってね」
> 私「はい！(全然知らないけど)」

forceオプションってなんとなく危ないっていう印象で全然使わず調べずだったのでこれを機に理解する。

### そもそもなぜ怒られているのか

`git pull --rebase`をしたことで**origin/feature/something-3**をfast-forwardによって更新できなくなったからである。怒られている内容にもある`git push --help`で**Note about fast-forwards**の項目について読むとプッシュはコミット履歴を失わないためにもfast-forwardによってローカルの変更を取り込める必要があるそうだ。

> // git push --helpから引用
When an update changes a branch (or more in general, a ref) that used to point at commit A to point at another commit B, it is called a fast-forward update if and only if B is a descendant of A.
コミットAから別のコミットBを指すようにブランチ(一般的にはref)が変更された時、BがAの子孫である場合のみそれはfast-forward更新と呼ばれる。

リモートの状態から新たに「Fix something-3」をコミットして`git pull --rebase`を実行するとローカルの状態になる。

![figure-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/69f9b47e-6058-4ad0-0b44-f5d03a8265f0.png)

この時の**something-3**ブランチのリモートとローカルの履歴は枝分かれするように異なっており、ローカルのコミットとリモートのコミットは子孫関係ではなくなってしまっている。よってfast-forwardによる更新ができず、先に挙げたエラーが出ている。

![figure-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/b92975ae-7fe4-3a9c-3dd3-b112b824dddc.png)

こういう状況が発生した時、リモートの「Fix something-3」を他の誰かがフェッチしていない時に限り、`git push --force`を実行してリモートの履歴をローカルの履歴で上書きすることができる。
状況は違うけれど`git push --help`には以下のように書いてあった。

> // git push --helpから引用
There is another common situation where you may encounter non-fast-forward rejection when you try to push, and it is possible even when you are pushing into a repository nobody else pushes into.After you push commit A yourself (in the first picture in this section), replace it with "git commit --amend" to produce commit B, and you try to push it out, because forgot that you have pushed A out already. In such a case, and only if you are certain that nobody in the meantime fetched your earlier commit A (and started building on top of it), you can run "git push --force" to overwrite it. In other words, "git push --force" is a method reserved for a case where you do mean to lose history.
他の人が同じレポジトリにプッシュしていない場合でもnon-fast-forward拒否に出くわすことがある。
コミットAをプッシュした後、それを忘れていて"git commit —-amend"でコミットBを作ってプッシュしようとした時である。そのような時、誰も以前のコミットAを取得していないことが確信できる場合のみに、"git push --force"を実行してそれを上書きすることができる。言い換えれば"git push --force"は履歴を失うことを意味するようなことをする時のために用意された方法である。

### git push --forceとgit push --force-with-leaseの違い
どちらも履歴を上書きして強制的にプッシュするという点は変わりない。例えば、**feature/something-3**で他の人も作業していて「Fix something-3 by other person」をコミットしリモートにプッシュしたとする。自分のローカルではそのコミットがプッシュされる前に`git pull --rebase`をしている。この状況において`git push --force`は実行できるが、`git push --force-with-lease`は実行できない。仮に`git push --force`を実行するとリモートの「Fix something-3 by other person」のコミットは「Fix something-3」で上書きされなかったことになってしまう。`git push --force-with-lease`ではこれを防ぐことができるようだ。

![figure-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/461619/b2052a41-a681-fc19-735b-746c021cd127.png)

> // git push --helpより引用
Usually, "git push" refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it. This option overrides this restriction if the current value of the remote ref is the expected value. "git push" fails otherwise. 
Imagine that you have to rebase what you have already published. You will have to bypass the "must fast-forward" rule in order to replace the history you originally  published with the rebased history. If somebody else built on top of your original history while you are rebasing, the tip of the branch at the remote may advance with her commit, and blindly pushing with --force will lose her work.This option allows you to say that you expect the history you are updating is what you rebased and want to replace. If the remote ref still points at the commit you  specified, you can be sure that no other people did anything to the ref. It is like taking a "lease"  on the ref without explicitly locking it, and the remote ref is pdated only if the "lease" is still valid.

なんとなくforceのオプションは危ないし使っちゃダメ！っていう認識だったけれど、自分しか作業しないブランチとか必要な上書きしたい時とか適切な場面で使えばあんまり危なくなさそう、と感じた。

## コンフリクトだ！

```
% git pull --rebase origin develop
From github.com:dys6/git-practice
 * branch            develop    -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: Implement something-6
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
CONFLICT (add/add): Merge conflict in something-6.md
Auto-merging something-6.md
error: Failed to merge in the changes.
Patch failed at 0001 Implement something-6
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort"
```

ちょこちょこコンフリクトを起こすが、手動で解消して`git add`して`git rebase --continue`でOK。なんかやばそうやっぱりリベースやめたいっていう時は`git rebase --abort`でリベース前に戻せる。`git rebase --skip`はコンフリクトを解消した結果リモートの最新コミットと差分がなくなり`git rebase --continue`で進めなくなった際に使う。

```
% git add something-6.md
% git rebase --continue
Applying: Implement something-6
No changes - did you forget to use 'git add'?
If there is nothing left to stage, chances are that something else
already introduced the same changes; you might want to skip this patch.
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort"
```

# 最後に
理解したことを間違いがないように気をつけて書きましたが、もし間違っていたら優しく教えてください。

# 参考文献
rebase以前にmergeとfetchも正直よくわかっていなかったのですが、ここを見て理解できた気がします。わかりやすくて大変勉強になりました！

- [git pull と git pull –rebase の違いって？図を交えて説明します！](https://kray.jp/blog/git-pull-rebase/)