分支创建与切换

  • 创建一个分支可以使用git branch命令,比如可以使用这个语句创建一个testing分支,这会在当前所在的提交对象上创建一个指针:

    1
    git branch testing
  • 创建完testing分支后,情况如下:

    HEAD 指向当前所在的分支。

  • 使用git branch创建好分支后并不会自动切换到新的分支,切换到一个分支,可以使用git checkout命令,比如这条命令将会切换HEAD指针到testing分支:

    1
    git checkout testing
  • HEAD指针指向了testing分支

    HEAD 指向当前所在的分支。

  • 当然,Git也可以在创建分支的同时切换到当前分支:

    1
    git checkout -b <branch-name>

分支提交

  • HEAD当前指向testing分支,那么现在更新一些内容然后进行提交:

    1
    2
    vim test.rb
    git commit -a -m 'update testing'
  • 提交完成后,分支情况就变成现在这样:

    HEAD 分支随着提交操作自动向前移动。

  • 如果现在再将HEAD移动回master分支:

    1
    git checkout master

    检出时 HEAD 随之移动。

    这个时候,你的工作目录和你在开始修改tetsing分支之前一模一样。当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。

  • 现在在master分支下再进行一次修改并提交:

    1
    2
    vim test.rb
    git commit -a -m 'update master'

    项目分叉历史。

项目分叉历史

  • 查看当前项目的分叉历史,可以使用git log命令进行查看,它会输出提交历史、各个分支的指向以及项目的分支分叉情况:

    1
    2
    3
    4
    5
    6
    7
    $ git log --oneline --decorate --graph --all
    * c2b9e (HEAD, master) update master
    | * 87ab2 (testing) update testing
    |/
    * f30ab add feature #32 - ability to add new formats to the
    * 34ac2 fixed bug #1328 - stack overflow under certain conditions
    * 98ca9 initial commit of my project
  • Git的分支实质上仅是包含所指对象校验和(长度为40的SHA-1值字符串)的文件,所以创建和销毁分支都可以做到非常的高效。

删除分支

  • 使用带有-dgit branch命令可以删除分支:

    1
    git branch -d testing

合并分支

  • 假设当前已有master分支如下:

    一个简单的提交历史。

  • 现在,需要解决追踪系统的#53问题,新建了一个iss53分支:

    1
    2
    git branch iss53
    git checkout iss53

    创建一个新分支指针。

  • 现在在iss53分支进行了一些更改:

    1
    2
    vim index.html
    git commit -a -m 'added a new footer [issue 53]'

     分支随着工作的进展向前推进。

快进式(fast-forward)合并

  • 现在需要对主分支进行一次紧急修复,切换到master分支并进行一次热修复:

    1
    2
    3
    4
    git checkout master
    git checkout -b hotfix
    vim index.html
    git commit -a -m 'fixed the broken email address'

    基于  分支的紧急问题分支(hotfix branch)。

  • 经过检验,热修复分支解决了问题,那么现在需要将master分支与hotfix分支进行合并:

    1
    2
    3
    4
    5
    6
    $ git checkout master
    $ git merge hotfix
    Updating f42c576..3a0874c
    Fast-forward
    index.html | 2 ++
    1 file changed, 2 insertions(+)

    由于你想要合并的分支 hotfix 所指向的提交 C4 是你所在的提交 C2 的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

     被快进到 。

二路合并

  • 二路合并是最简单的一种合并方式,将两个文件进行逐行比对,如果行内容有差异就报冲突,这时候就需要人工合并差异了。

