Git 和 GitFolw 的一些总结

总结一下 Git 日常开发使用常用指令,以及一个常见的 GitFlow 实践模型。

工具准备 - Chocolatey

因为日常主要在 Windows 平台做开发,本文又设计了一些软件工具,故而先行引入一个 Windows 平台的包管理工具,Chocolatey,使用Powershell运行以下脚本即可成功安装 Chocolatey 的开源免费版本:

1
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

之后用的的软件工具都会直接以 Chocolatey 的方式安装,当然也可以Google搜索直接下载安装软件包。
没装 Git 环境的可以直接使用 Chocolatey 安装:

1
choco install -y git

Git 常用命令

一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

新建代码库

1
2
3
4
5
6
7
8
# 在当前目录新建一个Git代码库
$ git init

# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]

# 下载一个项目和它的整个代码历史
$ git clone [url]

配置

Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

1
2
3
4
5
6
7
8
9
# 显示当前的Git配置
$ git config --list

# 编辑Git配置文件
$ git config -e [--global]

# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

增加/删除文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

代码提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 提交暂存区到仓库区
$ git commit -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

分支

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
39
40
41
42
# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

标签

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
# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

查看信息

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

远程同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

# 上传本地指定分支到远程仓库
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

撤销

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

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

其他

1
2
# 生成一个可供发布的压缩包
$ git archive

GitFlow(Git分支管理策略)


本文描述的是 Vincent Driessen 提出的一种比较成功的分支管理模型,非常适合借鉴甚至直接拿来使用。

两条主要分支

主分支 master

首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。
Git版本库初始化以后,自动建立的默认分支叫做 master,我们通常把它作为主分支。

开发分支 develop

主分支只用来分布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做 develop。
这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在 master 分支上,对 develop 分支进行”合并”(merge)。
由 master 分支创建 develop 分支:

1
git checkout -b develop master

将 develop 分支发布到 master分支:

1
2
3
4
5
# 切换到 master 分支
git checkout master

# 对 develop 分支进行合并
git merge --no-ff develop

这里稍微解释一下,上一条命令中的–no-ff参数是什么意思。
默认情况下,Git执行”快进式合并”(fast-farward merge),会直接将 master 分支指向 develop 分支。

使用 –no-ff 参数后,会在合并时在 master 分支上生成一个新节点。

为了保证版本演进的清晰,我们希望采用这种做法。
关于合并的更多解释,请参考Benjamin Sandofsky的《Understanding the Git Workflow》。

三条辅助分支

在主要分支 master 和 develop 之外,我们的开发模型使用各种辅助分支来帮助团队成员之间的并行开发,简化特性的跟踪,为生产版本做准备,并协助快速解决生产问题。
临时性分支主要有三种:

  • 特性(feature)分支
  • 预发布(release)分支
  • 热修复(hotfix)分支

与主要分支不同,这些辅助分支总是有一个有限的生命期,使用完以后,应该马上删除,使得代码库的常设分支始终只有 master 和develop。

特性分支(feature)

特性分支是为了开发某种特定功能,从 develop 分支上面分离出来,开发完成后,要再并入 develop。

特性分支(主题分支/功能分支)用于开发新的功能。
当开始开发一个功能时,包含这个功能的正式版本发布时间是不确定的。
特性分支的本质是,只要特性还在开发中,它就一直存在,最终会被合并回到 develop 中或者被删除。
特性分支的名字,可以采用feature-*的形式命名。

创建

创建一个特性分支:

1
2
# 由 develop 分支 检出新的特性分支 "feature-x"
$ git checkout -b feature-x develop

完成

开发完成后,将特性分支合并到develop分支:

1
2
3
4
# 切换到分支 'develop'
$ git checkout develop
# 非直进式合并
$ git merge --no-ff feature-x

Tips

删除特性分支

1
2
3
4
5
# 删除分支
$ git branch -d feature-x
Deleted branch feature-x (was 05e9557).
# 推送源码到服务器
$ git push origin develop

– no-ff 标志使 merge 即使可以通过快进执行 merge也始终创建一个新的提交对象。 这样可以避免丢失关于特性分支和特性组合在一起的历史存在的信息,这些特性组合在一起添加了特性。

