CVS

当我们在谈论git时,我们在谈论什么

Posted by Run-dream Blog on July 9, 2021

什么是 Git

简而言之,Git 是一种分布式版本控制系统。

和集中式的版本控制系统 SVN 进行比较,分析这两个关键词:

  • 版本控制

    SVN是增量式的版本控制,它不会把各个版本的副本都完整的保存下来,而只会记录下版本之间的差异(diff),然后按照顺序更新或者恢复特定版本的数据。这使得服务端的存储量会非常低。

    而Git 是基于快照的版本控制,其实现原理是为每个文件计算一个 hash 值然后压缩存储到 .git/objects 目录内,也就是说不同版本的同一文件会在git中存在多个object

  • 分布式

    SVN基于中心版本仓库,所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录;而进行版本管理操作或者与他人进行协作的前提也是:中心版本仓库必须始终可用

    对于Git而言,每个程序员都拥有一个本地git仓库,而不仅仅是一份代码拷贝,这个仓库就是一个独立的版本管理节点,它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上,程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下,任意两个git仓库之间可以进行点对点(比如Radicle)的协作,这种协作无需中间协调者(中心仓库)参与。

Git 是怎么实现的

Git 通过建立一个名为 .git 的隐藏文件夹,来存储相关的信息。

git目录

我们按照功能来依次分析

  • 文件存储 objects

    Object 是这个文件管理系统中最基本的单元,就代表了一个普通的文件,每一个被 git 管理的文件都会计算出一个 hash 值然后压缩放置于该目录中。同时一个提交和目录还有标签页被抽象成了对象的一种。

    • blob

      压缩过的普通文件

      注意这里是根据文件内容进行 hash,也就是不同文件名内容相同的文件只会生成一个 object。

    • tree

      目录,包含一个 entries 数组指向了当前目录下的文件或者其他目录,构成了一棵文件树。

      文件的文件名是存储在 Tree 中的。

    • commit

      版本,指向了属于该版本对应的目录以及所有的文件。同时还记录着提交人、时间、注释等其他信息。

      commit 也包含了指向上一个 commit 的指针数组 parent,这样所有的 commit 就能串成一条链表。

      注意 parent 可能会有多个,这也就意味着 commit 其实是一个网状结构,而不只是单纯的一条线。

    • tag

      标签,和 commit 关联,用来标记里程碑。

  • 引用 refs

    • heads

      每一个文件都代表一个分支,文件里则是对应 commit 的hash值。

    • remotes

      远端的分支

    • tags

      轻量级 tag

  • 工作区

    就是一个目录路径,代表了你在当前实际可见的文件集合,区别于备份于.git目录中的文件。

  • 暂存区 index

    指向的文件是工作区的文件,跟工作区唯一的区别就是,只有被 git add 过的文件才会出现在 index 中被 git 托管。

  • HEAD

    符号引用,指向目前所在的分支, 也就是 commit 的最尾端

  • 日志 logs

    日志,存储了各个分支的日志记录。

    └─refs
        ├─heads // 本地分支
        └─remotes // 远程分支
            └─origin
    
  • 钩子 hooks

    用于在特定的重要动作发生时触发自定义脚本。

Git 命令到底做了什么

将SVN命令按照使用场景来分类

  • 本地仓库

    可以理解成 .git 目录

    • git init
  • 工作区和暂存区

    • git add

      可以将一个文件或者目录加入到暂存区中,不过配置在 .gitignore 中的文件会被忽略

    • git rm

      rm 用于删除一个文件,git rm 默认会将文件从暂存区和工作区都移除,可以加 –cached 选项只从暂存区移除。只移除工作区的话,直接使用操作系统提供的原生 rm 命令。

    • git commit

      在调用 git commit 命令时,git 会将 index 引用的所有文件全部计算 hash 值,然后将当前指向的 commit 作为父节点,创建一个新的 commit 对象,然后让当前分支指向新创建的 commit 对象。

      --amend 上面这条命令会将最后一次的提交信息载入到编辑器中供你修改

    • git reset

      让当前分支 HEAD 指向指定的 commit,并且根据选项改动暂存区和工作区。

      • soft: 暂存区和工作区都不重置,仅仅改一下 head 指向的 commit。
      • mixed(默认): 暂存区会被重置,但是工作区不会。
      • keep:暂存区和工作区都会重置,但是变更会被重新应用。
      • hard: 暂存区和工作区都会重置到指定提交的状态。
  • 分支

    • git branch

      NOOP 列出所有分支

      [branch name] 新建分支

      -d -D 删除分支

    • git checkout / git switch

      [branch name]切换到某个新的分支

      -b 新增分支,然后切换过去

  • 合并

    • git merge

      将多个 commit 合并形成一个新的 commit。可以通过git merge -s 策略名字来强指定使用的策略类型

      • Fast-forward

        git 只需要将合并分支的ref指向最后一个 commit 节点。

        Fast-forward是 git 在合并两个没有分叉的分支时的默认行为。

      • Recusive

        通过算法寻找两个分支的最近公共祖先节点,再将找到的公共祖先节点作为base节点使用三向合并的策略来进行合并。

        无法合并的内容需要用户来手动解决冲突。

      • Ours & Theirs 参数

        在合并时我们可以带上-Xours-Xtheirs参数,表明合并遇到冲突时全部使用其中一方的更改。

        注意这两个参数只有遇到冲突时才会生效,这和Ours策略不一样

      • Ours

        无论有没有冲突,git会完全丢弃被合并分支上的内容,只保留合并分支的上的修改,只是在commit的记录上会保留另一个分支的记录。

      • Octopus

        如果现在有多个分支需要合并,使用Recursive策略进行两两合并会产生大量的合并记录。

        Octopus 在合并多个分支时只会生成一个合并记录,这也是git合并多个分支的默认策略。

      --squash 合并时,将对应分支的多个commit合并成一个commit

    • git rebase

      对一个分支做「变基」操作。将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

      使用场景:

      1. 合并 commit 记录,保持分支整洁

        git rebase -i HEAD~4
        
      2. 减少分支合并的记录

        # feature1
        git rebase master
        

        首先,git 会把 feature1 分支里面的每个 commit 取消掉。

        其次,把上面的操作临时保存成 patch 文件,存在 .git/rebase 目录下。

        然后,把 feature1 分支更新到最新的 master 分支。

        最后,把上面保存的 patch 文件应用到 feature1 分支上。

      潜在风险:

      多人协作时,如果你进行了rebase操作,可能导致其他人的commit丢失。

      使用建议:

      开发只属于自己的分支时尽量使用rebase,减少无用的commit合到主分支里,多人合作时尽量使用merge,一方面减少冲突,另一个方面也让每个人的提交有迹可循。

  • 远程仓库

    • git push

      尝试将本地分支的指针内容覆盖掉远端对应分支的指针 ORIGIN_HEAD,然后将本地指针指向的但是远端没有的对象,全部推送到远端。

      默认仅在 fast-forward 状态下才可以合并,即git push 在远端指针不是本地指针的祖先时会拒绝覆盖。

      而 –force,可以让 Git 不进行这个检查,直接覆盖远端对应 master 指针的内容。

    • git fetch

      基于本地的 FETCH_HEAD 记录,比对本地的 FETCH_HEAD 与远程仓库的版本号,获得当前的远程分支的后续版本的数据。

    • git pull

      默认情况下,git pull 相当于 git fetch + git merge

      --rebase 相当于 git fetch + git rebase, 不会因为拉取代码而生成新的commit记录

参考资料

Git实现原理

图解git原理的几个关键概念

git教程

git

git合并原理

聊下git merge –squash