【翻译】Git使用 —— 学习 Git 的工作原理

原文链接请点这里


团队博客: CSDN AI小组


1. 前言

Git 是一个常用的去中心化源代码仓库。它由 Linux 之父 Linus Torvalds 创建,用于管理 Linux 内核源代码。GitHub 的整个服务都基于Git。因此,如果您使用Git管理在 Linux 环境中的工程,或者将 IBM 的 DevOps 服务与 Git 结合使用,会帮助你更好地理解 Git。

在我刚开始使用 Git 的时候,我对并发版本系统 (CVS) 和 Apache Subversion (SVN) 已经有一些经验,因此我尝试从那些传统的源代码仓库系统的逻辑来理解它,这种思维方式限制了我对 Git 功能的了解。发现这个问题之后,我通过进一步的使用和学习,对 Git 有了更加深入的理解,所以这篇文章是一篇“个人笔记”,用来记录 Git 是如何工作的,也可以作为初学者的学习资料。本文假设您对其他传统的源代码仓库已经有了一定的了解,例如 CVS 或 SVN。

2. 基础

首先,让我们从传统源代码仓库中的一个基本示例开始,如图 1 所示。在传统源代码仓库中,包含文件和子文件夹的文件夹作为内容处理(CVS 和 Git 实际上并不处理文件夹,而只是处理位于特定路径的文件)。仓库包含工程内容的所有版本,而工作目录是您修改代码的地方。您将代码从仓库检出(checkout)到工作目录,并将您在此工作目录中所做的更改作为一个新版本提交回仓库中。
在这里插入图片描述
图1. 传统源代码仓库工作空间处理逻辑

Git 的每次提交都会创建一个新的子版本内容,该版本来源于修改过的上一个父版本,如图 2 所示。内容存储为一系列版本,也称为快照(snapshots),由提交操作创建的父子关系进行链接。通过提交在父版本和子版本之间发生更改的信息称为更改集。

这一系列版本称为流(stream)或分支(branch)。在SVN中,主分支称为trunk;在 CVS 中,通常称为HEAD;在 Git 中,它通常称为master。在实际的工程项目中,分支用来分离一个特定功能的开发,或着用来保留旧版本。
在这里插入图片描述
图2. 在传统仓库中创建一个新版本

3. Git 的工作原理

一旦你理解了Git 的主要工作原理,就会非常简单。

Git 可以管理版本中的内容,每个提交对应一个版本,并且知道如何在两个版本之间应用或回滚更改集。这是概念很重要,理解应用和回滚变更集的概念会让 Git 更容易理解和使用。

这是最基本的原则,其他一切都由此而来,因此,接下来让我们更深入地探索 Git。

3.1 Git 的使用

首先,列出 Git 中的常用命令:

git init —— 初始化仓库
git checkout <branch> —— 将存储库中的分支检出到工作目录中
git add <file> —— 将文件中的更改添加到更改集中
git commit —— 将工作目录中的更改集提交到仓库中

在使用Git之前,只需要运行 git init 命令,它会将当前目录转换为 一个Git 工作目录,并在 .git 目录下(隐藏目录)创建一个仓库。然后,就可以开始使用 Git 了。checkout 和 commit 命令与其他源代码仓库类似,但在 Git 中还有一个 add 命令,因为 Git 对更改集也很关注,这一点与 SVN 类似,该命令将工作目录中的更改添加到暂存区域为下一次提交做准备。这个暂存区通常称为索引。图 3 说明了创建从版本 A 到版本 B 的变更集的过程。

git status 命令用来跟踪当前所在的分支上已添加以及未添加的更改。

在这里插入图片描述
图3. 在 Git 中创建变更集

git log 命令用于显示工作目录中更改(提交)的历史记录,使用 git log <path> 命令可查看特定路径下的更改。

git status 命令可用于列出工作区中修改的文件以及索引中的文件,同时也可以使用 git diff 命令查看文件之间的差异。使用 git diff(不带参数)仅显示工作目录中尚未添加到索引中的更改,使用 git diff --cached 可用来查看已经添加到索引中的更改(staged change)。git diff <name> 命令用于显示当前工作目录和特定 commit 之间的差异,git diff <name> --<path> 命令相比于上一条命令,增加了特定路径的限制。<name> 可以是 commit ID、分支名称或其他名称。借此,下面将谈论命名的相关知识。

