00-前言

Git 介绍

简单的说,git 是一个版本管理软件,你可能听过 svn、cvs等,那都是老黄历了,不用去了解,更不要去学习。绝大数的 linux 内核维护工作很多时候都在处理代码的补丁、文件的归档等等很琐碎的事务上,在 2002 年的时候才使用 BitKeeper 管理软件,但是到 2005 年的时候,开发 BitKeeper 软件的公司和 linux 内核开源社区的合作关系结束了,收回了 linux 内核社区免费使用 BitKeeper 的权利。

linux 的缔造者林纳斯托瓦尔兹这就不爽了,不到 2 周的时间内自己搞了个版本管理系统,由于他自己深厚的功力和对 linux 的文件系统的设计经验,搞出来大家都觉得太好用了,他本人把这个版本管理系统成为是 the stupid content tracker,也就是傻瓜式的版本跟踪器(或者是给傻瓜用的版本管理器?),并命名为 git 有点像饭桶的意思,也有人理解成 global information tracker,反正估计他当时非常生气,大神一生气就不得了,世界就被改变了。git 不但改变了代码的管理方式,还改变了程序员协作的方式,更催生了 github 这样的开源社区,软件开发发生了极大的变化,大神就是如此。

Git 技能参考图

01-安装和配置

进入终端后直接输入 git,如果系统提示命令找不到,则说明还没有安装。对于 git 的安装很简单,直接用系统的包管理工具就可以安装了。

1. sudo apt install git (ubuntu)
2. sudo yum install git (centos)
3. brew install git (macos)

有些地方写成 git-core,这是 git 以前的名字,输入 git --version 确认一下 git 的版本。

git 的配置文件是 ~/.gitconfig,可以输入 git config -l 来查看,编辑的话可输入 git config -e --global 来编辑,当然你直接打开 ~/.gitconfig 文件来搞也是一样的,这里的 --global 表示你本机的全局配置,git 也可以单独针对一个项目(工作目录)下单独配置。现在不用了解太多的配置,但是配置你自己的身份是需要的。

[user]
  name = 你的昵称
  email = 你的邮箱

这两行,也可以直接通过命令来配置。

git config --global user.name "你的昵称"
git config --global user.email "你的邮箱"

就这么简单,现在你可以使用 git 来工作了。

最后正式学习之前,强烈建议大家不要去使用 git 相关的图形界面工具来操作,这些工具的菜单,你很难搞清楚它具体执行了什么 git 命令,常常会把仓库搞得一团乱。

02-版本库

版本库的英文名字是 repository,也就是仓库,存放你的劳动成果,还能跟踪你的工作内容的变化,你愿意的话,可以让时间倒流,回退到某个历史时刻。在 git 里新建一个版本库,很 easy,找个目录或者新建一个目录,输入 git init 就 OK 了。

wangbo@wangbo-VirtualBox:~/test/git-demo$ ls -l
total 0
wangbo@wangbo-VirtualBox:~/test/git-demo$ git init
Initialized empty Git repository in /home/wangbo/test/git-demo/.git/

git 提示初始化了一个新的仓库,那么这个目录有什么变化呢? ls -la 看一下,-a 参数表示要看隐藏文件。

wangbo@wangbo-VirtualBox:~/test/git-demo$ ls -la
total 12
drwxrwxr-x 3 wangbo wangbo 4096 12月 21 09:53 .
drwxrwxr-x 7 wangbo wangbo 4096 12月 21 09:53 ..
drwxrwxr-x 7 wangbo wangbo 4096 12月 21 09:53 .git

原来是多了一个 .git 隐藏目录,看看里面的结构,输入 tree .git -L 1。

wangbo@wangbo-VirtualBox:~/test/git-demo$ tree .git -L 1
.git
├── branches
├── config
├── description
├── HEAD
├── hooks
├── info
├── objects
└── refs

5 directories, 3 files

看来我们的历史文件就记录在这些文件夹里面,注意这个 HEAD,就是你当前工作的版本,其实就是你最近更新版本库的那次记录。 有了版本库以后,在这个目录下的文件就自动被管理了吗? 并不是,我们需要明确的提交到版本库让 git 来跟踪它。新建一个 readme.md 文件提交到版本库的操作如下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo learn git > readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git add readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'add readme file'
[master (root-commit) a0ae41e] add readme file
 1 file changed, 1 insertion(+)
 create mode 100644 readme.md

出现了一个码 a0ae41e,我们把这个码叫 hash 码,是这次提交记录的指纹,它指向了这次提交的所有文件的内容。

怎么看我们提交的记录呢? 输入 git log 即可查看到提交记录,不过这个 git log 默认的参数看起来不是很美观,如果配上一些参数就美了,输入下面的命令试试:

git log --graph --pretty='format:%C(red)%d%C(reset) %C(yellow)%h%C(reset) %ar %C(green)%aN%C(reset) %s'

效果如下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git log --graph --pretty='format:%C(red)%d%C(reset) %C(yellow)%h%C(reset) %ar %C(green)%aN%C(reset) %s'
*  (HEAD -> master) a0ae41e 5 minutes ago wangbo add readme file
wangbo@wangbo-VirtualBox:~/test/git-demo$

不过要输入这么长一个命令,可真是费劲,还是把它加到 git 的配置文件 (~/.gitconfig) 里,起一个别名比较方便。

[alias]
  ch = log --pretty=format:'%h-%an, %ar : %s' -10
  lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --
  plog = log --graph --pretty='format:%C(red)%d%C(reset) %C(yellow)%h%C(reset) %ar %C(green)%aN%C(reset) %s'
  tlog = log --stat --since='1 Day Ago' --graph --pretty=oneline --abbrev-commit --date=relative

现在试试 git ch,git lg,git plog, git tlog,不用管这些细节的命令参数,他们都用于格式化 git 提交的日志内容,我自己经常使用的是 git lg,我们再做一次修改提交看看。

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo make changes >> readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git add readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'make changes'
[master 3e8d68a] make changes
 1 file changed, 1 insertion(+)

