[基础知识] Git的学习与应用

135 阅读18分钟

相关链接

操作

分支与合并

git merge

在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”
image.png
(main*) > git merge bugFix
image.png

git rebase

Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。

case

  • 准备两个分支;注意当前所在的分支是 bugFix(星号标识的是当前分支)

我们想要把 bugFix 分支里的工作直接移到 main 分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
image.png

  • 在bugFix分支执行**git rebase main**

现在 bugFix 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3' 是我们 Rebase 到 main 分支上的 C3 的副本。
image.png

  • 在main分支执行git rebase bugFix

我们切换到了 main 上。把它 rebase 到 bugFix 分支上。由于 bugFix 继承自 main,所以 Git 只是简单的把 main 分支的引用向前移动了一下而已。
image.png

在提交树上移动

何为HEAD

  • HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
  • HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
  • HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。

image.png

git checkout c4
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log 来查查看提交记录的哈希值。

并且哈希值在真实的 Git 世界中也会更长(译者注:基于 SHA-1,共 40 位)。例如前一关的介绍中的提交记录的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌头都快打结了吧...

比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2 而不是上面的一长串字符。

相对引用

使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:

  • 使用 ^ 向上移动 1 个提交记录
  • 使用 ~ 向上移动多个提交记录,如 ~3

案例

  • 首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。

image.png

  • 执行git checkout main^

image.png

  • 你将 HEAD 作为相对引用的参照。下面咱们就用 HEAD 在提交树中向上移动几次

image.png

  • git checkout C3; git checkout HEAD^; git checkout HEAD^; git checkout HEAD^

image.png

强制修改分支位置

我使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级父提交。

  • 执行git branch -f main HEAD~3

相对引用为我们提供了一种简洁的引用提交记录 C1 的方式, 而 -f 则容许我们将分支强制移动到那个位置。
image.png

撤销变更

在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert。接下来咱们逐个进行讲解。

Git Reset

git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。

image.png

  • git reset HEAD~1。Git 把 main 分支移回到 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了。

image.png

Git Revert

虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert

image.png

  • 执行 git revert HEAD。在我们要撤销的提交记录后面多了一个新提交!这是因为新提交记录 C2' 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。revert 之后就可以把你的更改推送到远程仓库与别人分享啦。

image.png

整理提交记录

git cherry-pick

  • git cherry-pick <提交号>...

  • 可以一些提交复制到当前所在的位置(HEAD)下面

  • 这里有一个仓库, 我们想将 side 分支上的工作复制到 main 分支,你立刻想到了之前学过的 rebase 了吧?但是咱们还是看看 cherry-pick 有什么本领吧。

image.png

  • git cherry-pick C2 C4。这样就将提交记录 C2 和 C4,放到当前分支下了。

image.png

交互式Rebase

  • 如果知道所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了

  • 但是如果你不清楚你想要的提交记录的哈希值呢? 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了。

  • 交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i

  • 如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。

  • 在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。 考虑到课程的初衷,我弄了一个对话框来模拟这些操作

  • 当 rebase UI界面打开时, 你能做3件事:

    • 调整提交记录的顺序(通过鼠标拖放来完成)
    • 删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
    • 合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。

提交的技巧

案例1

你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。
此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。

  • 先用 git rebase -i 将提交重新排序,然后把我们想要修改的提交记录挪到最前
  • 然后用 git commit --amend 来进行一些小修改
  • 接着再用 git rebase -i 来将他们调回原来的顺序
  • 最后我们把 main 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!

当然完成这个任务的方法不止上面提到的一种(我知道你在看 cherry-pick 啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。 最后有必要说明一下目标状态中的那几个' —— 我们把这个提交移动了两次,每移动一次会产生一个 ';而 C2 上多出来的那个是我们在使用了 amend 参数提交时产生的,所以最终结果就是这样了。

也就是说,我在对比结果的时候只会对比提交树的结构,对于 ' 的数量上的不同,并不纳入对比范围内。只要你的 main 分支结构与目标结构相同,我就算你通过。

案例2

正如你在上一案例所见到的,我们可以使用 rebase -i 对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend 修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。下面还是看看 git cherry-pick 是怎么做的吧。
注意: cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)
image.png

  • git checkout main
  • git cherry-pick C2