直进式合并下不可能从 Git 历史记录中看到哪些提交对象一起实现了某个特性,如果一定需要,那就必须手动读取所有的日志消息,还原整个特性(即一组提交)是一件真正令人头疼的事情,而如果使用 – no-ff 标志,则很容易实现。
虽然它会创建更额外的空的提交对象,但是收益比成本大得多。

预发布分支(release)

预发布分支用来完成正式版本发布的准备工作。 可以在预发布分支上修复较小的错误,并为发行版准备元数据(版本号、构建日期等)。
当 develop (几乎)反映了新版本的期望状态,即针对即将构建的发行版的所有特性都已经合并到 develop 时,从 develop 分支检出一个预发布分支。
从发布分支的开始,即将发布的版本才会被分配一个版本号。
在此之前,开发分支反映了“下一个版本”的变化,但是在发布分支启动之前,还不清楚“下一个版本”最终会变成0.3还是1.0。
这个决定是在发布分支的开始时做出的,并根据项目中关于版本号碰撞的规则执行。
预发布分支是 develop 分支上面分出来的,预发布结束以后,必须合并进 develop 和 master分支。
它的命名,可以采用release-*的形式。

创建

假设1.1.5版本是当前的产品版本,我们即将发布一个大的版本。
开发的状态已经为“下一个版本”做好了准备,我们已经决定这将成为1.2版本(而不是1.1.6或2.0)。
PS:假设存在脚本 bump-version.sh 接收一个版本号参数,功能是修改项目的版本号相关文件以切换最终发布产品的版本。

1
2
3
4
5
6
7
8
9
# 由 develop 分支 检出新的预发布分支 "release-1.2"
$ git checkout -b release-1.2 develop
# 版本升级到 1.2
$ ./bump-version.sh 1.2
# 提交
$ git commit -a -m "Bumped version number to 1.2"

