Git


Git分支

komantao 2020-01-06 2020-01-06 字数 13796 阅读量


几乎所有的版本控制系统都以某种形式支持分支。使用分支意味着可以将工作从开发主线上分离开来,以免影响开发主线。Git的分支模型是一个强大而又独特的“必杀技特性”,使得Git从众多版本控制系统中脱颖而出。

Git的一个显著优点:分支的创建、切换、删除和合并非常快捷易用。Git只是改变指向当前版本的指针,成本几乎为零。但是,太方便了也会产生副作用。如果不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。

一、概述

时间线(timeline)是一种形象称谓,按照提交时间依次将多个版本(称为节点,第一个至最新版本)人为串成一条线,图谱化显示了各个版本的演变历程(族谱关系)。时间线上的节点是一个已提交的版本。

Git 的分支(branch),本质上是指向提交对象(commit object)的可变指针,会在每次的提交操作中自动向前移动(始终指向最后提交的版本)。

Git 的默认分支名字是master。习惯上,使用分支名代表一条时间线。

Git的“master”分支并不是一个特殊分支,它就跟其它分支完全没有区别。之所以几乎每一个仓库都有master分支,是因为git init命令默认创建它,并且大多数人都懒得去改动它。

分支

1、Git的对象

Git存储的是一系列不同时刻的文件快照(blob对象),不是文件的变化或差异内容。为了真正理解Git处理分支的方式,需要回顾一下Git是如何保存数据的。

Git的一次提交过程生成3个对象,存入Git的数据库(.git/objects目录):

  1. 尚未暂存(git add)时,已修改内容的工作文件尚未被Git跟踪

  2. 暂存(git add)后,一个文件快照创建一个blob对象

    blob对象存储已修改内容的工作文件(简称文件快照)。

  3. 提交(git commit)后,先后创建tree对象和commit对象

    tree对象存储文件快照的目录位置(目录树结构)和blob对象索引。

    commit对象存储tree对象索引和所有提交信息(parent、author、commiter、提交备注)。

.git/objects目录存储Git的对象(commit对象、tree对象、blob对象),哈希值的首2个字符构成在objects目录内的子目录名,剩余部分构成文件名。

使用文本编辑器查看对象内容,只会显示乱码。使用以下命令查看Git的对象内容:

命令 功能
cat 文件名 查看工作文件存储的文本内容
ls -al 查看当前目录下的所有文件(包括隐藏文件)
git cat-file -t 对象哈希值 查看对象的类型
git cat-file -p 对象哈希值 查看对象的存储内容
git cat-file -s 对象哈希值 查看对象的大小(size)

假设一个提交对象的分解示意图如下所示:

提交对象

  • commit对象

    存储某一次提交的信息,包括所在的tree(目录结构,包含blob对象)、parent(父对象,祖先关系)、author(作者)的姓名和邮箱、commiter(提交者)的姓名和邮箱、提交时输入的备注信息。

    首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

    查看commit对象的存储信息:

    $ git cat-file -p 456a329
    tree 0cf1fd7821d40d3a85b9997e5489f3b192f9bdb8            # 所属tree(一个引用)
    author 作者名 <邮箱> 1577673591 +0800                     # 作者
    committer 修改者名 <邮箱> 1577673591 +0800                # 修改者
      
    master分支新增new.txt文件                                 # 提交时的备注信息
    
  • tree对象

    tree对象相当于目录结构(所处位置),包含blob对象或其它tree(若有)的索引。

    查看tree对象的存储信息:

    $ git cat-file -p 0cf1fd78
    100644 blob f015593a2713371b83af3e09200083ac2b6dc38d    new.txt
    
    • 100644:文件类型

      100644表示是普通文件、100755表示是可执行文件、120000表示是符号链接。

    • blob:对象类型是blob

    • f015593…:blob对象的哈希值(一个引用)

    • new.txt:工作文件名

  • blob对象

    blob对象即文件快照,存储已跟踪(git add)工作文件(累计至提交时的所有修改内容)。

    查看blobt对象的存储信息:

    $ git cat-file -p f015593a2
    master分支增加111                 # 工作文件的所有修改内容
    

2、分支的好处

  • 同时并行推进多个功能开发,提高开发效率
  • 开发过程中,如果某一个分支开发失败,不会对其他分支有任何影响。失败的分支删除重新开始即可

若将所有commi都串连到master分支上(即只在一个分支上开发),意味着分支线会非常非常长。假如某一次提交后出现了冲突,而这个冲突很难解决,那么开发就停滞,无法向后再开发;又或者master上的分支出现了很大的问题,同样也无法接着开发。

3、分支类型