image.png

  • git commit --amend

image.png

  • git cherry-pick C3

image.png

git tag

多次Rebase

我们准备了很多分支!并将这些分支 rebase 到 main 上。
image.png

  • git rebase main bugFix

image.png

  • git rebase bugFix side

image.png

  • git rebase side another

image.png

  • git rebase another main

选择父提交记录

  • 操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。
  • 操作符 ^: 指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
  • 操作符 ~: 用来指定向上返回几代
  • Git 默认选择合并提交的“第一个”父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。

举个例子

  • 这里有一个合并提交记录。如果不加数字修改符直接检出 main^,会回到第一个父提交记录。(在我们的图示中,第一个父提交记录是指合并提交记录正上方的那个提交记录。)

image.png

  • git checkout main^

image.png

  • 在第一步的基础上,选择另外一个父提交:git checkout main^2

image.png

再举个例子
image.png

  • 执行git branch bugWork HEAD~^2~

image.png

纠缠不清的分支

现在我们的 main 分支是比 one、two 和 three 要多几个提交。出于某种原因,我们需要把 main 分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。

one 需要重新排序并删除 C5,two 仅需要重排排序,而 three 只需要提交一次。

image.png ----> image.png
方法一:

  • git rebase main one
  • git rebase -i C1
  • git rebase main two
  • git rebase -i C1
  • git rebase C2 three

方法二:

  • git checkout one
  • git cherry-pick C4 C5 C2
  • git checkout two
  • git cherry-pick C5 C4 C3 C2
  • git branch -f three C2

与远程分支交互-Git Fetch

Git 远程仓库相当的操作实际可以归纳为两点:

  • 向远程仓库传输数据以及从远程仓库获取数据。
  • 既然我们能与远程仓库同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代码、文件、想法、情书等等)

git fetch 做了些什么

git fetch 完成了仅有的但是很重要的两步:

  • 从远程仓库下载本地仓库中缺失的提交记录
  • 更新远程分支指针(如 o/main)

实例1:
image.png
执行 git fetch 。C2,C3 被下载到了本地仓库,同时远程分支 o/main 也被更新
image.png

git fetch 不会做的事

git fetch 并不会改变你本地仓库的状态。它不会更新你的 main 分支,也不会修改你磁盘上的文件。

理解这一点很重要,因为许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。

所以, 你可以将 git fetch 的理解为单纯的下载操作。

git fetch 参数

  • git fetch origin foo:Git 会到远程仓库的 foo 分支上,然后获取所有本地不存在的提交,放到本地的 o/foo 上。
  • git fetch origin <source>:<destination>:source 现在指的是远程仓库中的位置,而 才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。
    • 如果本地分支不存在source,将会创建立本地分支, 就像是 Git 在 push 时,如果远程仓库中不存在目标分支,会自己在建立一样。
  • git fetch origin :<destination>: 如果 fetch 空 到本地,会在本地创建一个新分支

与远程分支交互-Git Pull

既然我们已经知道了如何用 git fetch 获取远程的数据, 现在我们学习如何将这些变化更新到我们的工作当中。
其实有很多方法的 —— 当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:

  • git cherry-pick o/main
  • git rebase o/main
  • git merge o/main
  • 等等

实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是我们要讲的 git pull。

  • git fetch+git merge

image.png

  • 执行 git fetch; git merge o/main 或者 git pull
  • 我们用 fetch 下载了 C3, 然后通过 git merge o/main 合并了这一提交记录。现在我们的 main 分支包含了远程仓库中的更新(在本例中远程仓库名为 origin)

image.png

  • git fetch+git rebase
    • 执行 git fetch; git rebase o/main 或者 git pull --rebase
    • git push

image.png

git pull 参数

同git fetch

与远程分支交互-Git Push

git push 负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。一旦 git push 完成, 你的朋友们就可以从这个远程仓库下载你分享的成果了!
案例1:
image.png

  • 执行 git push。远程仓库接收了 C2,远程仓库中的 main 分支也被更新到指向 C2 了,我们的远程分支 (o/main) 也同样被更新了。所有的分支都同步了

