Git 的使用

Git 使用规范流程

下面是 ThoughtBot 的 Git 的使用规范流程:

img

第一步:新建分支

首先,每次开发新功能,都应该新建一个单独的分支

1
2
3
4
5
6
# 获取主干最新代码
$ git checkout master
$ git pull

# 新建一个开发分支 myfeature
$ git checkout -b myfeature

分支管理策略

  1. 主分支 Master
  2. 开发分支 Develop

这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在 Master 分支上,对 Develop 分支进行“合并”(merge)。

1
2
3
4
5
6
7
8
9
> # 创建 develop 分支
> git checkout -b develop master
> # 切换至 master 分支
> git checkout master
> # 对 develop 分支进行合并
> git merge --no-ff develop
>
> ## --no-ff 默认情况为“快进式合并”(fast-farward merge),会直接将 master 分支指向 develop 分支;使用 --no-ff 参数会执行正常合并,在 master 分支上生成一个新节点,保证了版本演进的清晰
>
  1. 临时性分支

应对一些特定目的的版本开发,临时性分支主要有:功能分支(feature)、预发布分支(release)、修补 bug(fixbug)。为临时需要,使用完以后应该删除。

  1. 功能分支
1
2
3
4
5
6
7
> git check out -b feature-x develop
>
> git check out develop
> git merge --no-ff feature-x
>
> git branch -d feature-x
>
  1. 预发布分支
1
2
3
4
5
6
7
8
9
> git checkout -b release-1.2 develop
>
> git checkout master
> git merge --no-ff release-1.2
> git tag -a 1.2
> git checkout develop
> git merge --no-ff release-1.2
> git branch -d release-1.2
>
  1. 修补 bug 分支
1
2
3
4
5
6
7
8
9
10
11
> git checkout -b fixbug-0.1 master
>
> git checkout master
> git merge --no-ff fixbug-0.1
> git tag -a 0.1.1
>
> git checkout develop
> git merge --no-ff fixbug-0.1
>
> git branch -d fixbug-0.1
>

第二步:提交分支 commit

分支修改后,就可以提交 commit 了。

1
2
3
$ git add --all
$ git status
$ git git commit --verbose

第三步:撰写提交信息

提交 commit 时,必须给出完整扼要的提交信息,下面是一个范本:

1
2
3
4
5
6
Present-tense summary under 50 characters

* More information about commit (under 72 characters).
* More information about commit (under 72 characters).

http://project.management-system.com/ticket/123

第一行是不超过50个字的提要,然后空一行,罗列出改动原因、主要变动、以及需要注意的问题。最后,提供对应的网址(比如 bug ticket)。

第四步:与主干同步

1
2
$ git fetch origin
$ git rebase origin/master

第五步:合并 commit

分支开发完,很可能有一堆 commit,但是合并到主干的时候,往往希望只有一个(或最多两三个)commit,这样不仅清晰,也容易管理。

1
$ git rebase -i origin/master

git rebase命令的参数i表示互动(interactive),这时 git 会打开一个互动界面,进行下一步操作。下面为 [Tute Costa] 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# 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.
#
# Note that empty commits are commented out

上面的互动界面中,先列出当前分支的最新的4个 commit(越下面越新)。每个 commit 前面有一个操作命令,默认是 pick,表示该行 commit 被选中,要进行 rebase 操作。

下面是可以使用的命令:

  • pick:正常选中
  • reword:选中,并且修改提交信息
  • edit:选中,rebase 时会暂停,允许你修改这个 commit
  • squash:选中,会将当前 commit 与上一个 commit 合并
  • fixup:与 squash 相同,但不会保存当前 commit 的提交信息
  • exec:执行其他 shell 命令

上面这 6 个命令当中,squash 和 fixup 可以用来合并 commit。先把需要合并的 commit 前面的动词,改成 squash (或者 s)。

1
2
3
4
pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

这样一改,执行后,当前分支只会剩下两个 commit。第二行和第三行的 commit,都会合并到第一行的 commit。提交信息会同时包含,这三个 commit 的提交信息。

1
2
3
4
5
6
7
8
9
# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2nd commit message:
Fix PostChecker::Post#urls

# This is the 3rd commit message:
Hey kids, stop all the highlighting

如果将第三行的 squash 命令改成 fixup 命令。

1
2
3
4
pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

运行结果相同,但新的提交信息里面,第三行 commit 的提交信息会被注释掉。

1
2
3
4
5
6
7
8
9
# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2nd commit message:
Fix PostChecker::Post#urls

