跳转至

Git版本控制管理

前言

环境

  • 操作系统:CentOS8 on Docker(homqyy/dev_env_centos8)
  • Git版本: 2.31.1

约定

  • 对于例图有如下约定:
    • 圆形:提交(commit)对象
    • 三角形:树(tree)对象
    • 矩形:内容(blob)对象
    • 平行四边形:标签(tag)对象
    • 圆角矩形:分支

第1章 介绍

  • 优点
    • 有助于分布式开发
    • 能够顾胜任上千开发人员的规模
    • 性能优异
    • 保持完整性和可靠性
    • 强化责任
    • 不可变性
    • 原子事物
    • 支持并鼓励基于分支的开发
    • 完整的版本库
    • 一个清晰的内部设计
    • 免费自由

第2章 安装Git


第3章 起步

3.1 帮助

  • 查看git支持的子命令:
    • git
    • git --help
  • 查看子命令的使用信息:

    • git help _subcommand_
    • git --help _subcommand_
    • git _subcommand_ --help
  • 每个命令都有对应的连字符命令,比如git commit等效于git-commit

  • 使用“裸双破折号”(--)来分离一系列参数:
# checkout the tag named "main.c"
git checkout main.c

# checkout the file named "main.c"
git checkout -- main.c

3.2 创建版本库

  • 创建初始版本库

    mkdir  ~/git_example && cd ~/git_example
    
    echo 'My git example' > README.md
    
    git init
    
    • 执行git init时会在版本库的根目录下生成一个.git/的隐藏目录,由Git维护
  • 将改变提交到版本库中:

    • 为了避免频繁的改动,Git将提交到版本库的动作分为两个步骤,分别为“暂存(add)”和“提交(commit)”

      • 暂存改动

        git add README.md
        git status
        
        # On branch master
        #
        # No commits yet
        #
        # Changes to be committed:
        # (use "git rm --cached <file>..." to unstage)
        #         new file:   README.md
        
      • 提交改动

        git commit -m "Initial contents of git example" --author="homqyy <homqyy@example.com>"
        
        # [master (root-commit) 1896487] Initial contents of git example
        #  Author: homqyy <homqyy@example.com>
        #  1 file changed, 0 insertions(+), 0 deletions(-)
        #  create mode 100644 README.md
        
        • commit除了可以通过-m参数来编辑提交日志外,还可以通过编辑器的方式编辑,只需要设置环境变量GIT_EDITOR即可:
          • 比如在Bash中:export GIT_EDITOR=vim
  • Git会尽力确定每一次提交的作者,设置方法:

    • 命令:
      • 姓名:git config user.name "homqyy"
      • 邮箱:git config user.email "homqyy@example.com
    • 环境变量:
      • 姓名:GIT_AUTHOR_NAME;邮箱:GIT_AUTHOR_EMAIL
  • 查看提交日志:

    • 执行git log命令查看:

      git log
      
      # commit 9fe3d4f187608edfe28db6696487f7b7c0fd9e39
      # Author: homqyy <homqyy@example.com>
      # Date:   Wed Aug 3 15:39:12 2022 +0000
      #
      #     first commit
      
    • 查看详细信息,执行git show命令查看:

      git show
      
      # commit 33c79c6038978aad4bed37f3bb6b0277bd74ca06 (HEAD -> master)
      # Author: homqyy <homqyy@example.com>
      # Date:   Wed Aug 3 15:40:07 2022 +0000
      #
      #     add head 2
      #
      # diff --git a/README.md b/README.md
      # index e59c974..fda9cda 100644
      # --- a/README.md
      # +++ b/README.md
      # @@ -2,3 +2,5 @@
      #
      #  ## head 1
      #
      # +## head 2
      # +
      
      • 查看特定提交的信息:git show 189648713da8d48ed9360cac1395341db80d6271
    • 查看当前分支的概要信息,执行git show-branch命令查看:

      git show-branch --more=10
      
      # [root@3b05e5c9c2d3 git_example]# git show-branch --more=10
      # [master] add head 2
      # [master^] add head 1
      # [master~2] first commit
      
      • --more:展示近期最多10个版本
  • 查看提交差异:

    • 执行git diff命令查看:

      git diff 2d92e19c36de18853d7b9b6307a0c4af70c3b34e f20a96972b2359a133a5b0ededd25aaece23f665
      
      • 3.git-diff
  • 删除文件:git rm example.txt

  • 重命名文件:

    • mv子命令:

      git mv old_name.txt new_name.txt
      git commit -m "rename"
      
    • 先删除,后添加:

      mv old_name.txte new_name.txt
      git rm old_name.txt
      git add new_name.txt
      git commit -m "rename"
      
  • 创建版本副本:

    git clone .\git_example\ .\git_example.bk
    
    # Cloning into '.\git_example.bk'...
    # done.
    

3.3 配置文件

  • 配置文件路径:

    • 版本库配置:.git/config;可用--file选项修改,是默认选项
    • 用户配置:~/.gitconfig;可用--global选项修改
    • 系统配置:/etc/gitconfig;可用--system选项修改
  • 设置配置:git config ...

  • 移除配置:git config --unset ...
  • 别名:
    • 设置别名show-graphgit config --global alias.show-graph 'log --graph --abbrev-commit --pretty=oneline'
    • 使用别名:git show-graph

第4章 基本的Git概念

4.1 基本概念

  • Git维护两个主要的数据结构:

    • 对象库(object store):复制时,是完整的
    • 索引(index):暂时的信息,私有的
  • Git对象类型:

    • 块(blob,binary large object):文件的每个版本视为一个blob。仅保存文件中的数据,不包含文件的任何元数据。
    • 目录树(tree):一个tree对象代表一层目录信息。tree记录了“blob标识符”、“路径名”和“在一个目录里所有文件的一些元数据”,同时也能引用其他的tree对象。
    • 提交(commit):一个commit对象保存版本库中每一次变化的元数据,包括“作者”、“提交者”、“提交日期”和“日志消息”。每个commit对象指向一个tree对象。除了根root commit外,其余commit都有parent commit
    • 标签(tag):一个tag对象分配一个任意的且人类可读的名字给一个特定对象,通常是一个commit对象。
  • 索引:索引是一个临时的,动态的二进制文件,它捕获项目在某个时刻的一个整体结构。项目状态包括committree,它可以来自项目历史中的任意时刻,或则它可以是你正在开发的未来状态。

  • 可寻址内容名称:Git通过SHA1来散列存储对象的内容,并将其作为对象的ID。(SHA1是一个160位的数,通常用40位的16进制表示)

  • Git追踪内容:

    • Git不仅是一个VCS,同时还是一个内容追踪系统(content tracking system)。
    • Git追踪内容表现在:
      1. Git的对象基于其对象内容的散列计算的值,而不是基于用户原始文件布局的文件名或目录名设置。Git追踪的是内容而不是文件。因此如果两个文件内容完全一样,Git在对象库里只保存一份blob形式的内容副本。
      2. Git存储的是 每个文件的每个版本,而不是他们的“差异”。Git通过文件内容的散列值来作为文件名,因此Git是用不同散列值的blob之间的区别来计算这个历史。
  • Git将路径名也作为一个数据进行存储,因此文件路径与文件内容在Git中是分离的。

  • 打包文件(pack file):
    • Git通过pack file进行存储,即它会计算非常相似的全部文件,并为它们之一存储一个完整内容,之后计算相似文件之间的差异,并且只存储差异。
    • 由于Git是内容驱动的,因此它可以不用在乎路径,对所有文件进行差异分析。
    • pack file跟对象库中的其他对象存储在一起。

4.2 对象库

  1. 首次提交两个文件:

    first commit

  2. 创建一个包含一个文件的目录:

    add a directory of contain a file