image.png

git push 参数

在远程跟踪课程中,你已经学到了 Git 是通过当前检出分支的属性来确定远程仓库以及要 push 的目的地的。这是未指定参数时的行为,我们可以为 push 指定参数,语法是:git push <remote> <place>

  • git push origin main。切到本地仓库中的“main”分支,获取所有的提交,再到远程仓库“origin”中找到“main”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。
    • 我们通过“place”参数来告诉 Git 提交记录来自于 main, 要推送到远程仓库中的 main。它实际就是要同步的两个仓库的位置。
  • 当为 git push 指定 place 参数为 main 时,我们同时指定了提交记录的来源和去向。 如果来源和去向分支的名称不同呢?比如你想把本地的 foo 分支推送到远程仓库中的 bar 分支。
    • 要同时为源和目的地指定 的话,只需要用冒号 : 将二者连起来就可以了:git push origin <source>:<destination>
    • 指定的远程分支不存在,将会在远程仓库创建该分支
    • git push origin :<destination>: 如果 push 空 到远程仓库会如何呢?它会删除远程仓库中的分支!

偏离的工作

假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下, git push 就不知道该如何操作了。如果你执行 git push,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,亦或由于你的提交已经过时而直接忽略你的提交?
因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push 变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。

案例1

  • 如下图所示,如果直接执行 git push的话,git会拒绝你的推送请求。

image.png

  • 我们可以使用git rebase
    • 执行 git fetch; git rebase o/mian; git push
    • 我们用 git fetch 更新了本地仓库中的远程分支,然后用 rebase 将我们的工作移动到最新的提交记录下,最后再用 git push 推送到远程仓库。

image.png

  • 我还还可以使用git merge
    • git fetch; git merge o/main; git push
    • 尽管 git merge 不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。
    • 我们用 git fetch 更新了本地仓库中的远程分支,然后合并了新变更到我们的本地分支(为了包含远程仓库的变更),最后我们用 git push 把工作推送到远程仓库。

image.png

远程跟踪

Git 好像知道 main 与 o/main 是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 main 和本地的 main 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:

  • pull 操作时, 提交记录会被先下载到 o/main 上,之后再合并到本地的 main 分支。隐含的合并目标由这个关联确定的。
  • push 操作时, 我们把工作从 main 推到远程仓库中的 main 分支(同时会更新远程分支 o/main) 。这个推送的目的地也是由这种关联确定的!
  • main 和 o/main 的关联关系就是由分支的“remote tracking”属性决定的。main 被设定为跟踪 o/main —— 这意味着为 main 分支指定了推送的目的地以及拉取后合并的目标。

如何指定呢

你可以让任意分支跟踪 o/main, 然后该分支会像 main 分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMain 上执行 git push,将工作推送到远程仓库的 main 分支上。

  • 通过远程分支检出一个新的分支,执行:git checkout -b totallyNotMain o/main。这样就可以创建一个名为 totallyNotMain 的分支,它跟踪远程分支 o/main。
  • 设置远程追踪分支的方法就是使用:git branch -u 命令,执行:git branch -u o/main foo
    • 这样 foo 就会跟踪 o/main 了。如果当前就在 foo 分支上, 还可以省略 foo:git branch -u o/main

常用场景

git忽略文件不提交的三种情形

  • .gitignore文件:从未提交过的文件,从来没有被 Git 记录过的文件
  • git rm --cached Xml/config.xml 已经推送(push)过的文件,想从git远程库中删除,并在以后的提交中忽略,但是却还想在本地保留这个文件。执行该命令。后面的 Xml/config.xml 是要从远程库中删除的文件的路径,支持通配符*。比如,不小心提交到git上的一些log日志文件,想从远程库删除,可以用这个命令。
  • git update-index --assume-unchanged Xml/config.xml 。已经推送(push)过的文件,想在以后的提交时忽略此文件,即使本地已经修改过,而且不删除git远程库中相应文件执行命令。后面的 Xml/config.xml 是要忽略的文件的路径。适用于:git远程库上有一个标准配置文件,然后每个人根据自己的具体情况,修改一份链接信息自用,而且不会将该配置文件提交到库。