Git 入门

在了解了命令行的基本操作后,我们来尝试使用 Git。这部分的内容是命令和原理结合的,你可以在这里学到 Git 的基本操作和 Git 的工作原理。

获得一个 Git 仓库的方法有两种,一种是“将一个文件夹变成 Git 仓库”,一种是从远程克隆一个 Git 仓库。

将一个文件夹变成一个 Git 仓库

Git 仓库的本质是一个文件夹。所以,我们可以将一个文件夹变成一个 Git 仓库。

你可以输入以下命令,把当前文件夹变成一个 Git 仓库:

git init

或者,你可以输入以下命令,把指定文件夹变成一个 Git 仓库:

git init path/to/folder

Git 仓库和普通文件夹的区别在于,Git 仓库中有一个隐藏的.git文件夹,这个文件夹中存储了 Git 的所有信息,包括所有的git object,ref等。在任何情况下,你都不要应该修改.git文件夹中任何的内容。唯一的例外是.git/config文件,这个文件存储了当前仓库的配置信息,例如远程仓库的地址等。但是你也应该谨慎修改这个文件或者使用git config命令修改配置。

如果你不小心把一个文件夹变成了 Git 仓库1,直接删除.git文件夹就可以了。使用以下命令即可:

rm -rf .git

Git 在操作 Git 仓库时,会递归的从当前文件夹向上查找.git文件夹,直到找到第一个.git文件夹或者到达文件系统的根目录。所以,你可以在任何一个 Git 仓库的子文件夹中使用 Git 命令。当 Git 仓库嵌套时,Git 会使用离当前文件夹最近的.git文件夹。

克隆远程仓库

要从远程克隆仓库首先要确保你具有权限,对于 GitHub,GitLab, Bitbucket等代码托管平台的公开仓库,任何人都具有权限,因此可以直接克隆。对于私有仓库,你需要一个具有读取权限的平台账号,然后将平台账户和你的 SSH 密钥绑定。

具有权限后,就能够克隆仓库了,使用以下命令即可:

git clone https://github.com/NEUQ-CS/manual.git

记得将仓库地址更换成你想克隆的仓库的地址。克隆结束后,在当前目录下会产生一个与仓库名一致的文件夹,在本例中,是manual。 这个仓库就是你克隆的 Git 仓库。

clone命令还有一些其他参数可以使用

克隆到指定文件夹

在地址后面加上指定路径即可:

git clone https://github.com/NEUQ-CS/manual.git path/to/repo

注意,仓库里的文件会直接存在你指定的文件夹里面,而不是在你指定的目录里放仓库文件夹。

克隆指定分支

加上参数-b <分支名>即可,例如:

git clone https://github.com/NEUQ-CS/manual.git -b master

这将指定需要克隆master分支.

需要说明的是,即使你选择了一个分支,整个仓库的所有分支都实质上会被克隆下来,只是克隆完成后位于指定分支。

指定克隆深度

使用--depth <深度>来指定克隆深度:

git clone https://github.com/NEUQ-CS/manual.git --depth=1

仅克隆主分支上包含最新一个提交的完整仓库,克隆的仓库不含有任何历史记录。不推荐使用,除非你只是为了临时下载代码并且不需要历史记录和其他分支。

查看 Git 仓库的状态

在一个 Git 仓库中,你可以使用以下命令查看当前仓库的状态:

git status

你会看到以下输出:

On branch master
Your branch is up to date with 'origin/use-git'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/use-git/configure.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/use-git/try-git.md

当前在分支master上,你的分支是最新的。Changes to be committed中列出了你修改的文件,Untracked files中列出了你新建的文件。

Git 工作原理

在继续尝试前,我们先来了解一下 Git 的工作原理,你可能已经对上面的输出感到迷惑了。

在一个 Git 仓库的文件夹中,一个文件可能存在以下三种状态的其中一种:

  • 未跟踪 Untracked
  • 已跟踪 Tracked
  • 暂存中 Staged

未跟踪