4.3 Git在工作时的概念

  • .git/objects/目录用来存储所有对象
  • SHA1会产生160位数,因此用40字节的16进制串表示。Git咋前面两个数字后面插入一个/以提高文件系统效率(如果你把太多的文件放在同一目录中,一些文件系统会变慢);使用SHA1的第一个字节称为一个目录是一个很简单的办法,可以为所有均匀分布的可能的对象创建一个固定的、256(\(16^2\))路分区的命名空间。

  • 查看对象内容:git cat-file -p <Object ID>

  • 每次执行命令(比如:git addgit rm或则git mv)的时候,Git并不会立刻创建tree,而是用新的路径名和blob信息去更新index
  • 任何时候都可以从当前index创建一个tree对象。用命令git write-tree来捕获当前index的信息去创建tree对象,并打印到屏幕上。
  • 使用git ls-files命令可以查看当前indexwork的文件信息
    • git ls-files -s查看暂存后的内容
  • 使用git commit-tree -m <message> <tree ID>去创建一个commit对象,并输出到屏幕上。
  • 使用git commit -m <message>将改变提交到版本库中

  • 标签(tag):

    • 两种类型:
      • 轻量级的(lightweight):知识一个提交对象的引用,通常被版本库视为是私有的。这些标签并不在版本库中创建永久对象。
      • 带附注的(annotated):会创建对象,并可以提供一条消息。可以根据“RFC 4880”来使用GnuPG密钥进行数字签名。
    • Git在命名一个commit的时候对轻量级的tag和带附注的tag同等对待。
    • 创建taggit tag -m <message> <tagname> <commit>
    • 删除taggit tag -d <tagname>
    • 查看tag ID:git rev-parse <tagname>

第5章 文件管理和索引

  • Git在工作目录和版本库之间加设了一层index,用来暂存(stage)、收集或者修改。当你使用Git来管理代码时,你会在工作目录下编辑,在索引中积累修改,然后把索引中积累的修改作为一次性的变更来进行提交。 > Linux Torvalds在Git邮件列表里曾说,如果不先了解索引的目的,你就不能完全领会Git的强大之处。

  • 任何时候都可以通过git status查看索引状态

  • git diff可以用来查看两个commit的差异,或workcommit,或indexcommit