现在看看 git 日志长啥样? 输入 git lg:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git lg
* 3e8d68a - (HEAD -> master) make changes (38 seconds ago) <wangbo>
* a0ae41e - add readme file (11 minutes ago) <wangbo>

发现多了一个提交记录了,它的 hash 码是 3e8d68a,这个版本库已经有 2 个历史记录了,它记住了我们 2 次修改的内容,以及修改的时间、作者等。小结一下:

  1. git init 通知 git 在这里初始化一个版本库,我要在这里长期干活了,帮我记录劳动成果
  2. git add 命令把工作目录里的文件提交给 git 托管仓库,让它知道要跟踪这个文件的变更
  3. git commit 是给一次历史附上一些说明信息,告诉 git 这次提交记录具体做了什么事情
  4. git log 会显示每个历史干的事情,但是默认参数显示不好看,加了一些别名参数到 git 的配置文件里,以后输入git + 别名就可以啦。

恭喜你,已经学会把文件交给 git 托管跟踪了。 什么? 你现在就想体验一下时光倒流回到第一步,别着急,后面会告诉你的。

03-提交文件和仓库状态

已经学会了提交文件,不过还需要再详细解释一下。git add readme.md 是提交这单个文件,git commit 的 -m 是 --messages 的简写。

命令 git add 支持文件通配符,在 git add 之前,怎么知道哪些文件会被提交呢? 那就需要查看 git 仓库的状态。新建一个文件 echo hello > hello.txt 后,输入 git status 看看:

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo hello > hello.txt
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	hello.txt

nothing added to commit but untracked files present (use "git add" to track)

提示 Untracked files,看来新建的 hello.txt 文件,没有被纳入跟踪库,这个文件的状态叫未跟踪状态,git 提示我们需要使用 git add 来跟踪它。我们知道 readme.md 文件已经被 git 个跟踪了,我们修改这个文件,git 会怎么处理呢? echo make more changes >> readme.md,同样输入 git status 看看:

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo make more changes >> readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   readme.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	hello.txt

no changes added to commit (use "git add" and/or "git commit -a")

发现 git 对待 readme.md 的态度和 hello.txt 不一样,git 识别出 readme.md 这个文件已经被修改了,也提示我们使用 git add 继续操作,如果有很多文件的时候我们挨个 git add 单个文件点累人,因为 . 表示当前目录,输入 git add . 可以添加所有文件,操作后再次输入 git status 看看 git 仓库的状态:

这两个文件目前都提示可以 unstage 了,说明它们目前是 staged 状态,这个文件状态叫暂存状态,Changes to be committed 说明这些 changes 还没有被提交 (commit),输入 git commit 回车,发现系统打开了 nano 让我们填写提交说明,并且下面列出了所有要提交的文件和它的状态。

git commit -m 'xxx' 直接在命令行写简短信息的时候很方便,git commit 回车对于写分段的提交信息更方便,问题是我不希望用 nano 来操作想用 vim 怎么办呢? 直接 ctrl +x 退出 nano 编辑器, 在 ~/.gitconfig 配置中增加:

[core]
  editor = vim

再次 git commit 回车发现这次是 vim 编辑器了。

wq 保存以后,git 提示如下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit
[master d9a13af] say hello and update readme
 2 files changed, 2 insertions(+)
 create mode 100644 hello.txt

再次输入 git status 后发现 git 没有什么可以提交的,也就是当前工作目录里的文件没有变化:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
nothing to commit, working tree clean

当使用 git add . 的时候会暂存所有的文件,但工作中我们有一些文件,比如写代码的时候产生的临时文件,或者工作目录中一些测试数据文件,还有一些 IDE 工具的配置文件,我们并不想提交到 git 仓库中,怎么办呢? git 说可以,你把这些文件放到工作目录的 .gitignore 文件就可以了。我们新建一个 tmp.txt 文件,输入 git status 后:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	tmp.txt

nothing added to commit but untracked files present (use "git add" to track)

git 提示这个文件未跟踪,但其实我们根本就不想把文件纳入版本库管理,也即是这个文件的内容变化历史我们不关心,但是作为工作的一部分,这个文件又必须存在在当前目录里,这时候就可以把它在 .gitignore 文件注册一下,告诉 git 这个文件你就别管了。echo tmp.txt >> .gitignore 后输入 git status 看看:

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo tmp.txt >> .gitignore
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore

nothing added to commit but untracked files present (use "git add" to track)

git 现在提示 .gitignore 没有被跟踪,但是没有再提示 tmp.txt 了,说明 git 已经知道了 tmp.txt 是不需要跟踪的,已经忽略它了。但 .gitignore 这个文件本身我们希望把它纳入跟踪,因为我们以后可能还需要添加更多的忽略文件并频繁的修改它。现在可以放心的执行 git add . 来添加了:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git add .
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'add igno
[master 1e5fee4] add ignore
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore

思考一个问题,如果 tmp.txt 已经被纳入版本库了,怎么告诉 git 不要再跟踪它呢?

wangbo@wangbo-VirtualBox:~/test/git-demo$ git lg
* bba4235 - (HEAD -> master) add tmp (2 seconds ago) <wangbo>
* d9a13af - say hello and update readme (19 minutes ago) <wangbo>
* 3e8d68a - make changes (74 minutes ago) <wangbo>
* a0ae41e - add readme file (84 minutes ago) <wangbo>

这时候 tmp.txt 已经被跟踪了,在 .gitignore 里添加它已经晚了,还要多做一步,告诉 git 先删除它,执行:

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo tmp.txt > .gitignore
wangbo@wangbo-VirtualBox:~/test/git-demo$ git rm tmp.txt --cached
rm 'tmp.txt'
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    tmp.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore

wangbo@wangbo-VirtualBox:~/test/git-demo$ git add .
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'ignore tmp'
[master d5d999f] ignore tmp
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 .gitignore
 delete mode 100644 tmp.txt

我们再次对 tmp.txt 文件进行修改,发现 git 已经对它不感冒了。

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo more changes >> tmp.txt
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
nothing to commit, working tree clean

