你或许不知道的Git

开始使用 Git 后就绕不开的指令,一定有这么几个:

git add
git commit
git push
1
2
3

初学者肯定会被这几个指令绕晕。 从直觉上来讲,提交代码应该是一行命令就能搞定的事,为什么需要这么多条指令? 接下来我们就来分析一下这几条命令的作用。

# 文件的三种状态

说到上面的前两条命令,就不得不说,通过 Git 管理的文件的三种状态:modified、staged、commited。

# modified

modified,顾名思义,是指这个文件被修改过。 我们修改了某个文件的代码后,这个文件在 git 中的状态就会被标记为 modified。 这种状态在开发中最为常见。 开发结束就会使用 git add 命令把被编辑过的文件放到暂存区。

# staged

staged,可以翻译为已暂存。 这里就要介绍一下 git 中“暂存区”这个概念了。

被 git add 添加的文件并不会直接提交到 git 的版本管理中,而是会被放到暂存区内。 这样做的意义是,在这个文件被暂存后,我们还能够继续编辑这个文件。

比如有这样一个场景:小明对 a 文件进行了两处修改,第一处修改对应的新建了 b 文件,第二处修改对应的新建了 c 文件。 假如想要拆开提交两处修改,就需要取消 a 文件的第二处修改,并删除 c 文件,然后再还原,再提交。 而有了暂存区,就可以省略掉删除的操作,仅仅把想要提交的文件添加到暂存区,编写 commit message 后提交就行了。

类似的场景还有开发了一半的功能不想提交,只需要把开发好了的文件添加到暂存区再提交就行了。 当习惯了之后就会发现,暂存区实在是好用,有时甚至觉得一个暂存区都不够用。

# commited

commited,故名思议,已提交。 执行完 git commit 指令后,staged 状态的文件就会变成 commited 状态,这样才算是将代码提交到仓库内了。 同时也会生成一条对应的 commit 记录,上面还带有执行 git commit 时编写的 commit message。

# Git 仓库

上面提到,在执行了 git commit 之后,就已经生成了 commit 记录,那么 git push 又是干什么的呢? 其实 git 的绝大部分操作都是在本地执行的,这也是为什么在断网状态下还是可以进行提交操作。 这也就意味着,上面的所有操作都是在用户本地的 Git 仓库进行的。 要想把对应的记录提交到服务器,并让其他开发者也能拉取到你的修改,那么就必须使用 git push 命令将你的代码提交到服务器上的 git 仓库中。

一个 Git 本地仓库并不仅仅只能对应一个远程仓库。 在使用开源组件的时候,有时就会发现对应组件的 bug。 给对应的 github 仓库提个 issue 也很难被立刻解决。 既然代码就在这,不如自己动手,丰衣足食。 首先就产生了第一个问题:这个仓库你没有权限,没法直接提交代码。 这种时候,我们就会将这个库 fork 到自己的仓库内,再 clone 到本地进行开发。 有可能这个 bug 太棘手,也有可能最近的工作特别忙,过了两三周,问题还没解决。 这时,你 fork 的这个库,更新bug 解决了! 当然,你可以操作你 fork 的库,将新的代码合过来。 但假如当时的需求是基于这个开源库二次开发,频繁的这种操作就会让你很头疼。 这时候,在本地将这个开源库添加为另一个远程库就方便了很多。 使用这个命令:

git remote add 仓库别名 仓库地址
1

就可以将一个其他的远程仓库关联到你的本地仓库中。 添加完远程仓库后,常用的命令还有以下几个:

# 查看目前已关联的远程仓库
git remote -v
# 下载远程仓库的代码
# 注:下载代码并不会对当前分支产生任何影响,如果需要同步最新的代码,还需要配合合并代码的操作
git fetch 仓库别名
1
2
3
4
5

拉取完远端的代码后,就要将新的代码合并到当前的开发分支了。 这时就有两种合并代码的方式……

# merge VS rebase

将其他分支的代码合并到自己的开发分支上,有两种方法,git merge 和 git rebase。 这两种方法差别比较大,一般我们会根据不同场景使用不同的合并方法。

merge 会产生一条 commit message,在看分支图的时候会产生两条分支合并成一条的图形,比较丑,这算是唯一的缺点。 (最近发现了另一个缺点:在 merge request 里解决冲突不会体现在文件的修改记录内。 所以不恰当的解决冲突可能会对代码产生某些无法简简单单恢复的修改) 优点就是,有对应的 commit message,那么就能很方便的执行回滚操作(在某些特殊情况下,虽然不方便但也能回滚)。

而 rebase 则是变基操作,会将目标分支作为基准分支,将你提前于目标分支的 commit 都移动到目标分支的 HEAD 之前。 这种合并方式,对应的分支图是一根直线,看起来会更好看。 (说句题外话,有可能会有人觉得一根直线仅仅是为了满足强迫症。 但如果你有参与维护过多人开发的仓库,动辄密密麻麻十几条的分支、交错的几条往 master 合的分支看起来确实不太方便) 但缺点有这么几个:

第一,这个操作将其他分支的 commit 合到了自己的分支并移动到了自己的 commit 之下。 所以假如在 rebase 操作前,当前分支已经推送到过远端的话,那么 rebase 之后的分支会和当前分支冲突。 这时直接提交会产生一条 merge commit,而这条 merge commit 并不是我们的本意,所以需要在 git push 时添加 -f 参数解决。 但是 -f 参数需要慎用,可能会造成无法挽回的错误。 由此可以引申出,多人开发的分支千万不要用 rebase 来拉取其他分支的代码。

第二,rebase 之后很难反悔。 由于是将其他分支作为基础提交拉到了当前分支下。 所以假如想要反悔,则需要把当前分支的 commit 用 cherry pick 摘出来,才能够剥离出合并之前的分支,特别繁琐。 所以在进行重大 feature 合并到 master 操作时,还是建议使用 merge,方便回滚。

# rebase

git rebase 其实还有其他的妙用,这里仅介绍其中两种: 更改 commit message 和合并多条 commit。

git rebase 有一个 -i 参数。 添加 -i 参数后,后面需要再跟一个可以找到一条 commit 的信息。 这个参数常用的有两类,一类是 HEAD~数字,这个参数是指从 HEAD 往下数第 n 条 commit。 另一类是 commit 的 hash,这种参数在不方便数 commit 的个数时会更方便。 比如:

git rebase -i HEAD~4
git rebase -i e8d5ca...
1
2

输入这个命令并回车后,会进入到一个编辑 commit 的界面:

pick 29e36af2 commit message
pick db75a496 commit message
pick a0dcb940 commit message

# Rebase 操作 commit 范围的信息。下面还有对 hash 之前的单词的解释,可以先了解对应参数的含义。
#
# 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 <commit> = like "squash", but discard this commit's log message
1
2
3
4
5
6
7
8
9
10
11
12

编辑 commit message,通常用到的是 r 命令,执行方法就是,在上一个界面中,将想要修改的 commit 前的 “pick” 改为 “r”,并修改对应 commit 后面的 message,保存就可以了。 合并 commit 则是将 “pick” 改为 “s”,注意第一条必须不是 s 。保存后会进入 commit message 合并编辑界面,编辑后保存,对应的 commit 就被合并到前一条 commit 里了。

# 尾声

这篇文章有些杂乱,介绍了一些可能并不是特别常用的命令,希望可以抛砖引玉,能够让大家继续挖掘 git 中稍微冷门,却很强大的功能。