.git/refs目录记录了分支的引用(reference)内容:

  • .git/refs/heads目录记录了本地分支引用(简称本地引用)版本的哈希值
  • .git/refs/remotes目录记录了远程跟踪分支引用(简称远程引用)版本的哈希值

分支的类型有:

  • 本地分支:在本地仓库内创建的分支

    .git/refs/heads目录存储本地分支的引用(reference,指向最后提交的版本)。

  • 远程分支:在远程仓库内创建的分支

    在远程仓库的code(代码)界面可查看远程分支的名称和内容。远程分支的提交历史存放在远程仓库内。Git托管平台只能显示出远程分支最后版本的内容,不能显示远程仓库。

  • 远程跟踪分支(分支跟踪关系,track)

    在本地分支与远程分支之间人为设置某种对应的跟踪关系,称为远程跟踪分支。

    • .git/refs/remotes/远程仓库名目录存放某个远程仓库的所有远程跟踪分支

    • 远程跟踪分支的作用

      • 本地分支和远程分支建立关联关系

        如果本地分支和远程分支建立了关联关系,当对本地分支进行push或pull时,Git会知道推送(push)到哪个远程分支,或者从哪个远程分支拉取(pull)到本地分支。

      • 在本地仓库内记录最后一次跟踪时的版本

        无论本地分支或远程分支的版本如何更新变化,在未执行命令(pull、push)同步时,远程跟踪分支依旧保持最后一次跟踪时的版本。

        在执行命令(pull、push)时,Git根据远程跟踪分支记录的版本、本地分支版本和远程分支版本三者的变化情况,同步本地分支或远程分支。最后,远程跟踪分支更新跟踪版本。

Git切换分支(git checkout 分支名)时:

  • 首先在refs/heads目录内查看本地分支

    若有,则切换到本地分支。

  • 然后在refs/remotes目录内查找远程跟踪分支

    若有,则将远程跟踪分支检出(新建)为本地分支。

    例外情况:git clone时,在refs/remotes/远程仓库别名目录内只有一个HEAD文件,没有远程跟踪分支,此时,可在packed-refs文件中查找。

使用git branch -a可查看本地仓库内的所有分支(本地和远程跟踪分支):

$ git branch -a
* master                        # 本地分支(前有*号,表示当前分支)
  temp                          # 本地分支
  remotes/origin/master         # 远程跟踪分支

使用git branch -r可查看本地仓库内的远程跟踪分支:

$ git branch -r
  origin/master

使用git remote -v可查看本地仓库关联的远程仓库:

$ git remote -v
gitee   git@gitee.com:用户名/仓库名.git (fetch)
gitee   git@gitee.com:用户名/仓库名.git (push)
origin  git@github.com:用户名/仓库名.git (fetch)
origin  git@github.com:用户名/仓库名.git (push)

二、分支命令

1、创建分支

分支始终指向最新版本:

  1. git add阶段

    工作文件内容以快照(blob对象,以哈希值命名)的方式存入Git。文件快照(blob对象)存储的是一个完整的工作文件(累计至提交时的所有修改内容)。

  2. git commit阶段

    Git生成一个提交对象(commit object,简称版本,以哈希值命名)。提交对象存储有指向文件快照(blob对象)的索引等信息。

  3. 创建分支阶段

    分支本质是一个可以移动的指针(又称索引、引用)。

    Git创建分支时,默认是在最新提交对象上创建了一个可以移动的新指针,(若没有切换分支时)在每次提交操作中自动向前移动(始终指向最后提交的版本)。

1.1 分叉分支

在当前分支(已有)上新建分支(版本之间有祖先关系)。

  • 在指定提交对象上新建分支但不切换分支

    git branch 新分支名 start_point
    
    • start_point:分叉节点

      • 缺省时(默认),在当前分支指向的最新版本上分叉出分支
      • 若为某个分支名时,在指定分支指向的最新版本上分叉出分支
      • 若为某个分支上的某个版本的哈希值时,在指定版本上分叉出分支
    xxx/111 (master)
    $ git branch new
    

    创建分支

  • 新建且切换到新分支(HEAD指向新分支)

    git checkout -b 新分支名 start_point
    
    • start_point:分叉节点

      • 缺省时(默认),在当前分支指向的最新版本上分叉出分支
      • 若为某个分支名时,在指定分支指向的最新版本上分叉出分支
      • 若为某个分支上的某个版本的哈希值时,在指定版本上分叉出分支
    $ git checkout -b 123 bdef38c1
    Switched to a new branch '123'
    

1.2 orphan分支

orphan分支(孤儿分支),目的是创建独立分支(与当前分支的版本之间没有祖先关系)。