让 git 删除一个跟踪中的文件,为什么需要 git rm tmp.txt --cached 呢? 这个 --cached 参数的意思是告诉 git 把 tmp.txt 这个文件从版本仓库中拿掉,但是不要删除工作目录中的 tmp.txt 文件,如果执行 git rm tmp.txt,工作目录中的 tmp.txt 也会被删除掉,这不是我们期望的,我们只是希望 git 不要再跟踪它,但这个 tmp.txt 文件在程序运行的时候还是需要保留的。小结一下:

  1. git commit 回车可以更方便的写提交信息,可以在 ~/.gitconfig 中定义要使用的编辑器
  2. git status 用于查看工作目录中文件的状态,修改了内容和执行了 git 命令都可以运行确认一下,git status -s 可以看简洁状态
  3. .gitignore 是用来告诉 git 不要跟踪的文件列表,它其实是一行一个文件申明,并支持 ? * ! 等通配符,当通配符有冲突的时候顺序就很重要
  4. 如果一个文件已经被 git 跟踪了,想要忽略除了在 .gitignore 文件中添加申明外,还需要执行 git rm 文件 --cached,--cached 的意思是在工作目录中,保留文件不要彻底删除它
  5. git add . 可以添加工作目录中所有的文件,不用挨个去添加了,但是不会添加在 .gitignore 中的文件

04-历史日志的细节

开始之前为了方便说明,先安装有个小工具,它是 git 的反写,叫 tig,是一个字符界面的 git 日志工具。可以使用包管理器安装,也可以使用源代码编译安装,地址是 open in new window

安装完成后,在 git 仓库下输入 tig 命令,会显示提交的历史记录,通过方向键或者 vim 的 hjkl 键可以查看每个历史的信息,回车后显示提交的详细信息,q 键退出回到上一层。

首先是提交的指纹 d5d999f34327261749b891acbf06ac9339f90251,这个指纹码是一个 40 位 16 进制数,它是通过 SHA-1 算法得到,全称是 Secure Hash Algorithm 1,意思是安全散列算法1,这种算法是一个单向的函数,根据输出结果不能得到输入内容,但是只要输入内容有一丝一毫的变化,输出结果会不同(后来被证明可能发生碰撞,不过用来做指纹区分没有什么问题)。

在前面章节 git log 加上美化参数,把指纹的显示长度做了缩短,只取了前面的 7 位,因为前面 7 位就可以在仓库中作为唯一的指纹了,使用 git 命令的时就也不用全部输入指纹码。

在命令行中输入 git show 5d999f 和 git show d5d999f34327261749b891acbf06ac9339f90251,是相同的显示结果,多输入几位指纹码也可以,在目前的仓库状态下,输入 git show d5d9 都可以显示,不带指纹码只输入 git show 会显示最后一次提交的信息。

前面提到过 HEAD 就是版本库最后一次的提交,可以通过下面的命令看看 HEAD 的文件构成:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git cat-file commit HEAD
tree 3ecf800bdea3b30b1c7eaf76ef17c9a3d9f09c27
parent bba42353ad195470c311dd27a26248ae0ab5c006
author wangbo <wangbo@develop-developer.com> 1608521565 +0800
committer wangbo <wangbo@develop-developer.com> 1608521565 +0800

ignore tmp

这个 HEAD 的信息包含了文件树 tree 指纹、上一次的指纹 parent、以及提交作者信息,再加上 commit 的文本信息的字符数:

wangbo@wangbo-VirtualBox:~/test/git-demo$ printf "commit %s\0" $(git cat-file commit HEAD | wc -c)
commit 233

合并通过 SHA-1 算法就得到了提交的指纹码,用如下命令验证一下,发现和 git 的提交历史的指纹码相同:

(printf "commit %s\0" $(git cat-file commit HEAD | wc -c); git cat-file commit HEAD) | shasum

其中用到的 tree 指纹就是文件的内容,用 git cat-file -p 查看一下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git cat-file -p 3ecf800bdea3b30b1c7eaf76ef17c9a3d9f09c27
100644 blob 5c9a1f4e0f09d3e74ad8ac4c957364f52763bc81  .gitignore
100644 blob ce013625030ba8dba906f756967f9e9ca394464a  hello.txt
100644 blob be147a1ee013b8a6288fdd64ad748db29ed4ce41  readme.md

原来指纹码就是 git 把文件树的内容、作者信息、提交的信息、上一次提交的指纹码等综合在一起来算出来的,只要有其中一项不同,它的指纹码就不可能相同了。

关于指纹码的知识就到这里,大致了解怎么计算的即可,当需要使用指纹码的时候,通常使用前面 7 位就 OK 了。

05-工作区和暂存区

当前工作目录中的文件就是工作区,在工作区里有文件变化,可以通过 git status 查看状态:

wangbo@wangbo-VirtualBox:~/test/git-demo$ touch main.c
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	main.c

nothing added to commit but untracked files present (use "git add" to track)

通过 git add 后文件就被放到了暂存区,暂存的意思就是等待最终的提交,也就是打入版本库。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git add main.c
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status -s
A  main.c
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   main.c

如果在最终提交之前,希望了解一下暂存区和最后一次本地版本库的内容区别,输入 git diff --cached 后:

--cached 表示暂存区,事实这个这个命令是一个简写,完整的写法是 git diff --cached HEAD,比较暂存区和 HEAD(最后一次提交的版本库) 之间的区别,由于 HEAD 是默认参数,被省略了。

同样的如果我们修改了一个已经被跟踪的文件,输入 git diff HEAD 就可以比较工作区和最后一次版本库的区别,可以简写为 git diff 命令:

把这两个文件都提交以后,它们就进入了版本库 HEAD,提交之前先查看一下 .git 这个目录的存储大小。

wangbo@wangbo-VirtualBox:~/test/git-demo$ du -sh .git
304K	.git

提交之后比较一下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git add .
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'make some changes'
[master 8ef4571] make some changes
 2 files changed, 4 insertions(+)
 create mode 100644 main.c
wangbo@wangbo-VirtualBox:~/test/git-demo$ du -sh .git
328K	.git

其中 git commit -am 是 git add 和 git commit 的合并写法,提交之后比提交整个文件夹比之前多了 24K,我们计算一下当前工作区的文件大小是多大 (排除 .git 文件夹):

