对 git 的一些使用的笔记

对 git 的了解,仅限于对其概念,命令的了解是绝对不够的(实际上我现在也不能算了解),重要的是掌握相应的最佳实践。这里进行一些笔记。我对马上掌握 git 不抱任何期望——这玩意显然是应当在实践中掌握的。

纵览

一个 git 项目可以分为三部分——工作目录,暂存区,git 仓库。其中,工作目录代表项目某个版本提取出的内容(就像某时刻的快照——当前的也可以);git 仓库代表。git 目录下的内容,其是项目中最重要的部分,包括项目原数据以及对象数据库,从远程克隆仓库时,拷贝的就是 git 仓库的数据;暂存区是一个文件,保存下次提交时的文件列表信息。

git 的最简单的工作流程可以归纳如下——

  1. 克隆远程仓库(将会克隆整个 git 仓库的所有数据——这也就是说其会拉取每一个文件的所有版本到本地,而非仅当前所需要的文件)或初始化新仓库
  2. 修改文件
  3. 将文件的快照加入暂存区(git add命令)
  4. 提交暂存区内容到仓库——这将生成永久性的快照(git commit命令)

在这个工作流程中,文件在各个阶段将会有不同状态,这取决于文件是否被追踪,修改是否已经暂存。

关于将修改加入到暂存区这一步,如果在文件 add 后再次编辑文件。文件将同时处于 Modified 和 Staged 状态——Staged 是编辑前被 add 的文件快照,Modified 是编辑后的文件,这时需再次执行 add 命令,但是旧的 add 命令的结果会被覆盖——如果需要保存该结果,应当在此时进行 commit。

git status -s命令会紧凑地显示各被修改的文件的情况。其中,?? 代表未追踪,A 表示新添加到暂存区,左边的 M 表示文件修改且存入暂存区,右边的 M 表示文件修改且未存入暂存区。但这个或许没有必要了解——IDE 一般都提供了更丰富的表现方式。

1
2
3
4
5
6
$ git status -s
M README # 修改但未暂存
MM Rakefile # 暂存后被再次修改且未暂存
A lib/git.rb # 新添加到暂存区
M lib/simplegit.rb # 修改且暂存
?? LICENSE.txt # 未追踪

git diff命令将会显示当前尚未暂存文件和上一次提交/暂存文件的差别(未暂存的改动),git diff --staged命令显示当前暂存文件和上一次提交文件的差别(已暂存的改动)。

git commit命令将会把暂存区的快照进行提交并成为项目的一个快照(这个快照使能够完全回溯到项目的这个状态,快照是相对项目而言的,而非相对某个文件而言的;git 的版本控制指的是对项目的版本控制,而不是对各个文件的版本控制),在将来可以回退到该快照,或者进行比较等操作。也可以使用git commit -a来一次提交所有跟踪过的文件,而不需要进行暂存。

分支

分支——把工作从开发主线上分离开来,以免影响开发主线。git 鼓励在工作流程中频繁使用分支和合并。这是说,最好在每次要进行修改的时候都创建新分支,进行编辑和测试后合并到原分支。也可以说,分支的目的是为了更好的合并。

这分支图一定是分分合合的。git 的默认分支(同时一般也是“主干”)的名字是 master,git init默认会创建名为 master 的分支。

创建和切换分支

分支的实质是一个可以移动的指针,其能够指向任意一个快照。每次提交都会形成项目的一个快照,而创建新分支的操作则是创建一个可移动的指针引用当前快照,而进行提交(commit)时,提交的快照则会从这里进行分支,同时指针也会移动。创建新分支的命令是git branch <name>

如,下图使用git branch testing命令创建了一个新分支,这个分支和 master 指向同一个快照(下面称为操作对象)。这里的 HEAD 也指向一个分支——当前的本地分支,在这里是 master,创建分支时不会改变当前分支。

HEAD 分支可以直接引用操作对象吗?

图片中的箭头代表包含指针指向对象。每一个对象都有一个指针让它能够引用它的父对象。

git checkout <branchname> 命令使能够切换分支(这里的<branchname>也可以为操作对象)。

切换分支时,HEAD 会指向切换后的分支,同时工作目录的内容变为切换后分支的快照内容。在上面的例子里,如果这时进行编辑和提交,前后的分支就指向不同操作对象了。这时候再对前面的分支进行提交,就产生了分叉

分支可以通过git branch -f <branchname> <target>进行强制的移动。target 可以为相对路径,也可以为绝对路径。

关于对分支的使用,一般来说可以遵循这样的流程(就像 add,commit,push 这样的流程一样)——

  1. 为实现某需求,创建一个分支
  2. 在该分支上进行工作
  3. 完成工作后,合并分支到主分支

合并分支

合并操作显然是一个非常重要的部分。合并使用git merge <branchname>命令。合并时会将当前的分支和待合并的分支进行合并——在这里,当前的分支的指针会移动,而待合并的分支指针不动

下面是一个示例,在 C1 时,项目进行了分支以进行 bug 的修复,现在修复完了,而 main 分支也进行了更新,现在如何将 bugFix 的改动合并到当前的 main 分支上呢?

答案是将当前分支设为 main,并进行 merge 操作,具体命令如下——

1
2
$ git checkout main
$ git merge bugFix

最终会达到这样的结果——可以看到 bugFix 指针并未移动,main 则移动了。可见,main 是这次合并的主体,它把 bugFix 的内容融合进自己,让自己演进了。在进行合并操作时,应按照这样的语义来选择合并的主体。

git rebase也能够进行合并操作,它的作用就如其名——转移父对象,就是更改当前指针的父对象。其能造成更加线性的提交历史。

下面是一个示例,展示了执行下面命令的前后状态。

1
2
$ git checkout bugFix
$ git rebase main

可以看到,bugFix 的父对象成为 main 了。现在只需要移动 main 到新节点即可。

远程仓库

对远程仓库进行各种操作是 git 使用中不可或缺的部分。一个 git 仓库可以有多个远程仓库。

git remote [-v | --verbose]命令允许对项目的远程仓库进行查看,其中-v将会显示更详细的信息。

1
2
3
4
5
6
7
8
9
10
11
$ git remote -v
bakkdoor https://github.com/bakkdoor/grit (fetch)
bakkdoor https://github.com/bakkdoor/grit (push)
cho45 https://github.com/cho45/grit (fetch)
cho45 https://github.com/cho45/grit (push)
defunkt https://github.com/defunkt/grit (fetch)
defunkt https://github.com/defunkt/grit (push)
koke git://github.com/koke/grit.git (fetch)
koke git://github.com/koke/grit.git (push)
origin git@github.com:mojombo/grit.git (fetch)
origin git@github.com:mojombo/grit.git (push)

Continued…