阅读《Pro Git》有感

Table of Contents

git真是一个非常不错的版本控制工具!我以前只是在用add,commit,push,pull这几个操作,把github之流的网站当作是云备份平台。如今需要使用git中的一些分支、历史信息等功能,所以就自然而然重新找书读起来了。 这篇笔记是个人对git的一点理解,不无谬误。

1. VCS是什么?——以时间为方向的有向无环图。git是什么?——精妙的VCS

个人理解,git是服务于“文本文件”的,准确地说,是服务于程序代码那种文本文件的。所以git本自然的以 作为其最小粒度。所以后面在merge等处理冲突(或者说合并多个流)的时候,git都是以行为单位进行匹配。这样做比目前较为通用的云同步在粒度上稍微细了一点,因为后者主要是面向二进制文件的,而二进制文件,是一变百变的。 举个例子,如果有需求对docx之类的文档进行版本控制,可能就没有merge一说,但是对txt或者基于xml组织的odt文档,这种基于行的版本控制或许就是可行的了。

另一个问题:什么是版本控制?我理解的版本控制是历史和多人合作。

*历史*是指:通过一个个的离散的时间节点来形成一张有向无环图,该图反应了一个软件(或者说,一团数据)随着变量时间的演化过程。

*多人合作*是指:上述有向无环图的构成,每一个节点的创建者未必是同一个人。当然,通过权限,某些汇聚节点(入度大于出度)的创建或许需要控制在管理员手里。

我想,提供了一张完整的有向无环图的创建和维护方法,应该就是git的核心功能吧。该图不仅仅是说我可以知道哪两个节点之间发生了何种提交(也就是*边*),更是说:/git 可以提供整张图里每个节点的完整信息。/

那么,难道过去的VCS(version control system)不是这么干的吗?差不多,实现上稍微有一些不同。(但是,谈论过去的VCS什么样子其实没有太大的意义。我会用他们吗?)过去的VCS主要是以文件为基本单位,如果某一个版本,对文件目录中的某些文件进行了修改,那么这个版本仅仅会记录那些发生修改的文件的修改之处,所以本质上相当于是渐进式的一条长河,不太有利于处理分支(不是不行,而是不高效)。

git则有些不同。git不以文件为单位,而以版本为单位,版本就是图中的点。当然,这并不是说每一个点都是独立存在的,我认为git还是以文件、行或者目录为单位,采用指针进行了高效存储的。

上述文字阐述了git的主要功用。下面看一看基于这种作用,常见的git的使用场景是什么,以及在该场景之下,如何对项目系统进行基本的处理。

2. 在上述有向无环图基础上添加一个新的维度:空间。——git里面的“储物间”以及储物间之间的“移动”方式

从物理上而言,git一般包含以下两类物理地址:若干个本地终端;若干个云终端。对不对? 所谓的多人协作,最直白的一种方案是:将若干个本地终端推送到一个云终端上去合并。 当然后面会详细介绍详细的工作流。 基于以上认识,git本质上是在上述vcs的有向无环图的基础上,还考虑了空间位置。换句话说:每个物理位置上,其实都会有依照时间变化的一张图。这张图其实是有“目的论”味道的,他们都大概率会进化到几个最终节点(出度为0的节点),这些=最终节点=会被移动到象征整个项目系统官方物理位置的地方,作为一个新的分支被合并。上述说法很含糊,后面会在介绍工作流时举例说明。现在还是把核心放在新增的维度——“空间”上。

