注意:

  1. 在互联网上关于 Git 的文章已经多如牛毛,因此对于很多具体的细节并不会在此陈述。本文会给出一些大体的方向、使用点、重要要点等内容,通过这些内容以期待帮助笔者自己和读者更清楚:版本控制和 Git 的意义、它们在组织项目开发和个人项目开发中的地位和应用。
  2. 读者或者作者本人也可以参考这篇文章和本文引用的资料来查找具体参数和教程加深理解。
  3. 本文章/系列文章所包含的示例可能来自互联网的各个地方,我想说我认为这不是抄袭,把相关的内容放到一起就是一种总结,有利于我自己的学习。我会尽可能把引用源放到正确的位置,您可以自行查阅相关内容,如果本文/系列的总结和概括能够帮助您获得新的见解,则本人不甚荣幸。

基本思想

尽管 Git 的内容有很多,但是从整体上来看,它的设计仍然是简单而优雅的,因此我们有必要学习这样的设计,不仅仅是为了使用,也是为了增进设计能力。

  • 如果想更好的使用和认识 Git 就需要我们对他的一系列设计思想做出深刻认识

就我认为,Git 设计的优雅在以下几个方面体现出来了强大的力量:

  • 分布式设计
  • 由分布式设计带来的本地随心所欲和远端规范约束

而分布式设计是由多个更为关键的设计所支撑的:

  • 项目的三个阶段:工作区、暂存区、Git 目录
  • 分支
  • 提交快照

在分支上又衍生出一些灵活的应用:

  • 标签
  • 变基和合并

由于 Git 把项目分成三个阶段,把文件划分成不同状态,这相当于一定程度上增强了文件系统,而不是单独的另一套系统,这又给了很大的灵活性。(Git 的最初目标就是增强文件系统)

  • 增强文件系统给我们可以在此基础上做一些第三方工具的可能

而由于 Git 的分支和提交快照设计,以及它的分布式特点,使得它可以灵活选取工作模式和开发方式,从而极大的改善了工作过程。


其他:

  • Git 的几乎所有操作都是在本地完成的。
  • Git 几乎所有操作都指往数据库中添加数据,即很少有操作是从数据库里删除数据,那么这就意味着几乎所有的操作都是可逆的,文件几乎是不可能丢失的。

分布式设计

Git 是一个分布式版本控制系统(Distributed Version Control System,简称 DVCS)。这类系统相比之前的版本控制方案具有一些额外的优点,例如:

  • 分布式的设计去除了集中式版本控制的单点故障
  • 可以与多个远端仓库进行交互
  • 由于可以与多个远端交互,Git 从而实现了可以在多个工作流上工作,并且进一步的,可以根据需要选择不同的协作流程
  • Git 的分布式系统不只考虑单一的远程仓库,还考虑多个远程仓库的同步

Git 的设计使得它使用起来让人觉得很是优雅和有趣,它的设计使得我们可以灵活的在本地和远端之间交互,灵活的在本地不同分支交互,灵活的选取在不同工作模式上切换。这样灵活的设计思想真的是值得我们学习的。

随心所欲与规范约束:

Git 的基本原则之一是,由于克隆中有很多工作是本地的,因此你可以在本地随便重写历史记录。 然而一旦推送了你的工作,那就完全是另一回事了,除非你有充分的理由进行更改,否则应该将推送的工作视为最终结果。 简而言之,在对它感到满意并准备与他人分享之前,应当避免推送你的工作。

  • 即本地随意,一旦推送后就要谨慎

  • Git 的一些原则是不要给别人添麻烦!即考虑和远程仓库的交互时要慎重

    但是无论如何,这样的场景多半会发生,这个时候你要做的不是去咒骂这些同事或者自己如何愚蠢,而是需要明白解决方案

系统组成

可以看一下一个表示本地和远端的系统交互的图:

本地系统的图:

  • 此图显示了 :本地三个部分、文件状态转换

  • 当使用一个命令的时候,我们应当注意:工作区的变化,暂存区的变化,版本库的变化,我们基本就同这三者交互。

    如果加上了远端,我们就需要再关注一个远端版本库。


从上面两个图看出,系统主要由几个部分组成:工作区、暂存区、版本库、远程仓库

  • Workspace:工作区
    • 对项目的某个版本独立提取出来的内容。放到磁盘上以通过文件系统使用和修改
  • Index / Stage:暂存区
    • 暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中
  • Repository:版本库
    • Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方,是最重要的部分
  • Remote:远程仓库

Git 项目的一系列操作就是在这几个部分之间的交互,对于一个本地系统来讲,一般来说Git 项目操作三个部分:工作区、暂存区以及 Git 目录,涉及到远端则则划分为本地仓库和远程仓库两个部分。