# This is the 3rd commit message:
# Hey kids, stop all the highlighting

Pony Foo 提出了另一种合并 commit 的简便方法,就是先撤销过去5个 commit,然后再建一个新的。

1
2
3
4
$ git reset HEAD~5
$ git add .
$ git commit -am "Here's the bug fix that closes #28"
$ git push --force

squash 和 fixup 命令,还可以当做命令行参数使用,自动合并 commit

1
2
$ git commit --fixup
$ git rebase -i --autosquash

第六步:推送到远程仓库

1
$ git push --force origin myfeature

git push命令要加上force参数,因为rebase以后,分支历史改变了,跟远程分支不一定兼容,有可能要强行推送。

第七步:发出 Pull Request

提交到远程仓库后,就可以发出 Pull Request 到 master 分支,然后请求别人进行代码 review,确认可以合并到 master。

常用 Git 命令清单

Git 远程操作详解

git clone

1
$ git clone <版本库地址> <本地目录名>

git remote

为了便于管理,Git 要求每个远程主机都必须指定一个主机名,默认为 master。git remote命令就用于管理主机名。

不带选项的时候,git remote命令列出所有远程主机

1
2
$ git remote
origin
1
2
3
4
# 使用`-v`选项,可以参看远程主机的网址。
$ git remote -v
origin git@github.com:jquery/jquery.git (fetch)
origin git@github.com:jquery/jquery.git (push)
1
2
3
4
# 指定远程主机名为 jQuery
$ git clone -o jQuery https://github.com/jquery/jquery.git
$ git remote
jQuery
1
2
# 查看该主机的详细信息
$ git remote show <主机名>
1
2
# 添加远程主机
$ git remote add <主机名> <网址>
1
2
# 删除远程主机
$ git remote rm <主机名>
1
2
# 远程主机的改名
$ git remote rename <原主机名> <新主机名>

git fetch

一旦远程主机的版本有了更新(Git 术语叫做 commit),需要将这些更新取回本地,这时就要用到git fetch命令。

1
$ git fetch <远程主机名>

git fetch命令通常用来查看其他人的进程,因为它取回的代码对你本地的开发代码没有影响。

默认情况下,get fetch取回所有分支(branch)的更新。如果只想取回特定分支的更新,可以指定分支名。

1
$ git fetch <远程主机名> <分支名>

所取回的更新,在本地主机上要用“远程主机名/分支名”的形式读取。比如origin主机的master ,就要用origin/master读取。

git branch命令的-r选项,可以用来查看远程分支,-a选项查看所有分支。

1
2
3
4
5
6
$ git branch -r
origin/master

$ git branch -a
* master
remotes/origin/master

上面命令表示,本机主机的当前分支是master,远程分支是origin/master

取回远程主机的更新以后,可以在它的基础上,使用git checkout命令创建一个新的分支。

1
$ git checkout -b newBranch origin/master

上面命令表示,在origin/master的基础上,创建一个新分支。

此外,也可以使用git merge命令或者git rebase命令,在本地分支上合并远程分支。

1
2
3
$ git merge origin/master
# 或者
$ git rebase origin/master

git pull

git pull作用是取回远程主机某个分支的更新,再与本地的指定分支合并。它的完整格式稍稍有点复杂。

1
2
3
4
$ git pull <远程主机名> <远程分支名>:<本地分支名>

# 与当前分支合并
$ git pull origin next

上面命令表示,取回origin/next分支,再与当前分支合并。实质上,这等同于先做git fetch,再做git merge

1
2
$ git fetch origin
$ git merge origin/next

在某些场合,Git 会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。比如,在git clone的时候,所有本地分支默认与远程主机的同名分支建立追踪关系,也就是说,本地的master分支自动追踪origin/master分支

Git 也允许手动建立追踪关系

1
2
# 指定`master`分支追踪`origin/next`分支
$ git branch --set-upstream master origin/next

如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

1
$ git pull origin

如果当前分支只有一个追踪分支,连远程主机名都可以省略。

1
2
# 自动与唯一一个追踪分支进行合并。
$ git pull

如果合并需要采用 rebase 模式,可以使用--rebase选项。

1
$ git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

如果远程主机删除了某个分支,默认情况下,git pull不会再拉取远程分支的时候,删除对应的本地分支。这是为了防止,由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。

但是,你可以改变这个行为,加上参数-p就会在本地删除远程已经删除的分支。

1
2
3
4
$ git pull -p
# 等同下面的命令
$ git fetch --prune origin
$ git fetch -p

git push

git push命令用于将本地分支的更新推送到远程主机。它的格式与git pull命令相仿。

