赞
踩
在前面,我们简单地介绍了如何在本地使用 Git 完成不涉及到分支概念的一些基础操作。接下来马上介绍关于分支的概念及其操作,毕竟关于分支和提交的操作就是 Git 中最重要的操作。
Git 最初设计背后的驱动因素之一是支持大型、快速项目的非线性的开发,其特点就是特定开发与主开发线分离。使用分支意味着你可以把你的工作从开发主线(即前面提到的master分支)上分离出来,比如创建一个分支来开发特定的功能或者修改 bug,完成后再合并回去。
多条分支的存在就实现了项目的非线性的开发,因为随着项目规模和复杂性的增长,分支结构模型将极大地帮助管理开发过程。
所有 Git 操作无非就是对 Git对象的操作,Git 对象包括 Blob对象、Tree对象、Commit对象和 Tag对象。
所有 Git 操作无非就是对 Git对象的操作,Git 对象包括 Blob对象、Tree对象、Commit对象和 Tag对象。
Blob 对象是一个二进制大对象,代表各个文件,存储所有文件除元数据之外的内容数据。 Gıt 仓库中以 SHA-1 值来标识 Blob,所以不按文件名寻址而是按内容寻址。
git add filename
之后创建的。Tree 对象是一个二进制文件,代表各个目录,存储对 Blob对象和其它 Tree对象的引用(SHA-1 值),解决文件名的保存问题。Gıt 仓库同样以 SHA-1 值来标识 Tree。
Commit 对象是一个二进制文件,代表各个版本,存储对 Tree对象的引用(SHA-1 值)之外,还存储谁在什么时间以及为什么保存了某些快照等说明信息,解决快照的保存问题。Gıt 仓库同样以 SHA-1 值来标识 Commit。
Tag 对象是一个二进制文件,代表各个版本号,存储对 Commit对象的引用(SHA-1 值)以及指定给它的标签信息,解决 Commit对象的标注问题。 Gıt 仓库同样以 SHA-1 值来标识 Tag。
分支本质上是一个可变指针,是一个有名字的对 Commit对象的引用(SHA-1 值),分支始终指向最新提交,解决的是提交的分离与合并问题。
所以,Git 存储数据的过程大概是这样的:
git add暂存文件后,创建 Blob对象和 Tree对象:
git commit提交暂存区后,创建Commit对象:
3,多次提交后:
4,再打上标签v1.0:
5,分支的产生与切换:
6,在分支中工作:
7,合并分支:
Why GitHub renamed its master branch to main
You can select repository default branch
一个使用分支的实际例子:
正在此时,有个很严重的线上问题需要紧急修补。 你将按照如下方式来处理:
做准备:
dang@DFLubuntu:~$ mkdir git-tutorial dang@DFLubuntu:~$ cd git-tutorial dang@DFLubuntu:~/git-tutorial$ git init 已初始化空的 Git 仓库于 /home/dang/git-tutorial/.git/ dang@DFLubuntu:~/git-tutorial$ echo "111" >> a.txt dang@DFLubuntu:~/git-tutorial$ echo "111" >> b.txt dang@DFLubuntu:~/git-tutorial$ echo "<h1>test</h1>" >> index.html dang@DFLubuntu:~/git-tutorial$ git add . dang@DFLubuntu:~/git-tutorial$ git commit -m "第一次提交" [master (根提交) 0932815] 第一次提交 3 files changed, 3 insertions(+) create mode 100644 a.txt create mode 100644 b.txt create mode 100644 index.html dang@DFLubuntu:~/git-tutorial$ echo "222" >> a.txt dang@DFLubuntu:~/git-tutorial$ echo "222" >> b.txt dang@DFLubuntu:~/git-tutorial$ git add . dang@DFLubuntu:~/git-tutorial$ git commit -m "第二次提交" [master f9021ee] 第二次提交 2 files changed, 2 insertions(+) dang@DFLubuntu:~/git-tutorial$ echo "333" >> a.txt dang@DFLubuntu:~/git-tutorial$ echo "333" >> b.txt dang@DFLubuntu:~/git-tutorial$ git add . dang@DFLubuntu:~/git-tutorial$ git commit -m "第三次提交" [master 8441143] 第三次提交 2 files changed, 2 insertions(+) dang@DFLubuntu:~/git-tutorial$ git tag -a v0.0.1 -m "完成v0.0.1功能开发" 8441143
目前:
在分支中正常工作:假设根据问题追踪系统的 #53 问题提示,我们现在要对 c.txt 文件进行bug修复。
执行 git checkout -b branchname
命令创建并切换到新分支:
dang@DFLubuntu:~/git-tutorial$ git checkout -b iss53
切换到一个新分支 'iss53'
dang@DFLubuntu:~/git-tutorial$
目前:
然后再 iss53 分支中解决日常的 #53 问题:
dang@DFLubuntu:~/git-tutorial$ echo "444" >> b.txt
dang@DFLubuntu:~/git-tutorial$ git add .
dang@DFLubuntu:~/git-tutorial$ git commit -m "在 c.txt 末尾新增一行测试数据 [issue 53]"
[iss53 10d78c8] 在 c.txt 末尾新增一行测试数据 [issue 53]
1 file changed, 1 insertion(+)
dang@DFLubuntu:~/git-tutorial$
目前:
执行 git branch
命令能列出所有分支:
*
表示我们正在 iss53 分支上工作。也能执行 git branch --list 通配
命令列出满足条件的分支的信息:
dang@DFLubuntu:~/git-tutorial$ git branch --list iss*
iss53
dang@DFLubuntu:~/git-tutorial$
也能执行 git branch -vv
命令查看每一个分支的最后一次提交:
dang@DFLubuntu:~/git-tutorial$ git branch -vv
* iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53]
master 5ec814b 在b.txt中添加数据以产生合并冲突
dang@DFLubuntu:~/git-tutorial$
也能执行 git branch -r --contains commit_SHA-1_value --all
命令查看某个commit属于哪个分支。
也能执行 git log --graph --oneline --all --decorate
命令形象化列出分支关系:
dang@DFLubuntu:~/git-tutorial$ git lgd
* 10d78c8 (HEAD -> iss53) 在 c.txt 末尾新增一行测试数据 [issue 53]
* 8441143 (tag: v0.0.1, master) 第三次提交
* f9021ee 第二次提交
* 0932815 第一次提交
dang@DFLubuntu:~/git-tutorial$ git checkout master
执行 git checkout branchname
命令能切换到指定分支:
dang@DFLubuntu:~/git-tutorial$ git checkout master
切换到分支 'master'
dang@DFLubuntu:~/git-tutorial$ git branch -vv
iss53 8441143 第三次提交
* master 8441143 第三次提交
dang@DFLubuntu:~/git-tutorial$ git lgd
* 8441143 (HEAD -> master, iss53) 第三次提交
* f9021ee 第二次提交
* 0932815 第一次提交
dang@DFLubuntu:~/git-tutorial$
执行 git checkout -
命令能切换回上一个分支。
在切换分支前,最好保持好一个干净的工作状态,即工作目录和暂存区里的变更都已经被提交。
git checkout -f branchname
命令能强制切换分支并丢弃变更,要么撤销工作目录变更再切换分支。实际上,git checkout -b branchname
等价于 git branch iss53
和 git checkout iss53
两条命令,先创建分支,然后再切换到刚刚创建的这个分支上。
上面我们在 iss53 分支中正常修复了一个BUG并提交了,但此时我们接到运维的通知,说是有紧急的线上问腿需要修复。所以先切换到线上分支 master 中,然后立马再创建一个分支来修复紧急问题:未同意隐私政策就能使用电子邮件注册账号:
dang@DFLubuntu:~/git-tutorial$ git checkout master 切换到分支 'master' dang@DFLubuntu:~/git-tutorial$ git checkout -b hotfix 切换到一个新分支 'hotfix' dang@DFLubuntu:~/git-tutorial$ git lgd * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] * 8441143 (HEAD -> hotfix, tag: v0.0.1, master) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$ vim index.html dang@DFLubuntu:~/git-tutorial$ git add . dang@DFLubuntu:~/git-tutorial$ git commit -m "紧急处理线上问题:注册页面隐私协议单选崩溃" [hotfix 1f5b64b] 紧急处理线上问题:注册页面隐私协议单选崩溃 1 file changed, 34 insertions(+), 1 deletion(-) rewrite index.html (100%) dang@DFLubuntu:~/git-tutorial$
目前:
合并分支,其实就是把一个分支中的变更复制到另一个分支中,并进行一次提交。
在紧急问题修复成功后,就能将 hotfix 分支合并回的 master 分支来部署到线上。
执行 git merge branchname
命令将在当前分支中合并指定分支:
注意 HEAD 的变化,它相当于是跟随后面的 master 追上 hotfix 的,这种用后面的 master 分支合并前面的分支的变化的过程被称为“Fast-forward”,效果相当于 master 分支直接快进到被合并的分支的状态。
目前:
执行 git branch -d branchname
命令删除指定的分支:
由于紧急问题已经处理完了,这里就删除 hotfix 分支:
dang@DFLubuntu:~/git-tutorial$ git branch -d hotfix
已删除分支 hotfix(曾为 1f5b64b)。
dang@DFLubuntu:~/git-tutorial$ git lgd
* 1f5b64b (HEAD, master) 紧急处理线上问题:注册页面隐私协议单选崩溃
| * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53]
|/
* 8441143 (tag: v0.0.1) 第三次提交
* f9021ee 第二次提交
* 0932815 第一次提交
dang@DFLubuntu:~/git-tutorial$
然后就又能回到处理 #53 问题的分支中继续修复BUG并提交,
假设现在这个 # 53 问题已经完成维护,按照前面的做法,就是切换回 master 分支、合并 iss53 分支,最后删除 iss53 分支:
假设现在 iss53 还没删除,且我们已经回到了 master,如果此时我们修改 master 分支中的 b.txt 文件并提交:
dang@DFLubuntu:~/git-tutorial$ vim b.txt dang@DFLubuntu:~/git-tutorial$ git add . dang@DFLubuntu:~/git-tutorial$ git commit -m "在b.txt中添加数据以产生合并冲突" [分离头指针 5ec814b] 在b.txt中添加数据以产生合并冲突 1 file changed, 1 insertion(+), 1 deletion(-) dang@DFLubuntu:~/git-tutorial$ git lgd * 5ec814b (HEAD) 在b.txt中添加数据以产生合并冲突 * 1f5b64b (master) 紧急处理线上问题:注册页面隐私协议单选崩溃 | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$ git branch -vv * (头指针分离自 1f5b64b) 5ec814b 在b.txt中添加数据以产生合并冲突 iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] master 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 dang@DFLubuntu:~/git-tutorial$
此时会提示我们在是在 HEAD 分离态下产行了新提交 “头指针分离自 1f5b64b”。这是因为执行 git checkout [commit_id | branchname]
切换提交会导致 HEAD 指向该次提交,而不再指向分支,这种 HEAD 与分支的分离就会产生会 HEAD 分离态。
解决 HEAD 分离态的办法:
git branch branchname commit_id
命令给某个提交创建一个分支。dang@DFLubuntu:~/git-tutorial$ git branch temp 5ec814b dang@DFLubuntu:~/git-tutorial$ git branch -vv * (头指针分离自 1f5b64b) 5ec814b 在b.txt中添加数据以产生合并冲突 iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] master 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 temp 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git checkout master 之前的 HEAD 位置是 5ec814b 在b.txt中添加数据以产生合并冲突 切换到分支 'master' dang@DFLubuntu:~/git-tutorial$ git branch -vv iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] * master 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 temp 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git merge temp 更新 1f5b64b..5ec814b Fast-forward b.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) dang@DFLubuntu:~/git-tutorial$ git branch -vv iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] * master 5ec814b 在b.txt中添加数据以产生合并冲突 temp 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git branch -d temp 已删除分支 temp(曾为 5ec814b)。 dang@DFLubuntu:~/git-tutorial$ git branch -vv iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] * master 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git lgd * 5ec814b (HEAD -> master) 在b.txt中添加数据以产生合并冲突 * 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$
别忘了,Git 是一个 DVCS,尽管你能很好地规划你的计划,但你阻止不了别人做出些什么,难免别人会干扰到你的工作。
在前面,我们一边在 iss53 分支中修改了 b.txt 文件的第三行且已提交但暂未合并,同样又在 HEAD分离态下修改了 b.txt 文件的第三行并提交且已处理 HEAD分离态。
但这种在多个不同的分支中对同一个文件的同一个部分进行的不同的修改,将在合并这些分支时产生合并冲突。
比如:
dang@DFLubuntu:~/git-tutorial$ git branch -vv iss53 10d78c8 在 c.txt 末尾新增一行测试数据 [issue 53] * master 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git lgd * 5ec814b (HEAD -> master) 在b.txt中添加数据以产生合并冲突 * 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$ git merge 10d78c8 自动合并 b.txt 冲突(内容):合并冲突于 b.txt 自动合并失败,修正冲突然后提交修正的结果。 dang@DFLubuntu:~/git-tutorial$
此时执行 git status
就能看到是哪些文件产生了合并冲突:
再打开具体文件,就有特殊标记:
Git 的合并操作是很智能的,大多数情况是能自动完成合并的:合并 = 暂存 + 提交,比如说修改了不同的文件、相同文件的不同部分、文件名变更。
尽管 Git 能自动处理文件名的变更,但对于代码中对该文件的引用却不能变为更正后的名字;又比如一个用户修改了函数返回值而另一个用户还在用旧的返回值,但还是能合并;这就产生了逻辑冲突。
解决办法就是编写单元测试与持续集成。
合并中更多的会遇到一般冲突,就是不同分支中对同一文件的同一位置进行了修改,这时就需要先查看冲突的内容,然后协商一致后,手动统一保留某一方的修改。
确定冲突信息非常重要。
因为 Git 自动暂存合并成功的结果,所以 diff
只会得到现在还在冲突状态的区别:
执行 git diff --ours
命令查看本分支对源文件的改动:
执行 git diff --theirs
命令查看被合并分支对源文件的改动:
执行 git diff --base
命令查看双方对源文件的改动:
结合提交区间查看冲突提交,能让我们从更高的维度知道哪些提交中的修改在合并时产生了冲突。
首先看看双方都有做了哪些提交:
$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo
然后再看看哪些提交在合并时产生了冲突:
$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo
最后用 -p
选项具体查看所有冲突文件的区别。
产生合并冲突时有下面几种处理方法:
直接用用checkout
把本地或远程分支的改动全部取消:
git checkout --ours
git checkout --theirs
如果此时你打算暂时不合并,且又想取消哪些特殊的标记内容,执行
git merge --abort
命令将简单地退出合并。
dang@DFLubuntu:~/git-tutorial$ git status -sb ## master UU b.txt dang@DFLubuntu:~/git-tutorial$ cat b.txt 111 222 <<<<<<< HEAD “用于产生合并冲突”333 ======= 333 444 >>>>>>> 10d78c8 dang@DFLubuntu:~/git-tutorial$ git merge --abort dang@DFLubuntu:~/git-tutorial$ git status -sb ## master dang@DFLubuntu:~/git-tutorial$ cat b.txt 111 222 “用于产生合并冲突”333 dang@DFLubuntu:~/git-tutorial$
有时可能只是简单地多了写空格或空行,Git 提供两个 merge 选项:
-Xignore-all-space
选项在比较行时完全忽略空白修改。-Xignore-space-change
选项将一个空白符与多个连续的空白字符视作等价。手动处理就是将冲突文件中冲突地点的内容变更为被合并分支中该文件该处的变更:
dang@DFLubuntu:~/git-tutorial$ git lgd * 5ec814b (HEAD -> master) 在b.txt中添加数据以产生合并冲突 * 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$ git merge 10d78c8 自动合并 b.txt 冲突(内容):合并冲突于 b.txt 自动合并失败,修正冲突然后提交修正的结果。 dang@DFLubuntu:~/git-tutorial$ cat b.txt 111 222 <<<<<<< HEAD “用于产生合并冲突”333 ======= 333 444 >>>>>>> 10d78c8 dang@DFLubuntu:~/git-tutorial$ vim b.txt # 手动修改为合并后的版本,解决冲突。 dang@DFLubuntu:~/git-tutorial$ cat b.txt 111 222 333 444 dang@DFLubuntu:~/git-tutorial$ git add b.txt dang@DFLubuntu:~/git-tutorial$ git commit -m "手动处理第一次合并冲突" # 手动处理冲突后要在合并前提交修改 [master c686035] 手动处理第一次合并冲突 dang@DFLubuntu:~/git-tutorial$ git merge 10d78c8 已经是最新的。 dang@DFLubuntu:~/git-tutorial$ git lgd * c686035 (HEAD -> master) 手动处理第一次合并冲突 |\ | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] * | 5ec814b 在b.txt中添加数据以产生合并冲突 * | 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交
如果产生冲突的地方实在太多,初级的手动输入将非常困难,这时可以使用 Git 提供的 git merge-file
命令将产生冲突的本分支文件、目标分支文件和产生的合并文件合并成一个文件,它会自动处理各个文件中的差异。
1,首先执行 git ls-files -u
命令获取产生冲突的本分支文件、目标分支文件和产生的合并文件的 Git blob 对象的实际 SHA-1 值:
dang@DFLubuntu:~/git-tutorial$ git merge 10d78c8
自动合并 b.txt
冲突(内容):合并冲突于 b.txt
自动合并失败,修正冲突然后提交修正的结果。
dang@DFLubuntu:~/git-tutorial$ git ls-files -u
100644 641d57406d212612a9e89e00db302ce758e558d2 1 b.txt
100644 aae594d30ee629ed128adf2a23767236f72d59f9 2 b.txt
100644 337d41392b5b00970acb84577783563ef5796ba5 3 b.txt
2,然后取出这三个文件的内容:
dang@DFLubuntu:~/git-tutorial$ git show :1:b.txt > b.common.txt
dang@DFLubuntu:~/git-tutorial$ git show :2:b.txt > b.mine.txt
dang@DFLubuntu:~/git-tutorial$ git show :3:b.txt > b.others.txt
dang@DFLubuntu:~/git-tutorial$ ls -l
总用量 24
-rw-rw-r-- 1 dang dang 12 11月 21 23:33 a.txt
-rw-rw-r-- 1 dang dang 12 11月 22 03:55 b.common.txt
-rw-rw-r-- 1 dang dang 42 11月 22 03:56 b.mine.txt
-rw-rw-r-- 1 dang dang 16 11月 22 03:56 b.others.txt
-rw-rw-r-- 1 dang dang 87 11月 22 03:47 b.txt
-rw-rw-r-- 1 dang dang 1019 11月 22 00:53 index.html
dang@DFLubuntu:~/git-tutorial$
3,执行git merge-file
命令合并文件:
dang@DFLubuntu:~/git-tutorial$ git merge-file -p b.mine.txt b.common.txt b.others.txt > b.txt dang@DFLubuntu:~/git-tutorial$ ls -l 总用量 24 -rw-rw-r-- 1 dang dang 12 11月 21 23:33 a.txt -rw-rw-r-- 1 dang dang 12 11月 22 04:06 b.common.txt -rw-rw-r-- 1 dang dang 42 11月 22 04:06 b.mine.txt -rw-rw-r-- 1 dang dang 16 11月 22 04:06 b.others.txt -rw-rw-r-- 1 dang dang 98 11月 22 04:07 b.txt -rw-rw-r-- 1 dang dang 1019 11月 22 00:53 index.html dang@DFLubuntu:~/git-tutorial$ cat b.txt 111 222 <<<<<<< b.mine.txt “用于产生合并冲突”333 ======= 333 444 >>>>>>> b.others.txt dang@DFLubuntu:~/git-tutorial$
--help
选项查看 merge-file
支持的合并模式。4,然后执行 git clean -f
删除额外的文件:
dang@DFLubuntu:~/git-tutorial$ git clean -f
正删除 b.common.txt
正删除 b.mine.txt
正删除 b.others.txt
5,最后打开冲突文件,根据提示手动处理合并冲突。
有许多可视化的冲突处理工具,能更精确地显示冲突的内容并作出处理,比如解决 Git 冲突的 14 个建议和工具
现在的 IDE 中也都集成了 Git 插件,操作起来相当方便,比如IDEA 使用Git图文详解、在 Pycharm 中玩转 GitHub(图文详解)
或许是由于时间紧迫, 我们常常倾向于直接用合并工具选取这份或那份代码变更了事。这种贪图方便的行为是应该被抵制的。 如果经过diff
和log
工具配合“其他” 版本的分析之后,你依然无法确定解决冲突的方式, 那么你就应该撤销合并。
假设现在在一个主题分支上工作,不小心将其合并到 master 中,现在提交历史看起来是这样:
如果这个不想要的合并提交只存在于你的本地仓库中,最简单且最好的解决方案是在爆冲突的 git merge
后运行 git reset --hard HEAD~
,实际上就是重置分支指向,或者执行git reset --merge
命令重置合并操作,然后它们看起来像这样:
dang@DFLubuntu:~/git-tutorial$ git lgd * c686035 (HEAD -> master) 手动处理第一次合并冲突 |\ | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] * | 5ec814b 在b.txt中添加数据以产生合并冲突 * | 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$ git reset --hard HEAD~ HEAD 现在位于 5ec814b 在b.txt中添加数据以产生合并冲突 dang@DFLubuntu:~/git-tutorial$ git lgd * 5ec814b (HEAD -> master) 在b.txt中添加数据以产生合并冲突 * 1f5b64b 紧急处理线上问题:注册页面隐私协议单选崩溃 | * 10d78c8 (iss53) 在 c.txt 末尾新增一行测试数据 [issue 53] |/ * 8441143 (tag: v0.0.1) 第三次提交 * f9021ee 第二次提交 * 0932815 第一次提交 dang@DFLubuntu:~/git-tutorial$
缺点:
如果移动分支指针并不适合你,Git 给你一个生成一个新提交的选项,提交将会撤消一个已存在提交的所有修改。在这个特定的场景下,执行 git revert -m 1 HEAD
将以创建一个新提交的方式恢复当前分支在合并动作之前的内容,然后它们看起来像这样:
缺点:
撤销合并后,应该考虑:
cherry-pick
命令来对其采取些改进措施。现在了解分支的基本操作,那么接下来就介绍一些常见的利用分支进行开发的工作流程。
许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码。他们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试,在确保其中已完成的主题分支能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。
感受上是这样的一种流水线:
实际上是这样的:
稳定分支的指针总是在提交历史中落后一大截,它随着你的提交而不断右移,而前沿分支的指针往往比较靠前。
总之,就是利用稳定分支的“Fast-forward”合并实现稳定版本的快进效果。
好处就是在一个非常巨大或者复杂的项目中工作时很方便。
主题分支是一种短期分支,它被用来实现单一特性或其相关工作。前面用到的 hotfix 和 iss53 都是主题分支,在它们中提交了一些更新,并且在它们合并入主干分支之后,又删除了它们。
这种模式,让不同的流水线中每个分支都仅与其目标特性相关,因此,在做代码审查之类的工作的时候就能更加容易地看出你做了哪些改动。 你可以把做出的改动在主题分支中保留几分钟、几天甚至几个月,等它们成熟之后再合并,而不用在乎它们建立的顺序或工作进度。
主题分支的特点就是需求导向,这就让处理临时的问题变得非常容易:
然后选择性地进行分支合并:
分支的合并可能会产生两个及以上的父提交,可执行 git log --merges
命令查看分支合并历史:
在分支未合并之前,可以用提交区间来解决“这个分支还有哪些提交尚未合并到主分支?”的问题。
此时:
双点语法可以选出不在一个分支而在另一个分支中的提交:
$ git log master..experiment
D
C
$ git log experiment..master
F
E
$ git log origin/master..HEAD # 在当前分支中而不在远程 origin 中的提交。如果你执行 git push 并且你的当前分支正在跟踪 origin/master,由 git log origin/master..HEAD 所输出的提交就是会被传输到远端服务器的提交。
如果你留空了其中的一边, Git 会默认为 HEAD。
Git 允许你在分支名前加上 ^
字符或者 --not
来指明选出不在该分支中的提交:
$ git log refA..refB
等价于
$ git log ^refA refB
等价于
$ git log refB --not refA
多点语法支持更多的分支范围:
$ git log refA refB ^refC
$ git log refA refB --not refC
三点语法可以选择出被两个分支之一 包含但又不被两者同时包含的提交:
$ git log master...experiment
F
E
D
C
--left-right
选项会显示每个提交到底处于哪一侧的分支。 这会让输出数据更加清晰:
$ git log --oneline --left-right master...experiment
< F
< E
> D
> C
$ git lgd
* 0a59be8 (HEAD -> master) master分支第四次提交
* 041f24a master分支第三次提交
| * eda3ebc (experiment) experiment分支第二次提交
| * 21f7242 experiment分支第一次提交
|/
* 948f989 master分支第二次提交
* 2154b36 master分支第一次提交
$ git log --oneline --left-right master...experiment
< 0a59be8 (HEAD -> master) master分支第四次提交
< 041f24a master分支第三次提交
> eda3ebc (experiment) experiment分支第二次提交
> 21f7242 experiment分支第一次提交
如果用特性分支来开发某一个特性,在主分支中合并特性分支的结果就是主分支中多了一项特性,那么就可能存在这么一种请况:在稳定的master分支中,这次合并结果的上一次提交,就是对上一个特性的合并。这一串地特性合并提交,就是一份历史概览。
此时可以结合上面的提交区间,执行 git log --first-parent --oneline refA..refB
命令选择第一父提交区间:
在前面,我们在一个分支中工作时突然遇到 bug 要修复,就立马创建了一个主题分支去修复问题。但是当前的工作只进行到
一半,不适合进行提交。这时候就可以使用贮藏的功能:先将
工作目录中文件的修改与暂存区中的改动状态记录下来,保存到贮藏栈上,用户就能切换到其他分支处理临时的工作。等到临时工作处理完毕之后,再切换分支,将文件恢复到原来的状态。
执行 git stash
命令将贮藏当前文件状态;
执行 git stash list
命令能查看所有贮藏:
$ mkdir test $ cd test test$ git init Initialized empty Git repository in test/.git/ test$ echo "1111" >> a.txt test$ git add . test$ git commit -m "第一次提交" [master (root-commit) 5b8d68e] 第一次提交 1 file changed, 1 insertion(+) create mode 100644 a.txt test$ git checkout -b dev Switched to a new branch 'dev' test$ echo "111" >> dev.txt test$ git add . test$ git status On branch dev Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: dev.txt test$ git stash Saved working directory and index state WIP on dev: 5b8d68e 第一次提交 test$ git status On branch dev nothing to commit, working tree clean test$ git stash list stash@{0}: WIP on dev: 5b8d68e 第一次提交
默认情况下,git stash
只会贮藏已跟踪的文件。
如果指定 --include-untracked
或 -u
选项就能同时贮藏未跟踪文件;
如果指定 --all
或 -a
选项就能同时贮藏被忽略的文件。
如果指定 --patch
选项能交互式地指定需要贮藏哪些修改。
执行 git stash apply stash@{number}
命令将重新应用指定的的贮藏到工作目录;
执行 git stash apply stash@{number} --index
命令将重新应用指定的的贮藏到暂存区;
如果贮藏了一些内容,然后继续在该分支上工作,那么在重新应用贮藏时可能会遇到冲突问题。如果确实在修改后还想要应用之前的贮藏(比如简化多步骤地反悔操作),那么可以执行 git stash branch <new branchname>
命令能在储藏中创建一个新分支,然后手动检出贮藏工作时所在的提交,在重新在那应用贮藏,在应用成功后丢弃贮藏,最后就能用之前的贮藏覆盖贮藏之后做出的修改。
执行 git stash drop stash@{number}
命令将丢弃指定的贮藏:
执行 git stash pop
将应用最新的贮藏并立即从贮藏栈出栈。
这里将通过一个演示来表现 rebase 变基的特点。
假设现在 git 本地仓库是这样的:
然后执行 git rebase master
将 test_rebase 分支变基到 master 分支,效果就是这样:
在 test_rebase 分支 rebase master 的效果就是将 test_rebase 分支的起始点重放到 master 分支处。
看起来rebase变基就是重放。 但实际上,变基后的提交是拥有新 SHA-1 值的新提交,原来的会在短时间内保持存在,直到被垃圾回收机制回收。
既然重放的效果就是将一个分支的起点放到另一个分支上,那么,就能使用变基来简化分支合并的路线,避免 log 出来一大堆的分支路线图。
还是回到这个状态:
假设现在要将 libai 分支合并到 master 分支,按照之前的方法,要先从 libai 检出到 master,然后执行 merge,效果就是这样的:
如果继续在 libai 分支中进行开发,就可能存在多次与 master 的合并,那么,可能是下面这种效果:
但如果是 rebase 而不是直接 merge 的话,可能是下面这种效果:
前面基于 merge 实现的分支合并更利于处理主题工作,而对于某长期分支的开发合并不太友好。
因为长期分支需要很长的时间才能完成开发,同时 master 分支也在不断变更前进中,为了在这种并行关系中保持必要的基础程序架构的一致性,就需要定期将主分支中的变更合并到开发分支中,这就和前面 merge 操作相反,然后各个分支又独立前进,然后又定期合并,如此共同前进。同时也会出现相当复杂的分支路线图。
回到某个状态,假设 libai 分支的继续开发需要 master1 和 master3 提交中的内容:
在 libai 中 rebase master,获取所需内容后再继续开发,效果就是这样:
变基的结果就是能在 libai 分支的工作目录中看见原本只属于 master 分支的内容和属于 dufu 分支工作目录中的内容(之前合并到 master 中)。
变基很简单,在当前分支中执行 git rebase <basebranch>
命令将目标分支重放到当前分支。
也能执行 git rebase <basebranch> <branch>
命令明确指示将分支变基到基分支。
但变基时要注意一个问题,即变基子分支时是否需要携带父分支中的提交。
假设:
如果这时我们将 libai 变基到 master,命令默认会将 libai 及其父分支 dufu 相关的的提交都重放过去:
而执行 git rebase --onto <basebranch> <p_topicbranch> <s_topicbranch>
命令将只在s_topicbranch子分支而不在p_topicbranch父分支的提交重放到基分支:
在前面按说过,变基操作的实质是丢弃一些现有的提交,然后相应地重放一些内容一样但实际上 SHA-1 值不同的提交。
这种情况就要求我们最好不要对已经推送到远程仓库且极有可能已经被别人引用的提交或分支进行变基,否则将导致巨大的应用混乱。
到此为止,在 本地的 Git 操作已经差不多将完了,接下来就开始与他人合作吧。
为了能在任意 Git 项目上协作,你需要知道如何管理自己的远程仓库以及远程分支。 这里只是简单介绍,更详细的介绍和应用会在后面再说。
远程仓库是指托管在因特网或其他网络中的你的项目的版本库。 你可以有好几个远程仓库,与他人协作涉及管理远程仓库以及根据需要推送或拉取数据。
远程引用是对远程仓库的引用,包括分支、标签等等。 你可以通过 git ls-remote <remote>
命令获得远程引用的完整列表, 或者通过 git remote show <remote>
获得远程分支的更多信息。
假设你的网络里有一个在 git.ourcompany.com 的 Git 服务器。 执行 clone
命令将自动把某个仓库添加为远程仓库并命名为 origin,然后会抓取所有数据,再创建一个指向这个远程仓库的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 origin 的 master 分支指向同一个地方的本地 master 分支,这样你就有工作的基础:
然后你在本地的 master 分支做了一些工作。而同一段时间内如果有其他人推送提交到远程仓库并且更新了它的 master 分支,那么两处的 master 将在远程 master被更新后分道扬镳。 只要你不与 origin 通信(抓取数据或者推送数据),你的 origin/master 指针就不会移动:
如果此时执行 fetch
从远程仓库抓取本地没有的数据,origin/master 指针将移动到远程 master 最新的位置:
本地仓库与远程仓库的交流是如此重要,它通过本地分支与远程的交流来实现,最方便的就是设置远程跟踪分支,它是与远程分支有直接关系的本地分支。
前面说到,克隆远程仓库后会创建一对配对的 origin/master 和 master,效果就是自动地创建一个跟踪 origin/master 的 master 分支,这个本地的 master 分支就是一个远程跟踪分支:
~/test $ git clone https://github.com/libgit2/libgit2 Cloning into 'libgit2'... remote: Enumerating objects: 113722, done. remote: Counting objects: 100% (113722/113722), done. remote: Compressing objects: 100% (32170/32170), done. remote: Total 113722 (delta 81040), reused 112136 (delta 79581), pack-reused 0 Receiving objects: 100% (113722/113722), 56.05 MiB | 4.61 MiB/s, done. Resolving deltas: 100% (81040/81040), done. ~/test $ cd libgit2 ~/test/libgit2 $ git remote -v origin https://github.com/libgit2/libgit2 (fetch) origin https://github.com/libgit2/libgit2 (push) ~/test/libgit2 $ git branch -vv * master 6fdb1b2f5 [origin/master] Merge pull request #6122 from libgit2/ethomson/cleanup
而 执行 git checkout -b <branch> <remote>/<branch>
命令将从远程分支检出一个本地的远程跟踪分支。这是一个十分常用的操作所以 Git 提供了 --track
快捷方式:
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
管理i远程仓库的前提当然是你拥有一个远程仓库并能够与它通信。
我们通常使用 GitHub 来创建远程仓库,当然能你也能在自己的服务器上搭建 Git,然后执行 git remote add <shortname> <url>
命令添加已有的远程仓库。
<shortname>
是远程仓库 URL 的简称,通常使用 origin 来表示。使用 clone
命令克隆远程仓库将其默认添加它为远程仓库并命名为 origin。<url>
表示连接远程仓库的方式,比如git@github.com:username/repositoryname.git、https://github.com/username/repositoryname.git等。比如添加远程仓库first-pr:
$ git remote add origin https://github.com/ituring/first-pr.git
添加成功后就能在本地管理远程仓库了,比如:
执行 git remote
命令将列出所有已添加远程仓库的简称:
$ git remote
origin
执行 git remote -v
命令将列出所有已添加远程仓库的简称与其对应的 URL:
$ git remote -v
origin https://github.com/ituring/first-pr.git (fetch)
origin https://github.com/ituring/first-pr.git (push)
还能执行 git remote show <shortname>
命令查看更多远程仓库的信息:
$ git remote show origin
* remote origin # 远程仓库简称
Fetch URL: https://github.com/ituring/first-pr # 抓取信息的地址
Push URL: https://github.com/ituring/first-pr # 推送信息的地址
HEAD branch: gh-pages
Remote branches:
feature/move-jquery-from-cdn-to-local tracked # 已被本地跟踪的远程纷纷年至
gh-pages tracked
Local branch configured for 'git pull':
gh-pages merges with remote gh-pages # 执行 git pull 时哪些本地分支可以与它跟踪的远程分支自动合并
Local ref configured for 'git push':
gh-pages pushes to gh-pages (up to date) # 默认推送到哪个远程分支
执行 git branch -a
获取本地和远程的所有分支:
执行 git fetch <shortname>
命令能抓取远程仓库中有但本地没有的内容:
$ git fetch origin
remote: Counting objects: 43, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 43 (delta 10), reused 31 (delta 5)
Unpacking objects: 100% (43/43), done.
From https://github.com/ituring/first-pr.git
* [new branch] master -> origin/master
* [new branch] ticgit -> origin/ticgit
fetch
并不会修改工作目录中的内容,只会拥有那个远程仓库中所有分支的引用,需要手动合并来将远程变更应用到本地。执行 git pull <shortname>
命令能拉取远程仓库中有但本地没有的内容并自动合并。
git pull
都会先查找当前分支所跟踪的远程分支,然后从服务器上抓取数据并尝试合并。fetch
与 merge
命令。执行 git push <remote> <local_branch>[:<remote_branch>]
命令将本地 branch 分支推送到 remote远程仓库 [的remote_branch分支],比如将本地的稳定分支 master 分支推送到 origin远程仓库:
$ git push origin master
假设你已经通过远程分支和协作者已经完成了一个特性开发, 并且将其合并到了远程仓库的 master 分支(或任何其他稳定代码分支)。 执行 git push <remote> --delete <remotebranch>
命令将删除远程仓库中指定的远程分支。例如:
$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix
当然也能恢复被删除的远程分支:
git reflog --date=iso
命令以标准时间格式展示引用日志,找到目标分支最后一次的提交的 SHA-1 值;git checkout -b <branch> <remote>/<branch>
命令从远程分支检出一个本地的远程跟踪分支,就设置了跟踪效果;git push <remote> <branch>
命令推送本地分支到远程分支。执行 git remote rename <oldname> <newname>
命令将重命名远程仓库简称。
执行 git remote -rm <shortname>
命令将移除本地的远程仓库。
如果说 rebase
变基是将整条分支上的所有提交都重放另一分支上的话,cherry-pick
捡取则允许将分支上某些提交复制到另一分支上
此时的状态:
目标:将 client 分支第三条提交捡取到 dev 分支上。
切换并捡取后显示有冲突,
解决冲突后再提交就行:
目标完成:
执行 git cherry-pick <branchname>
命令将捡取指定分支的最新提交。
更多cherry-pick
详情可参考 阮一峰:git cherry-pick 教程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。