wangbo@wangbo-VirtualBox:~/test/git-demo$ du --exclude=".git" -sh
24K	.

原来 .git 在提交的时候,除了记录变更历史信息等操作外,我们有理由怀疑 git 直接把我们的文件进行了一次 backup,不基于内容差异化保存,这导致 git 的工作速度非常快,而且对非文本文件可以工作顺利,缺点就是会增加磁盘的使用,有些版本管理工具基于内容差异化保存,需要去存储文件的差异,工作就非常的慢,而且不能很好的处理非文本文件。

通过本文的小例子,希望你建立了对工作区、暂存区、版本库的概念,我觉得比直接解释那些晦涩的概念好用。

06-暂时性的隐藏文件

你正在编写一个程序,由于文件被修改了,暂时没有办法通过编译,文件启动失败,但你现在就想用没有改动过的代码启动一下程序,仅仅是想马上启动一下再切回来而已,我想你不会去做一次提交然后再倒退到历史版本,或者你不会去把文件 copy 一份单独放在另外一个目录,怎么办? git stash 可以做到,这个命令暂时保存你的修改内容,把工作区切换到和 HEAD 一样的内容,给你一个干净的工作区。

看命令 git stash 意思是小偷,也就是把文件的改动暂时的隐藏起来。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   hello.txt

no changes added to commit (use "git add" and/or "git commit -a")
wangbo@wangbo-VirtualBox:~/test/git-demo$ git stash
Saved working directory and index state WIP on master: 8ef4571 make some changes
wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
nothing to commit, working tree clean

现在看起来 hello.txt 好像并没有改动过一样,当你想恢复修改的时候,执行 git stash apply 后:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git stash apply
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   hello.txt

no changes added to commit (use "git add" and/or "git commit -a")

被小偷偷去的修改内容,又回来了,并且打印了 git status 的显示结果。

其实 git stash 可以保存你每次操作的记录,可以通过 git stash list 查看:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git stash list
stash@{0}: WIP on master: 8ef4571 make some changes
stash@{1}: WIP on master: 8ef4571 make some changes

每一次的记录都可以通过 git stash pop 弹出来,git stash drop 可以丢弃一次记录,git stash apply 和 git stash pop 的区别主要在于:

  1. apply 可以选择要恢复内容的编号,比如 git stash apply 编号,apply 后那个记录还是在 list 列表中
  2. pop 只能恢复最后一次记录的修改内容,并且 pop 之后记录被移除,一直 pop 到列表为空截止

关于 git stash 更多的用法,可以输入 git stash --help 看看。

其实不一定要使用 git stash,因为我们还没有学习 git 分支,所以你遇到文头的情况,可以暂时使用 git stash 先对付着。

07-各种后悔了该怎么办

世上没有后悔药,可是 git 里的后悔药却特别的多,够我们喝一壶了。

  1. 如果已经 commit 了信息,想修改一下怎么办?

已经提交了,想修改 make some changes 这个提交信息,可输入 git commit --amend 回车,使用编辑器输入新的文本信息保存,执行 git lg 看下已经完成了修改。

  1. 文件已经被改的乱七八糟了,想恢复怎么办?

代码写着写着不要想要了,或者太乱了想重新写过,可以执行 git checkout 丢掉对文件的修改。

而 git checkout . 则恢复所有的文件,使用这个命令要小心,它不会二次确认,执行后你的改动就没了,因为改动还从来没有被保存过。如果文件已经被暂存了,执行了不会生效,需要先从把文件从暂存区扯出来,请看下一条。

  1. 文件已经被暂存了,想撤回怎么办?

如果已经对文件执行过 git add 了,但是不想提交了,需要使用 git reset 文件名 来撤回。

要对所有的暂存文件进行操作,不指定文件名执行 git reset HEAD 即可,这个命令只是把暂存的文件状态恢复到工作区的修改状态,不会影响文件的内容。

  1. 已经完成提交了不想要了怎么办?

如果已经提交完成了,还想撤回来,也是使用 git reset 命令,但是需要加上 --soft 参数或者 --hard 参数。比如我们修改了 hello.txt 执行了提交,指纹码是 cf44772,执行 git reset --soft HEAD^ 后:

用 git status 查看文件的状态,发现 hello.txt 文件又重新处于暂存状态,等着再次被提交。

这里的 HEAD 是当前版本, HEAD^ 表示当前版本的上一个版本,上上一个版本可以用 HEAD^^ 来表示,而往上 100 个版本可以表示成 HEAD~100,HEAD^ 和数字也能集合使用,比如 HEAD^~1 表示父提交的上一次提交。

有 --soft 参数,就有 --hard 参数,这两个参数的区别是什么呢? 如果使用 --hard 参数,执行后提交记录上的文件不会处于暂存状态,工作区中的文件内容和重设的指纹码对应的文件内容完全相同,也就是看起来没有文件被修改过,--soft 重置暂存区,而 --hard 重置暂存区和工作区。

  1. 不想要最近的几次提交了怎么办?

不想要最近的几次提交可以使用 git reset 结合 HEAD~数字 来表示,不过往往使用指纹码更方便。

  1. 回到历史又后悔了,还是想回到未来怎么办?

经过上面的步骤,是不是那些 reset 的提交都消失了,我又想回去怎么办? 不可能让我再把代码写一遍吧? 不用,只要被 git 保存的东西都可以找回来,我们依靠 git reset 只需要一个指纹码就可以了。问题是 git log 已经看不到了啊,怎么找到指纹码? 这次得用 git reflog 命令了,你会发现所有的操作记录都存在,找到需要回复的提交记录,复制指纹码,执行 git reset --hard 指纹码后搞定。reflog 的意思是 references log,凡是提交过的都有个小辫子,抓住了就可以 reset 它。

  1. 重设并且提交本次的变动到历史记录中?

git reset 重设到指定的历史记录,就仿佛之后的提交都不存在了,而 git revert 不仅重设工区到指定的历史记录,而且把这次本身的修改也作为一次提交放到版本库历史中。下面是执行 git rever HEAD~2 后的结果:

通过上面几个例子,我们倒退到过去,再回到未来,还有一些复杂的情况没有列出来,不过这些就是典型的后悔药药方子,理解了也能推导处理其它的情况,去抓药吧。

最后,虽然 git reset 好用,但并不是唯一的办法,学习分支后,可以把对应的历史记录创建成新的分支,分支很多版本管理工具都支持,但是 git 的分支创建速度最快,也最有特色。

08-分支合并和典型用法

分支就是暂时从主线代码离开,去专注于开发一个新特性或者解决一个小 bug,git 创建和切换分支都能在瞬间完成,默认的 git 仓库,输入 git branch 可以看到只有一个 master 分支,通常这就是主分支。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch
* master

假设现在需要开发一个打印的新特功能,分支名称为 feature_print,从 master 分支离开去新的分支有几个写法:

  1. git branch feature_print(或 git branch feature_print master ) && git checkout feature_print(或 git switch feature_print)
  2. git checkout -b feature_print(或 git checkout -b feature_print master)
  3. git switch -c feature_print (或 git switch -c feature_print master)

其中 git switch 是新版本的 git 命令,以前的章节讲过 git checkout 用来恢复文件,checkout 本身是签出文件的意思,用来切换分支语义上有点模糊,后连就增加了语义更加明确的 switch 切换,随时都可以使用 git branch 查看分支的情况,前面的 * 表示你当前所在的分支。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch
* feature_print
  master

在新的分支下,可以专注于开发打印的功能了,我们添加一个 print.c 文件提交,从提交日志上看,工作成果是放到了 feature_print 分支上。

如果想知道两个分支的变化,可以使用 git diff 分支1 分支2,比如输入 git diff master feature_print,由于我们当前就在 feature_print 分支上,可以简化输入 git diff master 来和 master 分支相比较。

可以非常清楚的看到了新增的打印相关的代码,假设打印的功能已经开发完成了,我们需要希望在主干分支上合并新的打印功能的代码,以便下一步的开发或是发布,首先切换到主分支:

  1. git switch master
  2. git checkout master
wangbo@wangbo-VirtualBox:~/test/git-demo$ git switch master
Switched to branch 'master'
wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch
  feature_print
* master

git branch 命令清楚的显示当前已经在 master 主干分支了,现在需要将 feature_print 分支的代码合并到 master 分支上,怎么做呢? 有 2 种做法,我们分别来介绍。

第一种: 使用 git merge 指令,默认如果执行 git merge feature_print 就可以,但是这样不会为这次合并单独生成一个 commit 节点,为了以后能清楚的看到分支合并的情况,通常会使用一个参数 --no-ff,也就是使用 git merge --no-ff feature_print 来执行。

合并完成以后,提示消息如下:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git merge --no-ff feature_print
Merge made by the 'recursive' strategy.
 print.c | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 print.c

通过 tig 看看分支时间线的形态,可以清楚的看到了,master 分支合并了 feature_print 分支的情况。

通常很多功能分支或者解决 bug 的分支,我们在合并完成以后,会将它删除,因为它的代码已经被合并到主分支了,以后需要继续开发或者有 bug 的话可以从主分支再次创建单独的分支来解决。使用 git branch -d feature_print 来删除打印功能的分支,使用 tig 查看一下分支时间线的形态,发现feature_print 分支这个名称不会再显示了,但是形态并没有变化,依靠提交的注释能够清楚的知道了合并了 feature_print 分支的代码。

分支的时间线形态非常重要,尤其是我们需要回到历史的时候,实际工作的时候,可能同时有多个分支存在,如果合并后的时间线形态杂乱无章,对我们代码的维护会产生非常大的副作用。

上面是理想的 merge 合并的情况,处于 master 分支和 feature_print 分支的代码完全是新增的包含关系,也就是没有修改到重叠的代码,如果合并feature_print 分支的时候,master 分支上的打印功能也被修改了,那么合并就会产生冲突,而 git merge 会直接合并文件,并且报告冲突的代码情况,这时候需要手动修改文件解决冲突,完成后再次做一次提交。如下图,master 和 feature_print 都同时修改了同一行代码,在 master 分支上查看区别:

执行 git merge --no-ff feature_print 后显示:

提示有冲突尝试自动合并,但是自动合并失败了(如果没有修改到重叠的代码 git 可以自动化的成功合并),需要手动解决文件的差异了,因为 git 并不知道保留哪个分支的代码才是需要保留的,打开文件发现长这个样子,用 === 分割了 2 个分支的代码,使用 <<<< 表示当前工作分支, >>>> 表示了被合并的 feature_print 分支。

我们觉得 master 分支添加的换行和 feature_print 分支的 ok 文本都是需要保留的,修改文件如下:

使用 git status 查看一下仓库的状态:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   print.c

no changes added to commit (use "git add" and/or "git commit -a")

显示 print.c 的文件状态是 both modified,需要暂存提交,执行 git add . && git commit -m 'fix print text' 后使用 tig 查看分支形态:

所以 git merge 的缺点就是有冲突的时候,所有文件都被合并了,没有给用户留机会做出选择,并且单独生成了一个合并的 commit 提交节点(显示 M)。如果希望渐进式的合并,就需要使用第二种方法。

第二种: 使用 git rebase 命令,切换到 master 分支上,执行 git rebase feature_print 合并,如果没有冲突,直接合并成功是最理想的情况,有冲突的情况如下:

git 给了我们三种选择:

  1. 自己解决文件冲突后执行 git rebase --continue 后继续尝试后面的合并
  2. 执行 git rebase --skip 直接跳过后继续尝试合并,回头再来收拾残局
  3. 执行 git rebase --abort 放弃本次合并,代码切换到 master 为合并前的状态

这时候我们可以打开文件 print.c,修复文件冲突,执行下面 2 个命令继续合并:

git add .
git rebase --continue

continue 后 git 会继续尝试合并,如果还有文件冲突,重复执行这个过程,也可以随时 git rebase --abort 放弃,不想立刻解决就执行 git rebase --skip,直到整个合并完成。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git status
rebase in progress; onto 5a8323a
You are currently rebasing branch 'master' on '5a8323a'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
	both modified:   print.c