git checkout --orphan 新分支名
  • 首先,将当前分支的最新提交对象放入暂存区

  • 若需要,则提交(git commit)暂存区,或修改工作文件后添加到暂存区再提交

    新分支与旧分支之间没有祖先关系,但文件内容使用了旧分支的文件内容。

  • 若希望得到一个完全独立的分支(文件内容和祖先关系都与旧分支无关)

    1. 删除暂存区内容:git rm --cached <file>
    2. 删除未跟踪的工作文件:git clean -df
    3. 删除操作后,新分支(因为无提交内容,所以此时尚未创建)完全空白
    4. 修改新工作文件内容,添加到暂存区后再提交(提交后,才创建了新分支)
    5. git checkout命令,使HEAD指针指向了新分支

示例:

xxx/111 (master)
$ git checkout --orphan new
Switched to a new branch 'new'

孤儿分支

2、删除分支

  • 删除本地的单个分支

    # 删除本地已合并分支(fully merged,指定分支合并到当前分支后没有新提交)
    git branch -d 本地分支名          # d表示在遇到错误时自动终止删除
    # 删除本地未合并分支(慎用)
    git branch -D 本地分支名          # D表示强制删除
    
  • 删除远程的单个分支

    git push origin :远程分支名
    # 或
    git push origin -d 远程分支名
    # 或
    直接在远程仓库界面上删除远程分支
    
  • 批量删除本地分支(不能删除当前分支)

    git branch -a | grep 'n' | xargs git branch -D
    
    • git branch -a:表示列出本地所有分支

    • grep 'lyn_':Linux的grep命令用于查找文件里符合条件的字符串

      使用正则匹配(git branch -a的结果)分支名是否含有'n’

    • xargs:Linux的xargs命令是给命令传递参数的一个过滤器,捕获一个命令的输出,然后传递给另外一个命令

    • xargs git branch -D

      将以上匹配结果作为参数传给git branch -D,执行删除本地分支操作

    • |:是一个管道符,表示将上一段的结果传给下一段

  • 批量删除远程分支

    git branch -r| grep 'n' | sed 's/origin\///g' | xargs -I {} git push origin :{}
    
    • sed:Linux的sed命令是利用脚本来处理文本

      使用正则将接受到的分支都过滤掉origin/,得到实际的远程分支名

3、切换分支

切换分支时,即修改HEAD指针的指向(HEAD始终指向当前分支)。Git会用该分支的最后提交的快照替换工作目录内容。

git checkout branch_name
  • branch_name:已有的分支名(本地分支、远程跟踪分支)

    若不是已有的分支名,将触发错误:

    $ git checkout abdee
    error: pathspec 'abdee' did not match any file(s) known to git
    
  • 运行命令后,GIt切换到xxx分支,即HEAD指向了xxx分支

  • 将工作目录恢复为xxx分支所指向的快照内容

    若当前分支有修改内容尚未提交或贮存,将会触发错误并终止切换:

    xxx/111 (master)
    $ git checkout new
    error: Your local changes to the following files would be overwritten by checkout:
            new.txt
    Please commit your changes or stash them before you switch branches.
    Aborting
    
  • 若切换分支时使用-f参数,强制切换,这将丢弃本地修改

4、合并分支

合并,使用git merge(常用)和git rebase(不建议使用)命令,将指定分支的内容(从分叉节点累计到分支末端的所有commits)重现(replay)到当前分支中。

git merge 指定的分支名:将两个开发历史(分支)连接在一起,即将指定提交(显式指定某个分支)中的修改合并到当前分支中。

  • 两个开发历史(分支)有共同祖先

    例如:一个分支是从另一个分支的某个节点(版本)分叉发展而来。

  • 两个开发历史(分支)完全独立,没有共同祖先

    例如:其中一个分支是由orphan分支(孤儿分支)发展而来。

两个开发历史(分支)有共同祖先时的示例(假设所有节点都修改同一文件):