而一系列操作操作的文件就因此拥有了不同的状态,下面就这些状态进行说明


文件状态:

文件状态变化周期图:

  • 总体来说,Git 的文件其实就两种状态:已跟踪和未跟踪

  • 更细一点来说有三种状态:

    • 已修改(modified):

      表示修改了文件,但还没保存到数据库中。

    • 已暂存(staged):

      表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

    • 已提交(committed):

      表示数据已经安全地保存在本地数据库中。

      • 上图 unmodified
  • Git 使用 SHA-1 散列来计算校验和,以期保证完整性和生成提交标识。

  • 暂存操作会为每一个文件计算校验和(使用 SHA-1 哈希算法)而不是提交操作

  • Git 几乎所有操作都指往数据库中添加数据,即很少有操作是从数据库里删除数据,那么这就意味着几乎所有的操作都是可逆的,文件几乎是不可能丢失的。

    • 当然,如果你删除 .git 即删除了版本库,或者做了其他一些“蠢事”,那就无可挽回了
  • 要想更深入的理解文件状态,则需理解 Git 保存的是快照,这在 Git 原理的文章中讲解。

分支

分支是一个重要概念,他刻画了不同的工作流程,正是分支给了后面将要介绍的开发模式以不同的选择。

而 Git 的分支,其实仅仅是指向提交对象的可变指针。那 Git 是怎么创建新分支的呢? 很简单,它只是为你创建了一个可以移动的新的指针。

Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),

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

而Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。

分支和过去大多数版本控制系统形成了鲜明的对比:

它们在创建分支时,将所有的项目文件都复制一遍,并保存到一个特定的目录。 完成这样繁琐的过程通常需要好几秒钟,有时甚至需要好几分钟。所需时间的长短,完全取决于项目的规模。

而在 Git 中,任何规模的项目都能在瞬间创建新分支。 同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(译注:即共同祖先)也是同样的简单和高效。 这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支。

其他分支操作:

  • Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。
  • Git 可以只克隆一个分支

变基和合并

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。即合并和变基。

合并

合并有多种情况

  • 直接祖先合并:进行快进合并

    当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

  • 出现分叉的合并:

    Git 会使用两个分支的末端所指的快照(C4C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。三方合并的结果有不止一个父提交

  • 冲突合并:

    • 在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件
    • 如果你对结果感到满意,并且确定之前有冲突的的文件都已经暂存了,这时你可以输入 git commit 来完成合并提交。

快进合并:

分叉合并:

变基

变基的目的是为了确保在向远程分支推送时能保持提交历史的整洁

  • 如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

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

变基和合并的区别

注意:变基和合并的结果应当是一样的

变基和合并

  1. 变基或合并前的提交历史

  2. 操作后的历史

    • 合并操作

    • 变基操作

变基和三方合并:

  • 无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。

到底是进行变基还是合并

这有不同的观点

  • 有一种观点认为,仓库的提交历史即是 记录实际发生过什么,即他们认为通过变基改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。
  • 另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事,即怎么方便后来的读者就怎么写

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

分支和标签

标签用于标记和标记历史记录中的特定提交,通常用于标记发布点(例如v1.0等)

  • 其实好像目前就几乎全部都是用来发布版本,我也不知道有什么其他的用处,但是感觉应该有啊

尽管标签看起来可能类似于分支,但是标签不会改变。它直接指向历史记录中的特定提交,除非明确更新,否则不会更改。

如果标签不在您的存储库中,您将无法检出标签,因此首先,您必须fetch将标签移至本地存储库。


标签和分支的区别

  • 在名称空间上:

    • 标签驻留在refs/tags/名称空间中,并且可以指向标签对象(带注释的和可选的GPG签名标签)或直接提交对象(对于本地名称使用较少的轻量级标签),或者在极少数情况下甚至指向树对象Blob对象(例如GPG签名) )。
    • 分支位于refs/heads/名称空间中,并且只能指向commit对象。该HEAD指针必须引用分支(符号引用)或直接连接到一个提交(独立HEAD或无名分支)。
    • 远程跟踪分支位于refs/remotes//名称空间中,并遵循远程存储库中的普通分支。
  • 可以移动分支,不能移动标签(标链接到特定的提交)

    分支是项目的特定路径,因此分支标记随您而前进。完成后,您可以合并/删除分支(即标记)。当然,此时您可以选择标记该提交。

  • 可以选择推送分支,默认情况下不推送标签

  • 分支是分开的时间轴(平行世界),而标签是时间轴上的特定时刻

  • 分支机构视为您要去的地方(即实现某个特性的主题分支,解决某个bug的分支),将标签视为您去过的地方(例如版本发布)


