首页 Git 从基础到进阶
文章
取消

Git 从基础到进阶

前言

Git 是什么

想象这么一个场景,你和你的几个同事一起开发一个应用,假设同事 A 修改了代码 1,同事 B 修改了代码 2,你自己修改了代码 3,要如何将你们的修改安全、准确地同步到所有人的电脑上?如果有一天,应用突然运行不了了,你尝试 debug 无果,想要回退到上一次能够正常运行的代码版本,你要如何操作?

这就是为什么我们需要版本控制工具。它们对代码的提交和修改进行纪录,方便在不同的代码版本中切换,提供在不同分支之间合并代码的能力。

版本控制工具分为集中式版本控制工具分布式版本控制工具

集中式版本控制工具将代码仓库的提交信息储存在中心服务器中,你想要提交一笔代码,必须先从服务器拉取最新的修改,然后再将自己的修改推给服务器。其他操作例如回退版本也是需要全程连接服务器才能进行。

分布式版本控制工具不依赖中心服务器,每个人的电脑上都有一份完整的提交信息,可以在本地进行提交和回退,不需要联网也可以正常工作。当需要将自己的修改同步给其他人时,可以将提交打包,发给对应的人让他们合入到自己的电脑上即可。

Git 就是目前最流行的分布式版本控制工具

GitHub 是什么

在早期,分布式版本控制工具大都使用邮件的方式来同步修改。这种方式显然不太方便,因此虽然 Git 不依赖中心服务器,但是大部分项目仍然选择搭建一个服务器,将最新的代码仓库放在服务器上,当自己想要同步别人的修改时,就去服务器上拉取修改,当自己想要别人同步自己的修改时,就将修改推送到服务器上。

目前有许多公司推出了用来搭建 Git 服务器的工具,例如 Gitlab 和 Gerrit,大公司基本使用它们在内网搭建 Git 服务器以保证代码隐私。而对于买不起服务器的个人用户,或者不需要代码隐私的开源项目而言,像 Github 和 Gitee 这种网站则提供了代码托管的功能,你可以借用它们的 Git 服务器存放自己的代码仓库,配置自动构建流程,发布应用程序等。

Git 的安装与配置

对于 Linux 或类 Unix 系统,请使用各自的包管理器安装 Git。

对于 Windows 系统,请打开官网,点击右侧的 Download for Windows 进入下载页面,点击 64-bit Git for Windows Setup 下载安装包(除非你是 32 位系统,但是都 2022 年了应该不至于还在用 32 位系统吧?)。下载完成后,双击打开安装,对于大部分人而言,无脑 next 就可以了,如果你愿意,也可以仔细看看每一个选项是什么,根据自己的情况来勾选。

安装结束后,打开一个新的 cmd 或 powershell,输入 git 后回车,检查是否能识别 git 命令。如果不能,请在环境变量的 PATH 中添加 git.exe 所在路径。以我的电脑为例,我安装在 E:\Git,对应的 git.exeE:\Git\cmd\git.exe,因此我需要在 PATH 中添加 E:\Git\cmd

初次安装 Git,需要配置自己的用户名和邮箱:

1
2
git config --global user.name "你的名字"
git config --global user.email "你的邮箱"

注意,这并不是在注册 Git 账号,Git 并没有账号的概念,这些信息仅仅作为你的修改的署名,你可以随时修改它。

Git 基础使用

帮助文档

Git 内部又细分出了许多的命令,例如 git initgit commitgit diff 等等,对于每一种命令,都可以使用 git help 命令 来查询对应的帮助文档,例如 git help init 查询 git init 的帮助文档。同样的,你也可以使用 git 命令 --help

创建一个 Git 仓库

首先,在命令行中 cd 到你的代码文件夹,然后执行:

1
git init

将会在当前目录下创建一个 .git 文件夹,里面将会储存未来的所有版本信息。

将目前已有的所有代码保存为第一个版本,我们使用下面的流程:

1
2
git add .
git commit -m "init git repository"

暂存与提交

在上面的流程中,我们使用了 git addgit commit,它们分别对应暂存(Stage)和提交(Commit)。