[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

预发布分支存在的时间区间为:从完成下一个版本所有特性开发,到正式发布版本。
在此期间,可以在这个分支(而不是在 develop 分支)中提交 bug 修复。
这里严禁添加大型的新特性,大型新特性必须合并到 develop,已经在预发布阶段的情况下,新的大型新特性最好归入下一个版本。

完成

当发布分支的状态准备好成为真正的发布时,需要执行一些操作。
首先,将发布分支合并到 master 中(根据定义,master 上的每个提交都是一个新版本,请牢记这一点)。
接下来,必须对 master 上的提交进行标记(tip),以便将来参考这个历史版本。
最后,需要将在发行版分支上所做的更改合并回到 develop 中,以便将来的发行版也包含这些错误修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 切换到分支 'master'
$ git checkout master
# 非直进式合并
$ git merge --no-ff release-1.2
(Summary of changes)
# 打上标记
$ git tag -a 1.2

# 切换到分支 'develop'
$ git checkout develop
# 将预发布分支合并回分支 'develop'
$ git merge --no-ff release-1.2
(Summary of changes)

这一步很可能导致合并冲突,毕竟我们已经更改了版本号。 需要修复并提交。
到这里我们完成了预发布流程,发布分支可以直接删除:

1
2
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

热修复分支(hotfix)

热修复分支非常类似于预发布分支,因为它们也意味着为新的产品发布做准备,尽管是计划外的。
它们产生于生产版本发现的问题有立即采取行动的必要性时。
当生产版本中的关键错误必须立即解决时,从标记生产版本的主分支上的相应标记分检出新的热修复分支。
通常修复不会涉及所有团队成员,团队成员在开发分支上的工作可以继续,bug 涉及的成员在热修复分支上做快速修复。
修补bug分支是从 master 分支上面分出来的。修补结束以后,再合并进 master 和 develop 分支。
它的命名,可以采用hotfix-*的形式。

创建

假设版本1.2是当前正在运行的生产版本,并且由于一个严重的 bug 而导致了问题。
但是 develop 分支上的变化仍然是不稳定的。
这时我们可以由 master 分支检出一个 hotfix 分支并开始修复问题:
** 热修复通常意味着需要发布生产版本,所以同样需要更改版本号 **

1
2
3
4
5
6
7
8
# 检出热修复分支 "hotfix-1.2.1"
$ git checkout -b hotfix-1.2.1 master
# 版本升级到 1.2.1
$ ./bump-version.sh 1.2.1
# 提交
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

接下来,经过紧锣密鼓的排查,我们找到了错误点并做出了修复:

1
2
3
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

完成

当修复完成时,bug 修复合并回 master 中的同时也需要合并回 develop 中,以保证 bug 修复也包含在下一个版本中。
这与发布分支的完成方式完全相似。

首先,更新主版本并标记发布版本。

1
2
3
4
5
6
7
8
# 切换到分支 'master'
$ git checkout master
# 非直进式合并
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
# 打上标记
$ git tag -a 1.2.1

接下来,更新开发版本:

1
2
3
4
5
6
# 切换到分支 'develop'
$ git checkout develop
# 非直进式合并
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

这里还存在一种特殊情况,就是当预发布分支存在时,热修复应当合并到热修复分支而不是开发分支

最后,删除临时分支:

1
2
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

注意事项

1. 使用非直进式合并(git merge –no-ff)

默认情况下,Git执行”快进式合并”(fast-farward merge),会直接将 master 分支指向 develop 分支。

使用 –no-ff 参数后,会在合并时在 master 分支上生成一个新节点。

非直进式合并始终创建一个新的提交对象。 这样可以避免丢失关于特性分支和特性组合在一起的历史存在的信息,这些特性组合在一起添加了特性。
直进式合并下不可能从 Git 历史记录中看到哪些提交对象一起实现了某个特性,如果一定需要,那就必须手动读取所有的日志消息,还原整个特性(即一组提交)是一件真正令人头疼的事情,而如果使用 –no-ff 标志,则很容易实现。
虽然它会创建更额外的空的提交对象,但是收益比成本大得多。
为了保证版本演进的清晰,我们希望采用这种做法。

Tips:可以通过修改全局配置实现

1
2
3
4
# pull操作时禁止 --no-ff
git config --global pull.ff only
# merge操作时配置为 --no-ff
git config --global merge.ff false

2. 使用变基式拉取(git pull –rebase)

多人同时在同一分支开发的情况下,推荐在拉取代码时使用 –rebase,
这是因为默认的 git pull 操作相当于git fetch + git merge FETCH_HEAD,会经过一次合并,生成一个新的节点,之前的提交分开显示。
而 git pull –rebase 则相当于 git fetch + git rebase FETCH_HEAD,不会生成新的节点,是将两个分支融合成一个线性的提交。
想要更好的提交树,使用rebase操作会更好一点。
这样可以线性的看到每一次提交,并且没有增加提交节点。

merge 操作遇到冲突的时候,当前merge不能继续进行下去。手动修改冲突内容后,add 修改,commit 就可以了。

而rebase 操作的话,会中断rebase,同时会提示去解决冲突。
解决冲突后,将修改add后执行git rebase –continue继续操作,或者git rebase –skip忽略冲突。

Tips: 可以配置为总是使用 pull-rebase

1
2
# 配置 pull 操作时总是使用 -rebase
git config --global pull.rebase true

常用工具

TortoiseGit

TortoiseGit 是一个提供给 Git的 Windows Shell 界面程序,基于 TortoiseSVN。它是开源的,可以完全免费使用。

界面






……

安装

TortoiseGit-2.9.0.0-64bit.msi
TortoiseGit-LanguagePack-2.9.0.0-64bit-zh_CN.msi

1
choco install -y TortoiseGit

Sourcetree

https://www.sourcetreeapp.com/
Sourcetree 是一个针对 Windows 和 Mac 的免费 Git 客户端,简化了与 Git 仓库的交互,这样你就可以专注于编码。
通过 Sourcetree 的 Git GUI 可以可视化和管理存储库。

界面



安装

SourceTreeSetup-3.3.6.exe

1
choco install -y SourceTree

参考文献

[1]Vincent Driessen.A successful Git branching model[OL].nvie.com,2010.
[2]阮一峰.Git 工作流程[OL].www.ruanyifeng.com,2015.
[3]阮一峰.Git分支管理策略[OL].www.ruanyifeng.com,2012.