3.2 命名(Naming)

注意:由于 commit ID 的长度过长,本文在图中将使用“(A)”、“(B)”等缩写。

让我们来看一下Git中的命名机制。版本是 Git 中的主要元素,它们以 commit ID 命名,该 ID 是一个哈希 ID,例如“c69e0cc32f3c1c8f2730cade36a8f75dc8e3d480”,它来源于版本的内容,包括实际内容和一些元数据,如提交时间、作者信息等。commit ID 没有像 CVS 中的版本ID有点号,也没有像SVN中的事务编号(transaction number)。所以 Git 无法像在其他仓库中那样从版本名称中确定任何类型的顺序。为方便起见,Git 可以通过从 commit ID 开头取最少字符数将这些长哈希缩写为短名称,这样短名称在存储库中仍然是唯一的。例如,上面示例的短名称是“c69e0cc”。

通常一般不会使用 commit ID ,而是各种分支。在其他源代码仓库中,更改的命名流称为分支。而在 Git 中,更改流是更改集的有序列表,因为它们被一个接一个地被应用,并对应着从一个版本转到下一个版本。Git 中的分支只是一个指向特定版本的命名指针,它指出了使用此分支时对应新变更的位置。当要使用一个分支时,分支标签也会移动到新的提交。

工作空间中变更存放的位置就是 HEAD 指向的地方。HEAD 是上次检出代码到工作空间的地方,也是提交更改的地方。HEAD 通常指向上次检出的分支。注意,这不同于 CVS 将 HEAD 一词解释为默认分支开发的顶部。

tag 命令用来命名一次 commit,并且可以让你使用一个可读性强的名称去处理特定的某次提交,也就是说 tag 是 commit ID 的别名。此外也可以使用一些快捷方式来处理提交,HEAD 作为工作目录中的开发的顶部。HEAD^1 是 HEAD 的第一个父项, HEAD^2 是第二个,依此类推。

更多详细信息,可以参阅 gitrevisions 的主页。因为标签或分支等名称是对 commits 的引用,所以它们被称为 refnames。一个 reflog 显示了在名称的生命周期(从它被创建到当前状态)内发生了什么变化。

3.3 分支(Branching)

分支这个概念暗指每个版本可以有多个孩子。将第二个新的更改集应用到同一个版本会创建一个新的、单独的开发分支。如果它被命名,它被称为分支。
在这里插入图片描述
图4. Git 中的分支结构示例

如图4所示,当前正在开发某些功能的 master 分支指向版本 F。另一个 old 分支标记了一个旧版本,可能是一个潜在的修复开发点。feature 分支是针对特定功能的一些其他更改。变更集被标注为从一个版本到另一个版本,例如“[B->D]”。在这个例子中,版本 B 处有两个分支,一个对应 feature 分支,另一个对应 old 和 master 分支。commit ID B 对应一个别名 tag_fix123。

以下是 Git 中一些其他重要的命令:

git branch <branchname> —— 从当前 HEAD(工作目录)创建一个新分支
git checkout -b <branchname> —— 从当前 HEAD 创建一个新分支,并将工作目录切换到新分支
git diff <branchname> --<path> —— 显示当前工作目录和分支 branchname 之间在路径 path 下的差异
git checkout <branchname> --<path> —— 将分支 branchname 中路径 path 下的文件检出到当前工作目录中
git merge <branchname> —— 将分支 branchname 合并到当前分支
git merge --abort —— 取消存在冲突的合并

git branch <branch name> 命令基于当前 HEAD创建分支,git branch <branch name> <commit id> 命令基于指定的任何有效版本创建分支,这两个命令会在仓库中创建一个新的分支指针。需要注意的是,这两种创建方式并不会自动切换到新的分支上,需要手动切换到新的分支上,不然工作空间还是留在旧的分支上面。不过 git checkout -b <branch name> 命令可以一次性实现上述功能,即在创建新分支的同时,也将工作空间自动切换到新分支。

3.4 合并(Merging)

当开发一个新功能时,需要切换到一个仓库,例如切换到上文提到的 feature 分支上。当新功能开发完成后,需要将其合并回 master 分支,可以通过切换到 master 分支,然后使用 git merge <branch name> 命令将新功能合并到 master 分支。为了实现上述的合并功能,Git 将 feature 分支中的所有变更集应用到 master 分支的顶部。