三路合并(递归三路合并)

  • 对于较少差异的分支,使用二路合并尚且可以接受,但对于较大差异的分支,使用二路合并就非常麻烦了。因此出现了三路合并方法:首先找到两个分支的共同父节点(Base),如果A分支对某内容进行了修改,B分支对这个内容未进行修改,那么会应用A分支的修改,反之应用B分支的修改。

    img

  • 回到之前的情景。解决完紧急问题后,需要恢复到之前的工作中,进行后续工作的提交。同时删除hotfix分支,因为已经完成了此次修复,不再需要:

    1
    2
    3
    4
    5
    6
    7
    8
    $ git branch -d hotfix
    Deleted branch hotfix (3a0874c).
    $ git checkout iss53
    Switched to branch "iss53"
    $ vim index.html
    $ git commit -a -m 'finished the new footer [issue 53]'
    [iss53 ad82d7a] finished the new footer [issue 53]
    1 file changed, 1 insertion(+)

    继续在  分支上的工作。

  • 现在iss53问题已经被修复,需要与master分支进行合并:

    1
    2
    3
    4
    5
    6
    $ git checkout master
    Switched to branch 'master'
    $ git merge iss53
    Merge made by the 'recursive' strategy.
    index.html | 1 +
    1 file changed, 1 insertion(+)

    一次典型合并中所用到的三个快照。

  • 因为master分支并非是iss53分支的父节点,Git会对这两个分支进行一次简单的三路合并。虽然此次合并写作’recursive’策略,但并非递归三路合并,在Git的输出中,三路合并与递归三路合并都是’recursive’策略,递归三路合并策略通常用于更加复杂的合并场景。

一个合并提交。

处理合并冲突

  • 如果两个分支都对同一段内容做了更改,Git就无法完成合并操作,在合并时就会报合并冲突:

    1
    2
    3
    4
    $ git merge iss53
    Auto-merging index.html
    CONFLICT (content): Merge conflict in index.html
    Automatic merge failed; fix conflicts and then commit the result.

    可以使用git status查看当前产生冲突而未合并状态的文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ git status
    On branch master
    You have unmerged paths.
    (fix conflicts and run "git commit")

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

    both modified: index.html

    no changes added to commit (use "git add" and/or "git commit -a")
  • 任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

    1
    2
    3
    4
    5
    6
    7
    <<<<<<< HEAD:index.html
    <div id="footer">contact : email.support@github.com</div>
    =======
    <div id="footer">
    please contact us at support@github.com
    </div>
    >>>>>>> iss53:index.html

    这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:

    1
    2
    3
    <div id="footer">
    please contact us at email.support@github.com
    </div>
  • 手动合并完成后,对冲突文件使用git add将其标记为冲突已解决。

  • 如果想使用图形化界面来处理冲突操作,可以使用 git mergetool,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突。退出工具后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决: 你可以再次运行 git status 来确认所有的合并冲突都已被解决。

分支管理

  • 使用不带参数的git branch命令可以查看当前分支的列表:

    1
    2
    3
    4
    $ git branch
    iss53
    * master
    testing

    其中,带有*的master分支代表当前HEAD指针所指向的分支。

  • 如果需要查看每一个分支的最后一次提交,可以使用git branch -v命令:

    1
    2
    3
    4
    $ git branch -v
    iss53 93b412c fix javascript issue
    * master 7a98805 Merge branch 'iss53'
    testing 782fd34 add scott to the author list in the readmes
  • 还可以使用--merged--no-merged筛选当前已合并或未合并到当前分支的分支:

    1
    2
    3
    $ git branch --merged
    iss53
    * master
  • 可以使用git branch -d删除分支,如果删除分支包含了还未合并的工作,则会删除失败:

    1
    2
    3
    $ git branch -d testing
    error: The branch 'testing' is not fully merged.
    If you are sure you want to delete it, run 'git branch -D testing'.

    可以使用-D完成强制删除。

远程分支

  • 假设有一个在 git.ourcompany.com 的 Git 服务器。 如果从这里克隆,Git 的 clone 命令会为你自动将其命名为 origin,拉取它的所有数据, 创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 origin 的 master 分支在指向同一个地方的本地 master 分支。

    克隆之后的服务器与本地仓库。

  • 如果在本地的 master 分支做了一些工作,在同一段时间内有其他人推送提交到 git.ourcompany.com 并且更新了它的 master 分支,这就是说你们的提交历史已走向不同的方向。 即便这样,只要你保持不与 origin 服务器连接(并拉取数据),你的 origin/master 指针就不会移动。

    本地与远程的工作可以分叉。

  • 如果要与远程仓库同步数据,运行 git fetch <remote> 命令(本例中为 git fetch origin)。 这个命令查找 ``origin’’ 是哪一个服务器(在本例中,它是 git.ourcompany.com), 从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针到更新之后的位置。

     更新你的远程仓库引用。