暂存可以理解为一个临时版本,它不在代码历史中,但是你可以随时简单地恢复上一次暂存时的代码,直到你提交,让它成为一个正式版本为止。当你完成一个功能的一小部分代码时,你可能想要临时保存一下这个状态,以方便在后续的开发中出现问题时回退,但是它又不能作为一个版本提交,那么你就可以使用暂存。

git commit 只会提交已暂存的修改,因此在 git commit 之前,你需要暂存所有待提交的文件

git add . 中的 . 表示当前目录,如果你只想暂存某几个文件,你只需要将 . 替换为对应文件路径即可(可以同时写多个文件路径)。

对于所有需要填写文件路径的 Git 命令,都建议使用 -- 将命令与文件路径隔开,例如:

1
git add -- src/main.c

这是为了避免 Git 错误地将文件路径解释为命令参数。

git commit 如果不带任何参数执行,则会打开一个文本编辑窗口,你可以在里面键入提交信息,保存并关闭窗口后完成提交。如果提交信息比较短,可以使用 git commit -m "提交信息" 来简化这一操作。当提供多个 -m "提交信息" 参数时,多个提交信息会以分行的形式拼接。

忽略

有些文件你可能并不想让 Git 记录它的变更,比如,编译产物,nodejs/Rubygem/Cargo 的依赖锁文件等等。

你可以在项目中创建一个 .gitignore 文件,然后在里面添加你想要让 Git 忽略的文件或文件夹路径(相对于 .gitignore)。

.gitignore 支持模糊匹配:

  1. 使用 * 可以匹配除了 / 外的任意内容,并且可以匹配 0 或多个字符,例如 *.jp*g 可以匹配 a.jpgbcd.jpeg
  2. 使用 ? 可以匹配一个除了 / 以外的任何字符。
  3. 使用 [] 进行范围限定,例如 [a-zA-Z] 可以匹配任何一个字母。
  4. 使用 ** 可以匹配任何文件夹,例如 **/foo 可以匹配任何 foo,不管 foo 深藏于多少层级目录之后。注意到 ** 本身也可以匹配 .,即当前目录,例如 a/**/n 可以匹配 a/na/b/na/b/c/n
  5. 如果一个匹配项以 / 结尾,那么它只会匹配文件夹,不会匹配文件。

Git 也支持反操作,如果一个模糊匹配覆盖了一个你想要让 Git 记录的文件,你可以使用 !文件路径 让 Git 知道这是需要记录的文件。如果你的文件名以 ! 开头,而你想忽略它,则以 \! 来表示该 !

.gitignore 中空格会被忽略,文件路径包含空格时,应该使用 \ + 空格来代替

状态,历史与回退

使用 git status 可以查看当前仓库的暂存状态,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   _conio.h

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   Makefile

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        new.cpp

第一段是已经暂存的修改文件,第二段是没有暂存的修改文件,第三段是未跟踪的新文件。如果你修改并暂存了一个文件,之后再修改同一个文件,它会同时出现在暂存和未暂存两个区域。

对于已暂存的修改,可以使用 git diff --staged 来查看具体的修改内容。对于未暂存的修改,可以使用 git diff 来查看具体的修改内容。git diff 也可以指定目录或者文件路径。

如果你要恢复到上一次暂存的代码状态,则使用:

1
git checkout .

同样,. 代表当前目录,如果你只想恢复部分文件,替换为对应文件路径即可。

需要注意的是,未跟踪的新文件不在恢复的范围内,如果你想要删除未跟踪的文件,则可以使用:

1
git clean -xdf

如果你想将已暂存的修改恢复为未暂存的修改,并且不想丢失修改的内容,则可以使用:

1
git restore --staged .

或者

1
git reset .

同样 . 可以替换为文件路径。

对于已经提交的修改,我们可以使用 git log 来查看它的历史提交纪录,提交的格式通常是:

1
2
3
4
5
commit 7007c77594a8e37bd26c52a6929f00015ccf8aa5
Author: Nichts Hsu <NichtsVonChaos@gmail.com>
Date:   Thu Nov 14 14:40:11 2019 +0800

    init git repository

第一行以 commit 开头,紧随其后的是它的 commit id,该 id 能够唯一确定一个提交。第二行是提交者的署名,也就是 Git 配置过程中我们设置的名字和邮箱。第三行是提交的日期。之后一个空行,第五行是提交信息。

想要查看某个提交具体的修改,可以使用:

1
git show <commit-id>

<commit-id> 替换为实际 id 即可。在 Git 中,可以使用 HEAD 来表示最后一次提交的 commit-id,用 HEAD^ 来表示再上一次提交,用 HEAD~i 表示当前提交往前数的第 i 次提交。

想要将代码回退到某个提交之后的状态,可以使用:

1
git reset [--hard | --soft] <commit-id>

--hard 选项会清空所有修改,恢复到指定提交的代码状态,而 --soft 选项会把从指定的提交到当前状态之间的所有提交和修改都变成未暂存状态,不提供选项时默认为 --soft,不提供 commit-id 时默认为 HEAD。要删除最后一次提交,可以简单使用 git reset --hard HEAD^

如果你仅仅只想回退某一个以前的提交,则可以使用:

1
git revert <commid-id>

值得注意的是,git revert 并不是在提交历史中删除对应提交,而是创建一个与之相反的提交来抵消它的修改。

对于一些更加复杂的情况,我们可以使用 git reflog 查看操作历史:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fb3007f (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: commit (merge): merge upstream cotes2020/jekyll-theme-chirpy b0815b53c0505f93937e54378131ea8f6e9a929b..22c12a4d34bbf0cff23997ed3b24ef51b5d39d50
b25fdfb HEAD@{1}: checkout: moving from original-chirpy to master
22c12a4 (upstream/master, origin/original-chirpy, original-chirpy) HEAD@{2}: pull upstream master: Fast-forward
57a7609 HEAD@{3}: checkout: moving from master to original-chirpy
b25fdfb HEAD@{4}: pull: Fast-forward
d6f0a8c HEAD@{5}: pull: Fast-forward
0a97db0 HEAD@{6}: pull upstream master: Merge made by the 'recursive' strategy.
844667d HEAD@{7}: commit: update post:use valine, warn the timeliness
8902d58 HEAD@{8}: commit (amend): remove `/` of `/>`
8973cfd HEAD@{9}: commit: update gemfile.lock
a2f6ef7 HEAD@{10}: pull upstream master: Merge made by the 'recursive' strategy.
e307561 HEAD@{11}: checkout: moving from original-chirpy to master
57a7609 HEAD@{12}: pull upstream master: Fast-forward
7337fa5 HEAD@{13}: checkout: moving from master to original-chirpy

可以看到,里面详细纪录了我们每一次的 Git 操作,想要恢复到某一次操作之后,可以使用 HEAD@{i} 作为 git reset<commit-id> 参数。

Git 分支与远程

分支

在开发过程中,如果你突发奇想,想要做点主线之外的开发,然而这个开发如果直接在主线上开发并不合适,你就可以创建一个新的分支,在新分支开发新功能,旧分支按照主线继续开发。

1
git branch 新分支名

上述命令基于当前分支创建新的分支。如果需要指定被复制的分支,则加在新分支名的前面:

1
git branch 旧分支名 新分支名

创建分支后,可以使用下面的命令切换分支:

1
git switch 分支名

注意,如果你当前分支有修改未提交,会被 git switch 转移到新分支中。为了确保安全,建议使用下面的命令切换分支:

1
git checkout 分支名

因为 git checkout 切换分支前会检查未提交修改,如果存在则切换失败并给出警告。

创建和切换分支可以合并为一步完成:

1
git checkout -b 旧分支名 新分支名

列出当前所有的分支可以使用:

1
git branch -a

通常,默认的主分支叫做 master 或者 main

要删除某个分支,可以使用:

1
git branch -d 分支名

如果该分支存在异常状态,删除可能失败,则可以使用:

1
git branch -D 分支名

来强制删除。

要重命名某个分支,可以使用:

1
git branch -m 旧分支名 新分支名

要将分支 A 的修改同步到分支 B 中,可以先切换到分支 B,然后执行:

1
git merge 分支A

在合并过程中,可能会产生合并冲突导致合并失败,此时 Git 会对所有冲突的地方做以下标记:

1
2
3
4
5
<<<<<<< HEAD
当前分支的冲突代码
=======
合并来源分支的冲突代码
>>>>>>> 合并来源的分支名

通过全局搜索 <<<< 可以快速找到冲突位置,你需要根据实际情况来选择如何处理该冲突。当所有冲突修改完毕后,使用 git add .git commit(或 git merge --continue)来完成合并。

远程

远程仓库指的是托管在服务器上的的代码仓库,例如 Github。要想从远程下载一个代码仓库,可以使用:

1
git clone 远程仓库链接

这将会自动为你配置好 Git 的远程关联。

如果你需要手动将本地的代码仓库与远程的代码仓库相关联,你可以使用:

1
git remote add 服务器名 远程仓库链接

其中服务器名可以自定义,当你需要关联多个远程仓库时,合适的名字可以帮助你更好地区分。当你使用 git clone 时,Git 为你自动配置的服务器名是 origin,如果你只关联一个服务器,最好也使用默认的 origin 作为服务器名。

如果需要修改服务器名,可以使用:

1
git remote rename 旧服务器名 新服务器名

如果需要删除服务器关联,可以使用:

1
git remote remove 服务器名

如果需要修改远程仓库链接,可以使用:

1
git remote set-url 服务器名 新远程仓库链接

查看当前关联的所有服务器使用:

1
git remote -v

想要抓取远程仓库的代码,可以使用:

1
git fetch 服务器名

这会在本地创建或更新对应的远程分支,以该项目为例,执行 git branch -a 可以看到:

1
2
3
4
5
6
7
8
9
10
  dev/run-code
* master
  original-chirpy
  remotes/origin/HEAD -> origin/master
  remotes/origin/dev/run-code
  remotes/origin/develop
  remotes/origin/gh-pages
  remotes/origin/master
  remotes/origin/original-chirpy
  remotes/origin/release/4.3

带有 remotes/origin 开头的分支即是服务器 origin 上的远程分支。远程分支几乎可以用在任何分支操作上(使用 服务器名/远程分支名),例如,你可以使用 git merge 服务器名/远程分支名 来将远程分支合并到本地分支中。

事实上,我们可以使用 git pull 来简化这一操作:

1
git pull 服务器名 远程分支名

这将会更新 remotes/服务器名/远程分支名 并且将该分支合并到本地的当前分支中。

如果你想将本地的修改推送给服务器,你必须首先进行 git pull 合并远程分支的修改,然后使用:

1
git push 服务器名 远程分支名

将修改推送到服务器。

为了进一步简化这些工作,我们可以将本地分支和远程分支绑定(git clone 会自动绑定):

1
git branch -u 服务器名/远程分支名

这样,我们在省略参数调用 git pullgit push 时,会自动关联绑定的远程分支。

如果想要删除远程分支,可以使用:

1
git push 服务器名 --delete 远程分支名

强制同步

在做这些操作时需要十分谨慎,以避免丢失重要数据

如果想要放弃当前分支的修改,强制同步为另一个分支,可以使用:

1
git reset --hard 分支名

在某些时候,你可能需要放弃本地或放弃远程的修改,将本地强制同步为远程代码,或将远程强制同步到本地代码。 将远程强制同步为本地修改,也就是将本地修改覆盖远程修改,可以使用:

1
git push -f 服务器名 远程分支名

git pull 本质是 git fetch + git merge 它不能完成类似的操作,如果你想要将本地强制同步为远程修改,则可以在 git fetch 后使用:

1
git reset --hard 服务器名/远程分支名

上游

在 Github 上,如果我们看上了别人的代码,想要拷贝下来自己修改使用,Github 提供了一个非常便利的按钮 —— Fork:

Fork

Fork 之后,这个仓库就会拷贝一份到你的 Github 账户下,并且在仓库名的下面会显示来源:

Forked

此时,你的这个仓库可以像正常的远程仓库那样来使用。但是,如果原作者的仓库有了更新,你想要同步它的新代码,或者你觉得自己的修改不错,想要推送给原作者,此时,他的仓库和你的仓库就构成了上游(upstream)和下游(downstream)的关系。

对上游仓库,我们可以做的操作和自己的远程仓库几乎是一样的,可以使用 git remote 来关联上游仓库(建议服务器名使用 upstream),使用 git pull upstream 上游分支名 来拉取上游修改。不过需要注意的是,我们没有对上游仓库的 push 权限,因此是不可以直接 push 给上游仓库的。

如果你想要将自己的修改推送给上游,首先也需要先 pull 上游的修改,然后先推送到自己的远程仓库(最好是新建一个专门的分支,否则后续自己所有的提交都会自动推送),然后点击 Github 的 Contribute,然后点击 Open Pull Request:

Pull Request

Pull Request

左侧选择上游分支,右侧选择你自己的远程分支。正常来说,只要你先 pull 了上游修改,Github 都会提示你 Able to merge,此时再点击 Create Pull Request,就会在上游仓库创建一个合并请求,原作者看到后,如果觉得可以,他会同意该请求,将你的修改合并到他的仓库,此时在上游仓库右下角的 Contributors 名单中就能看到你自己。

进阶操作

追加提交

当你完成一个提交之后,你有点后悔,想要修改它的提交信息,或者想要再新增一点修改到里面,我们可以先 git add 暂存你要追加的修改,然后使用:

1
git commit --amend

会打开一个文本编辑器,可以编辑上一次提交的提交信息,并且会将已经暂存的修改追加到该提交中,这样你的提交历史中就不会出现大量零碎的修补提交。

合入补丁

当开发比较大型的项目时(例如 Linux 内核开发),可能会遇到一些问题需要专业人员的支持,对方可能会给你发一个或多个补丁(patch),通常是 *.diff 文件居多,使用文本文件打开可以看到里面的提交信息和修改内容。

要将这些补丁合入我们的项目,可以使用:

1
git apply 补丁文件 --reject

通常,如果合并失败那么 git apply 不会强制合并,而我们加上 --reject 的话,git apply 会把可以合并的部分先合并,然后将不能合并的部分单独列在一个 *.rej 文件中。你可以使用 git status 快速找到合并失败的文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Refresh index: 100% (60047/60047), done.
On branch * * *
Your branch is up to date with 'origin/* * *'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   drivers/usb/dwc3/dwc3-msm.c
        modified:   drivers/usb/host/xhci-plat.c
        modified:   drivers/usb/host/xhci.h

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        drivers/usb/dwc3/dwc3-msm.c.rej
        ef67006.diff

no changes added to commit (use "git add" and/or "git commit -a")

其中 ef67006.diff 是补丁文件,而 drivers/usb/dwc3/dwc3-msm.c.rej 就是合并失败的代码。我们可以打开该文件,然后手动将这些修改合并到 drivers/usb/dwc3/dwc3-msm.c。合并后删除 *.rej 和补丁文件,使用 git addgit commit 来提交修改。

如果你想要保留原始的提交信息,而不是使用自己的提交信息,则可以使用 git am,具体流程如下:

  1. git am 补丁文件,如果这一步合并成功,就不需要继续往下走了
  2. git apply 补丁文件 --reject
  3. 手动合并失败的部分
  4. 删除 *.rej 文件,注意先不要删补丁文件
  5. git add 添加所有待提交文件
  6. git am --continue

修改提交顺序

有时候,我们可能会需要修改提交的顺序,例如,我们想要给以前的一个提交增加一些修改,或者删除以前的某个提交。对于这种情况,我们可以使用变基(rebase)操作:

1
git rebase -i <commit-id>

将打开一个文本编辑框,里面列出了从 commit-id 对应提交之后,到当前版本的所有提交,最上面是最早的提交,最下面是最晚的提交,例如:

1
2
3
4
5
pick 9f9e80a change some 'if let Some()' to 'ok_or()'
pick 379fcd6 change version display
pick fae806a Update README.md
pick 94bae45 fix comment
pick ef58896 update dependencies

我们可以自行交换这些行的顺序,例如把第一行移到最后一行,也可以删除某些不再需要的提交,保存并退出后,通过 git log,我们会发现该提交顺序已经变成了我们修改后的顺序。

注意:如果你只是想修改历史提交的提交信息,不需要调整提交的顺序,只需要把待修改的提交的 pick 改成 reword,保存退出后,git 会自动打开一个新窗口用来编辑提交信息:

1
2
3
4
5
pick 9f9e80a change some 'if let Some()' to 'ok_or()'
pick 379fcd6 change version display
reword fae806a Update README.md
pick 94bae45 fix comment
pick ef58896 update dependencies
本文由作者按照 CC BY 4.0 进行授权

从实例看 Rust 的 HRTBs

[译] 安全引导与镜像验证技术概览