git merge应该只用于为了保留一个有用的,语义化的准确的历史信息,而但愿将一个分支的整个变动集成到另一个branch时使用。这样造成的清晰版本变动图有着重要的价值。git
全部其余的状况都是以不一样的方式使用rebase的适合场景:经典型方式,三点式,interactive和cherry-picking.shell
一个GIT用户的很是重要的技能是他们必须可以维护一个清晰的语义化的暴露给大众的变动历史。为了达到这个目的,他们必须依赖于四个主要的工具:安全
我常常看到人们将merge和rebase都堆放到一个篮子里,这里说明人们存在的广泛的误解:”获取别的branch的commits到个人branch上“。网络
可是实际上,这两个命令实际上并无什么共通之处。他们有着彻底不一样的目的,而且实际上应用他们的缘由也是彻底不一样的!less
我将不只要highlight出来他们各自的role,并且给你足够的知识和最佳实践技能以便你暴露给公众的历史信息不只是表意的,并且是语义化的(经过查看版本变动历史图就应该可以明显地反映出团队的不一样的开发的目的)。而这个组织良好的历史信息对于团队的价值也是明显的:好比有新的团队成员加入,或者过一段时间再回来维护项目,或者对于项目的管理,code review等等。。编辑器
正如merge的名字所隐含的意思:merge执行一个合并,或者说一个融合。咱们但愿在当前分支上往前走,因此咱们须要融合合并其余分支的工做,从而放弃其余的分支。分布式
你须要问你本身的问题是:这个其余分支到底表明了什么??ide
若是答案是yes,那么, it is not only useless but downright counter-productive for this branch to remain visible in the history graph, as an identifiable “railroad switch.”工具
若是merge的目标分支(好比说master分支)在这个分支建立后又往前走了,也就是说master分支(头tip)已经再也不是这个临时local分支的直接祖先了,咱们会认为咱们这个local分支too old了,因此咱们每每须要使用git rebase命令来在master的tip上从新运行咱们local分支上的commit以便保持一个线性的历史。可是若是master分支在咱们建立local分支以后一直没有改变,那么一个fast-forward merge就是足够的了。fetch
在这种状况下,咱们的这个分支可能表明了一个sprint或者说user story的实现过程,或者说表明了咱们的一个bug fix过程。
Is is then preferable, perhaps even mandatory, that the entire extent of our branch remain visible in the history graph. This would be the default result if the receiving branch (say master) had moved ahead since we branched out, but if it remained untouched, we will need to prevent Git from using its fast-forward trick. In both these cases, we will always use merge, never rebase.
正如他的名字所隐含的意思:rebase存在的价值是:对一个分支作“变基”操做,这意味着改变这个branch的初始commit(咱们知道commits自己组成了一颗树)。它会在新的base上一个一个地运行这个分支上的全部commits.
这一般在当本地的工做(由一些列的commits组成)被认为是在一个过期的base基础上作的工做的时候才须要用它。这可能天天都会有几回这样的场景出现,好比当你试图将local commits push到一个remote时而由于tracking branch(好比说origin/master)过于陈旧而被拒绝时(缘由是自从咱们上次和origin同步(经过git pull)后别的同事已经作了不少工做而且也push到了origin/master上):这种状况下,若是咱们强行将咱们的代码push过去将会覆盖咱们其余同事的并行工做成果。而这,每每是不容许的,因此push总会给出提示。
一个merge动做(每每pull就会内置执行这个merge动做)在这种状况下并非很好的应用场景,由于merge会产生一些杂乱的历史遗迹。
另一个对rebase的需求多是:好久之前你曾经启动过一个并行的工做(好比作一些实验,作一些r&d工做),可是一直没有时间就耽搁了下来,如今又有了时间来作这件事情的时候,而这个时候你的R&D工做的base可能已经很是落后了。当你再次来继续这个工做时,你必定但愿你的工做是在一个新的bas基础上来进行,以便你能够从已经解决的bugfix或者其余新的ready功能中获益。
最后还有一种场景:其实是更频繁的场景:实际上并非变基,而是为了清理你的分支上commits。
在使用git时,咱们一般很是频繁地向repo中作commit,可是咱们的commit自己每每是零散的不连续的,好比:
上面的各类行为只要是保留在local repo中,这是没有问题的,也是正常的,可是若是为了尊重别人同时也为了本身未来可以返回来我绝对避免将这些杂乱的历史信息push到remote上去。在我push以前,我会使用git rebase -i的命令来清理一下历史。
永远不要rebase一个已经分享的分支(到非remote分支,好比rebase到master,develop,release分支上),也就是说永远不要rebase一个已经在中央库中存在的分支.只能rebase你本身使用的私有分支
上面这个例子中展现了已经在中央库存在的feature分支,两个开发人员作了对feature分支针对master作rebase操做后,再次push而且同步工做带来的灾难:历史混乱,而且merge后存在多个彻底相同的changeset。
在执行git rebase以前,老是多问问你本身:“有没有其余人也须要这个分支来工做?”,若是答案是yes,那么你就须要思考必须使用一种非破坏性的方式来完成rebase同样的工做(就是须要合入别人的工做成果),好比使用git revert命令。不然,若是这个branch没有别人来使用,那么很好,你能够很是安全地为所欲为地re-write history(注意rebase每每会重写历史,全部已经存在的commits虽然内容没有改变,可是commit自己的hash都会改变!!!)
可是咱们要注意,即便对于上面的这个已经分享的feature分支,Bob和Anna也能够互相rebase对方的feature分支,这并不违反上面强调的rebase黄金定律,下面用图例再说明一下:
假如你和你的同事John都工做在一个feature开发上,你和他分别作了一些commit,随后你fetch了John的feature分支(或者已经被John分享到中央库的feature分支),那么你的repo的版本历史可能已是下面的样子了:
这时你但愿集成John的feature开发工做,你也有两个选择,要么merge,要么rebase,
记住在这个场景中,你rebase到John/feature分支的操做并不违反rebase的黄金定律,由于:
只有你的local本地私有(还未push的) feature commits被移动和重写历史了,而你的本地commit以前的全部commit都未作改变。这就像是说“把个人改动放到John的工做之上”。在大多数状况下,这种rebase比用merge要好不少
咱们在rebase本身的私有分支后但愿push到中央库中,可是却会因为rebase改写了历史,所以push时确定会存在冲突,从而git拒绝你的push,这时,你能够安全地使用-f参数来覆盖中央库的历史(同时其余对这个feature也使用的人员能够git pull):
git push --force
下面的几个心法是你在使用git时必须磨砺在心的,在本文的后面,咱们将具体说明哪些命令来负责执行这些心法:
1. 当我须要merge一个临时的本地branch时。。。我确保这个branch不会在版本变动历史图谱中显示,我老是使用一个fast-forward merge策略来merge这类branch,而这每每须要在merge以前作一个rebase;
2.当我须要merge一个项目组都知道的local branch时。。。我得确保这个branch的信息会在历史图谱中一直展现,我老是执行一个true merge;
3.当我准备push个人本地工做时。。。我得首先清理个人本地历史信息以便我老是push一些清晰易读有用的功能;
4.当个人push因为和别人已经发布的工做相冲突而被拒绝时,我老是rebase更新到最新的remote branch以免用一些无心义的micro-merge来污染历史图谱
前面讲过,你只有在须要合并融入一个分支所提供的全部feature时才作merge。在这时,你须要问你的核心的问题是:这个分支须要在历史图谱中展现吗?
当这个分支表明了一个团队都熟知的一块工做时(好比在项目管理系统中的一个task,一个关联到一个ticket的bugfix,一个user story或者use case的实现,一个项目文档工做等),那么在这种状况下,咱们应该将branch的信息永远留存在产品历史图谱中,甚至即便branch自己已经被删除。
不然,若是不表明一个well-known body of work,那么branch自己仅仅是一个技术意义上的实体,咱们没有理由将它呈如今产品历史图谱中。咱们得使用一个rebase+fast-forward merge来完成merge。
咱们来看看上面两种场景分别长什么样:
咱们假设咱们一个乘坐oauth-signin的feature branch,该branch的merge 目标是master.
若是master分支在oauth-signin分支从master建立后又往前走了一些commits(这多是因为其余的branch已经merge到了master,或者在master上直接作了commit,或者有人在master上cherry-picked了一些commits),那么这时在master和oauth-signin之间就产生了分叉(也就是说master不可能在不会退的状况下直接到oauth-signin)。在这种状况下,git将会自动地产生一个"true merge"
这是咱们要的也是咱们但愿的,并不须要任何额外工做。
然而,若是master在oauth-signin建立后并未向前走,后者就是master的直接后代(无分叉),这时GIT默认地在merge时是执行一个fast-forward的merge策略,git并不会建立一个merge commit而是简单地把master分支标签移动到oauth-signin分支tip所指向的commit。这时oauth-sigin分支就变成了一个"透明"的分支了:在历史图谱中没法得知oauth-signin分支的起始位置在哪里,而且一旦这个branch被删除,那么从历史上咱们再也没法看到任何关于这个开发分支曾经存在的历史渊源。
这不是咱们所想要的,因此咱们经过强制git产生一个真正的merge---经过使用--no-ff参数(no fast forward的意思)。
这是相反的状况:咱们的branch由于没有任何实质语义,因此咱们不但愿它在历史图谱中存在。咱们必须确保merge会使用fast-forward策略。
咱们假设咱们有一个仅仅为了开发的安全性起了一个local branch命名为quick-fixes,而master仍然是要merge到的目标分支。
若是master在quick-fixes建立以后再也没有往前走,咱们知道git会产生一个fast-forward的merge:
另外一方面,若是master在quick-fixes建立后又往前走了的话,咱们若是直接merge的话git会给咱们一个true merge,产生一个merge commit,那么咱们的branch就会污染历史图谱,这不是咱们想要的。
在这种状况下,咱们要作的事调整quick-fixes分支使得它从新成为master分支的直接后代(也就是再也不分叉),这样就能够fast-forward merge了。要完成这个目的,咱们须要使用git rebase命令。咱们但愿经过更改quick-fixes分支的base commit,以便它的base commit再也不是master的老tip,而是当前的tip(注意tip是随着commit的不断引入而不断往前移动的!)。这个动做会重写quick-fixes分支的历史,因为quick-fixes彻底是本地分支,重写历史是可有可无的。
在这里咱们特别要注意这个场景是如何运做的:
1.咱们有一个分叉过的分支可是咱们但愿透明化地merge,因此。。。
2.咱们首先变基到master的最新commit;
3.咱们随后到master上,执行merge命令就产生一个fast-forward
注意:我这里额外提醒一下,实际上咱们看到上面的word1,word2,word3的commit可能仍是不爽,咱们在第3.步骤中可使用git merge quick-fixes --squash,来说全部的word1,2,3都合并成一个commit;
若是在练习上面的操做时,你发现git并未如你所愿,你须要检查一下git对于merge的一些默认配置。
好比:branch.master.mergeoptions = --no-ff/merge.ff=false或者branch.master.mergeoptions=--ff-only/merge.ff=only
有时候你建立一个feature分支开始工做后可能很长时间没有时间再作这个feature开发,当你回来时,你的feature分支就会缺失不少master上的bugfix或者一些其余的feature。在这种个状况下,咱们先假设除了你没有其余人在这个分支上工做,那么你能够rebase你的feature分支:
git rebase [basebranch] [topicbranch] 注意这时git rebase的参数顺序,第一个为基分支,第二个为要变基的分支
(master) $ git rebase master better-stats
注意:若是那个feature分支已经被push到remote了的话,你必须使用-f参数来push它,以便你覆盖这个分支的commits历史,这时覆盖这个branch历史也无所谓,由于历史的全部commits都已经相应从新生成了!!。(一个分支的历史由分支的起始commit和头tip commit来描述.有一点须要注意:一旦咱们作一次rebase后,那么这个分支上的全部commit因为此次变基,其commit HASH都会改变!!)另外须要注意咱们只能对private分支作这个rebase而且git push --force操做!!
若是你正确地使用git,相信咱们都会频繁地作一些原子commit.咱们也要铭记如下警句:不要落入SVN人员的行为模式:commit+push,这是集中式版本控制系统的最多见工做模式:每个commit都当即push到server上。
事实上,若是那样作的话,你就失去了分布式版本控制系统的灵活性:只要咱们没有push,咱们就有足够的灵活性。全部咱们本地的commits只要没有push都是咱们本身的,因此咱们有彻底的自由来清理这些commit,甚至删除取消某些commits。为何咱们要那么每一个commit都频繁地Push从而失去咱们应该有的灵活性呢?
在一个git的典型工做流中,你天天可能会产生10到30个commit,可是咱们每每可能只会push 2到3次,甚至更少。
再次重申:在push以前,我应该清理个人本地历史。
有不少缘由会致使咱们的本地历史是混乱的,前面已经说起,可是如今还想再说一遍:
这些场景都会致使一个混乱的历史产生,很是难以阅读,难以理解,难以被他人所重用,注意:这里的他人也多是你本身哦,想一想两个月后你再来看这段代码吧。
幸运的是,git给你提供了一个漂亮的方法来不用花什么精力就能理清你的本地历史:
1. reorder commits;
2. squash them together;
3.split one up(trickier)
4.remove commits altogether;
5.rephrase commit messages
interactive rebasing就和普通的rebase很相像,它给你一个个地选择编辑你的commit的机会。
在咱们当下rebase -i的情形,rebase操做自己并不会实际的真真实实地变基。rebase -i操做仅仅会重写历史。在天天的工做场景中,可能那个分支已经在你的远端库中存在(也就是说已经发布了),你须要作的是清理自从最近一次git pull以后的全部local commits。假设你正在一个experiment分支。你的命令行多是这样的:
(experiment) $ git rebase -i origin/experiment
在这里你在rebase你的当前分支(experiment分支)到一个已经存在历史中的commit(origin/experiment).若是这个rebase不是interactive的话,那么这个动做是毫无心义的(实际上会被做为一个短路的no-op).可是正是有了这个-i选项,你将能够编辑rebase将要执行的这个脚本化过程。那个脚本将会打开一个git editor,就像咱们在commit提交时弹出的编辑框同样。
好比你在dev分支为了一个小的功能或者idea连续提交了多个commits,这时你但愿将这些commit合并为一个以避免污染了git的历史信息,这时,你能够作的就是:找到这些个commits以前最先的basecommit,运行上述命令,
其结果就是将这些commits合并而且放到basecommit之上
若是你但愿为那个工做过程建立一个alias,正如就像在push以前的条件反射同样,你可能并不想敲那个base参数。因为base每每就是当前分支的remote tracked branch,你可使用@{u}特殊语法(好比@{upstream}):
git config --global alias.tidy "rebase -i @{upstream}" (experiment) $ git tidy
咱们给出下面的snapshot做为后续行文展开的基础,你们能够看到从origin/experiment到本地experiment已经通过了从057ad88...2863a46共6个commit.
咱们但愿在push咱们的本地experiment以前来清理这些commits:
(experiment) $ git rebase -i origin/experiment
这时会弹出git editor展现出下面的script供咱们处理:
pick 057ad88 Locale fr-FR pick ef61830 Opinion bien tranchée pick 8993c57 ML dans le footer + rewording Interactive Rebasing pick dbb7f53 Locale plus générique (fr) pick c591fd7 Revert "Opinion bien tranchée" pick 2863a46 MàJ .gitignore # Rebase 34ae1ae..2863a46 onto 34ae1ae # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
默认状况下,这就是一个一般意义上的rebase: 将这个列表中的commits顺序采摘(cherry-picking),须要注意的是这个列表是按时间顺序排列的(不像git log,默认状况下最新的老是列在最上面)。
正如其余任何git里面的基于文本编辑器的操做:留空或者注释掉一行就会取消那行的操做。
咱们来详细看看各类不一样的use case:
Removing commits: 咱们只须要删除那个commit所在的行便可;
Reordering commits: 咱们只须要从新排序这些commits 行便可。然而注意实际结果最终是否成功并不能保证:若是commit B的changeset依赖于由commitA引入的代码,那么若是颠倒他们的顺序明显地将会产生问题。
Rewording commit messages:因为编辑错误,表达清晰度不够等缘由,咱们可能须要改写commit附带的消息。
squash commits together: squash这个术语包含着:将变动集和commit message糅合在一块儿的意思。
split a commit:
咱们使用两个commit来介绍所使用的场景:若是咱们简单地删除第一个commit是不行的:由于第二个commit将找不到它本身的changeset对应的code context,从而cherry-pick可能失败。在这里咱们要作的是将这两个commits打包squash起来。
要实现这一点,咱们从新组织这个script连贯化:
pick 057ad88 Locale fr-FR pick dbb7f53 Locale plus générique (fr)
因为咱们如今不想使用squash方法,咱们使用fixup选项:
reword 057ad88 Locale fr-FR fixup dbb7f53 Locale plus générique (fr) …
在这个特定情形下,起初的第一个commit消息不是很精确,因此咱们使用reword来改变第一个commit.
咱们再来看看下面的场景:
git checkout feature git rebase -i HEAD~3 //或者使用下面的命令先列出feature分支拉出来时在master上的那个commit $ git merge-base develop master f96e3c4057cfe2713619d3050ad9c1a3943ae8cb Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop) $ git rebase -i f96e3c4057cfe2713619d3050ad9c1a3943ae8cb [detached HEAD 3c63e67] dev develop.c line1 Date: Fri Apr 1 14:43:01 2016 +0800 2 files changed, 4 insertions(+) create mode 100644 develop.c Successfully rebased and updated refs/heads/develop. Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop) $ git lg * 3c63e67 (HEAD -> develop) dev develop.c line1 | * 11bddec (master) master branch updated |/ | * 4922bbd (hotfix) hotfix added | * 18515e8 dev develop.c line2 | * c8bc641 (origin/develop) dev dev.c line2 before merged with updated l.git | * 61025fc dev develop.c line1 |/ * f96e3c4 (origin/master, origin/HEAD) dev mod a.c line3 * 6bdb183 dev dev.c line1 * 9e6c445 a.c line3 in l.git and l1.c new added * 227eb73 l add a.c line2 * af23226 l add a.c line1 Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop)
到如今咱们到了最后一个reabase相关的话题: git pull
若是咱们在一个分支上不须要协同,一切都很是简单:咱们全部的git push都能成功,不须要频繁地git pull操做。可是只要有其余人一样在咱们共同的分支上工做(这其实是很是常见的场景),咱们就很是广泛地碰到下面的障碍:在咱们的最近一次同步(使用git pull)和咱们须要发布local history(要使用git push)的这个时刻,另一个同事已经分享了他们的工做到中央库上面,因此remote branch(好比说origin/feature分支上) 是比咱们的本地拷贝要更新一些。
这样,git push的时候,git就会拒绝接受(由于若是接受就会丢失历史)
(feature u+3) $ git push To /tmp/remote ! [rejected] feature -> feature (fetch first) error: failed to push some refs to '/tmp/remote' hint: Updates were rejected because the remote contains work hint: that you do not have locally. This is usually caused by hint: another repository pushing to the same ref. You may want hint: 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' hint: for details. (feature u+3) $
在这种状况下,大多数人都会接受git的建议,先作一次git pull,随后再git push.这看起来没有问题,可是仍是有些须要思考的:
pull实际上包含了两项顺序执行的操做:
1. 将local copy repo和remote repo作一次网络同步。这实际上就是一次git fetch,也只有此次咱们须要有和remote repo的网络链接;
2.默认的,一个git merge操做(将remote tracked branch merge到咱们的local trakcing branch,好比说orgin/featurex->featureX)
为了便于演示,咱们假设若是我当前在feature分支上,而它的remote track branch是origin/feature,那么一个git pull操做就等效于:
1. git fetch;2.git merge origin/feature
因为我有了local的变动,而remote又有另外的一些变动,这样因为local 和 remote有了分叉,所以git pull中的merge就会产生一个真实的merge,就像咱们以前看到过的同样,咱们的repo库的历史图谱就会像下面这个样子:
而这明显和咱们一直宣导的原则:一个merge动做表明了咱们将一个well-known branch须要合并融入主流的动做,而不是一次繁文缛节的技术动做!
在这里,咱们运气确实不是那么好:有人在咱们push以前抢先push了代码。在一个理想的场景中,他们可能push的更早一些(在咱们最后一个git pull以前发生生的),或者push的更晚一些(在咱们push以后才push的)的话,咱们的历史图谱就依然可以保持线性的。
而这种在执行一个git pull操做动做时保持这个分支历史信息的线性化,每每是咱们但愿达到的。而要达到这一点,咱们惟一须要作的就是要求git pull操做时不要执行merge操做,而是执行rebase操做,因此git pull执行的结果就是让你的local commits一个一个地在新拉下来的base基础上从新run一遍。
咱们能够经过git pull --rebase来明确要求git,可是这不是一个很可靠的解决方案,由于这须要咱们在git pull操做时时时保持警戒,但这每每并不太可能,由于只要是人就容易犯错误。
上面这个案例要求git pull使用git rebase,而不是merge.虽然很cool,可是往外容易丢三落四犯错误。
咱们能够经过一个配置选项来保证咱们不会忘记这件事儿(要求git pull时使用rebase而不是merge),这个配置能够在branch级别(branch.feature.rebase = true),或者global级别,好比pull.rebase=true.
从GIT1.8.5开始,有另一个更好的配置选项,为了理解为何说更好,咱们须要了解一下pulling over a local history that includes a merge的问题。
默认地,rebase操做会作inline merge.咱们既然但愿确保咱们的merge有清晰的语意,那么这种inlining实在是使人讨厌,咱们来看看下面的场景:local master已经有过一次merge动做(fast forward),随后咱们再作git pull --rebase,获得的历史图谱:
在这种状况下,git pull --rebase以后,咱们只有了线性历史,看得咱们头晕目眩。为了阻止这种行为,咱们须要使用--preserve-merges(或者是-p)参数。然而在1.8.5版本以前,并不存在这样的选项。这个选项可使用git pull --rebase=preserve来调用,或者直接卸载配置选项中
pull.rebase = preserve
在上图中,咱们使用pull with rebase策略,可是却保留了local merge,很是清爽!