5.1 Git中的文件分类

  • 已追踪的(Tracked):已经在index或版本库中的文件
  • 被忽略的(Ignored):被版本库主动声明为忽略的文件(.gitignore.git\info\exclude
  • 未追踪的(Untracked):不在前两类中的文件(在工作目录中,但未被忽略,也未被加入到index或版本库中的文件)

5.2 git add

  • git add用来暂存一个文件内容,如果作用的是文件夹则将该目录下的文件和子目录递归暂存。
  • 在发出git add命令时每个文件的全部内容都将被复制到对象库中,并且按文件的SHA1名来索引。暂存一个文件也称作缓存(caching)一个文件,或则叫“把文件放进索引”
  • 通过命令git ls-files --stage可以查看当前索引快照下的文件的SHA1值。
  • 通过命令git hash-object <file>...可以计算文件的hash值

5.4 git commit 注意事项

  • 自动暂存所有未暂存和未追踪的文件内容:git commit --all ...

    • 但是如果有整个目录未追踪的情况,则不会自动暂存该目录
  • 通过编辑器编写commit日志的时候,如果不想提交了,可以直接退出。或则把已经保存的内容删除后保存退出即可。

5.3 git rm

  • git rm <file>可以从索引或则同时从索引和工作目录中删除一个文件。
  • git rm --cached <file>会删除索引中的文件,但不会删除工作目录,等效于git add <file>的反向操作。
  • Git在删除一个文件之前,会先确保工作目录下的该文件的版本与当前分支中的最新版本(HEAD)是匹配的。这个验证可以防止文件的修改丢失。

5.4 git mv

  • git mv <old> <new>用来重命名一个文件,其等效于以下3个步骤:

    1. mv old new
    2. git rm old
    3. git add new
  • 重命名一个文件并提交到版本库后,通过git log <file>查看日志,会发现无法查看重命名之前的记录,此时可以增加--follow参数来查看:git log --follow <file> > VCS的经典问题之一就是文件重命名会导致它们丢失对文件历史记录的追踪,而Git即使经历过重命名,也仍然能保留此信息。

5.5 git clean (自增)

  • git clean \<path>:丢弃在path下的改动。常用来丢弃工作目录中的改动

5.6 .gitignore

  • .gitignore文件用来告诉Git需要忽略哪些文件

    • 可以放置于于任何目录中。
    • 每个文件都只影响同级目录及其子目录。
    • 级联的,可以覆盖高层目录中的规则(取反的话会覆盖掉高层目录中的正向结果)
  • 书写格式支持模式,文件中的每一行都为一条规则:

    • 空行会被忽略,而以#开头的行可以用于注释。
    • 一个简单的字面文件名匹配任何目录中的同名文件。
    • 目录由末尾的/标记。这样能匹配同名的目录和子目录,但不匹配文件或符号链接。
    • 包含shell通配符。(如*,可以通配任何字符,但注意,它不能跨越目录)
    • 起始的!会对该行其余部分的模式进行取反。此外,被之前模式排除,但被取反规则匹配的文件是要被包含的。它会覆盖低优先级的正向结果。

      # 排除所有“.o”结尾的文件
      *.o
      
      # 除了“main.o”之外
      !main.o
      
  • 优先级从高至低:

    1. 在命令行上指定的模式
    2. 从相同目录的.gitignore文件中读取的模式
    3. 从上层目录中的模式,向上进行。因此当前目录的模式能推翻上层目录的模式,而最接近当前目录的上层目录的模式优先于更上层的目录的模式。
    4. 来自.git/info/exclude文件的模式
    5. 来自配置变量core.exludefile指定的文件中的模式。
  • .gitignore会随着版本传播,但.git/info/exclude不会。因此一般情况下,只有当模式普遍适用于所有派生的版本库时,才应该把条目放进版本控制下的.gitignore文件中。否则,那些仅适用于与自身版本库的模式,应当写入.git/info/exclude

5.7 对象模型


第6章 提交

  • 提交是将变更引入版本库的唯一方法,任何版本库中的变更都必须由一个提交引入。
  • 虽然最常见的提交情况是由开发人员引入的,但是Git自身也会引入提交(比如合并)。
  • 对于提交不要有任何负担,Git非常适合频繁的提交,并且它还提供了丰富的命令集来操作这些提交。
  • 提交是一个原子性的操作,一张提交快照代表所有文件和目录的变更,它代表一棵树的状态,而两张提交快照之间的变更集就代表一颗完整树到树的转换。
  • Git还会记录每个文件的可执行模式标识。标识的改变也算是变更的一部分。
  • Git的提交有“显示”和“隐式”两种引用,显示表达为ID,隐式如HEAD

6.1 识别提交

  • 绝对提交名

    • 每一个提交的散列ID都是全局唯一的,不仅仅是对某个版本库,而且是对任意和所有版本库都唯一。(如果连个版本库中的ID是一致的,说明他们有一个相同内容的相同提交)

      • git log -1 --pretty=oneline HEAD
      • git log -l --pretty=oneline 33c79c6038978aad4bed37f3bb6b0277bd74ca06
      • Git允许用前缀来表达ID:git log -1 33c79c
    • 引用和符号引用

      • 定义
        • 引用(ref)是一个SHA1散列值,指向Git对象库中的对象。虽然ref可以指向对象,但是通常指向commmit对象。
        • 符号引用(symref, symbolic reference)间接指向Git对象。
      • 类别
        • 本地特性分支名称:refs/heads/ref/
        • 远程跟踪分支名称:refs/remotes/ref/
        • 标签名:refs/tags/ref/
      • 使用引用的时候可以用全称也可以用简称,当使用简称出现冲突时,会按如下优先级搜索,从高到底:

        1. If $GIT_DIR/<refname> exists, that is what you mean (this is usually useful only for HEAD, FETCH_HEAD, ORIG_HEAD, MERGE_HEAD and CHERRY_PICK_HEAD);
        2. otherwise, refs/<refname> if it exists;
        3. otherwise, refs/tags/<refname> if it exists;
        4. otherwise, refs/heads/<refname> if it exists;
        5. otherwise, refs/remotes/<refname> if it exists;
        6. otherwise, refs/remotes/<refname>/HEAD if it exists.
        • From Manual git rev-parse --help

          • HEAD names the commit on which you based the changes in the working tree. FETCH_HEAD records the branch which you fetched from a remote repository with your last git fetch invocation. ORIG_HEAD is created by commands that move your HEAD in a drastic way, to record the position of the HEAD before their operation, so that you can easily change the tip of the branch back to the state before you ran them. MERGE_HEAD records the commit(s) which you are merging into your branch when you run git merge. CHERRY_PICK_HEAD records the commit which you are cherry-picking when you run git cherry-pick.
          • Note that any of the refs/* cases above may come either from the $GIT_DIR/refs directory or from the $GIT_DIR/packed-refs file. While the ref name encoding is unspecified, UTF-8 is preferred as some output processing may assume ref names in UTF-8.
        • <refname>可以是:masterheads/masterrefs/heads/master

        • 从技术角度来说,Git的目录名.git是可以改变的,因此内部文档都使用$GIT_DIR表示。 - 特殊符号引用:这些引用可以在使用提交的任何地方使用
        • HEAD:HEAD始终指向当前分支的最近提交
        • ORIG_HEAD:某些操作,例如合并(merge)和复位(reset),会把HEAD改为最新值前,将其值赋值给ORIG_HEAD
        • FETCH_HEAD:使用远程库时,git fetch命令将所有抓取分支的头记录到.git/FETCH_HEAD中,并且仅在刚刚抓取操作之后有效。
        • MERGE_HEAD:当一个合并操作正在进行时,~其他分支的头暂时记录在MERGE_HEAD中~将那些准备合入到你分支的提交记录到此头部中。
  • 相对提交名称:

    • ^:选择哪个父提交 相对提交

      • 第一个父提交:C^1C^
      • 第一个父提交的第一个父提交:C^1^1C^^(等效于C~2

        • ~:指定前几提交 相对提交
      • 前1提交:C~1C~

      • 前2提交:C~2C~~
        • 注意:这些名字都相对于引用的当前值。如果当前引用指向一个新提交,那么提交历史图将变为“新版本”,所有父提交“辈分”都会上升一层。
        • 示例:在git源码中执行git show-branch --more=35 | tail -10
      [root@3b05e5c9c2d3 git]# git show-branch --more=35 | tail -10
      [master~14] Merge branch 'ds/rebase-update-ref'
      [master~14^2] sequencer: notify user of --update-refs activity
      [master~15] Merge branch 'kk/p4-client-name-encoding-fix'
      [master~15^2] git-p4: refactoring of p4CmdList()
      [master~16] Sync with 'maint'
      [master~16^2] Downmerge a handful of fixes for 2.37.x maintenance track
      [master~16^2^] Merge branch 'tk/rev-parse-doc-clarify-at-u' into maint
      [master~16^2^^2] rev-parse: documentation adjustment - mention remote tracking with @{u}
      [master~16^2~2] Merge branch 'll/ls-files-tests-update' into maint
      [master~16^2~2^2] ls-files: update test style
      

6.2 查看提交记录

  • 查看旧提交:

    • git log等效于git log HEAD,从HEAD开始回溯
    • git log <commit>从指定提交开始回溯
    • git log <since>..<until>,查看从sinceuntil范围内的提交记录,数学表达为\((since, until]\)git log --pretty=short --abbrev-commit master~12..master~10
    • 参数:
      • -p:产生补丁
      • --pretty=<oneline|short|full>:输出格式
      • -<n>:输出n条日志
      • --stat:每个提交的变更统计情况
      • --graph:查看图形表达
  • 提交图:

    • 有向无环图(DAG):图中每条边都从一个节点指向另一个节点(称为有向)。其次,从图中任意一节点开始,沿着有向走不存在可以回起始点的路径(称为无环): DAG
    • 简化图: 提交图
  • 提交范围:

    • 可达性:

      • 如果从A节点出发,根据规则沿着图中的边走,并且可以到达节点X,那么我们就称为X节点是A节点的可达节点。A节点的所有可达节点就组成A节点的可达节点集合(S
      • git log Y的意思就是给出Y的所有可达节点。
      • git log ^X Y的意思就是给出Y的排除X及其之前的所有可达节点,用简单表达就是X..Y\((X, Y]\),也可以理解为: $$ S=Y-X $$
        • 分支间的范围:
      • A..B:表示的是在B中,但不在A中的提交。可以理解为: $$ S = A - (A \cap B) $$

        分支范围 - 对称差(Symmetric difference):A...B - 对称差顾名思义是对称的,因此可以还可以表达为:B...A,实际表达方式为:git rev-list A B --not $(git merge-base --all A B) $$ S = (B \cup A) - (B \cap A) $$

      分支差

    • 任意序列的包含和排除:git log ^branch1 ^branch2 ^branch3 master

  • 查找提交

    • git bisect:利用二分法来查找一个坏的版本(提交)
      • 参数
        • start:启动
        • bad:声明此提交为一个坏的提交
        • good:声明次提交为一个好的提交
        • log:查看处理日志
        • reset:切回主分支
    • git blame可以识别文件中的内容是谁修改的,哪次提交的。
    • git log -S<string>:查看关键字string在文件差异历史中搜索。这个搜索方式称为"pickaxe"

第7章 分支

7.1 使用分支的原因

  • 常见理由:

    • 一个分支通常代表一个单独的客户发布版。如果你想开始项目的1.1版本,但你知道一些客户想要保持1.0版,那就把旧版本留作一个单独的分支。
    • 一个分支可以封装一个开发阶段,如原型、测试、稳定或临近发布。你也可以认为1.1版本发布是一个单独的阶段,也就是维护版本。
    • 一个分支可以隔离一个特性的开发或则研究特别复杂的Bug。例如,可以引入一个分支来完成一个明确定义的、概念上孤立的任务,或在发布之前帮助几个分支合并。
      • 只是为了解决一个Bug就创建一个新分支,这看起来可能是杀鸡用牛刀了,但Git的分支系统恰恰鼓励这种小规模的使用。
    • 每一个分支可以代表单个贡献者的工作。另一个“集成”分支可以专门用于凝聚力量。
  • Git把列出的这些分支视为特性分支(topic branch)或开发分支(development branch)。“特性”仅指每个分支在版本库中有特定的目的。Git也有追踪分支(tracking branch)的概念。

  • 分支或标签:应避免使用相同的名称

    • 标签:静态的
    • 分支:动态的

7.2 分支名

  • 默认名:master
  • 层次分支:类似于UNIX的路径名,比如:在bug分支下创建bug/pr-1023bug/pr-17分支。
  • Git搜索分支时支持通配符:git show-branch 'bug/*'
  • 命名规则:
    • 可以使用/创建一个分层的名称,但是该分支名不能以斜线结尾。
    • 不能以-开头。
    • /分割的组件不能以.开头,如:feature/.new是不被允许的。
    • 不能包含两个连续的.
    • 不能包括一下符号:
      • 任何空格或其他空白符。
      • 在Git中具有特殊含义的字符:~^:?*[
      • ASCII码控制字符,即值小于\040\0177字符

7.3 使用分支

  • 活动分支:操作的默认目标
    • 可以把任意分支设置成活动分支:git checkout <master>
  • 每个分支在一个特定的版本库中必须有唯一的名字,这个名字始终指向该分支上最近提交的版本。一个分支的最近提交称为该分支的头部(tip或head)
  • 通过源分支名和分叉出的新分支名,可以查找到分叉点:git merge-base <original-branch> <new_branch>

7.4 创建分支

  • 命令:git branch <branch_name> [commit]

    • 如果没有提供commit,则默认为HEAD
  • 注意:git branch <branch_name>命令指示把分支名引进版本库,分支名就好比C语言的指针,指向某个提交,或者说是动态的tag

7.5 列出分支名

  • 命令:git branch [-r|-a]

    mbp:git-example wang_hongqi$ git branch
      b1
    * b2
      master
    
    • *指示活动分支
    • 参数:没有提供-r-a的话,列出的是本地特性分支
      • -a:列出所有分支
      • -r:列出远程追踪分支

7.6 查看分支

  • git show-branch,这个命令列出的信息比git branch详细:

    mbp:git-example wang_hongqi$ git show-branch
    ! [b1] add dir1/file3
     * [b2] add file5
      ! [master] add file4
    ---
     *  [b2] add file5
      + [master] add file4
    +*+ [b1] add dir1/file3
    
    • --符号上方是分支名,下方是每个分支的提交矩阵:
      • 分支名:
        • 活动分支用*表示
        • 分支名后面是日志消息的第一行
      • 提交矩阵:
        • *突出活动分支
        • +表示提交在一个分支中
        • -表示一个合并
    • 分支中的提交是有序的,但是分支之间是无序的。
  • 当调用时,git show-branch遍历所有显示的分支上的提交,在它们最近的共同提交处停止。 > 在第一个共同提交处停止是默认启发策略,这个行为是合理的。 据推测,达到这样一个共同的点会产生足够的上下文来了解分支之间 的相互关系。如果由于某种原因,你想要更多提交历史记录,使用--more=<n>选项,指定你想在共同提交后看到多少个额外的提交。

  • git show-branch [branch_name]...:指定要查看的分支
    • 支持通配符*

7.7 检出分支

  • git checkout <branch_name>用于检出指定分支,即切换到指定分支,此时目标分支为活动分支。
    • 参数
      • -f:强制检出
      • -m:merge,可以将本地的修改和目标分支之间进行一次合并
        • 注意:此时一定要查看是否有冲突存在
  • 检出动作的 影响
    • 在要被检出的分支中但不在当前分支中的文件和目录,会从对象库中检出并放置到工作树中;
    • 在当前分支中但不在要被检出的分支中的文件和目录,会从工作树中删除;
    • 这两个分支都有的文件会被修改为要被检出的分支的内容。
  • 检出动作的 安全机制
    • 不会变动哪些未被追踪的文件或目录
    • 如果一个本地文件被修改且不同于目标分支上的变更,则拒绝检出,并提示导致拒绝的原因
      • 当出现此问题时,可以通过查看本地分支和目标分支上关于差异文件的情况:
        • 查看当前分支文件:cat example_file
        • 查看当前分支文件的差异:git diff example_file
        • 查看目标分支文件:git show branch_name:example_file
  • 创建并检出到新分支:git checkout -b <branch_name> [commit]
    • 当检出一个提交,而非分支的时候,如tag,Git回自动创建一个分离的HEAD(detached HEAD, 临时的),如果我们改动并持久化,这时候可以通过-b参数,来同步创建一个对应的分支。以下情况回产生分离HEAD:

      • 检出的提交不是分支的头部。
      • 检出一个追踪分支
      • 检出一个tag
      • 启动git bisect
      • 使用git submodule update
      mbp:git-example wang_hongqi$ git checkout test_tag
      Note: switching to 'test_tag'.
      
      You are in 'detached HEAD' state. You can look around, make experimental
      changes and commit them, and you can discard any commits you make in this
      state without impacting any branches by switching back to a branch.
      
      If you want to create a new branch to retain commits you create, you may
      do so (now or later) by using -c with the switch command. Example:
      
        git switch -c <new-branch-name>
      
      Or undo this operation with:
      
        git switch -
      
      Turn off this advice by setting config variable advice.detachedHead to false
      
      HEAD is now at 0213937 add file5
      

7.8 删除分支

  • 命令:git branch -d <branch_name>

  • 安全机制:

    • 不允许删除当前分支
    • 不允许删除一个 不存在于当前分支中 的提交 的分支
  • -D而不是-d可以强制删除一个分支

  • 如果一个提交不可达,会被git gc工具回收
  • 意外删除的恢复工具:
    • git reflog
    • git fsck

第8章 diff

  • diff [-u] [-r]:Linux/Unix系统中用来比较两个文件的差异

    • -u:可选项,用来产生合并格式的差异(unified diff)

      [admin@iZj6ciigioovfebodr8251Z git-example]$ diff -u file1 file2 
      --- file1   2022-09-04 19:29:55.971028382 +0800
      +++ file2   2022-09-04 19:29:12.644588336 +0800
      @@ -1,2 +1,2 @@
      1
      -3
      +2
      
      • ---:原始文件被其标记
      • +++:新文件被其标记
      • -:减号开始的行表示从原始文件删除改行以得到新文件
      • +:加号开始的行表示从原始文件中添加该行以产生新文件
      • :以空格开始的行是两个版本都有的行
    • -r:递归;遍历每个目录的文件,并总结每个文件的差异。

  • git diff [-r]:用来比较两颗树对象之间的差异

    • -r:遍历两颗树对象,并比较其差异

8.1 git diff 命令的格式

  • 基本命令形式

    • git diff:比较workindex
    • git diff commit:比较workcommit
    • git diff --cached commit:比较indexcommit
      • 也可以用--staged代替--cached,等效的
    • git diff commit1 commit2:比较commit1commit2
  • 参数:

    • -w--ignore-all-space:比较时忽略空白符
    • --stat:显示针对两个树状态之间差异的统计数据
    • --color:给输出结果上色

8.2 git diff和提交范围

  • git log关心的是每一次提交(变更),而git diff关心的是两次提交的差异。因此

    • git log操作一系列提交
    • git diff操作两个不同的节点
  • git diff commit1 commit2等效于git diff commit1.. commit2


8.3 路径限制的git diff

  • git diff commit1 commit2 directory|file:比较directoryfilecommit1commit2中的差异

  • git diff -S<string> master~10:查看在master分支中最近10次提交中关于string的改动(即string的新增或删除)

8.4 比较SVN和git如何产生diff

  • SVN为了节省空间和开销,只存储文件间的差异(diff)。
  • 因此当要比对两个版本间的差异时,SVN需要获取两个版本之前的所有差异(diff),然后将其合并成大的diff并发送给客户端。
  • 而git则可以直接通过两个版本的提交获取差异,并生成diff交给用户,因此速度快得多。

第9章 合并

9.1 合并例子

  • 合并操作

    git checkout branch        # 切换到目标分支branch
    git merge other_branch     # 将other_branch合并到当前分支(branch)
    
    • git merge操作是区分上下文的。当前分支始终是目标分支,其他一个或多个分支始终合并到当前分支
  • 作为一般规则: > 如果每次合并都从干净的工作目录和索引开始,那么关于Git的操作将会容易很多

  • 合并出现冲突,用git diff查看冲突:

    [admin@iZj6ciigioovfebodr8251Z git-example]$ git diff
    diff --cc file1
    index 1191247,7ecb0bf..0000000
    --- a/file1
    +++ b/file1
    @@@ -1,2 -1,3 +1,7 @@@
    1
    ++<<<<<<< HEAD
    +2
    ++=======
    + 333
    + 4
    ++>>>>>>> alt
    
    • 改变的内容:<<<<<<<=======之间。
    • 替代的内容:=======>>>>>>>之间。
    • +-来表示各合并分支相较于当前HEAD的一个增删情况
      • 第一列符号显示相对于目标(当前)分支的更改,第二列显示相对于另一个分支的更改
    • 三方合并标记线(<<<<<<<=======>>>>>>>)是自动生成的,但是他们只是提供给你看的,而不是给程序看的,一旦解决了冲突,就应该在文本编辑里删除它们。

9.2 合并冲突

  • 当存在冲突时,git会在索引中把他们标记为冲突的(cfonflicted)

  • 定位冲突文件:

    • git status
    • git ls-files -u-u是unmerge的意思
  • git给第二个父版本(HEAD^2)起了特殊名字:MERGE_HEAD

  • git diff --oursgit diff HEAD的同义词,翻译为“我们的”;

  • git diff MERGE_HEADgit diff --theirs同义,翻译为“他们的”;
  • git diff --base可以查看与合并基础的差异;等效于git diff $(git merge-base HEAD MERGE_HEAD)
  • 对于有冲突的文件执行git diff只会显示真正有冲突的部分。
  • 如果只有一边有变化,这部分就不显示。
  • 对冲突使用git log

    git log --merge --left-right -p
    
    • --merge:只显示跟产生冲突的文件相关的提交
    • --left-right:如果提交来自合并的“左”边则显示<(“我们的”版本,就是你开始的版本),如果提交来自合并的“右”边则显示>(“他们的”的版本,就是你要合并到的版本)。
    • -p:显示提交信息和每个提交相关联的补丁。
  • Git追踪分支的方法由几部分组成:

    • .git/MERGE_HEAD存储合并时“他们的”分支的HEAD对应的SHA1值
    • .git/MERGE_MSG存储当冲突解决后执行git commit命令时用到的默认合并消息
    • Git的索引包含每个冲突文件的三个副本:合并基础、“我们的”版本和“他们的”版本
    • 冲突的版本(合并标记和所有内容)不存储在索引中。相反,它存储在工作目录中的文件里。当执行不带任何参数的git diff命令时,始终比较索引与工作目录中的内容。
  • 使用git diff :1:hello :3:hello来查看合并基础与“他们的”版本的差别:

    diff --git a/:1:hello b/:3:hello
    index ce01362..562080a 100644
    --- a/:1:hello
    +++ b/:3:hello
    @@ -1 +1,3 @@
     hello
    +world
    +Yay!
    
  • 冲突期间(1.6.1版本+),通过git checkout --ours检出我们的版本,git checkout --theirs检出他们的版本。

  • 当解决一个冲突后,执行git addgit rmgit update-index来清除冲突状态

  • 当查看一个合并提交时,应注意三件有趣的事:

    • 在开头第二行写着的“Merge:”。通常在git log或者git show中不显示父提交,但当一个提交是来源于合并操作时,会则通过Merge:字段来显示每个祖先的SHA1。
    • 自动生成的提交日志消息有助于标注冲突的文件列表。如果事实证明一个特定的问题是由合并引起的,这将十分有用。通常,问题都是由不得不手动进行合并的文件引起的。
    • 合并提交的差异不是一般的差异。它始终处于组合差异或者“冲突合并”的格式。认为Git中一个成功合并是完全没有变化的;它只是简单地把其他已经在历史中的变更组合起来。因此,合并提交的内容里只显示与合并分支不同的地方,而不是全部的区别
  • 中止或重启合并:

    • 如果合并还未提交:想立即把工作目录和索引都还原到git merge命令之前,执行:

      git reset --hard HEAD
      
    • 如果合并已经提交,则执行:

      git reset --hard ORIG_HEAD
      
    • 如果冲突解决方案搞砸了,想返回尝试解决冲突前的冲突状态,则执行:

      git checkout -m
      

9.3 合并策略

  • 退化合并:虽然执行了合并操作(git merge),但是实际上并不引入一个合并提交。

    • 已经是最新的:来自其他分支(HEAD)的所有提交都存在于目标分支上时,即使它已经在自己的分支上前进了,目标分支还是已经更新到最新的。因此,没有新的提交添加到你的分支上。
    • 快进的:当分支HEAD已经在在其他分支中完全存在或表示时,就会发生快进合并。(前一种的反向场景)
  • 常规合并:

    • 解決策略: > 解决策略只操作两个分支,定位共同的祖先作为合并基础,然后执行一个直接的三方合并,通过对当前分支施加从合并基础到其他分支HEAD的变化。
    • 递归策略: > 一次只处理两个分支。处理两个分支之间有多个合并基础的情况。在这种情况下,Git会生成一个临时合并来包含所有相同的合并基础,然后以此为基础,通过一个普通的三方合并算法导出两个给定分支的最终合并。

      递归策略 - 章鱼策略:

      • 章鱼合并策略是专门为合并两个以上分支而设计的。在内部,它多次调用递归合并策略,要合并的每一个分支调一次。
      • 然而这个策略并不能处理需要用户交互解决的冲突,在这种情况下,必须做一系列常规合并,一次解决一个冲突。
  • 特殊提交:

    • “我们的”策略:

      • 合并任意数量分支。丢弃其他分支的改动,仅适用当前分支的文件。
      • 该策略用于合并历史记录
    • 子树策略: > 将一个分支合到当前分支的某个子树下面。无需指定,Git自动决定。

  • 应用合并策略

    1. 尝试“退化合并”,消除不重要的,简单的情况
    2. 常规合并:
      • 如果指定了超过2个分支,则使用章鱼策略
      • 否则选择默认策略(早期版本是“解决策略”,后期默认策略改为“递归合并策略”)
        • 手动使用解决策略:git merge -s resolve Bob
  • 合并驱动程序:合并策略是由合并驱动程序处理的,一个合并驱动程序会产生三个临时文件名来代表“共同祖先”、“目标分支版本”和“文件的其他分支版本”。

    • 文本(text)合并驱动程序:留下三方合并标志。
    • 二进制(binary)合并驱动程序:简单保留文件的目标分支版本,在索引中把文件标记为冲突的。(迫使你手动处理二进制文件)。
    • 联合(union)驱动程序:把两个版本的所有行留在合并后的文件里。

9.4 Git如何看待合并

  • 合并后的tree对称地代表源分支两边,但仅在目标分支有commit指向。
  • Git的理念,所有分支生而平等。

  • 压制合并:将源分支的所有变动作为一个patch提交到目标分支中,这样源分支的历史记录将丢失。Git并不提倡压制分支

    • git mergegit pull中使用--squash选项可以启用压制合并。

第10章 更改提交

  • 修改或返工某个提交或提交序列的理由:

    • 可以在某个问题变为遗留问题之前修复它
    • 可以将大而全面的变更分解为一系列小而专题的提交。相反,也可以将一些小的变更合并成一个更大的提交
    • 可以合并反馈评论和建议
    • 可以在不破坏构建需求的情况下重新排列提交序列
    • 可以将提交调整为一个更合乎逻辑的序列
    • 可以删除意外提交的调试代码
  • 是否应当更改历史有一个哲学探讨,我倾向于不变更历史:

    • 历史结合“频繁小而全的提交”的理念,可以轻易的复盘提交者的想法。
    • 完整的历史可以保留发展过程,调整历史意味着有可能抹去此过程。
  • 注意事项:

    • 分支若已发布,则相应的提交不应被修改。反之,则可以修改

10.1 git reset

git reset是有“破坏性的”,因为它可以覆盖并销毁工作目录中的修改。事实上,数据可能会丢失。

  • git reset --soft:将HEAD引用指向给定提交,索引和工作目录的内容保持不变。这个版本的命令有“最小”影响,只改变一个符号引用的状态使其指向一个新提交。
  • git reset --mixed:将HEAD引用指向给定提交,索引内容也跟着改变,但工作目录不变。默认模式
  • git reset --hard:将HEAD引用指向给定提交,索引和工作目录也跟着变更。
选项 HEAD 索引 工作目录
--soft Y N N
--mixed Y Y N
--hard Y Y Y
  • 执行此命令后,会将原始HEAD值存在ORIG_HEAD中。
  • 重置HEAD的方法:
    • git log查看reset前的SHA1值
    • git reflog查看reset前的SHA1值:

      69fcd79 (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: commit: 如果没有提供build_type会出问题
      b41da0b HEAD@{1}: commit: clean时会出问题
      beeca34 HEAD@{2}: pull: Fast-forward
      ba29bdf HEAD@{3}: pull: Fast-forward
      570ae93 HEAD@{4}: commit: 解决jenkins编译的时候报错没有输入设备
      bc28786 HEAD@{5}: commit: 解决Jenkinsfile格式有误
      e01cc27 HEAD@{6}: commit: 解决test时报错
      d679eac HEAD@{7}: commit: 解决容器编译错误
      8185dc8 HEAD@{8}: commit: 解决在容器中构建失败
      128979d HEAD@{9}: pull: Fast-forward
      b59c76e HEAD@{10}: commit: 解决容器编译失败
      4dd8234 HEAD@{11}: pull: Fast-forward
      6b17e50 HEAD@{12}: pull: Fast-forward
      f56fe48 HEAD@{13}: clone: from https://gitee.com/homqyy/hcore
      
      • 例如用HEAD@{2}来表示beeca34
      • 通过git reset HEAD@{2}来重置HEAD

10.2 git cherry-pick

  • git cherry-pick命令通常用于把版本库中一个分支的特定提交引入一个不同的分支中。常见用法是把维护分支的提交移植到开发分支。
  • git cherry-pick commit:取出指定提交并应用到当前分支上
  • git cherry-pick commit1..commit2:取出指定的提交范围并应用到当前分支上

10.3 git revert

  • git revert提交命令跟git cherry-pick大致相同,重要区别为: 它应用给定提交的逆过程。因此此命令用于引入一个新提交来抵消给定提交的影响。
  • 此命令可以翻译为“撤销某个提交”:git revert commit

10.4 修改最新提交

  • git commit --amend:修改文件并提交到索引后,再执行此命令。
    • 修改作者使用选项--author
    • 此命令不会引入新提交

10.5 rebase

  • git rebase常见用途是保持你正在开发的一系列提交相对于另一个分支是最新的,那通常是master分支或来自另一个版本库的追踪分支。

    • 变基操作会把原始分支历史(带合并)线性化,想明确保留使用--preserve-merges选项
  • 变基本概念:

    • 变基把提交重写成新提交;
    • 不可达的旧提交会消失;
    • 任何旧的、变基前的提交的用户可能被困住;
    • 如果你有个分支用变基前的提交,你可能需要反过来对它变基。
    • 如果有个用户有不同版本库中变基前的提交,即使它已经移动到了你的版本库中,他仍然拥有该提交的副本;该用户现在必须也修复他的提交历史记录。
  • topic变基为master当前的HEAD

    • 变基前: 变基提交
    • 执行变基:

      git checkout topic
      git rebase master
      # 或者
      git rebase master topic
      
    • 变基本后: 变基后

  • 使用--onto参数把一条分支上的开发线整个移植到完全不同的分支上:

    # 将(main^, feature]提交移植到master上
    git rebase --onto master main^ feature
    
  • 变基操作一次只迁移一个提交,出现冲突后会暂停,解决后执行git rebase --continue恢复。

  • 在检查变基冲突的时候,执行git rebase --skip跳过当前提交。
  • 执行git rebase --abort中止变基操作。
  • git rebase -i可以修改分支的提交顺序或合并提交:

    pick 60816f8 alt file1
    pick 32b859d add hcore.git to ./hore
    
    # Rebase 8a28e1d..32b859d onto 8a28e1d (2 commands)
    #
    # Commands:
    # p, pick <commit> = use commit
    # r, reword <commit> = use commit, but edit the commit message
    # e, edit <commit> = use commit, but stop for amending
    # s, squash <commit> = use commit, but meld into previous commit
    # f, fixup <commit> = like "squash", but discard this commit's log message
    # x, exec <command> = run command (the rest of the line) using shell
    # b, break = stop here (continue rebase later with 'git rebase --continue')
    # d, drop <commit> = remove commit
    # l, label <label> = label current HEAD with a name
    # t, reset <label> = reset HEAD to a label
    # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
    # .       create a merge commit using the original merge commit's
    # .       message (or the oneline, if no original merge commit was
    # .       specified). Use -c <commit> to reword the commit message.
    #
    # 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.
    #
    
    • 从最老到最新排序

第11章 储藏和引用日志

11.1 储藏

  • 储藏可以捕获你的工作进度,允许你保存工作进度并且当你方便时再回到该进度。
  • 储藏是一种快捷方式,它让你仅通过一条简单的命令就全面彻底地捕获工作目录和索引。它使你的版本库时干净整洁的,准备好向另一个方向开发。有另一条简单的命令可以完全还原工作目录和索引,让你回到你离开时的状态。

  • 操作

    # 开发中...
    
    git stash save # 储藏当前的工作状态(工作目录和索引)
    
    # 完成临时工作...
    
    git stash pop # 弹出上一个储藏的工作状态
    
    # 继续开发...
    
    • 储藏:git stash [--include-untracked] [--all] [save [message]]

      • 常用缩略语:"WIP(work in progress)"
      • git stash默认操作是save
      • 索引和工作目录的内容实际上另存为独立且正常的提交,它们可以通过refs/stash来查询:
      • 使用--include-untracked参数来储藏未追踪的文件

        git show-branch stash
        
      • 使用--all选项来储藏所有文件包括“未追踪的”、被“.gitignore”和“exclude”忽略的

    • 恢复并删除储藏:git stash pop:仅在干净工作目录中可用

      • 恢复:git stash apply
      • 删除:git stash drop
    • 将储藏的内容放到一个分支中:git stash branch \

      • 该分支基于储藏时的基本提交,而不是执行此命令时的HEAD。
    • 列出储藏栈:git stash list

      [admin@iZj6ciigioovfebodr8251Z git-example]$ git stash list
      stash@{0}: On master: modify file1 and file2
      
    • 查看变更:git stash show

      • 显示的是特定储藏条目相对于它的福提交的索引和文件的变更记录。
      • git diff的所有选项也适用于此
      [admin@iZj6ciigioovfebodr8251Z git-example]$ git stash show stash@{0}
      file1 | 2 ++
      file2 | 1 +
      2 files changed, 3 insertions(+)
      
      [admin@iZj6ciigioovfebodr8251Z git-example]$ git stash show -p stash@{0}
      diff --git a/file1 b/file1
      index 7ecb0bf..1d5d911 100644
      --- a/file1
      +++ b/file1
      @@ -1,3 +1,5 @@
       1
       333
       4
      +12
      +123124124
      
  • git stash savegit stash pop这两条命令实现了储藏状态栈,因此它允许我们在中断工作流的情况下再次中断。栈上每个储藏的上下文都可以通过正常提交流程来单独管理。

  • 当需要解决冲突时,Git将不会自动丢弃状态,以防你想要尝试不同的方法或还原到不同的提交。一旦清理了合并冲突并希望继续,就应该使用git stash drop来删除储藏。

  • git stash的另一个经典应用场景是所谓的“在脏树中进行拉取”:

    git pull # 拉取失败,因为工作树是脏的
    
    git stash save
    git pull # 拉取成功
    git stash pop
    

11.2 引用日志

  • 引用日志(reflog)记录非裸版本库中分支头的改变。每次对引用的更新,包括对HEAD的,引用日志都会更新以记录这些引用发生了哪些变化。把引用日志当做面包屑轨迹一样指示你和你的引用去过那里。以此类推,也可以通过引用日志来跟随你的足迹并回溯你的分支操作。

  • 会更新引用日志的基本操作:

    • 复制;
    • 推送;
    • 执行新提交;
    • 修改或创建分支;
    • 变基操作;
    • 重置操作;
  • 从根本上说,任何修改引用或更改分支头的Git操作都会记录。

  • 默认情况下,引用日志在飞裸版本库中是启用的,在裸版本库中是禁用的。明确地说,引用日志是由配置选项core.logAllRefUpdates控制的。可以通过git config core.logAllRefUpdates <true|false>命令启用或禁用引用日志。

  • 查看引用日志:git reflog show

    [admin@iZj6ciigioovfebodr8251Z git-example]$ git reflog show
    32b859d (HEAD -> master) HEAD@{0}: reset: moving to HEAD
    32b859d (HEAD -> master) HEAD@{1}: rebase (abort): updating HEAD
    32b859d (HEAD -> master) HEAD@{2}: rebase (abort): updating HEAD
    0d8261a HEAD@{3}: commit: modify file1
    166373a HEAD@{4}: commit (amend): modify file2
    81d6da3 HEAD@{5}: commit: modify file2
    32b859d (HEAD -> master) HEAD@{6}: reset: moving to HEAD
    32b859d (HEAD -> master) HEAD@{7}: commit: add hcore.git to ./hore
    537e9ad HEAD@{8}: checkout: moving from alt to master
    60816f8 (alt) HEAD@{9}: checkout: moving from master to alt
    537e9ad HEAD@{10}: commit (merge): Merge branch 'alt'
    8a28e1d HEAD@{11}: commit: alt line 2
    3116b8a HEAD@{12}: checkout: moving from alt to master
    60816f8 (alt) HEAD@{13}: commit: alt file1
    3116b8a HEAD@{14}: checkout: moving from master to alt
    3116b8a HEAD@{15}: commit (initial): add file1 and file2
    
    • 此命令一次只显示一个引用的事务
    • 查看指定分支的引用:git reflog <branch_name>
    • HEAD@{1}始终指向分支的前一次提交
    • Git针对引用大量基于日期的限定符。其中包括如yesterday, noon, midnight, tea, 星期, 月份, A.MP.M标识,这些绝对时间或日期,还有像last monday, 1 hour ago, 10 minutes ago和这些短语的组合(如1 day 2hours ago等),相对时间或日期。
    • 省略引用名的形式@{...},默认引用就是当前分支
    • 为了简化断字问题,Git允许以下几种形式:

      git log 'dev@{2 days ago}'
      git log dev@{2.days.ago}
      git log dev@{2-days-ago}
      
  • 通常情况下,一个提交,如果既不能从某个分支或引用指向,也不可达,将会默认在30天后过期,而哪些可达的提交将默认在90天后过期。

    • 设置超时时间:git config gc.reflogExpireUnreachablegit config gc.reflogExpire
    • 删除指定条目:git reflog delete
    • 立即清除过期项目:git reflog expire
      • --expire=now --all选项:等于立刻清除所有引用
  • 引用日志都存储在git/logs目录下。.git/logs/HEAD文件包含HEAD值的历史记录,它的子目录.git/log/refs/包含所有引用的历史记录,其中也包括储藏(stash)。它的二级子目录.git/logs/refs/heads包含分支头的历史记录:

    [admin@iZj6ciigioovfebodr8251Z git-example]$ ls .git/logs/refs/
    heads  remotes  stash
    
    • 在引用日志中存储的所有信息,特别是.git/logs目录下的一切内容,归根结底还是临时的,不重要的。抛弃.git/logs目录或关闭引用日志不会损坏Git的内部数据结构;它只意味着诸如master@{4}这样的引用不会被解析。

第12章 远程版本库

  • 一个克隆是版本库是版本库的副本。一个克隆包含所有原始对象;因此,每个克隆都是独立、自治的版本库,与原始版本库是真正对称、地位相同的。一个克隆允许每个开发人员可以在本地独立地工作,不需要中心版本库,投票或者锁。归根结底,克隆使Git易于扩展,并允许地理上分离的很多贡献者一起协作。

  • 分离版本库的优点:

    • 开发人员独立自主工作。
    • 开发人员被广域网分离。在相同地区的一群开发人员可以共享一个本地库来积累局部变化。
    • 一个项目预计在不同的发展线上有显著差异。虽然前面几章展示的正常分支和合并机制可以处理任何数量的独立开发,但是产生的复杂性带来的麻烦可能会比它们的价值更多。相反,独立的开发线可以使用单独的版本库,到适当的时候再进行合并。
  • 远程版本库(remote)是一个引用或句柄,通过文件系统或网络指向另一个版本库。

  • 一旦远程版本库建立,Git就可以使用推模式或拉模式在版本库之间传输数据。
  • 要跟踪其他版本库中的数据,Git使用远程追踪分支(remote-trackin branch)。
  • 你可以将你的版本库提供给他人。Git一般指此为发布版本库(publishing a repository)。

12.1 版本库概念

  • 一个Git版本库要么是一个裸(bare)版本库,要么是一个开发(非裸)(development, nobare)版本库。
  • 一个裸版本库没有工作目录,并且不应该用于正常开发。罗版本库也没有检出分支的概念。裸版本库可以简单地看做.git目录的内容。换句话说,不应该在裸版本库中进行提交操作。

  • 裸版本库用处:作为协作开发的权威焦点。

    • 其他开发人员从裸版本库中clonefetch,并push更新。
  • 创建裸版本库:

    • git clone --bare URL
    • git init --bare
  • 在正常使用git clone命令时,原始版本库中存储在refs/heads下的本地开发分支,会成为新的克隆版本库中refs/remotes下的远程追踪分支。原始版本库中refs/remotes下的远程追踪分支不会被克隆。标签也会被克隆。

    • 远程追踪分支与原始版本库是一个单向关系
    • --origin来改变默认名
    • 提取规则:fetch = +refs/heads/*:refs/remotes/origin/*
  • 远程版本库

    • git remote:创建、删除、操作和查看远程版本库
    • git fetch:提取远程版本库的对象及元数据
    • git pullgit fetch + git merge(或git rebase)
    • git push:转移对象及相关的元数据到远程版本库
    • git ls-remote:显示一个给定的远程版本库(在上游服务器上)的引用列表。
  • 追踪分支

    • 分类
      • 远程追踪分支:与远程版本库相关联,专门用来追踪远程版本库中每个分支的变化。
      • 本地追踪分支:与远程卓在那个分支相配对。它是一种集成分支,用于收集本地开发和远程追踪中的变更。
      • 特性/开发分支:任何非追踪分支
      • 远程分支:设在非本地的远程版本库的分支
    • 在常规特性分支上可以执行的所有操作都可以在追踪分支上执行。然而,需要遵守一些限制和指导:
      • 因为远程追中分支专门用于追踪另一个本本库中的变化,因此应该当成只读
      • 不应合并或提交到一个远程追踪分支,这样做会导致你的远程追踪分支变得和远程版本库不同步。

12.2 引用其他版本库

  • 引用远程版本库

    • /path/rto/repo.git
    • file:///path/to/repo.git
    • git://example.com/path/to/repo.git
    • git://example.com/~user/path/to/repo.git
    • ssh://[user@]example.com[:port]/~user2/path/to/repo.git
      • useruser2不同表示的是,user是验证会话的用户,user2是访问主目录的用户
    • user@example.com:~user/path/to/repo.git:scp格式
    • http://example.com/pth/to/repo.git
    • https://example.com/path/to/repo.git
    • rsync://example.com/path/to/repo.git:不鼓励使用rsync
  • refspec:

    • 语法:[+]source:destination
      • +:加号表示不会再传输过程中进行正常的快进安全检查。
      • 在某些引应用,source是可选的;在另一些应用中:destination是可选的
    • 默认行为:
      1. 如果git push没有指定远程版本库,它会默认使用origin
      2. 如果没有refspec,git push会将你的提交发送到远程版本库中你与上游版本库共有的所有分支。
      3. 新分支必须显示的使用分支名来推送:
        • git push origin branch
        • git push origin branch:refs/heads/branch
  • 远程版本库操作

    1. 添加一个命名的远程仓库:git remote add origin /home/admin/git-example/

      • -f:使用该选项会立即对远程版本库执行fetch
    2. 查看配置文件:cat .git/config

      [core]
          repositoryformatversion = 0
          filemode = true
          bare = false
          logallrefupdates = true
      [remote "origin"]
          url = /home/admin/git-example/
          fetch = +refs/heads/*:refs/remotes/origin/*
      
    3. 创建远程追踪分支:git remote update [remote_name]

      • 该命令会更新远程追踪分支
    4. 查看分支:git branch -a

      [admin@iZj6ciigioovfebodr8251Z local-git-example]$ git branch -a
      * master
      remotes/origin/alt
      remotes/origin/master
      
    5. master分支开发

    6. 推送变更:git push origin master
      • 这里分成两步:
        1. master的改动推送到origin命名的远程版本库
        2. master的改动添加到远程追踪分支origin/master
    7. 查看远程版本库的分支信息:

      • git ls-remote origin

        [admin@iZj6ciigioovfebodr8251Z local-git-example]$ git ls-remote origin
        edab2eef2d5577ed1a7f987464db4575b9f2e336    HEAD
        60816f830e3000fab6df3aca1f7eb176d0183467    refs/heads/alt
        edab2eef2d5577ed1a7f987464db4575b9f2e336    refs/heads/master
        2fd6a79f171a41c096d7ba1c3163924584e3ac13    refs/stash
        
      • git remote show origin

        [admin@iZj6ciigioovfebodr8251Z local-git-example]$ git remote show origin
        * remote origin
          Fetch URL: /home/admin/git-example/
          Push  URL: /home/admin/git-example/
          HEAD branch: master
          Remote branches:
            alt    tracked
            master tracked
          Local ref configured for 'git push':
            master pushes to master (up to date)
        
    8. 设置上游,用于默认pull:git branch --set-upstream-to=origin/master master

      • 查看配置文件:cat .git/config

        git { .highlight=[10,11,12] } [admin@iZj6ciigioovfebodr8251Z local-git-example]$ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] url = /home/admin/git-example/ fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master

        • pull第一步是fetch更新;第二步则是合并。
          • 合并的依据是基于merge配置:将远程版本库的refs/heads/master合并到master分支。
          • remote:表示分支对应的远程版本库。
    9. 获取上游更新:git pull [--rebase] [repository] [<refspec>...]

      • 如果没有指定repository则使用默认origin
      • 如果没有指定refspec则使用远程版本库的fetchrefspec
      • 如果指定了repository,但是没有指定refspec则抓取远程版本库的HEAD
      • --rebase:用变基替代合并
      • 选择合并还是变基:

        • 通过合并,每次拉取将有可能产生额外的合并提交来记录更新同时存在于每一个分支的变更。从某种意义上说,它真实地反映了两条开发路径独立发展然后合并在一起。在合并期间必须解决冲突。每个分支上的每个提交序列都基于原来的提交。当推送到上游时,任何合并提交都将继续存在。有些人认为这些是多余的合并,并不愿意看到它们弄乱历史记录。另一些人认为,这些合并是开发历史记录更准确的写照,希望看到它们被保留。
        • 变基从根本上改变了一系列提交是在何时何地开发的概念,开发历史记录的某些方面会丢失。具体而言,你的开发最初基于的原始提交将更改为远程追踪分支新拉取的HEAD。这将使开发出现的比它实际晚一些(在提交序列方面)。如果这样对于你没有问题,那么对于我也没问题。这只是跟合并的历史记录不一样,且更简单。当然,你在变基操作过程中仍然要去解决冲突。由于变基的变更仍然只在你的版本库中,还尚未公布,因此这次变基没有理由害怕“不改变历史记录”的禁咒。
        • 我倾向于看到简单线性的历史记录。在我个人的开发过程中,我通常不会太在意把我的变更相对于从远程追踪分支抓取的同事的变更做轻微地重新排序,因此我更喜欢变基选项。
        • 设置选项来实现自己的期望:设置branch.autosetupmergebranch.autosetuprebasetruefalsealways

12.3 图解远程版本库开发周期

  1. 创建版本库(未来作为远程版本库): ""

  2. 克隆版本库: ""

  3. 并行开发: ""

  4. 获取远端的更新 ""

  5. 合并历史记录 ""

  6. 合并冲突 ""

12.4 远程版本库配置

  • Git为建立和维护远程版本库信息提供三种机制(三种机制最终都体现在.git/config文件):

    • git remote命令
    • git config命令
    • 直接编辑.git/config文件
  • git remote:用来操作配置文件数据和远程版本库引用

    git remote [-v | --verbose]
    git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <url>
    git remote rename <old> <new>
    git remote remove <name>
    git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
    git remote set-branches [--add] <name> <branch>...
    git remote get-url [--push] [--all] <name>
    git remote set-url [--push] <name> <newurl> [<oldurl>]
    git remote set-url --add [--push] <name> <newurl>
    git remote set-url --delete [--push] <name> <url>
    git remote [-v | --verbose] show [-n] <name>...
    git remote prune [-n | --dry-run] <name>...
    git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)...]
    
    • 可以添加多个远程版本库URL来实现多源抓取和多目的推送。
  • git config:直接操作配置文件中的条目

    • 查看当前配置文件内容:git config -l

    • 示例

    git config remote.publish.url 'ssh://git.example.org/pub/repo.git'
    git config remote.publish.push '+refs/heads/*:refs/heads/*'
    

12.5 使用追踪分支

  • 使用远程追踪分支名的一个简单的检出请求会导致创建一个新的本地追踪分支,并与该远程追踪分支相关联。(仅当分支名与远程版本库的分支名相同才行)

  • git checkout [-b <branch>] [--track] <remote_branch>

    • 当远程分支名非唯一的时候需要使用--track指明具体分支
    • 当想要创建不同名的分支时,需要使用-b来设置分支名
      • 比如:git checkout -b test --track remote
  • 当创建本地追踪分支时,但并不想检出它,使用git branch --track <local branch> <remote branch>

  • 当已经存在一个特性分支,并想与远程版本库相关联,使用git branch --set-upstream <local branch> <remote brach>
  • 查看在master但不在origin/mastergit log origin/master..master
  • 查看各自新的提交:git log origin/master...master

12.6 添加和删除远程分支

  • 在远程版本库中创建分支:推送仅有源的refspec

    • git push origin foo:在origin中创建分支foo
    • git push origin foo:foo:在origin中创建分支foo
    • git push origin foo:refs/heads/foo:在origin中创建分支foo
  • 在远程版本库中删除分支:推送仅有目的的refspec

    • git push origin :foo:在origin中删除分支foo
    • git push origin --delete foo:在origin中删除分支foo

第13章 版本库管理

13.1 发布版本库

  • 带访问控制的版本库:Gitolite

13.1.1 git-daemon

  • 建立git-daemon允许使用Git原生协议导出版本库。

    • 在裸版本库的顶级目录创建git-daemon-export-ok文件来标记版本库可以被导出。
    • 为了避免单独标记每个版本库,可以在运行git-daemon命令时加上--export-all选项来发布所有在它的目录列表中可识别的(拥有objectsrefs子目录的)版本库。
  • 在服务器上建立git-daemon

13.1.2 HTTP守护进程


资源


遗留问题

  • git diff的对称差是什么?

评论