merge分支

  • 创建分支时

    • 从节点(版本)角度来看,当前分支和新分支都指向分叉节点2

    • 从克隆角度来看,新分支克隆了当前分支(从开始至分叉节点的开发历史)

      切换到新分支new后,运行git log命令,显示的提交历史包含1和2。

  • 各自分支不断开发(从时间线角度来看)

    • 分叉出分支的节点2称为分叉节点,分叉前的节点1和2称为共同祖先

    • 从分叉节点2分叉(diverge)出不同的时间线

      master分支的开发历史(时间线)为:1、2、5。

      new分支的开发历史(时间线)为:1、2、3、4。

      分支各自开发生成新的节点(在有需要时合并),分支始终指向开发历史(时间线)上的最新版本,HEAD指针始终指向当前分支。

  • 合并时

    当前分支假设为master,使用git checkout master命令切换到master,然后运行git merge new命令:

    xxx/111 (master)    # 当前分支是master,显示在状态栏
    $ git merge new     # merge命令是将指定分支(new)合并到当前分支
    

    to分支(当前分支master)受合并操作影响,from分支(指定分支new)不受影响:

    • 合并命令在当前分支master上新建一个节点6,节点6的父节点为4和5

      合并后的节点6简称为合并节点。

    • master向前移动,指向自己时间线上的最新节点6

    • 由于当前分支依然是master,所以,HEAD依然指向master

    • 合并后,new分支保持原状,依旧指向自己时间线上的最新节点4

4.1 语法