no changes added to commit (use "git add" and/or "git commit -a")
wangbo@wangbo-VirtualBox:~/test/git-demo$ git add .
wangbo@wangbo-VirtualBox:~/test/git-demo$ git rebase --continue
Applying: add blank line

执行完成以后我们看看分支的时间线形态:

重点来了,我们发现 git rebase 和 git merge 生成的时间线并不相同,merge 保留了提交历史和操作过程,rebase 去掉了合并过程的提交更加简洁,但是合并后的 commit 信息可能需要修改,比如这个例子的 add blank line 已经不能同时表达文本被更新成 ok 的含义了,应该改为 fix print text 可能更合适(取决于你合并到底保留了什么样的代码)。

merge 和 rebase 如何选择呢?

  1. 如果需要一个干净的时间线形态选择 rebase,如果希望渐进式的合并使用 rebase,可能会需要更新一次 commit 信息。
  2. 如果需要保留完整的历史记录使用 merge,有多个文件冲突的时候可能比较棘手,但是 commit 信息都会保留下来。

关于 git 分支还有一些命令需要掌握:

  1. 查看所有为合并的分支 git branch --no-merged
  2. 查看合并过的分时 git branch --merged
  3. 强行删除分支 git branch -D 分支名称
  4. 而 git branch -v 可查看每个分支最后一次提交的情况

特别说明,一般我们不会在 master 分支上直接修改代码,本章节为了简化说明问题(避免太多的分支扰乱你的视线),在 master 上直接修改了代码,第 12 节学习科学的分支开发模型,也就是如何更加合理规范的使用 git 分支。

09-远端仓库设置和提交

到目前为止,我们一直工作在本地仓库,这也是 git 的特色,不需要中央服务器就能完整的使用 git 所有功能,为了和别人协作开发,必须学习关于远程仓库的知识。

在本地 git init 初始化的仓库中,运行 git remote -v,什么也有没有,因为仓库还没有添加远端的站点,先要找一个存放我们代码的远端仓库,去 github.com 或 coding.net/gitee.com 注册一个账号,新建一个仓库取名为 git-demo。

创建以后,由于没有选择任何初始化的文件,github 提示了一些命令行操作。

首选有 https 和 ssh 两种选择,https 会通过用户名和密码来访问仓库,而 ssh 依靠我们的公钥来访问方库,推荐使用 ssh 方式,对 ssh 公钥不清楚的请访问linux 教程-网络操作open in new window 这个章节,如果是新注册的用户,先去用户中心把自己的公钥 ~/.ssh/id_rsa.pub 的内容放上去。

现在可以把这个远程的仓库地址添加到本地了,执行下面的命令:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git remote add origin git@github.com:developdeveloper/git-demo.git
wangbo@wangbo-VirtualBox:~/test/git-demo$ git remote -v
origin	git@github.com:developdeveloper/git-demo.git (fetch)
origin	git@github.com:developdeveloper/git-demo.git (push)

origin 什么意思呢? 它只是这个远端仓库地址的别名而已,你可以取其他的名称,比如可以改成 github:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git remote rename origin github
wangbo@wangbo-VirtualBox:~/test/git-demo$ git remote -v
github	git@github.com:developdeveloper/git-demo.git (fetch)
github	git@github.com:developdeveloper/git-demo.git (push)

为了习惯呢,还是改回 origin 来讲解,远端仓库地址有了,接下来可以使用 git push 命令把我们本地的仓库推送到远端了,这样别人就能看到了。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git push origin master
......
Enumerating objects: 28, done.
Counting objects: 100% (28/28), done.
Compressing objects: 100% (20/20), done.
Writing objects: 100% (28/28), 2.60 KiB | 444.00 KiB/s, done.
Total 28 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
To github.com:developdeveloper/git-demo.git
 * [new branch]      master -> master

刷新一下网页发现我们的仓库内容已经同步到了 github 的仓库,有了远端仓库被人怎么使用呢? 首选需要把仓库 clone 成本地仓库:

git clone git@github.com:developdeveloper/git-demo.git // 工作区名称为 git-demo
或
git clone git@github.com:developdeveloper/git-demo.git learn-git // 工作区名称为 learn-git

现在来看看分支的情况,这次需要加一个 a 参数:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch -a
  feature_print
* master
  remotes/origin/master

发现多了一个远程分支 remotes/origin/master。

而在 clone 的仓库 learn-git 里执行 git branch -a 结果如下:

wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

这里并没有 feature_print 分支,因为 feature_print 分支我们并没有推送到远端,我们可以把它也推上去,执行:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git push origin feature_print
......
To github.com:developdeveloper/git-demo.git
 * [new branch]      feature_print -> feature_print
wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch -a
  feature_print
* master
  remotes/origin/feature_print
  remotes/origin/master

多了一个远端的分支 remotes/origin/feature_print,切换到 learn-git 工作区,因为分支并不能自动的同步,我们需要执行 git fetch 同步命令才行。

wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
wangbo@wangbo-VirtualBox:~/test/learn-git$ git fetch
Warning: Permanently added the RSA host key for IP address '13.250.177.223' to the list of known hosts.
From github.com:developdeveloper/git-demo
 * [new branch]      feature_print -> origin/feature_print
wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature_print
  remotes/origin/master

怎么获得一个对应的本地 feature_print 分支呢?

wangbo@wangbo-VirtualBox:~/test/learn-git$ git switch feature_print
Branch 'feature_print' set up to track remote branch 'feature_print' from 'origin'.
Switched to a new branch 'feature_print'
wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch -a
* feature_print
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature_print
  remotes/origin/master

你也可以使用 git checkout 来操作:

git checkout -b feature_print
或
git checkout -b feature_print origin/feature_print

现在克隆的仓库也有本地的 feature_print 分支了,需要更新 feature_print 上的代码,可以直接输入 git pull,但这个命令其实分为 2 个步骤,第一步是 git fetch 更新远端的代码到本地 remotes/origin/feature_print 分支上,第二步是和本地 remotes/origin/feature_print 分支进行 merge 合并操作,也可以通过 git pull --rebase 指定为 rebase 合并操作,对 merge 和 rebase 不清楚的请参考前面分支合并的章节内容。

