继上次学习Git学习-起步后,本章将学习Git基础,本章涵盖了你在使用 Git 完成各种工作时将会用到的各种基本命令。 在学习完本章之后,你应该能够配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)或提交(commit)更改。 本章也将向你演示了如何配置 Git 来忽略指定的文件和文件模式、如何迅速而简单地撤销错误操作、如何浏览你的项目的历史版本以及不同提交(commits)之间的差异、如何向你的远程仓库推送(push)以及如何从你的远程仓库拉取(pull)文件。

获取 Git 仓库

获取 Git 仓库有两种方式:

  1. 将尚未进行版本控制的本地目录转换为 Git 仓库(git init
  2. 从其他服务器 克隆 一个已存在的 Git 仓库(git clone

在已存在目录中初始化仓库

在已存在的目录路径下,执行 git init

执行完之后,Git 会在该目录下创建一个.git的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。

克隆现有的仓库

语法:git clone <url>

<url>就是 GIt 仓库的远程地址,Git 支持多种数据协议,例如httpsgitssh

执行git clone命令后,Git 会在执行命令所在目录下创建一个 Git 仓库目录,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。

例如:git clone https://github.com/libgit2/libgit2

Git 仓库的目录名称就为libgit2,如果希望在创建仓库时自定义仓库名称,可以通过额外的参数指定新的目录名。

例如:git clone https://github.com/libgit2/libgit2 mylibgit

Git 仓库的目录名称就为mylibgit

Git 仓库文件的状态变化周期

image-20231207152109416

结合上图,GIt 仓库下的文件分为四种状态:

  • Untracked:未跟踪。
  • Unmodified:已跟踪且未修改。
  • Modified:已跟踪且已修改。
  • Staged:已暂存。

通过上图的箭头可知,从左到右文件的状态变更流程如下:

  1. Untracked -> Staged,使用 git add 暂存文件,将未跟踪的文件进行跟踪并暂存。
  2. Staged -> Unmodified,使用 git commit 提交文件,Git记录文件版本。
  3. Unmodified -> Modified,编辑文件,文件的状态从“未修改”变更为“已修改”。
  4. Modified -> Staged,使用 git add 暂存文件,将已跟踪且已修改的文件进行暂存。
  5. Unmodified -> Untracked,使用 git rm 将未修改的文件从已跟踪的文件列表中移除并在目录中删除。
  6. Modified/Staged -> Untracked,使用 git rm -f强制将已修改或已暂存的文件从已跟踪的文件列表中移除并在目录中删除。

从上诉可知,Git 仓库的文件主要分为两种状态:已跟踪未跟踪。已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。

初次克隆 git clone 某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。反之,通过git init初始化的Git仓库,工作目录中的所有文件都属于未跟踪文件。

git status

可以用 git status 命令查看 Git 仓库下的文件状态。

如果在克隆仓库后立即使用此命令,会看到类似这样的输出:

1
2
3
4
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

这说明你现在的工作目录相当干净。换句话说,所有已跟踪文件在上次提交后都未被更改过。 此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪状态的新文件,否则 Git 会在这里列出来。 最后,该命令还显示了当前所在分支,并告诉你这个分支同远程服务器上对应的分支没有偏离。

再看下面截图的情况:

image-20231207163143786

上图分为三种文件状态:

  • Changes to be committed:表示已暂存文件。
  • Changes not staged for commit:表示已跟踪且已修改的文件。
  • Untracked files:表示未跟踪的文件。

git add

git add 是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。 将这个命令理解为“精确地将内容添加到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。

.gitignore忽略文件

一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件的模式。

关于.gitignore文件的详细解释,可以参考 Git学习-命令 章节中的1.3小节。

git diff

当想知道具体修改了什么地方,可以用 git diff 命令。不过不太推荐这个命令。。。建议使用图形化工具,例如 IDEA、SourceTree 等。

git commit

使用 git commit 命令,会将暂存区中的文件创建为一次提交记录,并生成一个完整 SHA-1 校验和的版本号。

git commit命令会要求每次提交都带有提交说明,在未指定-m参数时,git会启用文本编辑器来输入提交说明。

尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。 Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤。

git commit只提交指定的文件,而非暂存区所有的文件,命令如下:

1
git commit path/to/your/file.ext -m "Your commit message"

在这个命令中,path/to/your/file.ext 是要提交的特定文件的路径,-m 选项用于添加提交消息。

如果要提交多个文件,可以将它们一起列出:

1
git commit path/to/your/file1.ext path/to/your/file2.ext -m "Your commit message"

git rm

要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。 可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

git rm一般用于已跟踪且未修改(Unmodified)的文件。如果是未跟踪文件,直接使用rm删除即可。如果是已跟踪且已修改(Modified) 或者 已跟踪且已暂存(Staged)的文件,则需要使用-f参数,强制删除。

当想让文件保留在磁盘,但是并不想让 Git 继续跟踪。 当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。 为达到这一目的,使用 --cached 选项:

1
git rm --cached README

git mv

Git 并不显式跟踪文件移动操作。 如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。

语法:git mv file_from file_to

它会恰如预期般正常工作。 实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:

1
2
3
4
5
6
7
8
$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

renamed: README.md -> README

其实,运行 git mv 就相当于运行了下面三条命令:

1
2
3
$ mv README.md README
$ git rm README.md
$ git add README

如此分开操作,Git 也会意识到这是一次重命名,所以不管何种方式结果都一样。 两者唯一的区别在于,git mv 是一条命令而非三条命令,直接使用 git mv 方便得多。 不过在使用其他工具重命名文件时,记得在提交前 git rm 删除旧文件名,再 git add 添加新文件名。

查看提交历史

使用 git log 即可查看提交历史。

image-20231207171033768

不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

使用 git log --pretty=oneline命令,可以只查看提及记录的版本号和提交备注。

image-20231211172914540

使用-n参数,可以只查看最近n次提交历史。

使用 --stat参数,显示在每次提交的下面列出所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了。 在每次提交的最后还有一个总结。

image-20231207171305899

git log在终端提供了许多参数,但个人还是推荐使用图形化工具,例如IDEA、SourceTree等。

撤销操作

撤销操作可以指替多个方面,比如撤销工作目录的修改、撤销提交历史、撤销暂存等。

撤销命令有两个,一般使用git restore撤销工作目录中的文件,使用git reset撤销提交历史。

现在有如下图所示的工作目录状态:

image-20231208110044957

使用 git restore --staged <file> 会将暂存区中的文件撤销到未暂存已修改状态下,即撤销了 git add 命令的操作,但没有还原文件的修改记录。

使用 git restore <file> 撤销工作目录中文件的修改记录,还原到上一次提交记录的版本。

git reset 命令功能,可以参考 Git命令-reset 文章。


在使用git commit时,有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend 选项的提交命令来重新提交:

1
$ git commit --amend

这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,而你所修改的只是提交信息。文本编辑器启动后,可以看到之前的提交信息。 编辑后保存会覆盖原来的提交信息。

例如,你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作:

1
2
3
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

最终你只会有一个提交——第二次提交将代替第一次提交的结果。

当你在修补最后的提交时,与其说是修复旧提交,倒不如说是完全用一个 新的提交 替换旧的提交, 理解这一点非常重要。从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。

修补提交最明显的价值是可以稍微改进你最后的提交,而不会让“啊,忘了添加一个文件”或者 “小修补,修正笔误”这种提交信息弄乱你的仓库历史。

远程仓库的使用

Git使用远程仓库实现Git项目协作功能。远程仓库是指托管在因特网或其他网络中项目的版本库。远程仓库用于用户来说,具有权限划分,例如只读(fetch)、读写(push)。一个项目可以有多个远程仓库。

Git的远程仓库并不是指一定是互联网上的仓库,也可以是本地主机,“远程仓库”更多的是想表达它在别处的意思。把远程仓库部署在本地主机,在通过Git项目推送到远程仓库,也是一种管理远程仓库的概念。

查看远程仓库

每个远程仓库都会被git定义一个简写的仓库名称,一般默认为orign。

使用 git remote 可以查看当前项目下所有的远程仓库简写名称

image-20231211165852679

使用 git remote -v 查看远程仓库的简写名称与其对应的URL,并标注远程仓库的读写权限。

image-20231211170031846

添加远程仓库

获取Git仓库 章节中,使用 git clone 可以从远程仓库克隆项目到本地,并且会为本地Git项目添加该远程仓库地址。

而对于本地使用 git init 初始化的GIt项目,就需要手动添加远程仓库。

运行 git remote add <shortname> <url> 为Git项目添加一个新的远程仓库。其中的 shortname 表示远程仓库简写名称,uerl 表示远程仓库地址。

添加远程仓库之后,可以运行 git fetch <remote> 拉取远程仓库信息。其中的 <remote> 表示远程仓库简写名称,即 <shortname

使用 git fetch之后,这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。

推送到远程仓库

使用 git push <remote> <branch> 推送Git项目的具体某个分支到远程仓库。关于推送功能的注意点:

只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。 当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先抓取他们的工作并将其合并进你的工作后才能推送。

上述这句话简而言之就是,在 git push 之前,建议先 git pull

查看某个远程仓库

当想要查询某个远程仓库的更多信息,例如URL、分支等。可以使用 git remote show <remote> 命令。

image-20231211171903258

它同样会列出远程仓库的 URL 与跟踪分支的信息。 这些信息非常有用,它告诉你正处于 main 分支,并且如果运行 git pull, 就会抓取所有的远程引用,然后将远程 main 分支合并到本地 main 分支。 它也会列出拉取到的所有远程引用。

远程仓库的重命名和移除

你可以运行 git remote rename 来修改一个远程仓库的简写名。 例如,想要将 pb 重命名为 paul,可以用 git remote rename 这样做:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

值得注意的是这同样也会修改你所有远程跟踪的分支名字。 那些过去引用 pb/master 的现在会引用 paul/master

如果因为一些原因想要移除一个远程仓库——你已经从服务器上搬走了或不再想使用某一个特定的镜像了, 又或者某一个贡献者不再贡献了——可以使用 git remote removegit remote rm

1
2
3
$ git remote remove paul
$ git remote
origin

一旦你使用这种方式删除了一个远程仓库,那么所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除。

Git标签

Git 可以给仓库历史中的某一个提交打上标签,以示重要。 比较有代表性的是人们会使用这个功能来标记发布结点( v1.0v2.0 等等)。

列出标签

在 Git 中列出已有的标签非常简单,只需要输入 git tag (可带上可选的 -l 选项 --list):

1
2
3
$ git tag
v1.0
v2.0

这个命令以字母顺序列出标签,但是它们显示的顺序并不重要。

你也可以按照特定的模式查找标签。 例如,Git 自身的源代码仓库包含标签的数量超过 500 个。 如果只对 1.8.5 系列感兴趣,可以运行:

1
2
3
4
5
6
7
8
9
10
$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3
v1.8.5.1
v1.8.5.2
v1.8.5.3
v1.8.5.4

按照通配符列出标签需要 -l--list 选项

如果你只想要完整的标签列表,那么运行 git tag 就会默认假定你想要一个列表,它会直接给你列出来, 此时的 -l--list 是可选的。

然而,如果你提供了一个匹配标签名的通配模式,那么 -l--list 就是强制使用的。

创建标签

Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)。

轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。

而附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

使用 git show <tag> 可以查看标签信息,比较轻量标签和附注标签最简单的方式,就是看标签信息最前面是否有 标签版本Tagger(打标签者的信息)Date(打标签的时间点)标签备注

附注标签

在 Git 中创建附注标签十分简单。 最简单的方式是当你在运行 tag 命令时指定 -a 选项:

1
2
3
4
5
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4

-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息,Git 会启动编辑器要求你输入信息。

通过使用 git show 命令可以看到标签信息和与之对应的提交信息:

1
2
3
4
5
6
7
8
9
10
11
12
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700

changed the version number

输出显示了打标签者的信息、打标签的日期时间、附注信息,然后显示具体的提交信息。

轻量标签

另一种给提交打标签的方式是使用轻量标签。 轻量标签本质上是将提交校验和存储到一个文件中——没有保存任何其他信息。 创建轻量标签,不需要使用 -a-s-m 选项,只需要提供标签名字:

1
2
3
4
5
6
7
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5

这时,如果在标签上运行 git show,你不会看到额外的标签信息。 命令只会显示出提交信息:

1
2
3
4
5
6
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700

changed the version number

后期打标签

你也可以对过去的提交打标签。 假设提交历史是这样的:

1
2
3
4
5
6
7
8
9
10
11
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function
4682c3261057305bdd616e23b64b0857d832627b added a todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a started write support
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme

现在,假设在 v1.2 时你忘记给项目打标签,也就是在 “updated rakefile” 提交。 你可以在之后补上标签。 要在那个提交上打标签,你需要在命令的末尾指定提交的校验和(或部分校验和):

1
$ git tag -a v1.2 9fceb02

可以看到你已经在那次提交上打上标签了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
v1.5

$ git show v1.2
tag v1.2
Tagger: Scott Chacon <schacon@gee-mail.com>
Date: Mon Feb 9 15:32:16 2009 -0800

version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <mchacon@gee-mail.com>
Date: Sun Apr 27 20:43:35 2008 -0700

updated rakefile
...

共享标签

默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push <remote> <tagname>

如果想要一次性推送很多标签,也可以使用带有 --tags 选项的 git push 命令。 这将会把所有不在远程仓库服务器上的标签全部传送到那里。

image-20231211174337165

使用 --tags 推送所有标签是,不会区分轻量标签和附注标签,Git也没有提供参数支持各自推送。

删除标签

要删除掉你本地仓库上的标签,可以使用命令 git tag -d <tagname>。 例如,可以使用以下命令删除一个轻量标签:(该方法不会从任何远程仓库中移除这个标签)

1
2
$ git tag -d v1.4-lw
Deleted tag 'v1.4-lw' (was e7d5add)

删除远程仓库标签的方式为 git push <remote> --delete <tagname>

检出标签

如果你想查看某个标签所指向的文件版本,可以使用 git checkout 命令, 虽然这会使你的仓库处于“分离头指针(detached HEAD)”的状态——这个状态有些不好的副作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git checkout 2.0.0
Note: checking out '2.0.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b <new-branch>

HEAD is now at 99ada87... Merge pull request #89 from schacon/appendix-final

$ git checkout 2.0-beta-0.1
Previous HEAD position was 99ada87... Merge pull request #89 from schacon/appendix-final
HEAD is now at df3f601... add atlas.json and cover image

在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。 因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支:

1
2
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'

如果在这之后又进行了一次提交,version2 分支就会因为这个改动向前移动, 此时它就会和 v2.0.0 标签稍微有些不同,这时就要当心了。

Git 别名

Git 别名功能类似于Linux上的alias命令,但 Git 只是简单地将别名替换为对应的命令。它通过git config配置。

例如,配置 git config --global alias.st status,当想要执行git status时,可以直接输入git st

当想要执行外部命令,而不是一个 Git 子命令时。 如果是那样的话,可以在命令前面加入 ! 符号。 如果你自己要写一些与 Git 仓库协作的工具的话,那会很有用。 我们现在演示将 git visual 定义为 gitk 的别名:

1
$ git config --global alias.visual '!gitk'

总结

至此,Git 的基础操作基本就介绍完了。大致包括有:创建或克隆一个仓库、进行更改、暂存并提交这些更改、浏览仓库从创建到现在的所有更改历史。