根据两个分支中更改的类型以及可能发生的冲突,在合并时会存在以下三种可能:

(1) Fast forward merge(快速向前合并)

从新建 feature 分支后,master 分支上没有产生任何的更改。master 分支一直指向 feature 分支创建前的最后一次 commit 的位置。在这种情况下,Git 将 master 分支的指针直接向前移动,如图 5 所示。由于除了向前移动指针之外没有进行其他的操作,故该情形称为 Fast forward merge。
在这里插入图片描述
图5. Fast forward merge

(2) No-conflict merge(无冲突合并)

master 和 feature 两个分支都有产生变更,但两者之间的变更并不冲突。例如,两个分支中的变更修改的是不同的文件。Git 可以自动将来自 feature 分支的所有变更合并到到 master 分支,并创建包含这些变更的新 commit,然后 master 分支向前移动到该 commit 的位置,如图 6 所示。

注意,新合并的 commit 有两个父项,图6中没有标出相应的更改集。原则上,从 (E) 到 (H) 的变更集应该是从 (B) 到 (G) 路径上的所有变更集的集合,这条路径涉及到的变更集过多,故没有在图上明确画出来。
在这里插入图片描述
图6. No-conflict merge(无冲突合并)

(3) Conflicting merge(冲突合并)

master 和 feature 两个分支都有变更,且两者的变更存在冲突。在这种情况下,冲突的详情将保留在工作目录中,用户可以根据冲突详情进行修复和提交,或者使用 git merge –abort 取消合并。

值得一提的事,如果两个分支中都进行了某些相同的变更,这种情况通常会导致冲突,但由于 Git 足够智能,实际上合并时可以检测到两者的变更是相同的,此时相当于进行了一次 Fast forward merge。

回滚(rolling back)和重放变更集(replaying change sets)的概念中包含了 Git 中一些更高级的特性,例如变基(rebasing)和择优挑选(cherry picking)。

有时在 feature 分支上开发了一个新的功能时,master 分支上的开发也在同时进行,这时你还不想合并新功能到 master,随着时间的推移,两个分支之间的差异越来越大。为了避免两个分支差异过大给后续合并带来的一些不必要的麻烦,Git 提供了 rebase 和 cherry-picking 操作,可以将变更集从一个分支应用到另一个分支。

3.5 变基(Rebasing)

rebasing 相关的 Git 命令:

git rebase —— 将当前分支重新定位到给定其他分支的尖端。
git rebase -i —— 交互式变基。

假设你正在 feature 分支上开发一个新的功能,并且需要将 master 分支上最新的变更合并到 feature 分支,以跟上最新的开发进度。上述操作称为变基(rebasing)feature 分支,即重新复位 feature 分支的基底;具体而言,该操作将两个分支之间的起始分岔点在其中一个分支上向上移动,即将其中一个分支(feature 分支)上的变更集放在另一个分支(master 分支)的顶端,如图7的所示,并且为每个分支中原始 commit 创建新的 commit。
在这里插入图片描述
图7. Rebasing a branch(变基分支)

如果上述操作导致了冲突,rebase 会在第一个发生冲突的位置停止,并将冲突状态留在工作目录中供用户修复,然后用户可以继续或中止 rebase。

如果想要将分拆点在 master 分支上向上移动到指定的版本, 可以使用 --onto 选项指定版本ID。

3.6 择优挑选(Cherry picking)

Cherry picking 相关的 Git 命令:

git cherry-pick —— 中止导致冲突的 cherry-pick。

假设你现在正在 feature 分支上开发一个功能,并且已经做一些应该立即放到 master 分支中的更改。这些更改可能是一个错误的修复,或者是一个很酷的新功能,但你现在还不想将整个 feature 分支合并到 master 分支,或这对 feature 分支进行变基。此时,Git 允许你使用则有挑选(Cherry picking)功能将更改集从一个分支复制到另一个分支。

如图 8 所示,Git 只是将 feature 分枝上特定版本的更改集应用到 master 分支的 HEAD 上。git cherry-pick G 中的G通常是commit ID。
在这里插入图片描述
图8. 择优选择一个提交(Cherry picking a commit)

3.7 恢复(Revert)

Revert 相关的 Git 命令:

git revert —— 恢复一个补丁。