现在 git-demo 工作区更新一下 readme.md 文件并推送到远端。

wangbo@wangbo-VirtualBox:~/test/git-demo$ echo add remote info >> readme.md
wangbo@wangbo-VirtualBox:~/test/git-demo$ git add .
wangbo@wangbo-VirtualBox:~/test/git-demo$ git commit -m 'add info'
[feature_print 07f8e9a] add info
 1 file changed, 1 insertion(+)
wangbo@wangbo-VirtualBox:~/test/git-demo$ git push origin feature_print
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 286 bytes | 286.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:developdeveloper/git-demo.git
   5a8323a..07f8e9a  feature_print -> feature_print

切换回 learn-git 工作区,确保你在 feature_print 分支下,执行 git pull --rebase:

wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch
* feature_print
  master
wangbo@wangbo-VirtualBox:~/test/learn-git$ git pull --rebase
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
Unpacking objects: 100% (3/3), 266 bytes | 266.00 KiB/s, done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
From github.com:developdeveloper/git-demo
   5a8323a..07f8e9a  feature_print -> origin/feature_print
Updating 5a8323a..07f8e9a
Fast-forward
 readme.md | 1 +
 1 file changed, 1 insertion(+)
Current branch feature_print is up to date.

你已经获得了最新的代码,查看 readme.md 文件确认一下:

wangbo@wangbo-VirtualBox:~/test/learn-git$ cat readme.md
learn git
make changes
add remote info

你已经学会了基本的本地和远端同步内容的方法。关于远端仓库还有一些补充命令:

  1. git remote show origin 查看 origin 远端的信息
  2. git remote rm origin 可以删除 origin 远端
  3. git fetch origin 可以更新 origin 远端的内容
  4. git fetch -p 或 git fetch origin -p 如果远端的分支不存在了,本地也删除对应的分支
  5. git push origin master 同步到远端是 git push origin master:master 的简写,冒号后面表示远端的分支
  6. git push origin --delete feature_print 和 git push origin :feature_print 都可以删除远端的分支,后者理解成把 nothing 推向到远端
  7. git push -f origin master 可以强行用本地分支内容覆盖远端的分支内容,因为默认本地分支需要同步远端内容后才能推送到远端
  8. git remote set-url origin git@github.com:zhangsan/abc.git 修改远端仓库的 url 地址

注意,远端仓库的操作非常重要,下个章节继续学习远端仓库的同步操作。

10-本地仓库和远端仓库同步

git fetch 会抓取或者克隆远端推送的新内容,它把数据下载到本地仓库里,但是不会自动合并或者修改你当前分支的内容。抓取以后我们可以查看 、比较远端分支的代码,或者从远端分支的内容创建一个新的分支。

  1. git diff origin/master
  2. git checkout origin/master
  3. git checkout -b review_branch origin/master 或者 git switch -c review_branch origin/master

git pull 只有设置了跟踪远程分支才有效,git clone 默认设置本地 master 分支跟踪远程仓库的 master 分支,先执行 git fetch,再执行 git merge 或者 git rebase 是相同的效果。怎么查看本地分支和远程分支的跟踪关系呢? 输入 git branch -vv 看看:

wangbo@wangbo-VirtualBox:~/test/learn-git$ git branch -vv
* feature_print 07f8e9a [origin/feature_print] add info
  master        40b2935 [origin/master] add blank line

所以当我们在 feature_print 分支的时候,执行 git pull 会去拉取 origin/feature_print 的分支内容,然后两者进行合并,在 master 的时候会拉取 origin/master 来合并。如果你不想着合并代码,就先执行 git fetch origin 更新后,看看上面的提交信息和文件内容,确定后自行合并。

git push 的时候也遵循这个远程跟踪分支的设定,所以 push 的时候往往没有写远端的分支名称,可以通过命令手动设置跟踪的分支,push 的时候增加 -u 参数也是 --set-upstream 的意思。

git branch --set-upstream 本地分支名 origin/远程分支名

执行 git pull --help 可以看到文档解释第二部是操作 git merge FETCH_HEAD,这个 FETCH_HEAD 是什么呢? 它是 .git 文件夹下的一个文件,看看它的内容:

wangbo@wangbo-VirtualBox:~/test/learn-git$ cat .git/FETCH_HEAD
07f8e9a6adc9dc61feac4cb96e7e9f427cebeba3		branch 'feature_print' of github.com:developdeveloper/git-demo
40b293536dddd0fd2354a9dbfe11bb78ae44a8c9	not-for-merge	branch 'master' of github.com:developdeveloper/git-demo

切换到本地的 master 分支,再做一次 git fetch origin,再次查看这个文件的内容:

wangbo@wangbo-VirtualBox:~/test/learn-git$ git switch master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
wangbo@wangbo-VirtualBox:~/test/learn-git$ git fetch origin
wangbo@wangbo-VirtualBox:~/test/learn-git$ cat .git/FETCH_HEAD
40b293536dddd0fd2354a9dbfe11bb78ae44a8c9		branch 'master' of github.com:developdeveloper/git-demo
07f8e9a6adc9dc61feac4cb96e7e9f427cebeba3	not-for-merge	branch 'feature_print' of github.com:developdeveloper/git-demo

看来 FETCH_HEAD 的意思是最近 fetch 的那个提交点,它是一个引用。

希望你大概理解了 pull 和 push 的作用和原理了。

11-打上标签和同步标签

在 git 里打标签非常的简单,合并代码测试后,执行 git tag 标签名 就行了。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git tag v1.0
wangbo@wangbo-VirtualBox:~/test/git-demo$ git tag
v1.0

默认 git tag 是对最新的提交打标签,可以显示的指定指纹码,比如要对 5a8323a 打个标签 v1.0-beta 可以执行:

wangbo@wangbo-VirtualBox:~/test/git-demo$ git tag v1.0-beta 5a8323a
wangbo@wangbo-VirtualBox:~/test/git-demo$ git tag
v1.0
v1.0-beta