git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
	[--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
	[--[no-]allow-unrelated-histories]
	[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>…​]
git merge (--continue | --abort | --quit)
  • 合并方法:

    • --ff:默认值。将合并解析为快进(fast-forward,仅移动指针,不创建合并节点)

    • --no-ff:即使可以使用fast-forward,也要创建一个新的合并节点

      保留某个功能分支的开发记录信息,保证了版本演进的清晰。它将创建更多的提交对象,但收益远大于成本,这对于以后代码进行分析特别有用。强烈建议使用。

      附:将--no-ff设置为默认值的方法

      git config branch.master.mergeoptions  "--no-ff"
      

      或将以下内容添加到$(REPO)/.git/config文件(选择配置文件的级别)中:

      [branch "master"]
          mergeoptions = --no-ff
      
    • --ff-only:除非当前HEAD节点已经up-to-date(最新节点),或能使用fast-forward模式进行合并,否则的话将拒绝合并,并返回一个失败状态

    • 缺省参数时,Git首先尝试使用--ff参数(fast-farward merge);若失败,则尝试使用--no-ff参数(no-fast-farward merge)

  • 合并策略:

    -s strategy:指定合并时策略。

    若缺省-s选项,则使用内置的策略列表:

    • 合并节点只有单个父节点时,采用recursive策略
    • 合并节点有多个父节点时,采用octopus策略
  • 合并操作

    • --continue:继续合并

      解决合并冲突(保存协商内容,且使用git add命令添加到暂存区)后,使用git merge --continue(等同于git commit)命令继续完成合并,此命令在调用git commit前检查是否存在正在进行中(暂时中断)的合并。

    • --abort:终止合并

      合并冲突后决定不合并,终止合并过程并尝试重建合并前状态

    • --quit:放弃合并

      忘记当前正在进行的合并,保持索引和工作树不变。

警告:若当前分支有修改内容尚未提交(commit)或贮存(stash)时运行git merge,将会在冲突中难以回退。所以,Git将会触发错误并终止合并:

xxx/111 (master)
$ git merge new
error: Your local changes to the following files would be overwritten by merge:
        new.txt
Please commit your changes or stash them before you merge.
Aborting

当指定分支和当前分支指向的版本内容不一致时,将会产生合并冲突:

xxx/111 (master)
$ git merge new
Auto-merging new.txt
CONFLICT (content): Merge conflict in new.txt
Automatic merge failed; fix conflicts and then commit the result.

xxx/111 (master|MERGING)

4.2 合并方法

4.2.1 fast-farward merge

Git合并时触发fast-farward merge(快进式合并)的前置条件:

  1. 当前分支(HEAD指向的分支)指向分叉节点,即分叉后当前分支没有新增节点

    分叉节点是所有分叉分支的根节点(root commit)。

  2. 分叉后,被合并分支有新增节点

符合上述条件,Git自动触发fast-farward merge(不创建合并节点,仅移动指针);否则,自动触发no-fast-farward merge(创建合并节点,且移动指针)。

xxx/111 (master)
$ git merge new
Updating 53edba0..6c779a9  # 更新此区间内的版本
Fast-forward               # 表示当前合并使用Fast-forward。若没有此行信息,表示当前合并使用no-Fast-forward
 new.txt | 2 ++
 1 file changed, 2 insertions(+)

ff合并

合并过程详解:

  • 在当前分支(假设为master)的节点2分叉出new分支

    • 节点2称为分叉节点,分叉出两个分支(master、new)
    • new不断发展,最新版本(顶部节点)为节点4
    • master没有任何提交,其顶部节点依然为节点2
  • 将new合并到master(git merge new

    符合触发fast-farward的前置条件,使用fast-forward策略合并:

    • master向前移动,指向节点4,没有产生一个合并节点
    • fast-forward合并策略实现了合并的快速化(不创建合并节点,仅移动指针),但丢弃了from分支(new)的合并历史,版本演进历史不清晰

删除被合并分支(new)后,使用git log --graph --all命令(或Sourcetree第三方工具)查看当前分支的图示化提交历史,没有显示new分支的被合并历史。

4.2.2 no-fast-farward merge

显式使用--no-ff参数或不符合使用--ff参数的条件时,Git将自动使用no-fast-farward merge(非快进式合并)。

xxx/111 (master)
$ git merge new --no-ff
Merge made by the 'recursive' strategy.
 new.txt | 2 ++
 1 file changed, 2 insertions(+)

no-ff合并

合并过程详解:

  • 在当前分支(假设为master)的节点2分叉出new分支

    • 节点2称为分叉节点,分叉出两个分支(master、new)
    • new不断发展,其最新版本(顶部节点)为4
    • master没有任何提交,其顶部节点依然为2
  • 将new合并到master

    from分支(new)不受合并操作影响,to分支(master)受影响:

    • 使用no-fast-forward策略合并:创建一个合并节点5,master向前移动,指向节点5
    • 相对于fast-farward合并,no-fast-farward慢了很多,因为多新建了一个版本,但保留了from分支(new)的被合并历史,即版本演进历史清晰

删除被合并分支(new)后,使用git log --graph --all命令(或第三方工具Sourcetree)查看当前分支的图示化提交历史,依旧保留new分支的被合并历史(只是没有分支名)。

4.3 merge冲突

在合并(merge)或撤销(主要是revert)过程中,可能会产生冲突(conflict)。

revert冲突参阅:revert冲突

示例:直接运行git merge new命令。

xxx/111 (master)   # 当前分支是master,显示在状态栏
$ git merge new                # 将new分支合并到master分支
Auto-merging new1.txt          # Git尝试自动合并
CONFLICT (content): Merge conflict in new1.txt   # 触发冲突:new1.txt文件产生合并冲突
Automatic merge failed; fix conflicts and then commit the result.  # 自动合并失败,Git建议手动合并(修改冲突,然后提交修改结果)

xxx/111 (master|MERGING)    # 进入合并状态,暂停合并命令,等待修改冲突结果

合并时,Git尝试自动合并(Auto-merging),两个分叉分支(new和master)和分叉节点进行一次简单的三方自动合并。

冲突合并

  • 合并前

    • 节点2分叉出master分支和new分支
    • new不断发展,其最新版本(顶部节点)为4
    • master不断发展,其最新版本(顶部节点)为5
    • 假设当前分支为master,使用git checkout master命令切换到master
  • 合并时(假设将new合并到master)

    首先,Git用两个分支的末端(4和5)和分叉节点(2)进行一次简单的三方自动合并。

    假设两个版本(5和4)都修改了同一文本内容:

    • 运行合并命令时,触发合并冲突

      • 提示要求修复冲突(fix conflicts and then commit the result)
      • master分支(当前分支)的状态转变为:master|MERGING
4.3.1 冲突原因

合并时,Git首先进行冲突判断:

  • 判断涉及的节点

    对当前分支master、被合并分支new和分叉节点进行三方判断。

    分叉节点是所有分叉分支的根节点(root commit),是判断的基准数据。

  • 判断内容

    三方节点的同一文本内容,是指同一文件的同一位置(单位:行)上的文本内容。

  • 判断依据

    对同一文本内容是否有多次修改。基于分叉节点,两个分支是否都有不同的修改。

    • 若两个分支各自修改了不同文件,合并时不会产生冲突

      • 示例:new新建文件,master修改了分叉节点上的文件(或新建文件)

        将new合并到master时,master增加了new的新建文件。

      • 示例:分叉节点有两个文件A和B,new修改了文件A,master修改了文件B

        将new合并到master时,master增加了文件A的修改内容。

    • 若两个分支都修改了分叉节点上的相同文件

      • 若没有修改相同位置(同一行)上的文本内容,合并时不会产生冲突

        • 示例:new修改了第1行,master修改了第4行

          new相对于分叉节点修改了第1行,没有修改第4行;master相对于分叉节点修改了第4行,没有修改第1行;将new合并到master时,分叉节点的第1行只有一个修改,所以,master的第1行被new的第1行修改了;new没有修改第4行,所以第4行依然为master的第4行。

      • 若修改了相同位置(同一行)上的文本内容,合并时会产生冲突

        解决方法:协商保留冲突内容,将协商结果添加到暂存区且提交为新版本。

产生冲突(CONFLICT)的原因:两个分叉分支都修改了同一文本内容,即基于分叉节点,同一文件的同一位置上有两个不同的修改内容,Git无法判断应该如何选择哪个分支的修改。

4.3.2 解决冲突

自动合并失败后,需要手动解决冲突。

方法:在冲突结果文件中协商保留哪些内容(删除不需要内容),然后将协商内容添加到暂存区且提交为新版本。

  1. 简单方法:使用文本编辑器修改产生冲突的工作文件

    冲突后,工作文件被修改为(示例):

    111                  # 共同祖先(分叉节点)的第1行内容
    222                  # 共同祖先(分叉节点)的第2行内容
    <<<<<<< HEAD         # 上面是共同祖先(分叉节点)的内容
    333                  # 当前分支(HEAD指向的分支)的第3行内容
    444                  # 当前分支(HEAD指向的分支)的第4行内容
    =======              # =======和<<<<<<< HEAD之间是当前分支内容(不含共同祖先)
    aaa                  # 被合并分支(new)的第3行内容
    bbb                  # 被合并分支(new)的第4行内容
    >>>>>>> new          # =======和>>>>>>> new之间是被合并分支内容(不含共同祖先)
    
    1. 删除所有的分隔符:<<<<<<< HEAD=======>>>>>>> new

    2. 协商保留内容

      <<<<<<< HEAD>>>>>>> new之间是冲突内容。

      原则上,同一位置(同一行)只保留一个修改内容(删除另一分支同行内容)。

      实际上,保留内容是协商后的结果,即协商后可随意增删改内容。

    3. 保存协商结果

      工作文件进入未暂存状态,等待添加到暂存区。

    4. 将协商结果添加到暂存区

      xxx/111 (master|MERGING)
      $ git add new.txt
      
    5. 将协商结果提交为新版本

      使用git merge --continuegit commit -m "提交备注"命令继续合并(将协商结果提交为新版本):

      xxx/111 (master|MERGING)
      $ git merge --continue
      [master c66f1b0] Merge branch 'new'
            
      xxx/111 (master)                          # 状态栏恢复为master
      

      提交后,在当前分支创建一个commit(6),master指向版本6,master|MERGING状态恢复为master。

  2. 直观对比方法:使用第三方工具(例如:Beyond Compare 4)修改冲突内容

    1. 下载Beyond Compare中文版后安装

    2. 破解

      Beyond Compare 4的30天试用期过后的破解方法:

      • 方法一:重命名BCUnRAR.dll

        在安装目录下找到文件BCUnRAR.dll,重命名(任意)或删除。

      • 方法二:修改注册表

        在CMD运行regedit,打开注册表。

        删除项目:计算机\HKEY_CURRENT_USER\Software\Scooter Software\Beyond Compare 4\CacheID

      重新启动,就可以正常使用(继续30天试用期)。

    3. 配置Beyond Compare 4

      打开C:/Users/用户名/.gitconfig文件,拷贝以下内容:

      [difftool "bc4"]
        cmd = 'D:/Git/Beyond Compare 4/BComp.exe' \"$LOCAL\" \"$REMOTE\"
      [mergetool "bc4"]
        cmd = 'D:/Git/Beyond Compare 4/BComp.exe' \"$LOCAL\" \"$REMOTE\" \"$BASE\" -o \"$MERGED\"
        trustExitCode = true
      [diff]
          tool = bc4
      [difftool]
          prompt = true
      [merge]
          tool = bc4
      [mergetool]
          prompt = true
      

      使用此配置在Git Bash命令行内不能打开Beyond Compare工具,最后采用了Beyond Compare + Sourcetree。

5、恢复分支

删除分支:

  • 只是删除了一个指针

    • .git/refs/heads目录中不再有指定分支
    • 时间线上不再显示指定分支名称
  • 不会删除提交对象,.git\objects目录依然存放已删除分支的提交对象

恢复分支,本质上就是在指定版本上创建分支,不是指恢复已删除分支的整条时间线:

  1. 查看当前分支的提交历史,获取恢复版本(已删除分支的某个版本)

    git log -g   # -g:遍历reflog条目,从最新到开始(from the most recent one to older ones)
    
  2. 创建新分支(在指定版本分叉出新分支)

    git branch 新分支名 start_point
    
  3. 切换分支(自动恢复指定版本的工作文件内容)

    git checkout 新分支名
    

三、分支管理

参考:A successful Git branching model

Git的核心功能之一是分支。Git非常提倡使用分支,创建分支、合并分支等简单快捷。分支不在多而在恰到好处,如果分支创建多了,管理起来就麻烦了。

1、分支策略

一个简单的分支管理模型(分支策略branching scheme):

分支管理

1.1 主要分支

开发模型的远程仓库(中央存储库)应该包含两个无限生命的主要分支:

  • 主分支,例如命名为master

    源代码的稳定分支,反映稳定版本(Stable Release)的开发状态。

  • 开发分支,例如命名为develop

    源代码的开发分支(或称整合分支),反映测试版本(Nightly Builds)的开发状态。

1.2 支持分支

在主要分支(master、develop)的旁边,开发模型使用各种支持分支帮助团队成员之间并行开发,简化对特性的跟踪,为生产版本做准备,并帮助快速修复实际的生产问题。与主要分支不同,这些分支的生命周期总是有限的,因为它们最终会被移除。

这些分支都有特定用途,并受严格的规则约束,即哪些分支可能是其原始分支,哪些分支必须是其合并目标。

支持分支有:

  • 功能分支(Feature branches)

    或称主题分支,用于为即将发布或遥远的将来版本开发新功能。

    当开始开发功能时,可能不知道将合并该功能的目标版本。功能分支的本质是只要功能正在开发中就存在,但最终会合并到develop(确保将新功能添加到即将发布的版本中)或丢弃(以防实验失败)。

    功能分支通常仅存在于开发人员的本地仓库中,而不存在于远程仓库中。分支命名随意(master、develop、release-xxx、hotfix-xxx除外)。

  • 预发布分支(Release branches)

    用于准备新版本的发布,允许修复小量bug和为发布准备元数据(版本号、构建日期等)。通过在发布分支上完成所有这些工作,开发分支就可以接收下一个大版本的特性。

    从develop分叉出发布分支的关键是:develop达到了新版本的期望值。

    分支命名约定为:release-xxx。

  • 修复分支(Hotfix branches)

    修复分支类似于发布分支,都是计划外,但旨在为新版本做准备。它们源于对不期望的实时稳定版本状态立即采取行动的必要性。当必须立即解决稳定版本中的严重错误时,可以从master分支上的相应标记中分支出一个修补程序分支。

    一个人正在修复master分支,团队成员可以继续在开发分支上的工作。

    分支命名约定为:hotfix-xxx。

2、项目跟踪

  1. 查看版本历史

    查看项目的版本库的发展变化以及比较差异的命令:

    • git show-branch:查看版本库中每个分支的世系(父系,指版本的族谱历史,例如master^是指分支master的上一版本)发展状态,可以看到每次提交的内容是否已进入每个分支
    • git diff 分支A 分支B:查看两个版本的差异
    • git whatchanged:查看当前分支的发展

    最实用的还是Git的第三方图形工具:Sourcetree + Beyond Compare 4

  2. 逆转(Undo)和恢复(Redo)版本(某一阶段的工作文件)

    git reset命令:重置版本

    将当前分支的HEAD指针定位到任何已提交版本后,有三个重置的算法选项:

    • --mixed:默认值,只重置暂存区

    • --soft:不重置任何文件,仅更新ORIG_HEAD指针(.git/ORIG_HEAD文件)

    • --hard:重置暂存区和工作目录

  3. 合并分支

    1. 合并自己(本地仓库)的分支

      运行git merge命令。

    2. 合并其他人(远程仓库)的分支

      远程合并就是:“抓取(git fetch)一个远程仓库中的工作到一个临时标签中”,然后再运行git-merge命令。

  4. 提取数据

    运行git checkout -f commit_id命令,在“detached HEAD”状态查看指定版本内容:

    xxx/111 (master)
    $ git checkout -f e363454
    Note: switching to 'e363454'.
       
    You are in 'detached HEAD' state.
    …………
    
  5. 交换工作

    假设情景:远程仓库单向下载后,在本地修改了版本,但自己无权限上传到远程仓库,其他人也无权限访问本地机器抓取工作。这种情景应该如何交换工作?

    1. 复制远程仓库(clone或fetch)

    2. 本地工作(在本地仓库新增了版本)

    3. 抓取远程仓库最新版本

      git fetch origin    # 假设远程仓库的别名为origin
      

      目的:防止origin不是最新的公共版本,产生错误的补丁文件。

    4. 将本地新增工作迁移到远程跟踪分支(远程分支)

      git rebase origin
      
    5. 生成补丁文件

      git format-patch origin
      

      在当前目录下生成一个大概名为0001-xxx.txt补丁文件, 建议使用文本编辑器查看这个文件的具体形式,然后将这个文件以附件的形式发送到项目维护者的邮箱。

    6. 项目的维护者将补丁文件合并到项目

      当项目维护者收到邮件后,只需运行git-am命令,就可以将补丁合并到项目中。

      git checkout -b xxx            # 新建分支用于合并补丁
      git am /path/to/0001-xxx.txt   # 将补丁合并到项目
      
  6. 协同工作

    所有人都对远程仓库有双向权限(下载和上传)。

    命令有:clone、fetch、pull、push等。

  7. 打包

    .git/objects目录保存了创建的所有Git对象,这对于自动和安全地创建对象很有效,但是对于网络传输则不方便。Git对象一旦创建了,就不能被改变,但可以“打包”优化存储。

    xxx/111 (master)
    $ git repack
    Enumerating objects: 21, done.
    Counting objects: 100% (21/21), done.
    Delta compression using up to 4 threads
    Compressing objects: 100% (7/7), done.
    Writing objects: 100% (21/21), done.
    Total 21 (delta 1), reused 0 (delta 0)
    

    .git/objects/pack目录存放了“打包”结果,有两个文件:

    • pack-xxx.pack:保存着所有被打包的数据的文件
    • pack-xxx.idx:保存着随机访问的索引

    若已经打包了对象,则可以删除已被打包的原始对象:

    git prune-packed
    
  8. 发布工作

    从本地(私有)仓库发布更新(commit)到远程(公共)仓库中,需要远程机器的写权限。

    首先,在远程机器上创建一个空的版本库存放公共版本库。

    这个版本库只需要在开始时创建一次,以后将通过push保持更新。

3、项目负责人

项目领导人(project lead)的工作:

1. 在自己的本地机器上准备好主版本库。你的所有工作都在这里完成

2. 准备一个能让大家访问的公共版本库 如果其他人是通过默认协议方式(http)导入版本库,那么你有必要保持这个协议的友好性。git-init-db后,复制自标准模板库的$GIT_DIR/hooks/post-update,将包含一个对git-update-server-info的调用,但是post-update默认是不能唤起它自身的。通过chmod +x post-update命令使能它。这样让git-update-server-info保证那些必要的文件是最新的。

3. 将你的主版本库推入公共版本库

4. git-repack公共版本库。这将建立一个包含初始化提交对象集的打包作为项目的起始线,可能的话,执行一下 git-prune,要是你的公共库是通过 pull 操作来从你打包过的版本库中导入

5. 在你的主版本库中开展工作,这些工作可能是你自己的最项目的编辑,可能是你由email收到的一个补丁,也可能是你从这个项目的“子系统负责人” 的公共库中导入的工作等等。你可以在任何你喜欢的时候重新打包你的这个私人的版本库。

6. 将项目的进度推入公共库中,并给大家公布一下。

7. 经过一段时间以后,“git-repack” 公共库。并回到第5步继续工作。

项目子系统负责人(subsystem maintainer)也有自己的公共库,工作流程大致如下:

1. 准备一个你自己的工作目录,它通过git-clone克隆自项目领导人的公共库。原始的克隆地址(URL)将被保存在.git/remotes/origin中。

2. 准备一个可以给大家访问的公共库,就像项目领导人所做的那样。

3. 复制项目领导人的公共库中的打包文件到你的公共库中,除非你的公共库和项目领导人的公共库是在同一部主机上。以后你就可以通过objects/info/alternates文件的指向来浏览它所指向的版本库了。

4. 将你的主版本库推入你的公共版本库,并运行git-repack,如果你的公共库是通过的公共库是通过pull来导入的数据的话,再执行一下git-prune 。

5. 在你的主版本库中开展工作。这些工作可能包括你自己的编辑,来自email的补丁,从项目领导人,“下一级子项目负责人”的公共库哪里导入的工作等等。你可以在任何时候重新打包你的私人版本库。

6. 将你的变更推入公共库中,并且请“项目领导人”和“下级子系统负责人”导入这些变更。

7. 每隔一段时间之后,git-repack公共库。回到第 5 步继续工作。

一般开发人员不需要自己的公共库,大致的工作方式是:

1. 准备你的工作库,它应该用git-clone克隆自“项目领导人”的公共库(如果你只是开发子项目,那么就克隆“子项目负责人”的)。克隆的源地址(URL)会被保存到.git/remotes/origin中。

2. 在你的个人版本库中的master分支中开展工作。

3. 每隔一段时间,向上游的版本库运行一下git-fetch origin 。这样只会做git-pull一半的操作,即只克隆不合并。公共版本库的新的头就会被保存到.git/refs/heads/origins 。

4. 用git-cherry origin命令,看一下你有什么补丁被接纳了。并用git-rebase origin命令将你以往的变更迁移到最新的上游版本库的状态中。

5. 用git-format-patch origin生成email形式的补丁并发给上游的维护者。回到第二步接着工作。

感谢您的赞赏支持:

Copyright © 2020 komantao. All Rights Reserved.