Git学习笔记 #2 本地仓库使用

本文最后更新于:2022年5月19日 中午

接上文,本文在命令行的基础上介绍了常用指令的在本地的使用情景

本文大部分内容参考了 RCY 同学的教程,部分参考了 廖雪峰教程-Git菜鸟教程-Git,以及 Git 官网文档 Git-Documentation

本地的 Git

预先准备

在正式的操作前,你需要先配置你的用户信息,这件事情通常在你的机器上只需要干一次即可,因为我们使用了全局配置。在任意⼀个目录打开你的终端,配置你的用户名和邮箱:

1
2
3
$ git config --global user.name <username> # 替换为 GitHub 用户名
$ git config --global user.email <email> # 替换为绑定的邮箱
# username 中如果有空格,需要加双引号

基本操作

在想用 Git 管理的项目路径下右键进入 Bash 终端,然后将当前目录初始化为 Git 仓库。

1
$ git init

手动新建一个文件,随便写点什么,譬如一个 hello.md,此时文件存在于工作区。下面将其跟踪并提交:

1
2
$ git add hello.md      # 如果有更多文件,也可直接追加在后面
$ git commit -m "Init" # -m: 表示 message,备注本次提交信息

注意到,add 后面是文件名,也可以是目录通配符,因此也支持以下写法:

1
2
3
4
$ git add .          # 相对路径中 . 表示当前目录,因此这一选项会添加当前目录下所有文件
$ git add *.cpp # * 代表任意字符串,所有 .cpp后缀的文件被添加
$ git add test?.cpp # ? 代表单个字符,不得为空,因此如 test1.cpp 将被添加,而 test.cpp 等不会
$ git add dir/ # 添加路径下的 d 文件夹内全部文件, / 有没有皆可

此外,为了体现三个区,我们可以使用一些指令来对比:

1
2
3
4
$ git status         # 查看工作区、暂存区的文件,一般前者是红色,后者是绿色
$ git diff           # 查看工作区和暂存区的差异,适用于暂存后又修改的内容
$ git diff --cached  # 查看暂存区与 Git 仓库的差异,适用于提交前查看
$ git diff HEAD      # 同时查看其他两个区和 Git 仓库的差异
Diff用法图解

在工程中,通常我们不用命令行查看差异,而是用 VSCode、GitHub Desktop、GitLab 等可视化工具。

如果对于已暂存的文件后悔了,也可以取消暂存:

1
2
$ git reset <filename>  # 对于该命令,如果不带参数则会清空暂存区
$ git restore --stage <filename>

在完成了几次提交后,可以查看提交历史:

1
$ git log

提交历史中,每个提交记录都有对应的 commit hash 值,唯一标识了这次提交,这是 Git 用 SHA-1 hash 生成的加密字符串。

注:如果 log 比较长或者窗口比较小,这会触发「导航」模式,很多人第⼀次见到可能不知所措,不会退出该页面,此时可以:

  • 上下键移动或 Page Up / Down 翻页;
  • 输入 \ 接字符来全局查找 ;
  • 输入 q 退出,与其他系统中的导航模式类似;
  • 其他操作可以通过查询关键字「Linux less 导航」来查到。

分支 (Branch)

有了前面的知识,我们已经在脑海中想象⼀副快照变更图了,本节中我们将快照称作「结点」,若干结点组织成了版本树——Git 本身正是使用了红黑树对结点进行高效管理。

以下内容强烈推荐结合 Git 分支在线教程 来学习!

目前我们的结点树基本是串行的(除了回退、重新提交会导致分叉),那么所谓的并行开发如何体现呢?

注意到了反复有⼀个单词 main 出现在命令行,这即是 Git 默认的分支:主分支(旧版本叫 master)。main 即是这个分支的名字,也是一个指针,指向了该分支的最新结点。

和它⼀起的还有⼀个单词 HEAD,这表示头指针,指向当前所处的结点。在你做分支相关的操作前,会有 HEAD -> main -> 最新结点,直到你将它们分开。

创建分支

与初始化仓库类似,创建分支也很方便:

1
$ git branch test  # 创建⼀个名为 test 的分⽀

注意:创建完后分支的即会指向当前所在的结点,因此当前最新结点同时被 main、test、HEAD 指向,即 HEAD -> main(test) -> 最新结点

对于已有的分支,也可以通过以下命令查看、切换:

1
2
3
4
5
$ git branch		 # 查看本地分支,带 * 的为 HEAD 指向的当前分支
$ git checkout test # 检出,表示切换到 test 分支

# 上述的创建、切换操作也可以被缩减为一步操作
$ git checkout -b test # 创建⼀个名为 test 的分⽀并且切换过去

切换分支后,可以看到出现在命令行右侧的 main 已经变成了 test。查看 log 也可发现 HEAD -> test(main) -> 最新结点

合并分支

如果此时我们已经在不同的分支提交了不同修改 C2 和 C3,那么如何将分支合并到一起,使得并行开发结果汇总呢?