当你使用 git init初始化一个仓库时,原有的所有文件都是未跟踪。Git 不会管理任何未跟踪的文件,但是这个文件仍然存在于这个文件夹中。不会管理的意思在于,当你通过 git 操作仓库的状态时,未跟踪的文件不会被修改或删除。如果一定会造成修改2,git 会阻止这次操作,直到你解决完冲突。

已跟踪

这应该是仓库中大部分文件存在的状态。当你刚远程克隆完一个仓库时,所有文件都处于已跟踪状态。这是显然的,因为只有被跟踪的文件才会被提交到远程服务器。

跟踪未跟踪的文件

要把一个未跟踪的文件变为已跟踪状态的方法是,先添加到暂存区,然后再commit。以下是命令行教程

# 添加a_new_file到暂存区
git add a_new_file
# commit在暂存区的所有文件
git commit -m "add `a_new_file` to this repo"
将已跟踪的文件删除

你可以使用git rm <文件>来删除,这会直接把删除操作直接添加到暂存区,文件系统上的文件也会被删除。它等价于以下操作:

rm an_old_file
git add an_old_file

要取消跟踪一个文件,但希望在文件系统上保留这个文件,使用git rm --cached file

暂存中

准确地说,暂存的不是文件,是操作。通常来说,可以暂存

  • 修改 Modify/Update
  • 添加 Add
  • 删除 delete
  • 移动 Move

这几种操作。

当你在文件系统上修改一个文件,然后用git add的时候会暂存一个修改操作到暂存区。 当你在文件系统上添加一个文件,然后用git add的时候会暂存一个添加操作到暂存区。 当你在文件系统上删除一个文件,然后用git add的时候会暂存一个删除操作到暂存区。 当你在文件系统上移动或重命名一个文件,然后用git add的时候会暂存一个移动操作到暂存区。

将操作从暂存区撤出,使用以下命令:

git restore --staged file

对于这些状态的理解,我推荐使用Visual studio code的内置Git功能进行观察,他是Git的一个前端。可以代替手动的命令行,但是,就我个人而言,更会倾向使用命令行,因为这些前端不支持较为复杂的操作。

现在我们再来看这段输出:

On branch master
Your branch is up to date with 'origin/use-git'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/use-git/configure.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/use-git/try-git.md

Changes to be committed部分就是存在于暂存区的文件,当我们使用git commit的时候,就会跟踪并把这些文件添加到git仓库。

Untracked files是前文说过的未跟踪文件,这是因为这个文件才创建,当使用git add来添加时,会在暂存区添加一个添加操作。

代码提交

代码提交使用git commit命令,提交的时候需要一段简短的话描述你的这次修改。同时,你的姓名,邮箱也会被记录在这条commit记录中。

关于如何编写commit信息,我会在后面讲解良好的git使用习惯时讲到。

git commit命令常用的形式有以下两种:

  1. 将信息作为参数直接提交
git commit -m "<提交信息>"
  1. 使用代码编辑器编写提交信息
git commit -a

这会打开你在安装时选择的代码编辑器,信息编写完成后,保存关闭即可。

有时候,你觉得提交信息写的不好,打算重写编写提交信息时,可以采用以下命令:

git commit --amend -m "<提交信息>"

或者使用编辑器

git commit --amend -a

这会完全重新提交一次commit并获得不同的commit哈希3,以及时间戳。

使用git commit --amend的操作等价于以下操作:

# 保留修改回到上一次提交
git reset --soft "HEAD^"
# 重新提交
git commit -m "<提交信息>"

我个人更喜欢这种方法,因为更为灵活,你可以在重新提交过程中做一些修改。

查看提交历史

使用git log命令可以查看提交历史,这会列出所有的提交记录,包括提交哈希,提交信息,提交时间,提交者等信息。

推送到远程仓库

尽管我还没有教你配置远程仓库,但是我还是会告诉你如何推送到远程仓库。

使用git push命令可以将本地仓库的修改推送到远程仓库。这个命令的形式是:

git push <远程仓库名> <本地分支>:<远程分支>

通常来说,远程仓库名是origin。并且,大多数时候,本地分支和远程分支是一样的,所以可以省略远程分支:

git push origin <分支>

这会将本地分支推送到远程分支,如果远程分支不存在,会自动创建。

有时候,远程分支上包含了你不希望存在的修改,这时候你想用本地分支覆盖远程分支,可以使用-f参数:

git push -f origin <分支>

-f选项会强制推送,这会覆盖远程分支上的所有修改,所以请谨慎使用。如果你把别人的代码或者作业给覆盖了,小心被打。

在推送之前,你需要先确保你具有远程仓库的所有历史记录,如果本地仓库不包含远程仓库存在的一些更改,你就需要先拉取远程仓库的更改。

拉取远程仓库的修改

拉取远程仓库的修改使用git pull命令,这会将远程仓库的修改拉取到本地仓库。这个命令的形式是:

git pull <远程仓库名> <远程分支>:<本地分支>

同样,大多数时候,远程仓库名是origin,远程分支和本地分支是一样的,所以可以省略远程分支:

git pull origin <分支>

这会将远程分支拉取到本地分支,如果本地分支不存在,会自动创建。

git pull命令实际上是git fetchgit merge的组合,git fetch会将远程仓库的修改拉取到本地仓库,但不会应用更改,而是会在一个<远程名>/<分支名>的分支上保存,git merge会将这个特殊分支合并到本地分支。

分支

前面提到了那么多关于分支的内容,但是我还没有告诉你什么是分支。

分支是 Git 的一个重要概念,它是一个指向一个提交的指针。在一个 Git 仓库中,你可以有多个分支,每个分支指向一个提交。默认情况下,你会有一个master1分支。

分支的意义在于它和其他分支可以并行开发,你可以在一个分支上开发一个新的功能,而不影响其他分支。当你开发完成后,你可以将这个分支合并到主分支上。合并会将当前分支上所有领先于基础分支的patch应用到基础分支上。这就是使用 Git 进行协作开发的原理。

了解了什么是分支,现在我来教你如何管理分支。

查看分支

使用git branch命令可以查看当前本地仓库的所有分支。加上-a参数可以查看所有分支,包括远程分支。

样例输出:

caiyi@archlinux ~/r/manual (use-git)> git branch
  fix-ci
  fix-format
  main
  master
* use-git
caiyi@archlinux ~/r/manual (use-git)> git branch -a
  fix-ci
  fix-format
  main
  master
* use-git
  remotes/origin/HEAD -> origin/main
  remotes/origin/fix-ci
  remotes/origin/fix-format
  remotes/origin/main
  remotes/origin/master
  remotes/origin/use-git

上面的输出表明,我们当前在use-git分支上,本地仓库中有fix-ci, fix-format, main, master, use-git五个分支,远程仓库中有fix-ci, fix-format, main, master, use-git五个分支。

创建分支

创建分支是创建当前分支的拷贝,因此要先切换到你想要拷贝的分支上,然后使用命令。

创建分支有两种命令:

git checkout -b <分支名>
git branch -m <分支名>

这会创建一个新的分支,并切换到这个分支上。

要创建一个分支但不切换到这个分支上,可以使用以下命令:

git branch <分支名>

切换分支

当两个分支里的文件不一样时,切换分支会修改当前文件系统上的文件,因为切换分支意味着你要切换到另一个分支的文件系统状态。

切换分支使用git checkout命令,这会将当前分支切换到指定分支上:

git checkout <分支名>

删除分支

删除分支使用git branch -d命令,这会删除指定分支:

git branch -d <分支名>

如果分支上有未合并的修改,Git 会拒绝删除这个分支。如果你确定要删除这个分支,可以使用-D参数:

git branch -D <分支名>

合并分支

合并分支有三种方法:

  • 合并 merge
  • 变基 rebase
  • 快进 fast-forward