对于物理上的空间(本地和远程),git其实也做了一些简单的区分。但是为了更加成体系地阅读,应该将本地地址进行进一步的细化,一般可以额外规划出来下面的区域:git法外之地,暂时存储地域,本地提交地域。加上远程地域,一共有四个:

  1. git法外之地:也就是还没有接受git管辖的区域。比如,在一个文件夹之下,我新建了一个文件,这个文件就是在git法外之地的,因为git不知道有它(untracked);
  2. 发生修改的文件区域(unstaged):这里的文件之前被提交过,但是又发生了更改。同时,更改还没有被暂存。
  3. 暂时存储地域(staged):顾名思义,这个地方所存储的文件,其变动和信息已经被正式捕获了,但是,这个地方还不能算是本地有向无环图中的一个点,所以才叫“暂存”。这个地方的文件一般包含两种情况:
    1. 之前在法外之地,没有被tracked的文件,被加入了git中。相当于是新加入的文件;
    2. 之前已经被归档到仓库里了,然后又进行了更改的文件。相当于是之前的文件发生了更改。
  4. 归档好的仓库(repository): 顾名思义,这里放着的就是正式决定存入仓库的东西(文件)。所谓的仓库,可以理解成git为该项目系统构建的一个档案袋。当一堆文件被加入到仓库里,那么一个新的“版本”就会出现,也就是有向无环图中的一个新的节点,就被创建出来的。与暂存区域不同,这里的数据是可回溯的。换句话说,当文件A被放入了归档好的仓库里之后,我们继续修改A,然后再把它放入仓库里,此时之前的文件A的状态,还是可以捕获的。这是版本控制的一大特点。
  5. 远程的仓库。如果对于单人使用,远程仓库的意义或许仅仅是云备份罢了。过会再去详细介绍它。

在了解了上述几个储物间之后,一个值得考虑的问题是:暂时存储地域是不是多余的?你看,我切换图中的版本节点,暂存的数据还是会消失。如果我要想让他们不消失,就必须放到仓库里。那么我为什么中间要加入一个暂时存储的位子呢?这个问题先搁置一下,后面会介绍。让我们用审视古董的角度先接受它,然后暂时先看看在几个区域之间移动的几个基础操作。 注意:上述操作的前缀都是git。

  1. add 将法外之地的文件添加到暂存区;
  2. add 将被修改(modified)的文件添加到暂存区;
  3. commit 将暂存区的文件放到仓库里,在当前节点的基础上,形成一个新的version节点。并使得上一个版本变成只读模式(是这个样子吗?)。
  4. push 将若干个本地的commit,传递到远程的服务器的某个分支上。比如=git push origin master=,是说在origin这个名字的服务器上有一个分支叫做master,我把本地仓库和这个orgin下的master进行对齐。
  5. rm 删除暂存区域中的某文件。
  6. rm –cached 将从法外之地移入暂存区的文件,再变为untracked的状态,但不删除文件。
  7. commit –amend 用于重新提供提交信息。取消上一次的提交信息,并输入新的。
  8. reset HEAD 把处于暂存区的、是modified过的文件,改为unstage状态。
  9. checkout – 把unstaged状态下的文件,也就是一个之前被commit过,但是修改了还没有commit的文件,还原回上一次commit时的状态。

值得注意的是,我在这里罗列一下他们,就仅仅是简单罗列一下。这些是不需要记忆的,因为用的熟了很多常用的就记住了,不常用的也没必要记录。再说,目前基本上是个IDE都会提供git工具,命令行就更不是必要的了。本文也是如此,理解外部原理就完全足够使用git的了。

下面还需要再看看本地仓库怎么跟远程的仓库建立关系。这一部分其实在branch之类的操作说完了之后才好讲解,因为这一过程是属于version-level而不是file-level的。

简单的跟远程相关的命令都包括git remote。如:

  • git clone +url以完整拷贝一个仓库到本地;
  • git remote,查看已有的远程仓库
  • git remote add [remoteName] [URL] 以添加一个新的远程仓库链接(此时没有新增任何的文件数据)
  • git fetch [remotename] 将该远程仓库下的所有东西作为新的若干个分支下载到本地

还有其他一些未介绍的命令:

  • git remote show 展示一个remotename的信息
  • git remote rename 重命名

下面就来看一看,version-level的一些工作。

3. 形成一张版本控制图

之前提及过,版本控制过程,本质上可以理解为一张有向无环图。那么两个操作是VCS中最重要的:建图,和看图。 关于建图,其实就是迭代“建立节点,并提供连接”这个过程。