1
2
$ git checkout main  # 切换到 main 分⽀
$ git merge test     # 让 main 分⽀合并 test 分⽀的结点

对于分支的合并,Git 有专门的图形化输出命令来进行查看版本树(也可以用其他可视化工具):

1
2
3
4
5
6
$ git log --graph --oneline --all
# oneline 表示用行来显示记录,最上方的是最新的提交
# all 表示显示所有分支,如果没有则只显示当前分支及其祖先

# 当然,还有其他许多参数可以美化版本树,这里贴一个大神的版本
$ git log --graph --all --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative

为展示方便,这里使用在线教程里的图例:

main分支合并test分支

可以看到,合并后产生了一个新结点 C4,该结点具有双父结点,指向原来的 C2 和 C3。需要注意的是,该结点属于 main 分支,是 main 分支的最新结点(被指针 main 所指),而 test 仍指向旧的 C2。

如果要把 test 分支也同步到新结点,只需要让 test 分支合并 C4,也就是合并当前的 main 分支:

1
2
$ git checkout test  # 切换到 test 分⽀
$ git merge main     # 让 test 分⽀合并 main 分⽀的结点

而由于 main 分支的最新结点 C4 继承自 C2,此时的 Git 不会有任何操作,只是简单地将指针 test 移动到指针 main 所在位置,即快速前移(fast-forward):

test分支合并main分支

多数情况下,我们会先用主分支合并从分支,如果之后需要再从分支继续开发,才会把从分支快速前移

冲突的合并

如果 C2 和 C3 修改的代码不在同一文件的同一处,上述的 merge 是没有问题的,但是一旦发生冲突,git merge test 命令时就会提示错误。

此时如果我们用 git status 查看,会发现工作区里有一个新的状态「未合并的路径」,里面就是冲突的文件。打开该文件,会发现 Git 已经在里面标记出了双方修改的内容(用 VSCode 等 IDE 将看得更清楚)。

而我们只需要手动维护冲突,将该文件手动加入暂存区,最后再提交,就会生成一个新的结点,该结点无异于直接使用 git merge test 命令得到的。

变基 (Rebase)

一个来回穿插的版本树是有点凌乱的,此时不得不提到第二种合并分支的办法:变基 (rebase) 操作。Rebase 实际上就是取出从分支的提交记录,「复制」它们,然后在主分支逐个的放下去。

Rebase 的优势就是可以创造更线性的提交历史。如果两个分支没有冲突,如上文提到的第一种情况,直接 Merge 会出现一个新的结点(实际上该结点并没有做出实质的修改,反而使版本树变得冗长)。

此时如果我们用 Rebase 操作,则可以简化版本树:

1
2
$ git checkout test  # 切换到 test 分⽀
$ git rebase main # 让 test 分⽀以 main 为基,因此 test 成为 main 的后代

注意,Rebase 操作通常是让从分支变基到主分支,这与 Merge 操作是相反的!

test分支变基到main

观察该图,我们可以发现 test 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。

而提交记录 C2 依然存在(树上那个半透明的节点),而 C2’ 是我们 Rebase 到 main 分支上的 C2 的副本,它们具有不同的 hash 值,可以通过 log 查看。

之后我们也可以通过类似的操作,把 main 快速前移到最新的结点:

1
2
3
$ git checkout main  # 切换到 main 分⽀
$ git rebase test    # 让 main 分⽀以 test 为基,快速前移
# 当然,z也可以用前面的 merge 操作

Rebase 操作在没有冲突时将非常舒适,可以避免没有意义的合并结点(尤其是涉及到远程仓库时),但是一旦发生了冲突,操作将十分繁琐!这里不再赘述,具体工程中如果遇到了请根据 Git 的自动提示逐步操作。

而对于 Rebase 后的从分支上的结点,就变成了所谓的「悬垂结点」,这些结点的访问将十分复杂。此外,如果这个从分支被废弃,我们也可以用以下指令将其删去:

1
$ git branch -d test  # 删除名为 test 的分⽀,用于合并后废弃的分支

分叉 (Branch Diverged)

分叉是分支的一种特殊情况,往往是因为某些「不友好」操作而产生,最终被废弃掉。对于单人操作的仓库,其产生原因可能是:

  • Reset 后旧分支:版本回退后重新提交,这种情况下往往是要弃用旧分支。但如果旧的分支仍有需要保留的更改,则需要 Cherry-Pick 等操作。
  • Rebase 后从分支:上文介绍到,Rebase 将创造更线性的主分支,但这样做的代价是从分支将被废弃,成为一个无用的分叉。

上述情形的发生往往可以人为进行控制,而对于多人操作的仓库,如果不同开发者同时对一个结点进行了更改,将会造成「不可控」的分叉,具体情形及解决方法将在下一节介绍。


Git学习笔记 #2 本地仓库使用
https://hwcoder.top/Git-Note-2
作者
Wei He
发布于
2021年8月31日
许可协议