版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。这里我们以Git作为标准,自底向上来介绍Git。

Git数据模型

Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。

快照

Git将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在Git的术语里,文件被称为Blob对象(数据对象),目录被称为”树“。

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

这里root和foo为树,bar.txt、baz.txt为blob对象。

历史记录建模:关联快照

在Git中,历史记录是一个由快照组成的有向无环图。

o <-- o <-- o <-- o
            ^  
             \
              --- o <-- o

上面是一个ASCII码构成的简图,其中的o表示一次提交(快照)。

箭头指向了当前提交的父辈,具有分支的情况如下:

o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Git中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。

数据模型及其伪代码表示

以伪代码的形式来学习 Git 的数据模型,可能更加清晰:

// 文件就是一组数据
type blob = array<byte>

// 一个包含文件和目录的目录
type tree = map<string, tree | file>

// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

这是一种简洁的历史模型。

Git 中的对象可以是 blob、树或提交:

type object = blob | tree | commit

Git 在储存数据时,所有的对象都会基于它们的SHA-1 hash进行寻址。

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。

例如,above例子中的树(可以通过git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d 来进行可视化),看上去是这样的:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

树本身会包含一些指向其他内容的指针,例如baz.txt (blob) 和 foo (树)。如果我们用git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85,即通过哈希值查看 baz.txte 的内容,会得到以下信息:

git is wonderful

引用

现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。

针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master 引用通常会指向主分支的最新一次提交。

master引用其实就是提交的一个别名

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

这样,Git 就可以使用诸如 “master” 这样人类刻度的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。

有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是”HEAD”。

仓库

Git仓库 = 对象+引用

在硬盘上,Git仅存储对象和引用:因为其数据模型仅包含这些东西。所有的git命令都对应着对提交树的操作,例如增加对象,增加或删除引用。

暂存区

Git中还提供了暂存区用来创建基于当前目录的当前状态的一个全新的快照。

Git的命令行接口

为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读Pro Git 中文版或可以观看本讲座的视频来学习。

基础

  • git help <command>: 获取 git 命令的帮助信息

  • git init: 创建一个新的 git 仓库,其数据会存放在一个名为 .git 的目录下

  • git status: 显示当前的仓库状态

  • git add <filename>: 添加文件到暂存区

  • git commit
    

    : 创建一个新的提交

  • git log: 显示历史日志

  • git log --all --graph --decorate: 可视化历史记录(有向无环图)

  • git diff <filename>: 显示与上一次提交之间的差异

  • git diff <revision> <filename>: 显示某个文件两个版本之间的差异

  • git checkout <revision>: 更新 HEAD 和目前的分支

分支和合并

  • git branch: 显示分支

  • git branch <name>: 创建分支

  • git checkout -b <name>
    

    : 创建分支并切换到该分支

    • 相当于 git branch <name>; git checkout <name>
  • git merge <revision>: 合并到当前分支

  • git mergetool: 使用工具来处理合并冲突

  • git rebase: 将一系列补丁变基(rebase)为新的基线

远端操作

  • git remote: 列出远端
  • git remote add <name> <url>: 添加一个远端
  • git push <remote> <local branch>:<remote branch>: 将对象传送至远端并更新远端引用
  • git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系
  • git fetch: 从远端获取对象/索引
  • git pull: 相当于 git fetch; git merge
  • git clone: 从远端下载仓库

撤销

  • git commit --amend: 编辑提交的内容或信息
  • git reset HEAD <file>: 恢复暂存的文件
  • git checkout -- <file>: 丢弃修改

Git 高级操作

  • git config: Git 是一个 高度可定制的 工具
  • git clone --shallow: 克隆仓库,但是不包括版本历史信息
  • git add -p: 交互式暂存
  • git rebase -i: 交互式变基
  • git blame: 查看最后修改某行的人
  • git stash: 暂时移除工作目录下的修改内容
  • git bisect: 通过二分查找搜索历史记录
  • .gitignore: 指定 故意不追踪的文件

杂项

  • 图形用户界面: Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
  • Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh,bash)。Oh My Zsh这样的框架中一般以及集成了这一功能
  • 编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 GIt 的常用插件
  • 工作流:我们已经讲解了数据模型与一些基础命令,但还没讨论到进行大型项目时的一些惯例 ( 有很多 不同的 处理方法)
  • GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作拉取请求(pull request)的方法来向其他项目贡献代码
  • Other Git 提供商: GitHub 并不是唯一的。还有像GitLabBitBucket这样的平台。

资源