revert 命令回滚工作目录上的一个或多个补丁集,然后在当前分支的顶端创建一个新的提交(commit)。revert 几乎可以认为是 cherry pick 的一个反向操作。如图 9中的例子所示。
在这里插入图片描述
图9. 恢复一个提交(Reverting a commit)

revert 命令将恢复操作记录为一个新的提交。如果你不希望将其记录下来,你可以将指针重置为较早的提交,但这超出了本文的范围,感兴趣的小伙棒可以查阅相关资料。

第3节 Git 的工作原理的相关讨论暂告一段落,如此详细地讨论这一节的原因,是因为本节的知识对下一节多人协作(Collaboration)特性的讨论至关重要。事实上,这一节你一旦理解了,那么下一节将会变得很简单。大多数多人协作的功能都是基于上述讨论的基本功能。

4. 多人协作(Collaboration)

Collaboration 涉及到的相关 Git 命令:

git clone —— 将远程仓库“克隆”到本地。
git remote add —— 添加一个名为给定连接 URL的远程仓库 。
git fetch —— 从远程仓库获取远程跟踪分支的变更到本地。
git pull —— 获取远程仓库的变更,并合并到本地仓库。
git push —— 将本地分支的更改通过远程跟踪分支推送到远程仓库。

在传统的源代码仓库中,总是有一个明确的概念,即分支是什么?它存在于中央仓库中。

但是在 Git 中,没有 master 分支这样的东西。等等,我不是在上文写了仓库中通常有一个主分支吗?是的,上文是这样说的。然而实际情况是,这个 master 分支只存在于你的本地。一个仓库中的 master 分支与另一个仓库中的 master 分支之间没有关系,除非你明确创建。
在这里插入图片描述
图10. 两个仓库

如果你已经有了一个仓库,可以使用 git remote add 命令添加远程仓库。然后,可以使用 fetch 命令在自己的存储库中获取远程分支的镜像。这称为远程跟踪分支(remote tracking branch),因为它跟踪的是远程系统上的开发。

当你切换到一个远程跟踪分支(而不是本地分支)时,Git 会自动从远程跟踪分支创建相对应的本地分支,并将其检出。

一旦你有了本地分支,你就可以将远程分支的内容合并到你自己的分支中。图 11 显示了检出到本地 master 分支的情况,除此之外,你也可以像使用正常的合并命令那样,将其合并到具有共同历史记录的任何其他分支中。

在这里插入图片描述
图11. 获取并检出远程分支(Fetching and checking out a remote branch)

另一种方法是使用 git clone 命令获取远程仓库,例如从主机服务(hosting service)获取。这会自动获取所有远程分支(但本地还没有引用)并自动切换到主分支。

正如你所见,此时出现了一种模式。因为远程仓库中的分支“只是一个分支”,所以上面讨论的所有关于分支、合并等的事情几乎都可以无缝地应用在当前这个场景,尤其当从远程仓库获取变更时。
在这里插入图片描述
图12. 获取远程变更(Fetching remote changes)

在图12中,展示了使用 git fetch 命令的例子。它将远程跟踪分支的内容更新到本地分支。然后你只需在远程跟踪分支和本地分支之间进行正常的合并操作,例如在图12的例子中使用命令 git checkout master 和命令 git merge repo1/master。fetch操作之后,你可以在 merge 操作中使用 FETCH_HEAD 这个名字代替 repo1/master 合并 fetch 到的远程跟踪分支的内容,即 git merge FETCH_HEAD 命令。同样的,与3.4节中讨论的类似,此合并可能会导致快进向前合并、无冲突合并或需要手动解决的冲突合并。

获取远程变更更方便的一个命令是 git pull 命令,该命令是 fetch 与 merge 两个命令的组合。

当变更已经 commit 到本地分支时,必须将这些变更传输到远程分支,可以通过 git push 命令将本地变更推送到远程分支。它的反向操作是 fetch ,而不是 pull 。但是,push 操作不仅仅是 fetch 的反向操作,因为它不仅更新远程分支的本地副本,而且还更新了其他仓库中的远程分支,如图 13 所示。push 操作还允许你在远程仓库中创建新的分支。
在这里插入图片描述
图13. 向远程仓库更新变更(Pushing a change)