变基

变基的使用

  • 在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。假设现在有两个分支分别提交了更新

    分叉的提交历史。

  • 如果使用merge合并两个分支,那么会产生一次新的快照。

    通过合并操作来整合分叉了的历史。

  • 使用另一种方法:提取C4中引入的补丁和修改,然后再C3的基础上应用一次,这种操作称为变基(rebase)。比如,检出experiment分支,将其变基到master分支上:

    1
    2
    3
    4
    $ git checkout experiment
    $ git rebase master
    First, rewinding head to replay your work on top of it...
    Applying: added staged command

    它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。

    将  中的修改变基到  上。

  • 现在回到master分支上,进行一次快进合并:

    1
    2
    git checkout master
    git merge experiment

     分支的快进合并。

    此时的C4’快照就和使用merge合并产生的C5快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。例如向某个其他人维护的项目贡献代码时,首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

  • 对于一个主题分支中再分出一个主题分支的提交,如下图结构

    从一个主题分支里再分出一个主题分支的提交历史。

    如果希望合并client到master分支,但不希望将server分支合并,此时就可以使用--onto选项,选中在 client 分支里但不在 server 分支里的修改(即 C8C9),将它们在 master 分支上重放:

    1
    git rebase --onto master server client

    这条语句的意思是:取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样。

    截取主题分支上的另一个主题分支,然后变基到其他分支。

  • 那么现在可以使用快进合并合并master分支:

    1
    2
    git checkout master
    git merge client

    快进合并  分支,使之包含来自  分支的修改。

  • 如果现在需要将server分支也合并进来,使用变基操作,完成合并,然后删除多余的分支:

    1
    2
    3
    4
    git checkout master
    git merge server
    git branch -d client
    git branch -d server

    最终的提交历史。

变基的问题

  • 如果当别人正在基于分支进行开发,那么不应该将分支使用变基合并到主分支上。变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

  • 比如,现在有一个公开仓库,你从当前仓库克隆到本地并在此基础上进行开发。

    克隆一个仓库,然后在它的基础上进行了一些开发。

  • 然后,某人向该仓库提交了一些修改,其中还包括一次合并。你抓取了远程分支上的修改,然后合并到你的本地分支上。

    抓取别人的提交,合并到自己的开发分支。

  • 接下来,这个开发者又决定将合并操作回滚,更换为使用变基合并,并使用git push --force命令覆盖了服务器上的提交历史。然后你向服务器抓取更新,会发现多出来一些新的提交。

    有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交。

  • 此时,如果你执行git pull命令提交,就会将已经抛弃的C4和C6快照恢复,最终仓库会如下图所示,变得混乱。

    你将相同的内容又合并了一次,生成了一个新的提交。

解决因变基产生的合并问题

  • 假如真的遇到了类似的问题,可以使用一些操作解决。实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。

  • 这种情况下,如果并非执行合并,而是使用git rebase teamone/master,Git就会执行以下操作:

    • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
    • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
    • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4’)
    • 把查到的这些提交应用在 teamone/master 上面

    那么将会产生如下结构:

    在一个被变基然后强制推送的分支上再次执行变基。

    不过,这种操作需要保证C4’和C4是一样的,否则变基操作将无法识别,并新建另一个类似C4的补丁。

  • 还有另一种简单的方法是使用git pull --rebase,或者手动完成这个过程,先 git fetch,再 git rebase teamone/master

Learn Git Branching

  • Learn Git Branching上,可以进行一部分git命令的教学实验,或者使用沙盒进行测试,通过具象化的图形可以帮助对Git操作的理解。