目录
分支操作
分支合并
merge:Fast forward、no-ff、squash
rebase
cherry-pick
分支冲突
简单分支管理的冲突解决
多人协作时的冲突解决
远程分支
高级合并
分支开发工作流:长期分支、特性分支
分支:本质是指向提交对象的可变指针,实质上是包含所指提交对象校验和的文件。创建分支即为相应的提交对象创建一个可以移动的新指针,相当于往一个文件中写入41个字节(40个字符的哈希值和1个换行符)
HEAD指针:指向当前所在的本地分支(指向指针的指针或别名)
创建分支:git branch
查看分支:git branch
-a:查看所有分支(本地、远程)
-r:查看远程分支
-v:查看每个分支的最后一次提交
--merged或--no-merge:过滤已经或者尚未合并到当前分支的分支
-f:强制移动分支位置
切换分支:git checkout
创建+切换分支:git checkout -b
合并指定分支到当前分支:git merge
删除分支:git branch -d
分支重命名:git branch -m oldname newname
➜ clone_repository git:(master) git branch //先查看当前分支
➜ clone_repository git:(master) git checkout -b dev //创建分支dev并转换到dev
Switched to a new branch 'dev'
➜ clone_repository git:(dev) git branch //再次查看分支
➜ clone_repository git:(dev) echo "change file1 on branch dev" >> file1 //在dev修改file1
➜ clone_repository git:(dev) ✗ git add file1 //在dev分支add
➜ clone_repository git:(dev) ✗ git commit -m "change file1 on dev" //在dev提交
[dev f415b36] change file1 on dev
1 file changed, 1 insertion(+)
➜ clone_repository git:(dev) git checkout master //回到master分支
Switched to branch 'master'
Your branch is up to date with 'origin/master'. //origin与master是一致的(因为是在dev修改,master没有修改)
➜ clone_repository git:(master) cat file1 //可以看到master分支中file1没变
new file in the clone repository
➜ clone_repository git:(master) git merge dev //将dev分支与master合并
Updating d87fc27..f415b36
Fast-forward
file1 | 1 +
1 file changed, 1 insertion(+)
➜ clone_repository git:(master) git branch -d dev //删除dev分支
Deleted branch dev (was f415b36).
在git中合并分支有三种方法:merge,rebase,cherry-pick
Fast forward:当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,则合并时直接将指针向前推进(指针右移),因为没有需要解决的分歧——这就叫做 “快进(fast-forward)”
三方合并:虽然出现分叉,但是各自改动的内容不冲突时,git会使用两个分支的末端所指的快照以及这两个分支的共同祖先,做一个简单的三方合并,即做一个新的快照并自动创建一个新的合并提交指向它
no-ff:禁用快速合并。接受被合并的分支上的所有工作,在master分支新建一个commit节点完成合并
squash:在当前分支新建一个commit节点,与no-ff唯一区别是不保留对合入分支的引用
git merge --no-ff branch:no-ff模式合并
git merge --squash branch:--squash选项接受被合并的分支上的所有工作,并将其压缩至一个变更集,使仓库变成一个真正的合并发生的状态,不会真的生成一个合并提交(即没有第二父提交)
git merge --abort:退出合并,尝试恢复到运行合并命令前到状态,当工作目录中有未储藏、未提交当修改时,不能完美处理
➜ clone_repository git:(master) git merge dev // fast-forward
Updating bb3bca3..d4ed2c2
Fast-forward
file | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
回退
➜ clone_repository git:(master) git reset --hard HEAD^
...
➜ clone_repository git:(master) ✗ git merge --no-ff dev // no-ff,//需要交互式地输入commit信息
Merge made by the 'recursive' strategy.
file | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
➜ clone_repository git:(master) git merge --no-ff -m "merge with no-ff" dev //带上-m选项在merge同时创建commit节点
Merge made by the 'recursive' strategy.
file | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
将当前分支的修改应用到变基操作的目标基底分支上,操作实质上丢弃一些现有的提交,然后相应地建立一些内容一样但是实际上不同的提交
变基的原则:不要对在你的仓库外有副本的分支执行变基。(只在从未推送至公共仓库的提交上执行变基命令)
因为别人可能基于这个分支的提交进行了后续工作,若此时应用变基会丢弃对方开发所基于的某些提交,若出现此情况,可以利用变基解决,即将自己的分支变基到对方变基后到分支上,或者用git pull --rebase命令
(1)正常rebase:没有冲突,比如两个分支不修改同一个文件,比如各自分支建立新文件或修改不同文件
➜ clone_repository git:(dev) touch dev_file
➜ clone_repository git:(dev) ✗ git add dev_file
➜ clone_repository git:(dev) ✗ git commit -m "dev add dev_file"
[dev 0083bea] dev add dev_file
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev_file
➜ clone_repository git:(dev) vi dev_file
➜ clone_repository git:(dev) ✗ git add dev_file
➜ clone_repository git:(dev) ✗ git commit -m "dev change dev_file"
[dev ff56952] dev change dev_file
1 file changed, 1 insertion(+)
➜ clone_repository git:(dev) git checkout master
Switched to branch 'master'
➜ clone_repository git:(master) touch master_file
➜ clone_repository git:(master) ✗ git add master_file
➜ clone_repository git:(master) ✗ git commit -m "master_file"
[master 89deaa4] master_file
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master_file
➜ clone_repository git:(master) git log --graph --oneline --all //分支出现分叉
➜ clone_repository git:(master) git checkout dev
Switched to branch 'dev'
➜ clone_repository git:(dev) git rebase master //到dev变基
First, rewinding head to replay your work on top of it...
Applying: dev add dev_file
Applying: dev change dev_file
➜ clone_repository git:(dev) git log --graph --oneline --all //变基后的分支
➜ clone_repository git:(dev) git checkout master
Switched to branch 'master'
➜ clone_repository git:(master) git merge dev //快进合并将master移动到最新提交
Updating a7eaae4..98f52bb
Fast-forward
dev_file | 1 +
1 file changed, 1 insertion(+)
create mode 100644 dev_file
➜ clone_repository git:(master) git log --graph --oneline --all //merge后的master
(2)有冲突的rebase:
➜ clone_repository git:(master) git checkout -b dev //在dev修改file:改了最后一行
Switched to branch 'dev'
➜ clone_repository git:(dev) vi file
➜ clone_repository git:(dev) ✗ git add file
➜ clone_repository git:(dev) ✗ git commit -m "dev change file"
[dev 29eaa63] dev change file
1 file changed, 1 insertion(+), 1 deletion(-)
➜ clone_repository git:(dev) git checkout master //在master修改file:删了最后一行
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
➜ clone_repository git:(master) vi file
➜ clone_repository git:(master) ✗ git add file
➜ clone_repository git:(master) ✗ git commit -m "master change file"
[master 9de3c25] master change file
1 file changed, 1 deletion(-)
➜ clone_repository git:(master) git log --oneline --decorate --graph --all //查看分支可知分叉了
➜ clone_repository git:(master) git checkout dev //回到dev变基
Switched to branch 'dev'
➜ clone_repository git:(dev) git rebase master
First, rewinding head to replay your work on top of it...
Applying: dev change file
Using index info to reconstruct a base tree...
M file
Falling back to patching base and 3-way merge...
Auto-merging file
CONFLICT (content): Merge conflict in file //提示出现冲突
error: Failed to merge in the changes.
Patch failed at 0001 dev change file
hint: Use 'git am --show-current-patch' to see the failed patch //提示用此命令查看失败的补丁(修改)
Resolve all conflicts manually, mark them as resolved with
"git add/rm ", then run "git rebase --continue". //手动解决冲突然后git add或者git rm删除冲突文件后再git rebase --continue
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
➜ clone_repository git:(9de3c25) ✗ git am --show-current-patch //查看failed patch
➜ clone_repository git:(9de3c25) ✗ vi file //手动解决冲突
➜ clone_repository git:(9de3c25) ✗ git add file //mark them as resolved
➜ clone_repository git:(9de3c25) ✗ git rebase --continue //解决冲突后继续rebase
Applying: dev change file
➜ clone_repository git:(dev) git log --oneline --decorate --graph --all //查看变基后的分支图
➜ clone_repository git:(dev) git checkout master
Switched to branch 'master'
➜ clone_repository git:(master) cat file //回到master分支可知file停留在rebase的状态(对应pro git page 84的C3状态)
new file in the clone repository
➜ clone_repository git:(master) git merge dev //快进合并后,master于dev的提交同步,见下图4
Updating 9de3c25..6ab9a0b
Fast-forward
file | 1 +
1 file changed, 1 insertion(+)
"复制"一个提交节点并在当前分支做一次完全一样的新提交
git cherry-pick
git cherry-pick -x
git cherry-pick
git cherry-pick
出现冲突:在两个不同分支中,对同一个文件的同一个部分进行了不同的修改,此时git无法自动干净地合并分支,就必须先解决冲突,然后再提交,合并完成。
解决冲突:把Git合并失败的文件手动编辑为我们希望的内容,再提交。
当我们在master和feature1两个分支上都对同一个文件进行了修改提交后,
解决办法:
A. 在文件中选择一个版本保留,删除有冲突不需要保存的部分内容后,保存文件
B. 再次提交(add,commit)
当你的小伙伴将本地dev上的一个文件push到origin/dev上后,而碰巧你也修改了同一个文件打算push到远程,但此时会和你的小伙伴的提交有冲突。
解决办法:
A.用git pull把最新的提交从origin/dev上抓取下来(第一次pull可能会失败,git可能会提示你没有指定本地的dev和origin/dev的链接,一般根据提示采用$ git branch--set-upstream dev origin/dev 建立链接,再次使用gtit pull)
B.现在你已经成功的拉取了远程最新的提交,但是合并有冲突,需要像上一例中手动解决,在本地合并再次提交(commit)
C.提交成功后就可以push到远程啦
➜ clone_repository git:(master) git branch feature1 //新建分支feature1
➜ clone_repository git:(master) git checkout feature1 //切换分支
Switched to branch 'feature1'
➜ clone_repository git:(feature1) echo "Creating a new branch is quick AND simple." >> file1 //在feature1修改并提交
➜ clone_repository git:(feature1) ✗ git add file1
➜ clone_repository git:(feature1) ✗ git commit -m "AND simple"
[feature1 18a91fc] AND simple
1 file changed, 1 insertion(+)
➜ clone_repository git:(feature1) git checkout master //回到master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
➜ clone_repository git:(master) echo "Creating a new branch is quick & simple." >> file1 //在master修改
➜ clone_repository git:(master) ✗ git add file1
➜ clone_repository git:(master) ✗ git commit -m "& simple"
[master 6f99fd1] & simple
1 file changed, 1 insertion(+)
➜ clone_repository git:(master) git merge feature1 //试图合并
Auto-merging file1
CONFLICT (content): Merge conflict in file1 //产生冲突
Automatic merge failed; fix conflicts and then commit the result.
➜ clone_repository git:(master) ✗ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add ..." to mark resolution)
both modified: file1 //两边都修改了的file1
no changes added to commit (use "git add" and/or "git commit -a")
➜ clone_repository git:(master) ✗ vi file1 //这种情况可以根据需求选择master上的内容或者feature1的内容,或者是用全新的内容来代替,然后再add和commit
➜ clone_repository git:(master) ✗ git add file1
➜ clone_repository git:(master) ✗ git commit -m "conflict fixed"
[master 8db0d94] conflict fixed
➜ clone_repository git:(master) git log --graph --pretty=oneline --abbrev-commit 显示分支合并情况
远程引用:对远程仓库对引用(指针),包括分支、标签等
远程分支相关命令:
新建远程分支
git push origin br1:br2(先新建本地分支,然后推送至远程分支,不过此时br1并没有跟踪远程的br2)
查看分支:git branch(以下选项可组合)
-a:所有分支
-r:远程分支
-vv:本地分支及其跟踪的分支
删除远程分支
git branch -d -r [remote/branch] //eg:origin/test
git push origin --delete [branch] //删除远程分支。从服务器上移除这个指针,服务器一般会保留数据一段时间直到垃圾回收,因此容易恢复
git push origin :[branch] //借用引用规格删除远程分支(origin后面有个空格)
git fecth:拉取远程仓库有而本地没有的数据(拉取到本地的远程跟踪分支remote/branch上),不会自动修改、合并当前工作目录的内容,需要手动合并
git pull:自动抓取然后合并远程分支到当前分支,大多数情况下其作用为git fetch紧跟一个git merge。查找当前分支所跟踪的远程分支,抓取数据尝试合并
git push
git push [remote] [branch]:将当前本地分支推送到远程仓库
git push [remote] [branch1]:[branch2] :将本地的 branch分支推送到远程仓库上的 branch2 分支
远程跟踪分支:以remote/branch命名的本地分支,是远程分支状态的引用,记录的是上一次与远程仓库通信时,各个远程分支的状态。当本地与远程进行通信操作(fetch,push,pull)时,它会自行移动。clone远程仓库时,git的clone命令会自动将仓库命名为origin,拉取它的所有数据,创建一个在本地名为origin/master的远程跟踪指针,指向origin仓库的master分支,同时git也会给你一个本地master分支,与origin的master分支指向同处。
跟踪分支:从一个远程跟踪分支检出一个本地分支会自动创建“跟踪分支”,它是与远程分支有直接关系的本地分支,在跟踪分支上使用git pull命令时,git能自动识别去哪个服务器上抓取、合并到那个分支。当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支
git checkout -b [bra] [remote/brb]:创建一个跟踪remote/brb的分支bra
git branch --set-upstream-to=[remote/brb] [bra] : 显式地设置已有的本地分支bra 跟踪远程分支brb,若不指定bra则设置当前分支跟踪brb
git branch -u [remote/brb] [bra]:修改bra正在跟踪的上游分支为brb,若不指定bra则修改当前分支的上游分支
git branch -vv:查看设置的所有分支,会列出所有本地分支,给出其所跟踪的远程分支,以及二者之间领先落后关系,由于对比的是从服务器最后一次抓取的数据,所以使用此命令前需要抓取所有远程仓库:git fetch --all; git branch -vv
➜ test git:(master) git branch -a //当前所有分支
➜ test git:(master) git branch -vv //当前本地分支及其所跟踪分支的情况(下图2:master跟踪远程master,teset没有跟踪远程分支)
➜ test git:(master) git checkout -b dev origin/dev //创建跟踪origin/dev的本地dev分支
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
Switched to a new branch 'dev'
➜ test git:(dev) git branch -vv //新增dev跟踪origin/dev
➜ test git:(dev) git branch --set-upstream-to=origin/master test //设置test的上游分支
Branch 'test' set up to track remote branch 'master' from 'origin'.
➜ test git:(dev) git branch -vv //test跟踪origin/master且落后一个提交
➜ test git:(dev) git branch -u origin/master //修改dev跟踪origin/master
Branch 'dev' set up to track remote branch 'master' from 'origin'.
➜ test git:(dev) git branch -u origin/dev test //修改test跟踪origin/dev
Branch 'test' set up to track remote branch 'dev' from 'origin'.
➜ test git:(dev) git branch -vv //下图5:可见dev落后
➜ test git:(dev) git fetch //从origin/master拉取更新到dev的远程跟踪分支origin/master
➜ test git:(dev) git merge origin/master //本地dev分支merge远程跟踪分支origin/master拉取到到内容
Updating 9f16864..c2b4224
Fast-forward
file | 1 +
1 file changed, 1 insertion(+)
➜ test git:(dev) git branch -a -vv
➜ test git:(dev) git branch -d -r origin/dev //删除远程分支dev
Deleted remote-tracking branch origin/dev (was 9f16864).
➜ test git:(dev) git branch -a -vv
➜ test git:(dev) echo "1" >> file //在dev修改file并提交,此时dev比其远程分支提前一个提交,需要push
➜ test git:(dev) ✗ git commit -a -m "1"
[dev 50aa179] 1
1 file changed, 1 insertion(+)
➜ test git:(dev) git push //直接git push发生错误
fatal: The upstream branch of your current branch does not match //错因:上游分支和当前分支名字不匹配
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:master //要按照此格式push
To push to the branch of the same name on the remote, use
git push origin HEAD
To choose either option permanently, see push.default in 'git help config'.
➜ test git:(dev) git push origin dev:master
...
➜ test git:(dev) git status
On branch dev
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
------------------------------------------------------
➜ test git:(dev) git checkout master //现在回到master分支,注意dev和master都跟踪远程都master分支
Switched to branch 'master'
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded. //这里提示本地master落后了
(use "git pull" to update your local branch)
➜ test git:(master) echo "2" >> file //先不管master落后的事实继续修改file并提交
➜ test git:(master) ✗ git commit -a -m "2"
[master 08ad215] 2
1 file changed, 1 insertion(+)
➜ test git:(master) git push //master推送失败,因为当前分支behind远程部分,要git pull
To ssh://git.sankuai.com/~tangxiaoling02/test.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'ssh://[email protected]/~tangxiaoling02/test.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.
➜ test git:(master) git pull //由于dev和master都修改了file,pull发生冲突,需要手动解决
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
➜ test git:(master) ✗ vi file //修改冲突并提交
➜ test git:(master) ✗ git commit -a -m "file"
[master b11b337] file
➜ test git:(master) git push //再次push成功
...
做一次可能有冲突的合并前,尽量保证工作目录是干净的(有未完成的工作可以提交临时分支或者储藏起来)
忽略空白
git merge -Xignore-all-space:忽略所有已有的空白
git merge -Xignore-space-change:忽略空白修改
手动处理文件后再合并
进入合并冲突状态后,得到文件的自己版本(ours)、对方版本(theirs)、共同版本(base)的拷贝,想手动修复其中一个,并为其重新合并。
Git 在索引中存储了文件的自己版本、对方版本、共同版本,在 “stages” 下每一个都有一个数 字与它们关联。 Stage 1是它们共同的祖先版本(分叉开始的位置),stage 2是你的版本,stage 3来自于 MERGE_HEAD,即你将要合并入的版本(“theirs”)
git show :1:file > filecommon:可用此类似命令将三个版本的文件拷贝出来
git ls-files -u:得到这三个版本文件的Git glob对象的SHA-1值
(1)对要合并入的文件在真正合并前运行dos2unix程序:dos2unix their(因为待合入的分支对文件做了unix2dos修改,所以这里手动修复以消除空白字符的改动)
(2)手动修复后重新合并文件:git merge-file -p ourfile commonfile theirfile > mergefile
git diff --ours:比较合并结果与在你分支的文件版本的区别
git diff --theirs:比较合并结果与在对方分支的文件版本的区别
git diff --base:查看文件在两边是如何改动的
一下三幅图片在书中的场景是:在whitespace分支将Unix换行符改为Dos换行符(都是空白字符)并把“world”改为“mundo”;在master分支加了“# prints out a greeting”
这里的-b用来去除空白
检出冲突
git checkout --conflict:重新检出文件并替换合并冲突标记
git checkout --conflict=diff3 filename:给出ours、theirs、base版本的冲突标记,如下图(--conflict=merge为默认选项)
git checkout --ours:无需合并,仅留下ours的修改版本
git checkout --theirs:无需合并,仅留下theirs的修改版本
合并日志
利用日志回顾历史找出不同分支修改同一文件的原因。git log找出某一分支的所有独立提交,详见前面的双点、多点、三点
git log --oneline --left-right --merge:只显示任何一边接触了合并冲突文件的提交
git log -p:按补丁格式显示各个更新之间的差异
撤销合并
合并之后不想要引入的修改了
方法1:修复引用
git reset --hard HEAD~:重置分支指向。因为会改写历史,所以若有人已经使用了这个合并提交,或者合并后又有新的提交,不建议使用
方法2:还原提交
git revert:撤销某个提交的修改
此时若又后悔了,想再合并topic分支就找不到C3、C4,无法引入其修改了,即使在C4后面再有新提交C7,合并topic也只能引入C7的修改
解决这个问题:用 git revert ^M 撤销^M这个提交,然后再git merge topic合并分支。如下图2,此图中M和^M抵消了,^^M引入了C3、C4的修改,C8在^^M基础上引入了C7的修改
其他类型的合并
(1)发生冲突时直接选择一边
git merge -Xours branch
git merge -Xtheirs branch
(2)ours合并策略:记录一个以两边分支作为父节点的合并提交,但实质上不关心合并入的分支,简单的把当前分支的代码当作合并结果记录下来
(3)子树合并:git read-tree 读取一个分支的根目录树到当前到暂存区和工作目录中
长期分支:在整个项目开发周期的不同阶段,可以拥有多个开放的分支,可以定期地把某些特性分支合并入其他分支中。比如在master分支上保留完全稳定的代码,然后用develop或next等平行分支做后续开发或者稳定性测试,等这些分支达到稳定状态就可以合并入master分支。不同分支具有不同级别的稳定性
特性分支:短期分支,用来实现单一特性或其相关工作。将工作分散到不同流水线中,不同流水线中的每个分支都仅与其目标特性相关——使你能快速并完整地进行上下文切换