push 操作有一个保护措施,即只有当 push 操作在远程仓库的分支中触发的合并方式是 fast-forward merge,该操作才会成功,否则会中止。如果不是 fast-forward merge,那么说明远程分支上已经有来自其他仓库或提交者提交的一些更改。Git 中止 push 操作并保持原样。然后您必须先 fetch 远程仓库上的更改,将它们合并到你的本地分支中,最后再次重新尝试 push。

请注意,在这种情况下,您可以进行正常的合并,但也可以选择进行变基合并,将本地分支中的变更变基到本地 fetch 远程内容后的本地分支的顶端。

除了 fetch 和 push 命令之外,还有另一种分发补丁的方式,即通过邮件。首先,使用 git format-patch <start-name> 命令,从给定 commit ID 到当前分支状态的每一个 commit 创建一个补丁文件。然后,使用 git am <mail files> 将这些补丁文件应用到当前分支。

注意事项

如果你尝试直接 push 变更到有其他人也在跟踪的仓库上时,这可能会对分支管理造成混乱,因此 Git 会警告并告诉你,应该首先使用 pull 操作将远程分支的状态同步到本地.

此外,你不应该对远程跟踪分支进行变基操作,这样会导致本地分支与远程分支不再匹配,所以当你进行 push 操作的时候无法触发 fast-forward merge,因为仓库的结构已经被破坏了。

5. 高级 Git(Advanced Git)

在这里插入图片描述
图14. 多存储库结构示例

通常情况下,即使是使用 Git,也会存在一个星型结构,再这个结构中存在一个中央仓库作为主仓库,以及每个用户有自己的本地仓库。但事实并非如此。你可以像在 Web 中一样添加远程仓库的连接,例如使用交叉连接,如图 14 所示。

上文已经将变基描述为在原始分支的不同分支点之上重放(replay)更改集。Git 通常按照提交的顺序进行重放。作为一项高级功能 git rebase -i 可以实际选择应按什么顺序进行哪些提交,甚至可以删除提交或可以合并两个提交(“压缩”)。只要确保你不要对已经推送的提交执行此操作,否则,那些基于这些提交的相关工作可能会发生很多冲突。

我还写了如何检出特定分支,实际上你也可以检出任何的 commit。这会让 HEAD 指针指向提交,而不是分支。这被称为分离 HEAD模式(detached HEAD mode)。当你在这种情况下提交更改时,您就开始了新的开发流。基本上你分支出来,但没有给这个新分支一个分支名称,只有使用提交 ID 才能访问开发的顶端,任何引用名都无法访问它。你可以使用 git branch <branchname> 命令从此 HEAD 创建一个分支。

任何引用都无法访问的 commits 会发生什么?如果你不做任何特殊的操作,它们将直接保存在仓库中。但是,你和主机服务都可以运行 git gc ,该 Git 垃圾收集器可以用来删除没用的文件。任何引用名都无法访问的提交被认为是无用的,因此会被删除掉。因此,始终在真正的分支上工作是一个好习惯,尤其是当在 Git 中能够快速和容易地创建新分支时。

6. 总结

一方面,Git 基于简单的原则,但它提供的灵活性有时会让人难以抗拒。第一个要点,Git 是用来管理版本,以及版本之间的更改集。命令在不同分支之间应用和回滚这些更改集。第二个要点,Git 是处理远程分支与处理本地分支基本相同,因为甚至还有远程分支的本地镜像。

本文提到的命令基本上涵盖了我使用 Git 所做的所有事情。所有命令的更多详细信息可以在相应的手册页中找到,并且借助此处提供的知识,希望你能够更好地理解和使用它们。此外,git status 通常会为下一步做什么提供有价值的提示。

另一个能帮助你理解 Git 的好工具是图形化的 gitk 工具,它显示了本地仓库的结构。用于gitk --all 显示所有分支和标签等。它还提供了一个简单的界面来执行 Git 相关的操作。

Linux 系统上通常已经安装了 Git。您可能还需要从你的包管理器中安装一些开发工具。对于 Windows 系统用户,可以在 Git 主页上进行下载。

我希望现你对 Git 的工作原理已经有了更好的理解,并且不害怕使用它的灵活性。

感谢关于这个主题的一些有趣的讨论,感谢我的同事 Witold Szczeponik,他比我更了解 Git。

原文链接请点这里

猜你喜欢

转载自blog.csdn.net/u010280923/article/details/122638269