关于 Tag 和 标签等的更多知识请查看 StackOverflow 的一个回答: https://stackoverflow.com/a/35981459,这个回答写的很好,它讲解了带有 refs/ 开头的字符串和常用命令的关系

开发模式

开发模式可以帮我们确定:

  • 选取使用 Git 的工作系统的架构方案
  • 知道如何为一个Git仓库做贡献
  • 如何管理一个Git仓库

分支的任务

  • 长期分支:

    分成 master 、 develop 、 next 的分支,其中在 master 中保留完全稳定的代码,develop 和 next 被用来后续开发和测试稳定性,一旦达到稳定性要求,就可以并入master

    稳定分支的指针总是在提交历史中落后一大截,而前沿分支的指针往往比较靠前。

  • 主题分支:

    这是一种短期分支,主要被用来实现一些单一功能

工作流程

Git 的分布式协作可以为你的项目和团队衍生出种种不同的工作流程

  • 集中式工作流
  • 集成管理者工作流
    • Pull Requests 机制 (更多见 GitHub 小节)
  • 主管与副主管工作流

在下面的具体工作流程讲解有一个介绍,在更下面的一个流程图有一个划分了主题的副主管的一个例子

考察我们的项目需要什么的工作流程是一个很基础的工作,也是一个很重要的工作。

较为详细的工作流程讲解


  • 集中式工作流

    • 如果两个开发者从中心仓库克隆代码下来,同时作了一些修改,那么只有第一个开发者可以顺利地把数据推送回共享服务器。

    • 第二个开发者在推送修改之前,必须先将第一个人的工作合并进来,这样才不会覆盖第一个人的修改

    更类似于从前的 SVN ?

  • 集成管理者工作流

    GitHub 和 GitLab 等集线器式(hub-based)工具最常用的工作流程

    • 可以持续地工作,而主仓库的维护者可以随时拉取你的修改,你的仓库和主仓库互不影响

    工作流程:

    1. 项目维护者推送到主仓库。
    2. 贡献者克隆此仓库,做出修改。
    3. 贡献者将数据推送到自己的公开仓库。
    4. 贡献者给维护者发送邮件,请求拉取自己的更新。
    5. 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。
    6. 维护者将合并后的修改推送到主仓库。
  • 主管与副主管工作流

    副主管 lieutenant 、主管 dictator:

    • 项目总负责人(即主管)可以把大量分散的集成工作委托给不同的小组负责人分别处理,然后在不同时刻将大块的代码子集统筹起来,用于之后的整合。

    工作流程:

    1. 普通开发者在自己的主题分支上工作,并根据 master 分支进行变基。 这里是主管推送的参考仓库的 master 分支。
    2. 副主管将普通开发者的主题分支合并到自己的 master 分支中。
  1. 主管将所有副主管的 master 分支并入自己的 master 分支中。
  2. 最后,主管将集成后的 master 分支推送到参考仓库中,以便所有其他开发者以此为基础进行变基。

当项目极为庞杂,或者需要多级别管理时,才会体现出优势

两个不同的开发模式

  • 一个简单的多人 Git 工作流程的通常事件顺序

  • 集成管理者工作流--副主管管理某些 feature :

    假设 John 与 Jessica 在一个特性(featureA)上工作, 同时 Jessica 与第三个开发者 Josie 在第二个特性(featureB)上工作。 在本例中,公司使用了一种整合-管理者工作流程,独立小组的工作只能被特定的工程师整合, 主仓库的 master 分支只能被那些工程师更新。 在这种情况下,所有的工作都是在基于团队的分支上完成的并且稍后会被整合者拉到一起。

GitHub

本节主要讲述两个事情:为项目做贡献即 Pull Request 、hack


用户对一个仓库进行贡献有两种模式:

  1. 用户为该仓库 collaborators ,具有读写该仓库权限

  2. 用户对该仓库发起 Pull Request

    即上面的开发模式所说的 集成管理者工作流

就我认为,使用 Pull Request 总是不错的,因为没有人想要让自己的仓库乱七八糟。这也是因为 GitHub 的一些权限管理没有像形如 GitLab 一样做的很好。

下面介绍一些常用方法和注意事项

将你收到的贡献加入到主题分支,并考虑是否将其合并到长期分支中去

如果你想向项目中整合一些新东西,最好将这些尝试局限在 主题分支——一种通常用来尝试新东西的临时分支中。 这样便于单独调整补丁,如果遇到无法正常工作的情况,可以先不用管,等到有时间的时候再来处理。

