内容纲要

🗂 | 查看 Git 专题可浏览更多内容


概念

在开始前,需要先了解一些概念。

Git 项目的三个阶段:

  • Working Directory;
  • Staging Area;
  • Repository;

Working Directory

「Working Directory」有时也称为「Working Tree」或「Working Copy」,也就是中文里常说「工作目录」或「工作区」。

工作区的文件是从 Git 仓库中的某个版本独立提取出来的内容,以供使用或修改。

Staging Area

「Staging Area」有时也被称为「Cache」或「Index」,也就是中文里常说的暂存区、缓存或索引。

暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。

Repository / .git Directory

「Repository」有时也称为「 Git 目录」,也就项目根目录下的 .git 目录。

「 Git 目录」是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。

工作流程

基本的 Git 工作流程如下:

  1. 在工作区中修改文件。
  2. 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。
  3. 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。

实践

通过之前的文章,你应该已经知道如何配置用户信息及创建一个 Git 项目,下面进行简单的回顾:

# 使用 Git 创建一个名为 article 的目录
$ git init article

# 进入项目目录
$ cd article

# 如果有需要为这个项目设置单独的用户信息(用户名及邮箱)
$ git config --local user.name "Conners Hua"
$ git config --local user.email [email protected]

接下来,我打算使用最简单的 Markdown 文件进行演示,使用 Git 管理这个杂文集项目。

查看状态

$ git status
位于分支 master

尚无提交

无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪)

通过命令 Git 告知我们当前处于 master 分支,目前「尚无提交」。

毕竟我们刚创建这个目录,这是一个「空目录」,我们并没有往里面放东西。

💡 之所以「空目录」是打引号的,是因为其实它并不空,在 Git 初始化后,目录下会生成一个名 .git 的隐藏目录,也就是之前提到的「Git 目录」。

现在创建一个名为 README.md 的文件,然后再次查看状态:

# 此处个人用命令创建了一个 READMD.md 文件,你可以通过其他方式创建:
$ touch READMD.md

$ git status
位于分支 master

尚无提交

未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
    README.md

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

💡 一般情况下在 Git 中文件有两种状态:「未追踪文件 (untracked) 」和「已追踪文件 (tracked )」,所谓的已追踪文件就是被 Git 纳入管理的文件,而未追踪的则没有被纳入管理。

即,不是将文件丢入项目目录,Git 就直接进行管理了。

所以不要将使用同步网盘的思维带入使用 Git。

添加文件到暂存区

使用 git add <文件名> 命令可将文件添加到暂存区:

$ git add README.md

💡 小技巧:文件名过长时可以使用 tab 键进行补全。以此为例,在输入到大写的 R 时就可以按下 tab 键,这样就会自动补全完整的文件名,不用一个个手打啦。
如果目录下有多个 R 开头的文件名就需要多打一点直到不相同的字符时再按下 tab 键。

到这里,不仅是对新文件 README.md 进行了追踪,也将其放到了暂存区 (Staging Area) 。

然后接着查看状态:

$ git status
位于分支 master

尚无提交

要提交的变更:
  (使用 "git rm --cached <文件>..." 以取消暂存)
    新文件:   README.md

在追踪后,Git 文件一般主要处于 3 种状态:

  • 已修改 (modified);
  • 已暂存 (staged);
  • 已提交 (committed);

此时 README.md 文件处于「已暂存 (staged)」状态,并且如果觉得准备妥当就可以交到仓库成为一个历史版本了。

提交修改成为历史版本

使用 git commit -m '<提交信息,在此说明为什么要修改以及修改了什么>' 命令提交修改:

💡 提交信息就是概括描述这次的修改改动,日后查看记录时就可以大致知道这次的历史改动了什么。

$ git commit -m '添加说明文件'
[master(根提交) de4d93a] 添加说明文件
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

使用 -m 选项就可以在输入命令时顺带输入提交信息,如果不带 -m 选项而只使用 git commit 命令,你将进入到一个交互界面:


# 请为您的变更输入提交说明。以 '#' 开始的行将被忽略,而一个空的提交
# 说明将会终止提交。
#
# 位于分支 master
# 要提交的变更:
#       修改:     README.md
#

在这个界面中,需要按下 I 键进入到「编辑模式」,然后就可以写入提交信息,注意井号表示注释,不要在开头也学着下面几行加上井号。

在填写完后按下 Ctrl + C,然后输入 :wq (英文冒号加上 wqw 表示写入 write q 表示退出 quit) 就可以保存退出回到之前的界面了。

