Git技能树(5):分支管理
git 分支
几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。 在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。
有人把 Git
的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git
从众多版本控制系统中脱颖而出。 为何 Git
的分支模型如此出众呢? Git
处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git
鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。 理解和精通这一特性,你便会意识到 Git
是如此的强大而又独特,并且从此真正改变你的开发方式。
git 分支简介
为了真正理解 Git
处理分支的方式,我们需要回顾一下 Git
是如何保存数据的。
前面我们了解到,Git 保存的不是文件的变化或者差异,而是一系列不同时刻的 快照 。
在进行提交操作时,Git
会保存一个提交对象(commit object)。 知道了 Git
保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。 首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象,
为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。 暂存操作会为每一个文件计算校验和,然后会把当前版本的文件快照保存到 Git
仓库中 (Git
使用 blob
对象来保存它们),最终将校验和加入到暂存区域等待提交:
$ git add readme.txt test.md LICENSE $ git commit -m 'The initial commit of my project'
当使用 git commit
进行提交操作时,Git
会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git
仓库中这些校验和保存为树对象。随后,Git
便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git
就可以在需要的时候重现此次保存的快照。
现在,Git 仓库中有五个对象:三个 blob
对象(保存着文件快照)、一个 树 对象 (记录着目录结构和 blob
对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)。
首次提交对象及其树结构
做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。
提交对象及其父对象
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master
。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master
分支。 master
分支会在每次提交时自动向前移动。
分支及其提交历史
创建分支
Git 会把仓库中的每次提交串成一条时间线,这条时间线就是一个分支。在 Git
里,每个仓库都会有一个主分支,即master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git 用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
当我们创建新的分支,例如dev
时,Git 新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上:
你看,Git 创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git 怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
所以 Git
合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:
真是太神奇了,你看得出来有些提交是通过分支完成的吗?
下面开始实战。
首先,我们创建dev
分支,然后切换到dev
分支:
$ git checkout -b dev Switched to a new branch 'dev'
git checkout
命令加上-b
参数表示创建并切换,相当于以下两条命令:
$ git branch dev $ git checkout dev Switched to branch 'dev'
然后,用git branch
命令查看当前分支:
$ git branch * dev master
git branch
命令会列出所有分支,当前分支前面会标一个*
号。
然后,我们就可以在dev
分支上正常提交,比如对readme.txt
做个修改,加上一行:
Creating a new branch is quick.
然后提交:
$ git add readme.txt $ git commit -m "branch test" [dev 4aac6c7] branch test 1 file changed, 1 insertion(+)
现在,dev
分支的工作完成,我们就可以切换回master
分支:
$ git checkout master Switched to branch 'master'
切换回master
分支后,再查看一个readme.txt
文件,刚才添加的内容不见了!因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:
现在,我们把dev
分支的工作成果合并到master
分支上:
$ git merge dev Updating 599dbdb..4aac6c7 Fast-forward readme.txt | 1 + 1 file changed, 1 insertion(+)
git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt
的内容,就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git 告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
$ git branch -d dev Deleted branch dev (was 4aac6c7).
删除后,查看branch
,就只剩下master
分支了:
$ git branch * master
因为创建、合并和删除分支非常快,所以 Git
鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全。
git switch
我们注意到切换分支使用git checkout <branch>
,而 Git
中撤销修改则是git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑。
实际上,切换分支这个动作,用switch
更科学。因此,最新版本的 Git
提供了新的git switch
命令来切换分支:
创建并切换到新的dev
分支,可以使用:
$ git switch -c dev
直接切换到已有的master
分支,可以使用:
$ git switch master
使用新的git switch
命令,比git checkout
要更容易理解。
分支管理策略
通常,合并分支时,如果可能,Git会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward
模式,Git 就会在merge
时生成一个新的commit
,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff
方式的git merge
:
首先,仍然创建并切换dev
分支:
$ git switch -c dev Switched to a new branch 'dev'
修改readme.txt
文件,并提交一个新的commit
:
$ git add readme.txt $ git commit -m "add merge" [dev f52c633] add merge 1 file changed, 1 insertion(+)
现在,我们切换回master
:
$ git switch master Switched to branch 'master'
准备合并dev
分支,请注意--no-ff
参数,表示禁用Fast forward
:
$ git merge --no-ff -m "merge with no-ff" dev Merge made by the 'recursive' strategy. readme.txt | 1 + 1 file changed, 1 insertion(+)
因为本次合并要创建一个新的commit
,所以加上-m
参数,把commit
描述写进去。
合并后,我们用git log
看看分支历史:
$ git log --graph --pretty=oneline --abbrev-commit * fc76cf7 (HEAD -> master) merge with no-ff |\ | * f52c633 (dev) add merge |/ * cf810e4 conflict fixed ...
可以看到,不使用Fast forward
模式,merge后就像这样:
分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
其次,干活都在dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev
分支合并到master
上,并在master
分支发布1.0版本;
你和你的小伙伴们每个人都在dev
分支上干活,每个人都有自己的分支,时不时地往dev
分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
在日常的开发工作中,除了以上介绍的 master
和 dev
两个常用分支外,我们还会有其他类型的分支使用策略:
bug分支
在开发过程中,bug
就像家常便饭一样。有了 bug
就需要修复,在 Git
中,由于分支是如此的强大,所以,每个 bug
都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的 bug
的任务时,很自然地,你想创建一个分支 issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交:
$ git status On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: hello.py Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.txt
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该 bug
,怎么办?
幸好,Git还提供了一个stash
功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
$ git stash Saved working directory and index state WIP on dev: f52c633 add merge
现在,用git status
查看工作区,就是干净的(除非有没有被Git
管理的文件),因此可以放心地创建分支来修复 bug
。
首先确定要在哪个分支上修复 bug
,假定需要在master
分支上修复,就从master
创建临时分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use "git push" to publish your local commits) $ git checkout -b issue-101 Switched to a new branch 'issue-101'
现在修复bug
,需要把“Git is free software …”改为“Git is a free software …”,然后提交:
$ git add readme.txt $ git commit -m "fix bug 101" [issue-101 8842ff5] fix bug 101 1 file changed, 1 insertion(+), 1 deletion(-)
修复完成后,切换到master
分支,并完成合并,最后删除issue-101
分支:
$ git switch master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use "git push" to publish your local commits) $ git merge --no-ff -m "merged bug fix 101" issue-101 Merge made by the 'recursive' strategy. readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
太棒了,原计划两个小时的 bug
修复只花了5分钟!现在,是时候接着回到dev
分支干活了!
$ git switch dev Switched to branch 'dev' $ git status On branch dev nothing to commit, working tree clean
工作区是干净的,刚才的工作现场存到哪去了?用git stash list
命令看看:
$ git stash list stash@{0}: WIP on dev: f52c633 add merge
工作现场还在,Git 把stash
内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply
恢复,但是恢复后,stash
内容并不删除,你需要用git stash drop
来删除;
另一种方式是用git stash pop
,恢复的同时把stash
内容也删了:
$ git stash pop On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: hello.py Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.txt Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
再用git stash list
查看,就看不到任何stash
内容了:
$ git stash list
你可以多次stash
,恢复的时候,先用git stash list
查看,然后恢复指定的stash
,用命令:
$ git stash apply stash@{0}
在master
分支上修复了bug
后,我们要想一想,dev
分支是早期从master
分支分出来的,所以,这个bug
其实在当前dev
分支上也存在。
那怎么在dev
分支上修复同样的bug
?重复操作一次,提交不就行了?在 Git
中还有比这更简单的方法可以实现。
同样的 bug
,要在dev
上修复,我们只需要把8842ff5 fix
bug 101
这个提交所做的修改“复制”到dev
分支。注意:我们只想复制8842ff5 fix
bug 101
这个提交所做的修改,并不是把整个master
分支merge
过来。
为了方便操作,Git 专门提供了一个cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
$ git branch * dev master $ git cherry-pick 8842ff5 [dev 0944c8c] fix bug 101 1 file changed, 1 insertion(+), 1 deletion(-)
Git 自动给dev
分支做了一次提交,注意这次提交的commit
是0944c8c
,它并不同于master
的8842ff5
,因为这两个commit
只是改动相同,但确实是两个不同的commit
。用git cherry-pick
,我们就不需要在dev
分支上手动再把修 bug
的过程重复一遍。
有些聪明的童鞋会想了,既然可以在master
分支上修复bug
后,在dev
分支上可以“重放”这个修复过程,那么直接在dev
分支上修复 bug
,然后在master
分支上“重放”行不行?当然可以,不过你仍然需要git stash
命令保存现场,才能从dev
分支切换到master
分支。
feature分支
在开发过程中,除了 bug
外,也还会有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature
分支,在上面开发,完成后,合并,最后,删除该feature
分支。
现在,你终于接到了一个新任务:开发代号为Vulcan
的新功能,该功能计划用于下一代星际飞船。
于是准备开发:
$ git switch -c feature-vulcan Switched to a new branch 'feature-vulcan'
5分钟后,开发完毕:
$ git add vulcan.md $ git status On branch feature-vulcan Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: vulcan.md $ git commit -m "add feature vulcan" [feature-vulcan d12cf23] add feature vulcan 1 file changed, 3 insertions(+) create mode 100644 vulcan.md
切回dev
,准备合并:
$ git switch dev
一切顺利的话,feature
分支和bug
分支是类似的,合并,然后删除。
但是!就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:
$ git branch -d feature-vulcan error: The branch 'feature-vulcan' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature-vulcan'.
销毁失败。Git 友情提醒,feature-vulcan
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D
参数。。
现在我们强行删除:
$ git branch -D feature-vulcan Deleted branch feature-vulcan (was d12cf23).
终于删除成功!
Git 解决冲突
解决冲突
人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的feature1
分支,继续我们的新分支开发:
$ git switch -c feature1 Switched to a new branch 'feature1'
修改readme.txt
最后一行,改为:
Creating a new branch is quick AND simple.
在feature1
分支上提交:
$ git add readme.txt $ git commit -m "AND simple" [feature1 2443c95] AND simple 1 file changed, 1 insertion(+), 1 deletion(-)
切换到master
分支:
$ git switch master Switched to branch 'master'
在master
分支上把readme.txt
文件的最后一行改为:
Creating a new branch is quick & simple.
提交:
$ git add readme.txt $ git commit -m "& simple" [master 06e5e3a] & simple 1 file changed, 1 insertion(+), 1 deletion(-)
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
这种情况下,Git 无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
$ git merge feature1 Auto-merging readme.txt CONFLICT (content): Merge conflict in readme.txt Automatic merge failed; fix conflicts and then commit the result.
果然冲突了!Git 告诉我们,readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
$ 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: readme.txt no changes added to commit (use "git add" and/or "git commit -a")
我们可以直接查看readme.txt
的内容:
Git is a distributed version control system. Git is free software distributed under the GPL. Git has a mutable index called stage. Git tracks changes of files. <<<<<<< HEAD Creating a new branch is quick & simple. ======= Creating a new branch is quick AND simple. >>>>>>> feature1
Git 用<<<<<<<,=======,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
Creating a new branch is quick and simple.
再提交:
$ git add readme.txt $ git commit -m "conflict fixed" [master fb8b190] conflict fixed
现在,master
分支和feature1
分支变成了下图所示:
用带参数的git log
也可以看到分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commit * fb8b190 (HEAD -> master) conflict fixed |\ | * 2443c95 (feature1) add simple * | 06e5e3a & simple |/ * 4aac6c7 branch test * 599dbdb understand how stage works * 1985ccf append GPL * 27f9df6 add distributed * e372504 wrote a readme file
最后,删除feature1
分支:
$ git branch -d feature1 Deleted branch feature1 (was 2443c95).
工作完成。
git cherry-pick
对于多分支的代码库,将代码从一个分支转移到另一个分支是常见需求。
这时分两种情况。一种情况是,你需要另一个分支的所有代码变动,那么就采用合并git merge
。另一种情况是,你只需要部分代码变动(某几个提交),这时可以采用 Cherry pick
。
git cherry-pick 基本用法
git cherry-pick
命令的作用,就是将指定的提交commit
应用于其他分支。
$ git cherry-pick <commitHash>
上面命令就会将指定的提交commitHash
,应用于当前分支。这会在当前分支产生一个新的提交,当然它们的哈希值会不一样。
举例来说,代码仓库有master
和feature
两个分支。
a - b - c - d Master \ e - f - g Feature
现在将提交f
应用到master
分支。
# 切换到 master 分支 $ git checkout master # Cherry pick 操作 $ git cherry-pick f
上面的操作完成以后,代码库就变成了下面的样子。
a - b - c - d - f Master \ e - f - g Feature
从上面可以看到,master
分支的末尾增加了一个提交f
。
git cherry-pick
命令的参数,不一定是提交的哈希值,分支名也是可以的,表示转移该分支的最新提交。
$ git cherry-pick feature
上面代码表示将feature
分支的最近一次提交,转移到当前分支。
git cherry-pick 转移多个提交
Cherry pick
支持一次转移多个提交。
$ git cherry-pick <HashA> <HashB>
上面的命令将 A
和 B
两个提交应用到当前分支。这会在当前分支生成两个对应的新提交。
如果想要转移一系列的连续提交,可以使用下面的简便语法。
$ git cherry-pick A..B
上面的命令可以转移从 A
到 B
的所有提交。它们必须按照正确的顺序放置:提交 A
必须早于提交 B
,否则命令将失败,但不会报错。
注意,使用上面的命令,提交 A
将不会包含在 Cherry pick
中。如果要包含提交 A
,可以使用下面的语法。
$ git cherry-pick A^..B
git cherry-pick 配置项
git cherry-pick
命令的常用配置项如下。
-e,--edit
: 打开外部编辑器,编辑提交信息。-n,--no-commit
: 只更新工作区和暂存区,不产生新的提交。-x
: 在提交信息的末尾追加一行cherry picked from commit ...
,方便以后查到这个提交是如何产生的。-s,--signoff
: 在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。-m parent-number,--mainline parent-number
: 如果原始提交是一个合并节点,来自于两个分支的合并,那么Cherry pick
默认将失败,因为它不知道应该采用哪个分支的代码变动。-m
配置项告诉Git
,应该采用哪个分支的变动。它的参数parent-number
是一个从1开始的整数,代表原始提交的父分支编号。git cherry-pick -m 1 <commitHash>
表示,Cherry pick
采用提交commitHash
来自编号1的父分支的变动。一般来说,1号父分支是接受变动的分支,2号父分支是作为变动来源的分支。
git cherry-pick 代码冲突
如果操作过程中发生代码冲突,Cherry pick
会停下来,让用户决定如何继续操作。
(1)–continue
用户解决代码冲突后,第一步将修改的文件重新加入暂存区(git add .
),第二步使用下面的命令,让 Cherry pick
过程继续执行。
$ git cherry-pick --continue
(2)–abort
发生代码冲突后,放弃合并,回到操作前的样子。
(3)–quit
发生代码冲突后,退出 Cherry pick
,但是不回到操作前的样子。
git cherry-pick 转移到另一个代码库
Cherry pick
也支持转移另一个代码库的提交,方法是先将该库加为远程仓库。
$ git remote add target git://gitUrl
上面命令添加了一个远程仓库target
。
然后,将远程代码抓取到本地。
$ git fetch target
上面命令将远程代码仓库抓取到本地。
接着,检查一下要从远程仓库转移的提交,获取它的哈希值。
$ git log target/master
最后,使用git cherry-pick
命令转移提交。
$ git cherry-pick <commitHash>
Git 多人协作
查看远程分支
当你从远程仓库克隆时,实际上 Git
自动把本地的master
分支和远程的master
分支对应起来了,并且,远程仓库的默认名称是origin
。
要查看远程库的信息,用git remote
:
$ git remote origin
或者,用git remote -v
显示更详细的信息:
$ git remote -v origin git@github.com:michaelliao/learngit.git (fetch) origin git@github.com:michaelliao/learngit.git (push)
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push
的地址。
推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git 就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev
,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;bug
分支只用于在本地修复bug
,就没必要推到远程了;feature
分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在 Git
中,分支完全可以在本地,你可以根据分支的需求来决定是否需要推送到远程!
抓取分支
多人协作时,大家都会往master
和dev
分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key
添加到 GIT CODE
)或者同一台电脑的另一个目录下克隆:
$ git clone git@gitcode.net:codechina/learngit.git Cloning into 'learngit'... remote: Counting objects: 40, done. remote: Compressing objects: 100% (21/21), done. remote: Total 40 (delta 14), reused 40 (delta 14), pack-reused 0 Receiving objects: 100% (40/40), done. Resolving deltas: 100% (14/14), done.
当你的小伙伴从远程库clone
时,默认情况下,你的小伙伴只能看到本地的master
分支。不信可以用git branch
命令看看:
$ git branch * master
现在,你的小伙伴要在dev
分支上开发,就必须创建远程origin
的dev
分支到本地,于是他用这个命令创建本地dev
分支:
$ git checkout -b dev origin/dev
现在,他就可以在dev
上继续修改,然后,时不时地把dev
分支push
到远程:
$ git add env.txt $ git commit -m "add env" [dev 34bde7f] add env 1 file changed, 1 insertion(+) create mode 100644 env.txt $ git push origin dev Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 8 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 332 bytes | 332.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 remote: remote: To create a merge request for dev, visit: remote: https://gitcode.net/xiongjiamu/learing/-/merge_requests/new?merge_request%5Bsource_branch%5D=dev remote: To gitcode.net:xiongjiamu/learing.git
你的小伙伴已经向origin/dev
分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:
$ cat env.txt env $ git add env.txt $ git commit -m "Update env.txt" [dev d7e68661] Update env.txt 1 file changed, 1 insertion(+) create mode 100644 env.txt $ git push origin dev To github.com:michaelliao/learngit.git ! [rejected] dev -> dev (non-fast-forward) error: failed to push some refs to 'git@github.com:michaelliao/learngit.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git 已经提示我们,先用git pull
把最新的提交从origin/dev
抓下来,然后,在本地合并,解决冲突,再推送:
$ git pull There is no tracking information for the current branch. Please specify which branch you want to merge with. See git-pull(1) for details. git pull <remote> <branch> If you wish to set tracking information for this branch you can do so with: git branch --set-upstream-to=origin/<branch> dev
git pull
也失败了,原因是没有指定本地dev
分支与远程origin/dev
分支的链接,根据提示,设置dev
和origin/dev
的链接:
$ git branch --set-upstream-to=origin/dev dev Branch 'dev' set up to track remote branch 'dev' from 'origin'.
再pull
:
$ git pull Auto-merging env.txt CONFLICT (add/add): Merge conflict in env.txt Automatic merge failed; fix conflicts and then commit the result.
这回git pull
成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push
:
$ git commit -m "fix env conflict" [dev ad728db] fix env conflict $ git push origin dev Counting objects: 6, done. Delta compression using up to 4 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done. Total 6 (delta 0), reused 0 (delta 0) To github.com:michaelliao/learngit.git d7e6866..ad728db dev -> dev
因此,多人协作的工作模式通常是这样:
- 首先,可以试图用
git push origin <branch-name>
推送自己的修改; - 如果推送失败,则因为远程分支比你的本地更新,需要先用
git pull
试图合并; - 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用
git push origin <branch-name>
推送就能成功!
如果git pull
提示no tracking information
,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
多人协作小结
因此,多人协作的工作模式通常是这样:
- 首先,可以试图用
git push origin <branch-name>
推送自己的修改; - 如果推送失败,则因为远程分支比你的本地更新,需要先用
git pull
试图合并; - 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用
git push origin <branch-name>
推送就能成功!
如果git pull
提示no tracking information
,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
git rebase
发布一个版本时,我们通常先在版本库中打一个标签(tag
),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git 的标签虽然是版本库的快照,但其实它就是指向某个commit
的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push
的童鞋不得不先pull,在本地合并,然后才能push
成功。
每次合并再push
后,分支变成了这样:
$ git log --graph --pretty=oneline --abbrev-commit * f4e7ea8 (HEAD -> master) Merge branch 'dev' into master |\ | * ad728db (origin/dev, dev) fix env conflict | |\ | | * d7e6866 Update env.txt | * | 770ce3f new env | |/ | * 34bde7f add env | * 0944c8c fix bug 101 * | b003293 (origin/master) merged bug fix 101 |\ \ | * | 8842ff5 (issue-101) fix bug 101 |/ / * | fc76cf7 merge with no-ff |\| | * c7b409a add merge |/ * fb8b190 conflict fixed
总之看上去很乱,有强迫症的童鞋会问:为什么Git
的提交历史不能是一条干净的直线?
其实是可以做到的!
Git有一种称为rebase
的操作,有人把它翻译成“变基”。我们来看看 rebase
是怎么把分叉的提交变成直线。
在和远程分支同步后,我们对hello
.py这个文件做了两次提交。用git log
命令看看:
$ git log --graph --pretty=oneline --abbrev-commit * 582d922 (HEAD -> master) add author * 8875536 add comment * d1be385 (origin/master) init hello * e5e69f1 Merge branch 'dev' |\ | * 57c53ab (origin/dev, dev) fix env conflict | |\ | | * 7a5e5dd add env | * | d7e68661 Update env.txt ...
注意到Git
用(HEAD
-> master)和(origin/master)标识出当前分支的HEAD
和远程origin的位置分别是582d922 add author和d1
be385 init hello,本地分支比远程分支快两个提交。
现在我们尝试推送本地分支:
$ git push origin master To gitcode.net:xiongjiamu/learning-git.git ! [rejected] master -> master (fetch first) error: failed to push some refs to 'git@gitcode.net:xiongjiamu/learning-git.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull
一下:
$ git pull remote: Counting objects: 3, done. remote: Compressing objects: 100% (1/1), done. remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0 Unpacking objects: 100% (3/3), done. From gitcode.net:xiongjiamu/learning-git.git 3010226..9dbfd41 master -> origin/master Auto-merging hello.py Merge made by the 'recursive' strategy. hello.py | 1 + 1 file changed, 1 insertion(+)
再用git status
看看状态:
$ git status On branch master Your branch is ahead of 'origin/master' by 3 commits. (use "git push" to publish your local commits) nothing to commit, working tree clean
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用git log
看看:
$ git log --graph --pretty=oneline --abbrev-commit * 00f5a14 (HEAD -> master) Merge branch 'master' of gitcode.net:xiongjiamu/learning-git |\ | * 9dbfd41 (origin/master) set exit=1 * | 8789044 add author * | 01ebb65 add comment |/ * 3010226 init hello ...
对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push
到远程,有没有问题?
有!
什么问题?
不好看!
有没有解决方法?
有!
这个时候,rebase就派上了用场。我们输入命令git rebase
试试:
$ git rebase First, rewinding head to replay your work on top of it... Applying: add comment Using index info to reconstruct a base tree... M hello.py Falling back to patching base and 3-way merge... Auto-merging hello.py Applying: add author Using index info to reconstruct a base tree... M hello.py Falling back to patching base and 3-way merge... Auto-merging hello.py
输出了一大堆操作,到底是啥效果?再用git log
看看:
$ git log --graph --pretty=oneline --abbrev-commit * 22a1da7 - (HEAD -> master) add author * 00bae30 - add comment * 9dbfd41 - (origin/master) set exit=1 * 3010226 - init hello ...
原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git
把我们本地的提交“挪动”了位置,放到了f005
ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit
修改内容已经变化了,它们的修改不再基于d1
be385 init hello,而是基于f005
ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。
这就是rebase
操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。
最后,通过push
操作把本地分支推送到远程:
$ git push origin master Enumerating objects: 8, done. Counting objects: 100% (8/8), done. Delta compression using up to 8 threads Compressing objects: 100% (6/6), done. Writing objects: 100% (6/6), 565 bytes | 565.00 KiB/s, done. Total 6 (delta 3), reused 0 (delta 0), pack-reused 0 To gitcode.net:xiongjiamu/learning-git.git 9dbfd41..22a1da7 master -> master
再用git log
看看效果:
$ git log --graph --pretty=oneline --abbrev-commit * 22a1da7 (HEAD -> master, origin/master) add author * 00bae30 add comment * 9dbfd41 set exit=1 * 3010226 init hello ...
远程分支的提交历史也是一条直线。
以上就是 rebase
的详细介绍了,现在让我们来回顾一下:
- rebase操作可以把本地未
push
的分叉提交历史整理成直线 - rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比
到目前,关于使用 git
进行协作开发的基本内容都介绍完了。