这样的主题分支一般会带有贡献者名称简写,

  • 项目的维护者一般还会为这些分支附带命名空间,比如 sc/ruby_client(其中 sc 是贡献该项工作的人名称的简写)

一般来说,你应该对该分支中所有 master 分支尚未包含的提交进行检查。 通过在分支名称前加入 --not 选项,你可以排除 master 分支中的提交。

$ git log contrib --not master

拉取请求由于过时或其他原因不能干净地合并

如果你的拉取请求由于过时或其他原因不能干净地合并,你需要进行修复才能让维护者对其进行合并。

你有两种方法来解决这个问题。你可以把你的分支变基到目标分支中去 (通常是你派生出的版本库中的 master 分支),或者你可以合并目标分支到你的分支中去。

GitHub 上的大多数的开发者会使用后一种方法,基于我们在上一节提到的理由: 我们最看重的是历史记录和最后的合并,变基除了给你带来看上去简洁的历史记录, 只会让你的工作变得更加困难且更容易犯错。

源作者提交了一个改动, 使得拉取请求和它产生了冲突。现在来看我们解决这个问题的步骤:

  1. 将源版本库添加为一个远端,并命名为“upstream”(上游)
  2. 从远端抓取最新的内容
  3. 将该仓库的主分支的内容合并到你的分支中
  4. 修复产生的冲突
  5. 再推送回同一个分支

如果你一定想对分支做变基并进行清理,你可以这么做,但是强烈建议你不要强行的提交到已经提交了拉取请求的分支。 如果其他人拉取了这个分支并进行一些修改,你将会遇到 变基的风险 中提到的问题(有些人可能会拉取你的pull-request请求的分支,而这个分支正是基于你的GitHub分支的)

合并方法

一旦代码符合了你的要求,你想把它合并进来,你可以把代码拉取下来在本地进行合并,也可以用我们之前提到过的 git pull 语法,或者把 fork 添加为一个 remote,然后进行抓取和合并。

如果你正在处理 许多 合并请求,不想添加一堆 remote 或者每次都要做一次拉取,这里有一个可以在 GitHub 上用的小技巧: 合并多个请求引用

更多其他参考:


常见的使用操作就不再赘述了,对于一些不太熟悉的点或者说比较 hack 的点在这里额外陈述一下:

  • GitHub 可以选择把分支合并到 Pull Request 和并请求分支

  • GitHub 可以审视代码的提交,并在其中某一行进行评论

  • GitHub 可以使用形如 #1#3#11 等的编号来引用拉取请求和议题ISSUE,这是一种链接的简化。如果在 #2 引用了 #1,则 #1 也会有一个 #2 的反向引用提示

  • GitHub 可以识别的特殊文件是 CONTRIBUTING 。 如果你有一个任意扩展名的 CONTRIBUTING 文件,当有人开启一个合并请求时 GitHub 会显示 开启合并请求时有 CONTRIBUTING 文件存在

  • 项目可以移交、可以改变默认分支

  • 可以利用 GitHub 做一些衍生工具和自动化脚本:

Hack

  • 利用 Git 钩子做一些衍生工具和自动化脚本

  • 利用 Git 是增强文件系统的特点来继续增强文件系统

    Git 更像是一个小型的文件系统,提供了许多以此为基础构建的超强工具

    • 可以针对git status -s的输出,进行解析,从而实现新的功能

    • 可以针对 git log ...的输出,进行解析,从而实现新的功能

  • 基于Git 的协同工具也屡见不鲜,不少项目管理和项目跟踪工具都集成了 Git 、GitHub 等

  • 自建 Git 服务器:

    Git 支持多种数据传输协议。 上面的例子使用的是 https:// 协议,不过你也可以使用 git:// 协议或者使用 SSH 传输协议,比如 user@server:path/to/repo.git

    参考:4.2 服务器上的 Git - 在服务器上搭建 Git

  • 可以通过配置来设置一些常见命令或命令组合的别名,Git 只是简单地将别名替换为对应的命令。 然而,你可能想要执行外部命令,而不是一个 Git 子命令。 如果是那样的话,可以在命令前面加入 ! 符号。

    如果你自己要写一些与 Git 仓库协作的工具的话,那会很有用。

  • Git 属性

  • OctokitJGit

综上

建议的学习方法:

本系列其他注意:

  • 一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。所以我写了一篇综合 Git 命令wiki清单(从各处搬运) (这句话也是从阮一峰的网络日志搬运的)

更多资料请看参考

参考

  1. 阮一峰的网络日志:常用 Git 命令清单
  2. Git Book
  3. StackOverflow : How is a tag different from a branch in Git? Which should I use, here?