查看提交记录

使用 git log 可以查看历史:

$ git log
commit de4d93a0f98c0657188222bbc0053f6fdc63b4a7 (HEAD -> master)
Author: Conners Hua <[email protected]>
Date:   Wed Jan 29 23:38:05 2020 +0800

    添加说明文件
(END)

通过 git log 可以看到刚才的提交,包含了 commit 的 id、作者及邮箱、提交时间以及提交信息。

接着再次查看状态:

$ git status
位于分支 master
无文件要提交,干净的工作区

此时文件 READMD.me 文件处于「已提交 (committed)」状态。

会将「工作区」的修改提交成为「历史」吗

接着再来一个例子,此时对 README.md 文件进行修改,添加一个标题:

## 这是一个标题

这里类似 Word 里的二级标题,Markdown 中通过井号的数量来表达标题等级,书写和阅读都很方便。

然后添加到暂存区:

$ git add READMD.md

此时并不提交成为历史版本,而是继续对 README.md 文件进行修改如下:

## 这是一个标题

这是一段正文内容

❓ 问题:如果现在提交成为一次历史版本,那该历史版本的内容是否包含后续添加的「这是一段正文内容」呢?

先来看当前状态,在添加一个修改到「暂存区」后再对「工作区」中的文件进行修改,状态会变成:

位于分支 master
要提交的变更:
  (使用 "git restore --staged <文件>..." 以取消暂存)
    修改:     READMD.md

尚未暂存以备提交的变更:
  (使用 "git add <文件>..." 更新要提交的内容)
  (使用 "git restore <文件>..." 丢弃工作区的改动)
    修改:     READMD.md

在使用 git commit 命令提交「暂存区」中只含有标题的修改成为历史版本后,状态会变成:

$ git status
位于分支 master
尚未暂存以备提交的变更:
  (使用 "git add <文件>..." 更新要提交的内容)
  (使用 "git restore <文件>..." 丢弃工作区的改动)
    修改:     README.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

「尚未暂存以备提交的变更」,这表示刚才在放入暂存后所做的修改并未提交到版本库中。

那么如果刚才想将新加入的标题和正文内容一起作为一个历史版本进行提交,应该怎么做呢?答案是再次使用 git add 命令将带有正文内容的修改再次添加到「暂存区」

也就是说,可以不断添加所修改的内容到「暂存区」,即便是同一个文件,直到你认为可以作为一次历史版本进行提交。

一个好的历史提交应该是怎样的?

以下是一些建议:

  1. 完整的提交
    一次历史应该是一次完整的改动,不要将一件未做完的事情当作一个历史版本。如果你遇到别的紧急加塞任务,需要临时保存一下当前的改动,可以使用 Git 的「贮藏(Stash)」功能;

  2. 一次提交仅对应一个相关的改动
    一次提交应该只对应一个相关的改动,不要把毫无关联的改动共同提交为一个历史版本;
    例如,如果将修改了文章的格式风格与编写了文章的总结内容作为一次提交,那日后想要回滚到文章末尾的这个版本但又不想要所修改的格式时,就需要花费额外的功夫来修改,这也会造成管理混乱;

  3. 高质量提交信息
    说明为什么需要这次改动以及概述这次改动了什么。这样提交者或者团队的其他成员就可以非常简单的了解改动的目的以及修改的内容;

其他

删除文件

如果想要在 Git 中删除一个文件就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除)。

你可能直接就使用文件管理器删除了想要删除的文件或使用 rm命令,但更好的办法是直接使用 git rm 命令

$ git rm README.md

如果要删除的文件已经修改了,那么你会得到提示「error: 如下文件有本地修改」或是「error: 如下文件其暂存的内容和工作区及 HEAD 中的都不一样」,这时候就需要使用 -f 选项强制删除:

$ git rm -f README.md

还有一种常见情况是需要在 Git 中移除但想保留在「工作目录」作于他用,那么就可以使用 --cached 选项:

$ git rm --cached README.md

这时使用 git status 就可以看到在暂存区中已经删了想要删除的文件就等提交了。但因为文件保留在工作目录中,所以就可以看到一个未跟踪文件:

位于分支 master
要提交的变更:
  (使用 "git restore --staged <文件>..." 以取消暂存)
    删除:     README.md

未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
    README.md

重命名文件