特殊地,git的VCS其实每个节点最多只会有两个上游节点,最多也只会有两个下游节点。所以这张图是简单地不得了的。 更特殊地,版本控制中,大多数节点其实入度出度都是一,就像是一个一字长蛇阵。我们在上一章节已经看到了怎么去用commit形成这样的一个一字长蛇阵,因此此处要玩点花样,看看同样频繁使用的分支等功能的实现。

3.1. 通过branch、clone和fetch产生分支

分支(branch)很简单,就是为了验证一个新的功能,或者“开辟一个新的试验田”去干别的事情。所以产生分支的方式主要有两种:

  1. 基于当前本地的最终节点产生一个分支,此时完全是当前本地节点信息的复制。
    1. git branch branchname 定义产生一个空白新分支
    2. git checkout branchname 切换分支
  2. 基于别人的代码产生一个分支,此时多是通过下载远程的文件获得一个分支。
    1. git fetch remotename 通过获得远程仓库去创建一个新的分支

在切换分支时,当在前分支之下所修改新增的文件,需要commit之后才能切换。否则相关文件会丢失。

3.2. 通过merge和rebase合并分支

当分支上的功能完成了验证之后,后面就需要将其与主分支合并。一般而言,合并的方式有两种,merge形式,和rebase形式。二者的内核是一致的。

merge的过程是这样的:当前仓库处于分支A下,然后需要merge掉分支B,则使用=git merge B=,这时A就是融合之后的节点了,B没有发生改变。反映在有向无环图上,就是A的过去和B的最终节点一起指向了A的当下节点。

一般而言,merge的过程中可能会出现一些特殊情况:/两个分支对某个文件的某一行都进行了修改。这被称作*冲突*。/这时候找到冲突的文件,对冲突的行人工处理一下就可以了。 当然,在处理之后,由于解决冲突的过程人工编辑了文件,所以后面需要commit一下。

然后关注一下rebase操作。rebase的核心操作与merge是一样的,就像一句话:“力的作用是相互的”。 先看一下rebase的基础操作流程: 首先,对于一个仓库的两个分支A和B,需要将B分支的东西融入到A中。那么,不同于merge,我们需要先将当前分支切换为B,然后去查看B分支相比于A分支有什么不同,以得到B分支的“改动部分”。<这也就是将原来的以分支B的基础节点为base改为了以分支AB最年轻的祖先节点为base,所以叫做rebase。>然后我们切换到A分支,用merge的方式去将B分支融入进来。

可以看见,上述过程最终还是要执行merge,那么rebase是干什么的?多此一举吗?不是的。rebase的意义有二:首先,当出现冲突时,冲突肯定会出现在rebase这一步,而不是merge这一步。假如说这是个多人合作项目,有人给你提供新的patch,那么你肯定希望这个patch跟你项目的冲突提交者早就弄好了,而不是大家都发给你,你一个一个地阅读他们的源码。这是rebase的实际应用。除此之外,通过rebase的方式,可以生成更简洁的功能开发树,而不是具有多个乱七八糟的分支。

3.3. 删除分支

git branch -d branchname

3.4. 通过log访问图中的节点

前面已经对如何建立版本控制图进行了全方位的讨论,现在可以看看如何遍历地访问这张图中的一些节点了。

首先应当明确,对于版本控制流,其实历史不是那么重要。但是,也很重要。git里面这一系列的操作都被放在git log里面了。但是,git log其实很不好用,同样地,目前很多ide都提供了一些方便的工具,此处不赘述。

4. 工作流

4.1. 单人工作流

略。

4.2. 典型的多人工作流

累了不想写了。。。。

多人工作流包括以下几种:

  1. 同权限共同推进型;
  2. 一管理员多开发者型;
  3. 司令副官型;

不详细写了。

5. 我的git个人适配文档

有时间补充

5.1. 我的gitignore

5.2. 我的git脚本

没想到写一篇笔记的时间比看这本书的时间还长。。。


Author: liangzid (2273067585@qq.com) Create Date: Tue Sep 21 21:08:07 2021 Last modified: 2024-03-09 Sat 20:56 Creator: Emacs 28.1 (Org mode 9.5.2)