通常来说,使用merge即可,但是在一些特殊情况下,你可能需要使用rebase。快进是一种特殊的合并,当你的分支是基于基础分支的最新提交时,Git 会直接将基础分支指针指向当前分支指针,这就是快进。

合并分支使用git merge命令,这会将指定分支合并到当前分支上

git merge <另一个分支>

合并分支会产生一个新的提交commit,这个提交包含了两个分支的所有修改。

变基

变基是一个相对复杂的概念,相对于merge来说,它能够让提交记录更加线性。这是因为,在合并后,分支继承了基础分支,也同时继承了合并分支,同时具有两个父节点。这会使得提交记录变得复杂,也就是更加不线性

变基与合并不同的地方在于,它不会让分支同时继承两个父节点,而是将合并分支的提交记录放在基础分支的最新提交之后。提供更改的分支不会被继承,因而不会出现两个父节点。

变基使用git rebase命令,这会将指定分支变基到当前分支上

git rebase <另一个分支>

变基会产生一个新的提交commit,这个提交包含了两个分支的所有修改。

回退到某一个提交

有时候,你可能需要回退到某一个提交,这时候你可以使用git reset命令。命令的格式为

git reset [option] <提交哈希>

其中option有以下几种:

  • --soft 仅仅回退HEAD指针,不会修改暂存区和工作区
  • --mixed 回退HEAD指针和暂存区,不会修改工作区
  • --hard 回退HEAD指针,暂存区和工作区

通常情况下,建议显式指定选项,以避免错误使用命令。

提交哈希可以使用git log查看。当你想从当前版本向前回退几个版本时,可以使用HEAD~n,其中n是你想回退的版本数。

例如:

# 回退到上一个版本
git reset --hard HEAD~
# 回退到上上一个版本
git reset --hard HEAD~2

常见误区

Git 命令行 一般情况下仅在本地管理仓库,不会实时修改远程仓库。当且仅当执行以下命令时会通过网络与远程仓库交互:

git fetchgit pull

执行这两个命令时,仅会读取远程仓库的信息,不会修改远程仓库的信息。git fetch会将远程仓库的信息拉取到本地仓库,git pull会将远程仓库的信息拉取到本地仓库并合并到当前分支。

git push

执行这个命令时,会将本地仓库的信息推送到远程仓库,这会修改远程仓库的信息。

总结

本节课我们学习了 Git 的基本操作,包括将一个文件夹变成 Git 仓库,克隆远程仓库,查看 Git 仓库的状态,提交代码,查看提交历史,推送到远程仓库,拉取远程仓库的修改,分支的创建,切换,删除,合并,变基等操作。

这些操作需要你多加练习,只有多加练习,你才能熟练掌握这些操作。

要练习这些操作,我推荐使用这个网站:Learn Git Branching。使用这个网站时,你只需要先掌握基本操作,以及操作远程仓库的操作,就可以了。对于更加复杂的操作,等你对 Git 有了更深的理解再去尝试。

但更重要的是,你应该多使用 Git 来管理你的代码,这样你才能更好地理解 Git 的工作原理。

下一节课开始,我们将学习如何使用代码托管平台,本教程以 GitHub 为例。


  1. 去年有人才把整个C盘或者桌面都变成了Git仓库。 ↩︎ ↩︎

  2. 比如你有一个未跟踪的a.txt,然后你通过切换到另一个分支,合并另一个分支,拉取远程尝试覆盖(其他分支上存在已跟踪的a.txt)这个文件时,Git 会阻止这次操作。 ↩︎

  3. 还记得 Git 是去中心化这句话吗?使用哈希来描述对象是去中心化的典型特征。哈希是指向去中心化网络中对象的一个指针,具有这个指针就能够访问到这个对象,当没有人持有哈希时,这个对象就视为被删除,但是这个对象本身实际上不能被删除,它仍存在于去中心化网络中。Git 的commit也是,这也就意味着,当你在GitHub上删除一个仓库,分支时,它实际上从未被删除,也不能够被删除。 ↩︎