1
$ git push <远程主机名> <本地分支名>:<远程分支名>
1
2
# 存在追踪关系
$ git push origin master

如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。

1
2
3
4
$ git push origin :master
# 等同于
$ git push origin --delete master
# 表示删除 origin 主机的 master 分支

如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。

1
$ git push origin

如果当前分支只有一个追踪分支,那么主机名都可以省略。

1
$ git push

如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机。这样后面就可以不加任何参数使用git push

1
$ git push -u origin master

不带任何参数的git push,默认只推送当前分支,这叫做 simple 方式。此外,还有一种 matching 方式,会推送所有有对应的远程分支的本地分支。Git 2.0 版本之前,默认采用 matching 方式,现在改为默认采用 simple 方式。如果要修改这个设置,可以采用git config命令。

1
2
3
$ git config --global push.default matching

$ git config --global push.default simple

还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用--all选项。

1
$ git push --all origin

如果远程主机的版本比本地版本更新,推送时 Git 会报错,要求先本地做git pull合并差异,再推送到远程主机。这时,如果你一定要推送,可以使用--force选项。

1
$ git push --force origin

上面命令使用--force选项,结果导致远程主机上更新的版本被覆盖!除非你很确定要这样做,否则应该尽量避免使用--force选项。

最后,git push不会推送标签(tag),除非使用--tags选项。

1
$ git push origin --tags

Git 工作流程

  • git flow
  • github flow
  • gitlab flow

功能驱动

上述三种工作流程,都采用功能驱动式开发,它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。开发完成后,该分支就合并到主分支,然后被删除。

Git flow

特点

img

首先,项目存在两个长期分支:

  • 主分支 master
  • 开发分支 develop

前者用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的发布版;后者用于日常开发,存放最新的开发版。

其次,项目存在三种短期分支:

  • 功能分支 feature branch
  • 补丁分支 hotfix branch
  • 预发分支 release branch

一旦开发完成,他们就会被合并进 develop 或 master,然后被删除。

评价

Git flow 的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。大多数工具都将 master 当做默认分支,可是开发是在 develop 分支进行的,这导致经常要切换分支,非常烦人。

更大的问题在于,这个模式是基于“版本发布”的,目标是一段时间以后产出一个新版本。但是,很多网站项目是“持续发布”,代码一有改动,就部署一次。这时,master 分支 和 develop 分支的差别不大,没必要维护两个长期分支。

Github flow

流程

它只有一个长期分支,就是 master。

img

  1. 根据需求,从 master 拉出新分支,不区分功能分支或补丁分支
  2. 新分支开发完成后,或者需要讨论的时候,就向 master 发起一个pull request(简称 PR)
  3. pull request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码。
  4. 你的pull request被接受,合并进 master,重新部署后,原来你拉出来的那个分支就被删除(先部署再合并也可)

评价

Github flow 的最大优点就是简单,对于“持续发布”的产品,可以说是最合适的流程。

问题在于它的假设:master 分支的更新与产品的发布是一致的。也就是说,master 分支的最新代码,默认就是当前的线上代码。

可是,有些时候并非如此,代码合并进入 master 分支,并不代表它就能立刻发布。比如,苹果商店的 App 提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,master 分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于 master 分支。

上面这种情况,只有 master 一个主分支就不够用了。通常,你不得不在 master 分支以外,另外新建一个 production 分支跟踪线上版本。

Gitlab flow

上游优先

Gitlab flow 的最大原则叫做“上游优先”(upstream first),即只存在一个主分支 master,它是所有其他分支的“上游”。只有上游分支采纳的代码变化,才能应用到其他分支。

持续发布

img

对于“持续发布”的项目,它建议在 master 分支以外,再建立不同的环境分支。比如,“开发环境”的分支是 master,“预发环境”的分支是 pre-production,“生产环境”的分支是 production。

开发分支是预发分支的“上游”,预发分支又是生产分支的“上游”。代码的变化,必须由“上游”向“下游”发展。比如,生产环境出现了 bug,这时就要新建一个功能分支,先把它合并到 master,确认没有问题,再 cherry-pick 到 pre-production,这一步也没有问题,才进入 production。

只有紧急情况,才允许跳过上游,直接合并到下游分支。

版本发布

img

对于“版本发布”的项目,建议的做法是每一个稳定版本,都要从 master 分支拉出一个分支,比如 2-3-stable、2-4-stable 等等。以后,只有修补 bug,才允许将代码合并到这些分支,并且此时要更新小版本号。


参考引用

Git 工作流程