假设要将项目中的 README.md 文件重命名成 README,如果你是在文件管理器中对它右键重命名,那么在 Git 中会处理成将一个已知跟踪的文件删除了,同时出现了一个未跟踪的文件。

要正确重命名文件可以在重命名后使用 git rm 删除原文件,然后添加重命名后的文件到暂存区,最优做法是直接使用 git mv 命令:

$ mv README.md README
$ git rm README.md
$ git add README

# 更推荐的做法是使用 git mv 命令:
$ git mv <旧文件名> <新文件名>
$ git mv READMD.md README

批量添加多个文件至暂存区

可以使用空格隔开文件名以达到添加多个文件的效果:

# 添加多个文件时可以使用空格在多个文件名之前隔开
$ git add <文件1> <文件2>

# 或者某个目录下的所有文件,如 img 目录下的所有文件,使用通配符 * 表示所有文件
$ git add img/*

但需要添加的文件数量很多时就很麻烦了,这时可以用 .-A 或是 --all 选项:

# 对未跟踪或已跟踪的文件的修改、删除添加到暂存区
$ git add .
$ git add -A # --all 的简写

以上方法都可以对未跟踪的文件进行处理,但有时你只想针对已经跟踪并有所修改或删除的文件添加到暂存区,并不想处理未跟踪文件,这时可以使用 -u 选项

# 只对已跟踪的文件的修改、删除添加到暂存区
$ git add -u

忽略指定文件

会有一些文件是一直处于不想纳入 Git 的管理的情况,你可以在项目下创建一个名为 .gitignore 的文件,在这个文件中列出不想被 Git 管理的文件,这样它们就不会总出现在未跟踪文件列表。

比如 Windows 的 Thumbs.db 或是 macOS 的 .DS_Store,你可以将它们写在 .gitignore 里:

Thumbs.db
.DS_Store

文件 .gitignore 的格式规范如下:

  1. 所有空行或者以 # 开头的行都会被 Git 忽略。
  2. 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  3. 匹配模式可以以 (/) 开头防止递归。
  4. 匹配模式可以以 (/) 结尾指定目录。
  5. 要忽略指定模式以外的文件或目录,可以在模式前加上叹号 (!) 取反

更多例子:

# 忽略所有的 .a 文件
*.a

# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a

# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO

# 忽略任何目录下名为 build 的文件夹
build/

# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt

# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf

另外在 Github 上有一个 .gitignore 文件的模板集合,你可以在这上面找到更多例子用于系统开发语言环境:github / gitignore

跳过使用暂存区

尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。

在使用 git commit 时可以使用 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交。但也正因为如此并不推荐使用,它可能会将尚未做好准备的文件也提交成为历史版本。

$ git commit -a

使用 -a 选项不会将未跟踪的文件提交。

文件状态简览

git status 有一个 -s 选项可以以一种简洁的方式显示状态:

 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

说明:

  • M:表示已修改;
  • MM:表示该文件添加到暂存区后,工作目录中的也进行了修改;
  • A:新增的已跟踪文件;
  • ??:未跟踪文件;

小结

一个新的文件放入到项目的工作目录中并不是就直接被 Git 跟踪管理了,项目文件在工作目录中修改、将修改好的文件添加到暂存区,直到可以算作一次历史版本时提交从而成为一次历史,如此反复。

避免把 Git 当作网盘进行使用,每次提交应该是一次完整且独立的改动,并且在提交时在提交信息写清楚为什么需要这次改动以及大致改动了什么,这样在日后回头查阅时,你以及你的团队成员就可以很直接明了这次历史版本是怎么一回事。

  • 使用 git status 查看状态;
  • 使用 git log 查看提交历史;
  • 使用 git add <文件>添加文件到「暂存区」;
  • 使用 git commit 提交修改成为历史;
  • 删除文件需要使用 git rm 命令,如果删除的同时保留文件在工作目录中使用 --cached 选项;
  • 重命名或移动文件使用 git mv 命令;
  • 添加多个文件到暂存区可以以空格隔开文件名以达到添加多个文件的目的。当然更好的办法是使用 .-A 选项,如果不想添加未跟踪的文件则使用 -u 选项;
  • 有一些文件总是不想被 Git 接管的,但又不想在未追踪文件列表里看到它们,就可以设置 .gitignore 文件以隐藏它们;
  • 如果想要直接提交而省去暂存区这一步可以使用 -a 选项:git commit -am '<提交信息>'
  • git status 命令有一个 -s 选项可以以一种简洁的方式显示文件的状态;