git:到底什么是 fast-forwards ?

今天完成一个功能开发,提交代码的时候,突然提示如下错误:

1
2
3
4
5
6
7
To C:/Users/Alpha/AppData/Local/Temp/d20170730-15308-3dbr6w/.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'C:/Users/Alpha/AppData/Local/Temp/d20170730-15308-3dbr6w/.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.

意思就是本次提交被远程仓库拒绝了,因为当前分支无法与远程仓库对应起来。远程仓库对应分支默认有个指针指向最新提交到仓库的 commit ,而所有的本地仓库的分支都可以看做是从这个 commit 分散开来的。也就是本地分支的最后一次 push 到仓库的 commit 一定与仓库对应分支的最新一次 commit 是相同的,否则就无法对接。也就是会出现上面的错误提示。如果是正常 push 到仓库,正确的完成 commit 更新,那么这次更新就是一个 fast-forward 更新,而如果不理会错误警告用本地更新强制覆盖仓库,就是一次 no-fast-forward 更新,很明显,no-fast-forward 更新会导致记录丢失

那么这种问题是如何发生的呢?比如有两个人都是从仓库的 master 分支克隆到本地,然后分别开发。master 本身有一个指针 HEAD 指向最后一次 commit 记录 commit-0 。A 先完成一个功能,并 push 到仓库,这次 commit 记为 commit-A,这也就是一次 fast-forward 更新,此时仓库的 master 分支的 HEAD 指针就指向了 commit-A。接下来 B 也完成了一个功能,要向仓库 push commit-B,如果没有做额外操作,肯定会出现上面的错误。

知道错误是如何发生的,就可以避免了。既然仓库有了更新,那么就要先把仓库的更新拉取到本地。这里有两种方式可以拉取:一是直接使用 git pull 命令,该命令会在拉取的同时会直接与本地对应分支进行合并,如果确信仓库的更新与本地不会发生冲突,那么可以直接使用。但是很可能 A 与 B 都对同一些文件做出了修改,那么必然导致冲突。不过既然知道会冲突也只能老老实实解决冲突了,不管是 fetch 先解决冲突在合并还是 pull 先合并再解决冲突,这个过程少不了的,除非你确定仓库的更新是没用的可以直接抛弃,就可以执行 git push -f 强制覆盖到仓库,这会导致仓库中某些记录丢失。

我们借助于 githug 28 关来模拟看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ git push -u origin master
To C:/Users/Alpha/AppData/Local/Temp/d20170731-15124-1ywoym1/.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'C:/Users/Alpha/AppData/Local/Temp/d20170731-15124-1ywoym1/.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 log origin/master --oneline
68ad000 Fourth commit

$ git log --oneline
b977ec3 Third commit
6d15890 Second commit
5266aa2 First commit

$ git push -f -u origin master
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 546 bytes | 0 bytes/s, done.
Total 7 (delta 2), reused 0 (delta 0)
To C:/Users/Alpha/AppData/Local/Temp/d20170731-15124-1ywoym1/.git
+ 68ad000...b977ec3 master -> master (forced update)
Branch master set up to track remote branch master from origin.

$ git log origin/master --oneline
b977ec3 Third commit
6d15890 Second commit
5266aa2 First commit

可以看到,强制覆盖 push 后,仓库的 Fourth commit 已经不见了。

但如果不想丢掉 commit-A 的同时又不想与 commit-A 合并,B 想继续接着本地仓库工作,可以使用 git rebase origin/master ,表示将本地所有 commit 排在仓库 的 commit 记录之后。然后向仓库的 push 就会被接受。同样借助于 githug 28 关,而且,这才是 28 关正确的过关方式:

详细过关过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ git push -u origin master
To C:/Users/Alpha/AppData/Local/Temp/d20170731-14980-yb0fll/.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'C:/Users/Alpha/AppData/Local/Temp/d20170731-14980-yb0fll/.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 log origin/master --oneline
015383a Fourth commit

$ git log --oneline
38aa398 Third commit
1c03f48 Second commit
ad32a6d First commit

$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: First commit
Applying: Second commit
Applying: Third commit

$ git log --oneline
fbf0528 Third commit
03d1240 Second commit
9828360 First commit
015383a Fourth commit

$ git push -u origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 607 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
To C:/Users/Alpha/AppData/Local/Temp/d20170731-14980-yb0fll/.git
015383a..fbf0528 master -> master
Branch master set up to track remote branch master from origin.

但其实还有一种比较常见的出现 no-fast-forward 这种错误的情境,是在你向一个只有你自己可访问的仓库 push 的时候发生的。当你已经将一次 commit-A push 到仓库后,然后因为某些原因又使用了 git commit –amend 修改了 commit-A ,这个时候 commit-A 就变成了 commit-B,而此时本地仓库就没有关系 commit-A 的记录了,这个时候再次向仓库 push ,很明显,commit-B 无法与仓库的 commit-A 进行对接,所以出现了 no-fast-forward 错误。这种情况下其实也很好解决,如果你确定 commit-A 已经完全无用并且没有人将 commit-A 拉取到本地进行进一步开发之后,你就使用 git push -f 来覆盖仓库记录。之后,你就会永远丢失 commit-A 记录了。

而对比发现,我之所以会遇到本文开头的错误,就是因为之前使用了 git commit --amend 命令修改了已经 push 到仓库的 commit 的注释导致的。因此,一旦已经 push 到仓库,想要做出修改,就只能通过一次新的 commit 来完成对某次已经 push 到仓库的 commit 记录的修改了,可以参考 githug 52 关 revert。

参考:
[1]https://git-scm.com/docs/git-push/2.10.0#Note about fast-forwards
[2]极客学院 githug 通关攻略