有了 tag 有什么用呢? 第一个作用是显示的表示代码的里程碑特征,第二个作用是用于部署发布的时候,可以通过 tag 标签来指定代码的内容,为了详细的说明 tag 可以创建 tag 的时候写上说明。

git tag -a v1.0 -m "Version 1.0 here" 指纹码 执行 git tag 查看的时候,是按字母排序,可以使用 git show tag名称 来查看标签的信息。

使用 git checkout -b 分支名 标签名git switch -c 分支名 标签名 可以从标签创建新的分支。

wangbo@wangbo-VirtualBox:~/test/git-demo$ git switch -c v1_branch v1.0
Switched to a new branch 'v1_branch'
wangbo@wangbo-VirtualBox:~/test/git-demo$ git branch
  feature_print
  master
* v1_branch

使用标签需要注意的是,标签不会随着 git push 被推送到远端仓库,需要显示的推送一次。

git push origin v1.0 // 推送 v1.0 到 origin 远端仓库
git push origin --tags // 推送全部的 tags 到 origin 远端仓库

不再需要的标签可以使用 git tag -d tag名称 来删除,本地删除的 tag 标签,也不能在远端仓库自行删除,需要显示的推送删除。

git push origin --delete v1.0 // 删除远端仓库的标签
git push origin :refs/tags/v1.0 // 把 nothing 推送到远端仓库

12-多人协作开发和 PR 模型

由于 git 的分支非常的方便,因为 git 互相协作的方式也比较多,但是典型分支协作模型如下图,很多都是它的变种,所以需要理解下面的这张图,这张图的信息如下:

  1. 对于分支的规划: a) feature 分支,可发新功能的分支,通常是从 develop 分支签出来的 b) develop 分支,集合了所有的开发功能的分支,大部分时间它上面的代码是不稳定的 c) release 分支,经过测试了可以发布部署的分支代码 d) hotfix 分支,用于修复线上 bug 的分支,从 master 分支签出来
  1. 几个典型的事件: a) 最开始的时候没有 develop 分支,这时候从 master 分支签出了 develop 分支,develop 包含了目前所有的功能代码 b) 从 develop 分支签出的 feature 分支都合并会 develop 分支,开发到一定程度的时候,签出 release 分支 c) 如果 release 分支创建了,它之后只承担修复 bug 的作用,不再开发新功能,并且修复的 bug 也会合并回 develop 分支,防止 develop 分支以后的代码重现 bug d) 当 release 分支测试稳定后,就合并到 master 分支并打上 tag 标签 e) 处于 master 的分支通常足够稳定,但是出现了 bug 就会签出 hotfix 分支来修复bug,重点是修复完成后 hotfix 分支必须同时合并到 master 和 develop 分支

针对上面的分支模型,抽象出了 git-flow 工具链,安装参考 https://github.com/nvie/gitflow/wiki/Installationopen in new window

开发的大致使用流程如下:

  1. git flow init 初始化一个仓库,一般使用默认的分支名称比较清晰 (会创建 master 和 develop)
  2. git flow feature start abc 开始开发一个 abc 功能的分支 (会基于 develop 创建 feature/abc)
  3. git flow feature finish abc 提交开发完成的 abc 功能 (会把 feature/abc 合并到 develop 并删除 feature/abc)
  4. git flow release start v1.0 创建一个预发布的分支 (会从 develop 创建 release/v1.0)
  5. git flow release finish v1.0 创建正式的发布分支和 tag 标签 (会把 release/v1.0 合并到 master 并打上标签 v1.0,还会删除 release/v1.0)

修复 bug 的大致流程如下:

  1. git flow hotfix start abc 从 master 创建修复 bug 的分支 (会创建 hotfix/abc 分支)
  2. git flow hotfix finish abc 完成了 bug 的修复 (会把 hotfix/abc 合并到 master 递增 tag,还会合并到 develop 并删除 hotfix/abc 分支)

实际使用中还有一种特殊情况: 如果线上 bug 出现,创建 hotfix 的时候 release 分支处于预发布状态,根据实际情况考虑是在当前发布版本修复(hotfix 同时合并到 develop、release 分支)还是以后修复(只需合并到 develop 分支) 这个分支模型非常适用于基于版本的发布,如果要使用的话,团队里的成员都要使用,否则可能把分支搞乱。

最后,你可能经常听到 PR,PR 是 pull request 的简写,他是一种参与开源项目协作的方式。

如果你对一个仓库有权限提交,那么就可以直接发起 PR 或者直接合并代码,否则你需要 fork 仓库,clone 到本地,因为你只有对自己仓库的操作权限,你提交修复代码到分支,并推送到自己的仓库,然后去原仓库地址在线创建一个合并请求,选择你提交的分支,然后可以基于这个 PR 做讨论、测试、改进,直到你的提交被认可,可以主动去邀请一些人来 review 你的提交,最后被合并进主干分支。

13-了解一些奇技淫巧

有一些特殊的命令或者技巧可以参考一下:

  1. 抛弃本地的所有修改,回到远端仓库的状态
git fetch --all && git reset --hard origin/master
  1. 把所有改动都放回工作区,清空所有的 commit 提交,重新提交第一个 commit
git update-ref -d HEAD
  1. 有文件冲突的时候查看一下列表
git diff --name-only --diff-filter=U
  1. 快速的切换到上一个分支(和 cd - 差不多)
git checkout -
或
git switch -
  1. 关联远程分支
git branch -u origin/branch_xx
或
git push -u origin/branch_xx
  1. 重命名本地分支
git branch -m branch_new_name
  1. 显示一个文件每一行的修改记录,看看是谁把代码搞坏了
git blame filename
  1. 查看两周内的改动
git whatchanged --since='2 weeks ago'
  1. 把某个提交直接放到分支上来
git cherry-pick 指纹码
  1. 显示分支1有的 commit 提交但不在分支 2 上
git log branch1 ^branch2
  1. 通过 diff 来获得 patch 文件和应用到别的分支
git diff branch1 > file-name.patch
git checkout branch2
git apply file-name.patch 或 git apply file-name.patch --reject
Last Updated:
Contributors: Bob Wang