Git 实践指南(一)
一、Git 直觉
我们都试过了。我们的 Git 存储库处于不一致和不可调和的状态。我们已经发现了许多关于堆栈溢出的解决方案,并犹豫地粘贴到我们的命令行中。但是在每一次试图回到正常状态后,我们感觉自己离解决 Git 问题越来越远。因此,我们删除本地存储库,克隆存储库,然后重新开始。我知道我不止一次遇到过这种情况。这种情况很普遍,是对 Git 如何工作缺乏直觉的症状。我们倾向于选择阻力最小的路径,用 Git 术语来说,这意味着我们学会了承诺、推动和拉动。在某些情况下,我们学会了用树枝工作,但是如果我们偏离了快乐的道路,我们会变得不舒服。这本书恰恰想避免这一点。我们将建立一个坚实的直觉基础,在此基础上,我们将应用具体的命令和解决方案。这允许我们首先对我们所处的情况进行推理,然后从我们的工具包中选择正确的解决方案。因为我们已经实践过了,所以我们可以放心地应用解决方案。
这本书恰恰想避免这一点。我们将建立一个坚实的直觉基础,在此基础上,我们将应用具体的命令和解决方案。这允许我们首先对我们所处的情况进行推理,然后从我们的工具包中选择正确的解决方案。因为我们已经实践过了,所以我们可以放心地应用解决方案。
这一章将在一个高层次上建立我们的直觉,我们将首先研究直觉如何映射到 Git 命令,以及我们的工作空间和存储库如何反映这些命令。
版本控制
在这一节中,我们将讨论当我们使用 Git 时,我们试图解决什么类型的问题和什么具体的问题。这是我们整个努力的基础和动力。Git 是一个版本控制系统,但是它在我们的日常生活中意味着什么呢?
Git 也称为内容可寻址文件系统。这种东西渗透到 Git 感知世界的整个方式中,并为 Git 能做的事情设定了界限。这意味着 Git 的核心是管理文件。当与 Git 交互时,我们要么管理文件和目录的版本,要么调查工作区的历史。
我们中的许多人都经历过类似图 1-1 的情况,在那里我们有一个工作区,基于一些任意的命名约定,到处复制着一个项目的不同版本。这就是当我们不积极地对软件进行版本控制时的结局。
图 1-1
工作区中命名不明确的文件夹,使得最新版本是什么以及它们之间的关系不明显
这种特别的方法导致了各种各样的困难挑战。这里要指出的重要一点是,这些问题或挑战都不是我们试图解决的问题或我们工作方式所固有的。这些工具是免费提供的;简直就是选择了一种不正当的工作方式。下面列出了直接在文件系统中工作时不可能或不必要的困难:
-
每个文件夹是如何相互关联的?
-
最新版本是什么?
-
两个具体版本有什么区别?
-
两种产品变体的共同基础是什么?
-
如何还原产品变型中的特定变更?
-
这种变化是在什么时间点引入的,由谁引入的?
-
我如何合并这两个文件夹?
在图 1-2 中,我展示了如何将相同的文件夹合并到一个工作空间图中。这允许我们保持对我们的软件如何随时间发展的感觉。
图 1-2
图 1-1 中的文件夹映射到工作区的图表中。这极大地增加了我们对历史的了解
这些问题以及更多的问题是 Git 每天为全球开发者解决的。在我们开始研究我们的第一个 Git 存储库之前,我们需要描述一些基本概念。语言是传达理解的一种强有力的方式,所以我建议你在谈论 Git 的时候尽量学究气。这将有助于你内化这些概念。当你是大师的时候,你想怎么含糊就怎么含糊。
基本 Git 概念
既然我已经提供了这类问题的一个非常基本的概述,我将深入到我们需要建立对 Git 的理解的基本构件中。
仓库
当我们在高层次上谈论 Git 时,我们谈论的是存储库中的协作。许多软件开发人员在 GitHub 或 GitLab 等平台上将他们的代码作为开源共享。每个软件项目由一个或多个存储库代表,这取决于项目背后的组织包含的策略。在许多情况下,存储库代表一个单一的源组件,如软件库或您可以下载并在您的计算机或网站上运行的产品。
对于本书的大部分内容,我们将在一个单一的存储库中工作,并且对于本书的大部分内容,该存储库将是本地的。也就是说,我们不会协作或使用在线平台来同步我们的存储库。
存储库包含有关我们版本化工作空间的所有可用信息。这包括所有的提交,所有的引用,以及存储库的整个历史。
Note
新的 Git 用户,尤其是那些从另一个版本控制系统(比如 ClearCase 或 SVN)迁移进来的用户,担心整个存储库驻留在开发人员的本地 PC 上。他们担心存储库会占用不合理的空间,并且操作会很慢。深入探讨这个话题已经超出了本书的范围。简短的回答是,它不太可能成为大多数工作流的问题,如果它成为一个瓶颈,有工具和策略来处理它。
所有 Git 命令都在存储库的上下文中运行。无论我们是运行简单的命令与本地存储库交互,还是执行更复杂的在线协作命令,都是如此。本书中使用的所有练习都在存储库的环境中运行,您在日常生活中要做的所有工作也是如此。在存储库中开始工作有两种常见的方式。我们可以使用命令git init来初始化一个没有任何历史记录的新的本地存储库,并在那里开始我们的工作。这甚至可以在包含不受版本控制的内容的文件夹中完成。更常见的是,我们使用命令 git clone 来获得一个存储库的副本。存储库的来源通常是私有的(即本地的 Bitbucket)或公共的(即 GitHub cloud)存储库管理器。如果我们使用的是clone命令,我们通常称之为克隆一个存储库,我们称存储库的本地实例为克隆。
本地存储库通过一个名为 remote 的配置与源绑定在一起。除非您使用开源软件,否则您不太可能使用一个以上的遥控器。开源软件通常是用所谓的“基于分叉的”工作流开发的,我们将在后面的章节中介绍。协作通常是通过在本地和远程存储库之间推和拉变更来完成的。如何做到这一点将在后面介绍。
简而言之,存储库是软件项目历史的总和。这些都与元数据一起提交。存储库允许您在本地使用版本控制,并通过远程存储库与其他人协作。
Note
一些商业软件开发在内部使用基于 fork 的工作流。这可能是由于软件工程部门中不同的信任级别或低成熟度造成的。我认为基于 fork 的工作流是一种反模式,或者在这种情况下最多是一种变通方法。基于 Google 的研究表明,感知开发人员生产力的一个关键因素是来自团队外部的源代码的可见性和可用性。
提交
Git 的基本单位是提交。直观地说,Git 提交代表了工作区在给定时间点的完整状态。提交还包含以下元数据:
-
在它之前有什么承诺
-
作者和委托人
-
时间戳
-
提交消息,包含提交内容的信息
Caution
新的提交绝不会无缘无故地被创建。它们的创建由用户发起。这可能会给新用户带来一些挫折,他们不明白为什么在共享存储库中看不到他们的变更。经常发生的情况是,用户试图共享他们所有的新代码,但是没有创建新的提交,共享存储库已经是最新的,没有最新的更改。在分享之前,确保你提交了。
前一次提交被称为父提交。我们可以看到,我们创建了一个提交图,通过提交中的父指针连接在一起。提交可以有零个、一个或多个父级。
最常见的情况是只有一个父提交。当我们沿着一个单一的品系或链条前进时——创造一个又一个版本——就会发生这种情况。
存储库中的第一次提交是特殊的,因为它没有父提交。这是有意义的,因为在第一次提交之前什么都没有发生。第一次提交通常也称为初始提交。许多存储库的第一次提交具有消息“初始提交”,指示它是版本化历史的开始。如果我们看到大量的首次提交,这通常是开发人员在考虑版本控制之前做了太多工作的迹象。这是不好的,因为版本控制不应该是事后的想法。但是你在这里,所以你当然不会再在这种情况下结束。
一个提交也可以有任意多的父级。当分支合并时,提交以多个父代结束。我们稍后会谈到这一点,所以现在不用担心。我说提交可以有任意多的父类,这是真的。Linux 内核是一个有趣的地方,可以看到 Git 已经习惯了它的极限。Linux 和 Git 的发明者 Linus Torvalds 对章鱼合并有着臭名昭著的喜爱,在章鱼合并中许多分支被一次合并。这个工作流程显然适用于 Linux 内核和其他开源项目,但是我的建议是,如果您最终合并了两个以上的分支,那么您很可能做错了。
简而言之,Git commit 是一个表示工作空间的包,我们可以在任何时间点以闪电般的速度检索和研究它。
树枝和标签
Git 有两种类型的东西,对象和引用。我们之前描述的提交是不可变的,并且属于称为对象的类别。另一类有用的东西叫做引用,它要轻量级得多。
现在,我将介绍两种类型的引用,分支和标记。两者都指向我们使用提交构建的图中的特定提交,如前所述。
标签
标签是 Git 中最简单的引用。它只是一个指向提交的指针。标记的提交用途是用以具体版本命名的标记来标记发布的提交。
在图 1-3 中,我们看到一个带有标签的提交;这允许我们引用这个提交,而不使用它的 sha。
图 1-3
一个标签指向提交
标签永远不会改变。这意味着我们在任何时候都可以通过一个名字返回到一个提交。讨论“1.0 版”发生的事情比一个龙沙更容易理解是怎么回事。
树枝
以我的经验来看,分支给开发人员带来了巨大的力量和挫折。然而,这种沮丧是没有理由的。没有经过适当的教育就简单地使用 Git,往往会缺乏心智模型。可视化 Git 图和分支是所有人都可以使用的强大动力。
一个分支就像一个标签,除了分支应该是移动的。当我们进行开发和创建提交时,当前活动的分支向前移动并指向我们创建的新提交。当前活动的分支也被称为我们已检出的分支。
Note
虽然不一定要签出一个分支,但是这样做被认为是最佳实践,而且我认为在正常开发的所有情况下,您都要签出一个分支。当你已经检查出一个分支之外的东西时,你将最终处于所谓的超脱头状态,这听起来比它更危险。我们稍后将讨论如何结束这种状态以及如何安全地恢复。
在 Git 中,分支是非常轻量级的。它们的重量不超过 41 字节。这些字节代表一个提交 sha 和一个换行符。这意味着拥有许多分支的主要成本不是技术成本,而是受限于工程师在 Git 存储库中拥有许多指针所带来的认知开销。
在图 1-4 中,我们可以看到当我们创建多个提交时,当前活动的分支是如何移动的。我们将在第四章中讲述 Git 如何知道当前活动的分支是什么。
图 1-4
Git 分支随着更多提交的创建而移动
Git 使用名为 master 的分支作为默认分支。这意味着我们期望大师分支是真理的主要来源,也是最重要的分支。在其他版本控制系统中,这可以称为主干或主线。有一些约定期望默认分支被命名为 master,我强烈建议您不要将您的单一真理源分支命名为 master 以外的名称。这样做后果自负。解决任何实际问题的可能性极小。
在 Git 中,我们可以有许多分支,但是建议我们有少量的长期或永久分支。以我的经验来看,需要一个以上的永久分支机构是人为的和被曲解的。拥有许多分支通常会导致出现过于复杂的工作流,从而在开发过程中产生开销。复杂的工作流经常被引入,以创建更高质量、更安全的集成,以及类似的其他东西,但通常,它们最多是处理软件工作方式中更深层次问题的症状。
设置 Git 和 Git 表格
既然我们已经介绍了基础词汇,我们要确保一切都设置好了。然后,我们可以深入实际的 Git 库,在那里进行一些有意识的练习。我倾向于把所有东西都放在 Git 存储库中,所以你不会惊讶我们在本书中使用的练习是以 Git 存储库的形式交付给你的。
这就是为什么我们现在将介绍我们的第一个命令。如前所述,Git 是一个分布式版本控制系统。在您的日常生活中,您很可能会在云中托管的存储库或许多存储库管理器中的一个上进行协作。我们使用 Git clone 命令来获取存储库的本地副本,以便在其中工作。
Git 克隆
克隆是一个两步命令:首先,它下载 Git 存储库,然后,它将存储库的默认分支上最近的提交检出到工作区。
首先,这使您能够更改文件、编译代码和运行测试——所有这些任务都是您通常在带有源代码的工作空间中执行的。其次,当您在克隆期间下载整个存储库时,您可以比较代码的不同修订,并在存储库中以本地速度执行所有可能的版本控制命令。
克隆命令有许多变体,但就其基本形式而言,看起来是这样的:git clone <remote-repository> <local-path>。git clone https://github.com/eficode-academy/git-katas.git git-katas就是一个例子。这将把包含 git katas 的存储库下载到文件夹git-katas/.git/中,并将master分支上最新提交的工作区签出到文件夹git-katas中。
那个。git 文件夹
我写这本书的一个目标是让 Git 变得不可思议,变成一个你可以使用的令人敬畏的工具。Git 的一个比许多人觉得烦恼的部分是。git 文件夹。虽然它感觉像一个神奇的文件夹出现在你的工作区,但它却是一个抽象世界的理智之源。
我们将不会深入到许多事情的细节。git 文件夹,但是出于直观的目的,让它包含以下内容就足够了
-
存储库的整个历史,包括数据
-
本地配置
-
指向当前已签出内容的指针
-
指向克隆来源的指针
这决不是一个完整的列表,但是它强调了非常重要的一点:当您克隆一个存储库时,您会在您的本地计算机上获得整个存储库。有很多方法可以获得一个较小的存储库子集,但是假设您获得了完整的存储库,并且它不会导致性能问题或不必要的空间使用。相反,它允许您脱机、异步工作,并以本地系统的速度工作。
随着克隆命令的引入,我们将进入第一个练习,并下载我们将使用的练习。
SETTING UP GIT AND THE KATAS
是时候使用命令行了。我将展示在 Windows 中通过 Git Bash 执行的所有命令。这个命令行环境是随 Windows 一起安装的 Git 附带的,并且与 Linux 和 Mac 上的常见 shells 兼容,因此无论您选择什么平台,您都应该能够识别所有内容。一些用户报告了使用 zsh 命令行的问题。如果您遇到这种情况,请运行 bash 中的练习。
检查 Git 正在工作
首先,我们将打开一个命令行并运行git --version命令来检查一切是否按预期运行。
-
打开您喜欢的命令提示符。
-
在任何位置执行命令
git --version。 -
您应该会在命令行中看到正在运行的 Git 版本的输出。
预期的结果应该如下所示:
$ git --version
git version 2.25.0.windows.1
我们得到的是 Git 的安装版本。我的情况是版本2.25.0.windows.1。在撰写本文时,这是 Git 的最新版本。我建议你更新到这个版本。Git 中发布了许多好东西,包括性能和 UX 智慧。所以要跟上版本。
检索 Git 卡塔
我们将要用来练习我们正在介绍的概念的练习叫做 Git katas,可以通过 GitHub 获得。
在我们开始使用 Git 进行具体练习之前,这个练习将带您完成克隆 Git katas 存储库的过程,并检查您是否有完整的练习集。如果您对基本的 shell 命令感到不舒服,那么现在正是阅读这些命令的好时机。
-
启动命令行:打开您喜欢的终端,准备在其中执行命令。
-
导航到您希望存储源代码的位置。我更喜欢将我的文件存储在
~/repos/organization/repo中。 -
使用 Clone 命令克隆 Git 卡塔:
git clone``https://github.com/eficode-academy/git-katas.git -
cd到git-katas文件夹,使用ls查看练习列表。
如果我运行上述命令,看起来如下所示:
$ cd ~/repos/randomsort
$ git clone https://github.com/eficode-academy/git-katas.git git-katas
Cloning into 'git-katas'...
remote: Enumerating objects: 34, done.
remote: Counting objects: 100% (34/34), done.
remote: Compressing objects: 100% (31/31), done.
remote: Total 1690 (delta 16), reused 7 (delta 3), pack-reused 1656
Receiving objects: 100% (1690/1690), 486.60 KiB | 1.72 MiB/s, done.
Resolving deltas: 100% (708/708), done.
$ cd git-katas
$ ls
3-way-merge/ basic-staging/ ff-merge/ merge-driver/ rebase-interactive-autosquash/ test.ps1
advanced-rebase-interactive/ basic-stashing/ git-attributes/ merge-mergesort/ reorder-the-history/ test.sh
amend/ bisect/ git-tag/ objects/ reset/ utils/
bad-commit/ commit-on-wrong-branch/ ignore/ Overview.md reverted-merge/
basic-branching/ commit-on-wrong-branch-2/ img/ pre-push/ save-my-commit/
basic-cleaning/ configure-git/ investigation/ README.md SHELL-BASICS.md
basic-commits/ detached-head/ LICENSE.txt rebase-branch/ squashing/
basic-revert/ docs/ merge-conflict/ rebase-exec/ submodules/
$
在这个小例子中有很多事情在发生。首先,我们从 clone 命令中得到很多输出,但幸运的是,我们可以忽略它,除非我们试图调试某些东西。其次,有两件事我们可以忽略,大多数人通常会忽略。我们经常会忽略。git 远程存储库的一部分,并让 Git 和存储库管理器对其进行分类。许多人忽略了命令的最后一部分。这将把存储库克隆到与存储库同名的文件夹中,如下例所示:
$ git clone https://github.com/eficode-academy/git-katas
Cloning into 'git-katas'...
remote: Enumerating objects: 34, done.
remote: Counting objects: 100% (34/34), done.
remote: Compressing objects: 100% (31/31), done.
remote: Total 1690 (delta 16), reused 7 (delta 3), pack-reused 1656
Receiving objects: 100% (1690/1690), 486.60 KiB | 1.72 MiB/s, done.
Resolving deltas: 100% (708/708), done.
$ ls git-katas
3-way-merge/ basic-staging/ ff-merge/ merge-driver/ rebase-interactive-autosquash/ test.ps1
advanced-rebase-interactive/ basic-stashing/ git-attributes/ merge-mergesort/ reorder-the-history/ test.sh
amend/ bisect/ git-tag/ objects/ reset/ utils/
bad-commit/ commit-on-wrong-branch/ ignore/ Overview.md reverted-merge/
basic-branching/ commit-on-wrong-branch-2/ img/ pre-push/ save-my-commit/
basic-cleaning/ configure-git/ investigation/ README.md SHELL-BASICS.md
basic-commits/ detached-head/ LICENSE.txt rebase-branch/ squashing/
basic-revert/ docs/ merge-conflict/ rebase-exec/ submodules/
正如您在前面的数据中看到的,在大多数情况下,这足以获得想要的结果。这将我们需要记住的命令减少到了git clone <repository>。
故障排除:如果您正在键入克隆命令,并得到一个“权限被拒绝”的错误,这可能是因为您拼错了 URL。尝试复制粘贴命令,看看它是否有效。
现在你已经完成了我们的第一个练习,你已经确保你已经安装了 Git 并且正在运行,你也已经下载了 Git 表单,所以我们可以在本书的其余部分使用它们。
在仓库里找到我们的方向
现在,我们到了重要的部分:在 Git 存储库中工作。
在这一部分中,当我们与 Git 交互时,我们将使用几个 Git 命令来查看存储库内部,并使用几个 shell 命令来导航工作区。
我们将介绍命令:状态、日志和检验。
我们将深入讨论状态,但是日志和签出都是很大的命令,我们将在本书的过程中逐步介绍。
Git 状态
当我教 Git 的时候,我总是告诉我的学生:“如果你对将要发生的事情或者你应该做什么有疑问,只要运行git statusGit 就会告诉你。”虽然这有点夸张,但所有查询都应该以git status开头。
Git status 告诉您工作区的状态,以及它与当前签出的内容相比如何。如果工作区与检出的工作区相同,那么这个工作区就是干净的。如果工作区包含任何种类的变更,那么这个工作区就被称为脏的。可以修改、删除、添加或重命名文件。Git 还有一个被忽略路径的概念,从 Git 的角度来看,被忽略路径的变化不会使工作空间变脏。文件如何经历这些状态如图 1-5 所示。
图 1-5
文件可能处于的不同状态以及在这些状态之间转换的操作
Git 日志
当我们处于版本控制的领域中时,能够查看存在什么版本以及它们如何相互关联是一个重要的特性。Git 日志是我们查看存储库历史的最基本的方式。虽然 Git log 是一个基本命令,但它也是最容易配置的命令之一,而且标志和参数的数量可能会令人望而生畏。不要绝望,我会引导你自信地使用 log。
如果您查看下面的清单,您会看到一个没有任何命令和标志的 log 运行,配置了默认的 Git 2.25 安装。在这个存储库中,只有几个提交,在一个分支上,和一个标签。提交是按时间倒序进行的,这意味着最新的提交将首先打印,然后跟随每次提交的父指针,直到我们到达第一次提交。
在这个视图中,每个提交都包含大量信息。这里的所有数据都是在每次提交时打印的,这种行为对于大多数用途来说往往过于冗长。我们还可以看到它们指向的引用和提交。
$ git log
commit 335e019ac148297bd938f137ea9c7cf617c07576 (HEAD -> master, origin/master, origin/HEAD)
Author: Johan Sigfred Abildskov <randomsort@gmail.com>
Date: Thu Feb 13 11:10:55 2020 +0100
Clean up unused trainer-notes.md
commit c1514f22ebb31280d26b3062134a7066c59df737
Author: Alex Blanc <test@example.com>
Date: Sat Oct 19 17:38:32 2019 +0200
Add kata Rebase Interactive with autosquash
commit 032a8fcdef22a53f123f914a8b7b2d7d87cdd2e7
Author: Johan Abildskov <randomsort@gmail.com>
Date: Mon Feb 10 14:35:27 2020 +0100
Fix typos in submodule README
如前所述,log 命令的冗长对于获得一个概述来说不是很有用,所以在接下来的几个清单中,我们将配置我们的 log 命令,使其更加简洁。首先,我们将对与前面相同的日志命令使用标志- oneline,以获得更精简的日志视图。整个命令变成了git log --oneline,得到的输出可以在下面的清单中看到(下一个清单!).oneline 标志将提交消息截断为主题,并将 sha 截断为较短的前缀。这使得它更容易得到一个概述。
$ git log --oneline
335e019 (HEAD -> master, origin/master, origin/HEAD) Clean up unused trainer-notes.md
c1514f2 Add kata Rebase Interactive with autosquash
032a8fc Fix typos in submodule README
262c478 Fix three typos
1e07423 Expand on submodules kata
1ef8902 Use explicit numbering
dbfccc8 Added pointer to Overview also as Learning Path
1848caf Reordered katas on Overview and added missing ones
如果我们遗漏了前面视图中的引用,这可能是由于 Git 的一个过时版本。在这种情况下,我们可以使用标志--decorate让 Git 用相关的指针注释提交。我们的命令就变成了git log --oneline --decorate。在新版 Git 中,修饰是默认的--oneline行为。使用标志--no-decoration可以模拟一个旧版本 Git 的例子。
$ git log --oneline
335e019 Clean up unused trainer-notes.md
c1514f2 Add kata Rebase Interactive with autosquash
032a8fc Fix typos in submodule README
262c478 Fix three typos
1e07423 Expand on submodules kata
1ef8902 Use explicit numbering
dbfccc8 Added pointer to Overview also as Learning Path
1848caf Reordered katas on Overview and added missing ones
只要我们只使用一个分支,因此有一个线性的历史,没有不同的开发线程,这对我们来说就足够了。当我们在第三章中研究更复杂的历史时,我们会在日志命令中添加更多的工具。然而,有一个标志对于限制我们在日志输出中获得的提交数量非常有用。我们可以使用标志-n <number>将日志输出中的条目数量限制为<数量>。对于小数字,我们可以使用文字数字作为标志。例如,git log -3 将只输出三次提交。在清单 6 中,我们用标志--oneline、--decorate和-n 2运行。
335e019 (HEAD -> master, origin/master, origin/HEAD) Clean up unused trainer-notes.md
c1514f2 Add kata Rebase Interactive with autosquash
Note
您可以使用 gitk 代替 git log 来获得更漂亮的基于 GUI 的输出。有些人更喜欢这样,很少有人知道不使用 Sourcetree 或 Git kraken 之类的成熟 GUI Git 客户端也能获得这样的概览。你可以不带参数的使用。
摘要
在本章中,我们介绍了我们正在处理的基本问题空间,即维护和关联文件和目录集合的多个版本。我们还确保安装了 Git,并使用命令git clone下载了 Git katas。然后,我们使用git log简要地查看了一个小型存储库的历史。
二、构建提交
在本章中,我们将详细讨论提交。提交是我们历史的基本组成部分,既包含我们版本的实际内容,也包含定义我们历史的父指针。有意地形成提交并附上格式良好的提交消息是在协作环境中成为有价值的个人贡献者所需的基本技能。
工作区里有什么
在其核心,Git 是关于文件和目录。在软件开发中,我们存储项目特定文件和代码的地方通常称为工作区。当我提到我们的工作空间时,我指的是我们项目的根文件夹,包含构成项目的文件和目录,以及。git 文件夹。在下面的代码片段中,我们看到了一个同时在 shell 和 Windows 资源管理器中列出的目录。请注意。包含 git 存储库的 Git 文件夹是隐藏的。隐藏以.开头的文件和文件夹是一种常见的惯例。
$ ls
img/ index.js library.js README.md
如前所述,与存储库上当前签出的提交相比,我们的工作空间可以是脏的,也可以是干净的。脏不是一个不好的词;它只是意味着不同。这种差异当然是一件好事,因为这些是我们已经做出的改变,只是还没有实施。另一个可能使我们的工作空间变脏的来源是自动生成的文件和构建工件。我们将在后面介绍如何让 Git 忽略某些路径。
我们可以把我们的工作区看作总是由一个提交和一个应用于其上的变更集或差异来表示。图 2-1 显示了对象在一个干净的工作区和存储库中是如何相同的。
图 2-2
在存储库顶部更改文件 A 后的脏工作区
图 2-1
存储库顶部的干净工作区
因为工作区和存储库的内容是相同的,所以工作区被认为是干净的。在图 2-2 中,我们可以看到当我们改变文件 a 时,脏工作区是如何与存储库相关联的。
我们可以使用 Git status 命令看到这一点。当我们更改存储库中的文件并运行 git status 命令时,我们可以看到 git 如何告诉我们已经被 Git 跟踪的文件被修改或删除,或者我们第一次添加到工作区的文件如何在 Git 状态中显示为未被跟踪。
$ ls
A B C D
$ git status
On branch master
nothing to commit, working tree clean
$ echo testing > A
$ git status
On branch master
nothing to commit, working tree clean
$ git add A
$ git status
On branch master
nothing to commit, working tree clean
$ git commit -m 'Edit A'
On branch master
nothing to commit, working tree clean
$ git status
On branch master
nothing to commit, working tree clean
$ rm B
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: B
no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -am 'Remove B'
[master db1f9c6] Remove B
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 B
$ git status
On branch master
nothing to commit, working tree clean
$ touch D
$ git status
On branch master
nothing to commit, working tree clean
$ git add D
$ git status
On branch master
nothing to commit, working tree clean
$ git commit -m 'Add D'
On branch master
nothing to commit, working tree clean
$ git status
On branch master
nothing to commit, working tree clean
使用阶段准备提交
在 Git 中,文件可以表示在三个不同的位置,其中两个我们已经讨论过了:工作区和存储库。在这两者之间还有第三个区域,这个区域被称为指数或阶段。这一阶段的逻辑是,我们在这里设计下一步要提交的内容。工作流程如下:我们进行一些更改,我们准备我们希望成为下一次提交的一部分的更改,最后我们提交。冲洗并重复。这个流程可以在图 2-3 中看到。
图 2-3
本地存储库中的流程
这个流程是普通软件开发流程的简化视图,但是希望它是可识别的。我听过的对舞台最直观的描述是,我们想象自己是家庭聚会上的摄影师,我们在指导人们谁应该去拍下一张照片,当我们完成后,我们就拍照。然后,我们重复。以类似的方式,我们使用命令add向舞台添加一个路径。此操作也称为转移文件。
Note
当我们存放一个文件或目录时,我们不仅仅存放一个路径,我们告诉 Git 在下一次提交中包含这个路径。我们在运行git add命令时暂存内容。这意味着,如果我们在转移文件后更改它,我们需要再次转移它,以便在下一次提交时包含它。
正如我们在图 2-3 中看到的,从 Git 的角度来看,一个文件可以有几种不同的状态。下面的列表只省略了我们将在本章后面讨论的最后一个状态:
-
未修改的:这个文件在工作区和存储库中当前签出的提交中是相同的。
-
修改过的:这个文件同时存在于工作区和存储库中,但是不同。
-
Staged :该文件位于工作区、当前提交和 stage 中。请注意,该文件在所有三个位置都可以不同。
-
未跟踪:该文件在工作区中,但不在当前提交中。
MANIPULATING THE STAGE
在本练习中,我们将通过一些步骤来操作转移,以及在更改、转移和取消转移文件时会发生什么。
首先,让我们使用调查命令的基本工具包来看看我们的存储库中的内容。
首先,我们运行命令 pwd 来查看我们所在的路径,然后运行 ls 来查看目录包含的内容。
$ pwd
/c/Users/rando/repos/randomsort/practical-git/chapter2/stage-repo
$ ls
file1.txt subfolder/
$ ls -al
total 9
drwxr-xr-x 1 rando 197609 0 mar 13 12:18 ./
drwxr-xr-x 1 rando 197609 0 mar 13 12:18 ../
drwxr-xr-x 1 rando 197609 0 mar 13 12:19 .git/
-rw-r--r-- 1 rando 197609 14 mar 13 12:18 file1.txt
drwxr-xr-x 1 rando 197609 0 mar 13 12:18 subfolder/
请注意。git 文件夹在我们使用标志-a 之前不会显示,这是因为默认情况下ls不会显示隐藏的文件夹,正如前面提到的,惯例是文件和文件夹以句点(.)被认为是隐藏的。除此之外,我们可以看到我们有几个文件和一个目录。
现在我们已经对文件系统有了基本的了解,我们使用几个基本的 git 命令来控制存储库。我们使用 git status 来询问 git 我们的工作空间相对于我们的存储库的状态。我们得到了更多的信息,我们将忽略这些信息,直到下一章。
$ git status
On branch master
nothing to commit, working tree clean
$ git log --oneline --decorate
1cc4f2e (HEAD -> master) Initial commit
我们从 git 状态中得到的首先是确认我们确实在 Git 存储库中工作。我们还了解到,我们的工作空间是干净的,没有任何东西是精心布置的。从 git 日志中,我们了解到我们有一个历史非常短的存储库。
在接下来的步骤中,我们将对工作区进行更改,并在此过程中存放文件。我们将继续使用 Git status 来查看我们的操作如何反映在文件的状态中。记住,我们从一个干净的工作空间开始。
$ echo "test" > file1.txt
$ git status
On branch master
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: file1.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ git add file1.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
$ echo thing > file1.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
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: file1.txt
$ echo content > file_that_did_not_exist_before.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
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: file1.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
file_that_did_not_exist_before.txt
$ git add file
file_that_did_not_exist_before.txt file1.txt
$ git add file
file_that_did_not_exist_before.txt file1.txt
$ git add file_that_did_not_exist_before.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
new file: file_that_did_not_exist_before.txt
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: file1.txt
$ echo content > subfolder/subfile1.txt
$ echo content > subfolder/subfile2.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
new file: file_that_did_not_exist_before.txt
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: file1.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
subfolder/subfile1.txt
subfolder/subfile2.txt
$ git add subfolder/
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file1.txt
new file: file_that_did_not_exist_before.txt
new file: subfolder/subfile1.txt
new file: subfolder/subfile2.txt
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: file1.txt
$ git restore --staged file1.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: file_that_did_not_exist_before.txt
new file: subfolder/subfile1.txt
new file: subfolder/subfile2.txt
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: file1.txt
$ git commit -m "our first commit"
[master de09faa] our first commit
3 files changed, 3 insertions(+)
create mode 100644 file_that_did_not_exist_before.txt
create mode 100644 subfolder/subfile1.txt
create mode 100644 subfolder/subfile2.txt
$ git status
On branch master
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: file1.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ git log --oneline --decorate
de09faa (HEAD -> master) our first commit
1cc4f2e Initial commit
在前面的列表中,我们看到一些值得注意的项目。
-
当我们在转移 file1.txt 后对其进行更改时,它将同时被修改和转移。
-
我们可以在同一个阶段修改和添加文件。
-
当我们暂存一个目录时,所有子路径都会被暂存。
-
我们可以使用 git restore - staged 撤销路径的暂存。
犯罪
在上一节中,我们花了很多精力讨论如何控制提交的内容。在这一节中,我们将介绍如何进行实际的持久化,将阶段的内容持久化到我们称为提交的包中的存储库中。
去吧,Committee
为了创建提交,我们使用命令git commit。图 2-4 显示了提交流程的不同步骤。它假设我们已经在舞台上添加了一些变化。接下来发生的是用命令行标志-m传递的消息,变更集和一些自动生成的内容被保存在提交对象中。之后,当前签出的分支被更新以指向这个新创建的提交。
图 2-4
这是我们运行命令git commit时发生的情况,假设文件是暂存的
如前所述,我们主动控制提交的两个部分,而其余部分由 Git 自动处理。我们使用 stage 定义了提交的文件内容,同时我们控制在创建提交时将什么消息附加到提交。
指定提交消息的最常见方式是使用标志-m,并在命令中直接传递提交消息。这种方法的优点是提交消息很可能是简短的,并且我们在创建提交的过程中没有额外的步骤。这有几个缺点;稍后当我们讨论什么是好的提交消息时,我们将会谈到这些。命令行中最基本的提交流程如下所示:
$ git add file.txt
$ git commit -m “Add file.txt”
$ git log
commit 2a99799c0b9727dc22ae8a790d3978ac40273960 (HEAD -> master)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Fri Mar 13 13:17:41 2020 +0100
我们在前面的示例中看到的是,我们添加到阶段的变更集成为我们使用git commit命令创建的下一个提交的一部分。我们作为参数传递的消息显示在日志中。这很有用,因为我们可以给每个提交一个标题,给出变更集的原因。许多开发人员还使用消息来引用外部问题,这些问题在吉拉、GitLab 或 Azure DevOps Boards 等工具中维护。
如果我们在commit命令中省略了-m标志,Git 将打开一个文本编辑器,您可以在其中设计提交消息。这也为使用提交消息的主题和主体打开了大门。在大多数公司中,只使用 subject,因为附加信息是在版本控制之外保存的。在这些情况下,将任何问题引用放在正文中,不要放在主题中。这允许一个更干净的提交头,它简明地描述了任何 Git 提交引入的更改。
在开源工作流中,提交消息体更常用于添加更多关于变更集的文档。该信息不应与适当文件中的内容重复,如阅读材料、生成的文件、用户说明或类似文件。相反,它应该包含与具体变更集相关的补充信息。以下项目列表是此类文档中可能包含的内容的一些示例:
-
引入这个变更集的原因是
-
任何架构决策
-
设计选择
-
代码中不一定显而易见的权衡
-
可以考虑的替代解决方案的描述
-
变更集内容的更详细描述
在清单 2-1 中,我从 Git 核心源代码库中抓取了一条提交消息。这显示了使用提交消息的主题和正文的提交消息的示例。
mm, treewide: rename kzfree() to kfree_sensitive()
Listing 2-1An example commit message with the subject giving a high-level description of the feature and the body verbosely listing the content
正如莱纳斯所说:
对称命名只有在暗示使用中的对称性时才有帮助。
否则就是主动误导。
在“kzalloc()”中,z 是有意义的,是
来电者想要。
在“kzfree()”中,z 是有害的,因为可能在
将来我们真的可能想要使用“memfill(0xdeadbeef)”或
一些东西。界面的“零”部分甚至是不相关的。
kzfree()存在的主要原因是清除敏感信息
这不应该泄露给相同存储器的其他未来用户
物体。
在前面的代码中,我们看到了当我们在提交消息中都有一个 subject 和一个 body 时,它会是什么样子——在下面的代码中,我将展示当我们在命令行中没有指定提交消息的情况下执行创建提交的步骤时,它会是什么样子。
$ git commit
hint: Waiting for your editor to close the file...
[master 1a41582] Commit message from editor
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file2
$ git log -n 1
commit 1a41582591bedad5757914acf3fc8be562e468a4 (HEAD -> master)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Fri Mar 13 13:19:57 2020 +0100
Commit message from editor
This part of the message is written as a part of the message body.
Notice that there is a newline between the header and the subject.
但是前面的代码中缺少了一个步骤,很难在书中展示出来。图 2-5 显示了 Git 在我的系统上打开的编辑器的截图,其中有一个预填充的提交消息,我们可以根据自己的喜好进行更改。注意,如果我们保存并退出编辑器时只有空行和注释,提交过程将被中止。
图 2-5
命令行中未指定消息时提交消息编辑器的默认视图
开箱即用,Git 将默认您的 shell 正在使用的任何编辑器,在环境变量编辑器或 VISUAL 中定义。如果 Git 不能决定使用哪个编辑器,它将会退回到 vi,这让许多 Windows 用户感到沮丧。前面描述的行为可以用配置 core.editor 覆盖。我们将在后面介绍配置,但是请注意,您可以选择您最喜欢的编辑器。不一定所有的编辑器都支持这样使用,或者可以由 Git 运行,而不需要启动特定的配置。大多数常用的编辑器都是兼容的,或者可以很容易地配置为正常工作。这比谷歌搜索导致堆栈溢出差不了多少。
好的、坏的和难看的提交消息
正如我们前面提到的,提交是不可变的。这也意味着提交消息是永久的,因此在编写它们时投入一些思考以最大化消息对我们的合作者和未来的自己的价值是很重要的。有一种说法是,计算机科学中的一个难题是给事物命名。我想我们可以一起写提交消息。这很难,但很重要。
在这一节中,我们将介绍编写好的提交消息的一些基本规则,一些需要避免的事情,并给出好的和坏的提交消息的例子。
在深入细节之前,我想强调有用的提交消息的一个关键因素是存储库中合作者之间的一致性。在我看来,提交消息具有相同的语义和外观比它们被客观地优化编写要重要得多。能够浏览提交列表并且能够感受到存储库中正在发生的事情是非常强大的。有些人还将集成添加到他们的 ide 中,用最近更改代码行的提交的提交消息主题来修饰每个代码行。如果提交消息具有相同的基本形状,这种工作方式会更加强大。我们为此使用的 git 特性不太敏感地称为 Git 责备。它让我们能够指出是谁引起了特定的变化。虽然这很有用,但是责备这个词的内涵并不是很有建设性,也不利于健康的文化。我们将在稍后讨论 git 责备。
主题或标题
从图 2-6 中可以看出,无论从哪个角度看,提交消息主题的位置都很突出。在日志中,它是我们与提交相关联的内容;这是通过它们描述的事件来显示我们代码库的脉动。在存储库管理器中,通常显示的文件和目录用最后修改它们的提交来注释。有些人甚至在编辑器中逐行添加这些提交消息。例如,这可以在 Visual Studio 代码中使用 Git lens 来完成。
图 2-6
通过 GitHub 接口提交消息头
当大多数人讨论提交消息时,他们仅仅是指主题。有几类好的提交消息,如前所述,代码库的各个贡献者在策略上保持一致是很重要的。
我将介绍的第一个策略是简单地描述提交中发生了什么变化。在我看来,这不应该是描述一个实现细节。因此,好的提交消息可能是“启用单独的调试和信息记录”或“将签出功能移动到单独的类”,而坏的消息可能是“将 checkout()从 app.js 移动到 checkout.js”。我的观点是,我应该能够从提交头中获得比简单的 diff 更多的信息。
第二种风格是描述如果应用这个提交将会发生什么。这可以成功地用在开源项目中,或者用在开发人员为许多模块做贡献的现场,也许有些模块超出了他们的核心职责。使用这些语义还可以很好地表明提交和交付形状的意图。语言和交流塑造了我们的工作方式,因此这也是帮助构建一致的工作流程的一种强有力的方式。像这样的好的提交消息可能是“添加随机重试”或“更新 2020 模型的价格”。不好的例子可能是“删除不需要的文件”或“添加 JIRA-1234”。
如前所述,许多组织也使用提交头来引用一个或多个问题。这是一个我有很多意见的话题,但我会尽量保持最少的咆哮,只介绍几个项目来思考。我不反对在提交消息中引用问题,我认为这提高了可追溯性,并且在大多数情况下是一个好的实践。我反对使用问题引用来代替适当的、有用的提交消息。我更喜欢将问题引用放在消息体中,而不是占据提交头中的稀疏空间。提交头只有一行,所以最好不要超过 70 个字符。如果您有许多提交引用了同一个问题,或者需要从同一个提交中引用多个问题,您应该反思您是否以次优的方式处理了您的工作。这两种一对多的关系都是工作流的味道,表明您要么试图一次做太多的事情(在一次提交中有许多问题的情况下),要么在将变更集提交到公共存储库之前没有做基本的历史记录整理。当然,可能会有各种各样的特殊场景,但是在大多数情况下,在单个存储库中应该有一对一的提交与问题比率。
利用我们越来越多的高级终端和添加到 Unicode 中的表情符号,一种新的风格已经被引入。一些人在他们的提交信息中使用这些表情符号来表示改变的意图或类型。这有时被称为 Gitmojis。在图 2-7 中,你可以看到一些描述和用法的例子。
图 2-7
Gitmojis 用于简明地对变更集进行分类
使用 Gitmojis 可能看起来只不过是一个噱头,但如果持续使用并经过思考,它可能是一种非常有效的交流方式。然而,有效地使用它需要纪律。如果你正在使用 Gitmoji,就把它作为标题的第一个字符。请限制在提交消息中使用表情符号。如果没有别的,那么确保你使用的表情符号与代码库的工程文化一致。用于调味的表情符号应该添加在提交消息的末尾,这样它们就不会干扰所用 Gitmoji 的消息传递。最后,根据与存储库交互的工具的最新程度和定制程度,可能会有技术上的挑战。定制工具或过时软件可能与提交消息或路径中的表情符号不兼容。
以上是编写好的提交消息的一些不同的风格或策略。您所选择的内容会因项目和存储库的不同而不同。确保有高度的一致性,这样 git 日志看起来是干净的!我用一个错误提交消息的汇编列表来结束这一节,它应该作为如何不编写提交消息的警告。该列表可能基于也可能不基于实际的提交消息:
-
修正打字错误
-
虚拟提交
-
修复 CI(在十个提交的字符串上)
-
也许这能行
-
我甚至不知道为什么我咒骂删除关心了
-
提交消息的主体应该包含什么内容是非常依赖于项目的,但是如果没有别的,我建议您将提交消息放在这里——即使这会妨碍您使用 short -m 直接在命令行中设置提交消息。获得一个好看的历史记录当然值得那些额外的按键。我不会详细介绍提交消息体,因为这里有太多不同的方法要介绍。
用修正从哎呀时刻恢复
git commit 命令有一个名为amend的标志,允许我们编辑最近的提交。我说的编辑,是骗人的。我们将在后面更详细地介绍发生了什么,但是如前所述,提交是不可变的,所以我们并不真正编辑提交,而是创建一个新的几乎相同的提交,并指向它。旧的提交不会立即消失,但会在一段时间后被垃圾收集。
图 2-8 显示了当我们做amend时会发生什么。创建新的提交并更新分支指针。
图 2-8
修改提交会创建新的提交并更新指针。它留下了一个悬空提交,该提交将在某个时候被垃圾收集。新的悬空提交以淡化的轮廓示出,新的提交以实线示出
提交amend在很多情况下都是有用的。我们可能忘记了准备我们需要的一切,或者准备得太多了。我们可能已经写了一个可怕的提交消息,并立即后悔。在下面的部分中,我运行了一个提交示例,然后向提交中添加了一个文件。在本例中,我将使用-m 运行命令来指定消息。如果我们不使用-m,我们将打开我们的$EDITOR,并预先填充来自我们正在修改的提交的消息。
$ git log -n 2
commit 25cb0925de7cbc8c12803c6d51a7ddbc5f114509 (HEAD -> master)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Sat Mar 14 13:13:59 2020 +0100
Add Feature X
commit 67e7eef1a44f222a50207fc20b24477bd9e0ddd8
Author: Johan Abildskov <randomsort@gmail.com>
Date: Sat Mar 14 13:12:34 2020 +0100
Initial Commit
$ git add example.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: example.md
$ git commit –amend -m “ADD Feature X, with docs”
[master 8fb3eba] Add Feature X, with docs
Date: Sat Mar 14 13:13:59 2020 +0100
2 files changed, 2 insertions(+)
create mode 100644 example.md
create mode 100644 featurex.app
$ git log -n 2
commit 8fb3ebac82ac4940cf478746e04d73eb1882aa76 (HEAD -> master)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Sat Mar 14 13:13:59 2020 +0100
Add Feature X, with docs
commit 67e7eef1a44f222a50207fc20b24477bd9e0ddd8
Author: Johan Abildskov <randomsort@gmail.com>
Date: Sat Mar 14 13:12:34 2020 +0100
Initial Commit
$ git status
On branch master
nothing to commit, working tree clean
从前面的例子中我们可以看到,我们可以很容易地修复一个常见的错误。虽然修改历史和提交通常被认为是不好的做法,但这只适用于人们之间共享的项目。因此,只要我们致力于不共享的提交,我们就应该注意积极地编辑和塑造我们的历史,以便为后代提供尽可能多的价值。
使用获取干净提交。被增加
开发人员的一个常见错误是在他们的提交中添加了太多内容。这可能是编译的文件、日志或其他构建工件。如果我们正在构建 Python 代码,我们绝不会对持久化。pyc 编译 Python 文件作为我们版本化源代码的一部分。过多地进入我们的存储库可能会有一些不幸的结果。
首先,随着时间的推移,它会降低性能。进行初始克隆和通过签出将不同的提交放入工作区都可能成为长时间运行的任务。由于 Git 是不可变的和分布式的,这可能会有很高的代价,并且以后很难修复。
第二,它混淆了真正的变更,使得提交具有明确定义的边界和明显的变更集变得更加困难。如果一个提交包含了由于一堆变更的构建工件而导致的数万个文件的变更,那么很难辨别真正的逻辑变更隐藏在哪里。
第三,它强制实施坏习惯,只是将所有已经改变的添加到下一次提交中。作为专业的软件工作者,我们必须小心并确保我们交付的是我们想要交付的。以这种方式,简单地添加我们的存储库中存在的东西确实有助于我们形成深思熟虑的变更集。
幸运的是,Git 附带了一个解决方案,可以帮助我们避免意外弄乱我们的存储库。Git 附带了一个特性,允许我们将路径放入一个名为。gitignore,并且当我们暂存要提交的项目时,此处包含的文件将被忽略。这使得我们在发布内容时更加自由。使用 git add folder/比单独暂存文件要高效得多。
a。gitignore 文件包含要忽略的模式列表。如果您在一行前面加上感叹号,那么该模式将被包括在内,即使它以前被忽略了。在下图中,我们将展示根据。gitignore 文件。
清单 2-2 展示了一个例子。gitignore 适合 Python 项目的文件。
# Created by www.gitignore.io/api/python
# Edit at www.gitignore.io/?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
Listing 2-2A basic .gitignore file that can be used for Python projects to avoid cluttering the repository (generated from https://gitignore.io)
Note
比如说。gitignore 文件对于大多数常用的语言和框架,可以去 gitignore.io 下载一个合适的。
将. gitignore 文件添加到我们的存储库之后,我们可能还需要做一些清理工作。仅仅因为我们忽略了文件,并不能从工作区中删除已经提交的文件。更重要的是,它不会将它们从历史中删除,所以它们仍然会占用空间,即使它们不再使工作空间变得杂乱。如何处理这是一个完全不同的复杂问题,我们将在后面讨论。
在下面的命令行片段中,您可以看到添加. gitignore 文件如何更改 add 命令的行为:
$ ls
app.exe* example.md featurex.app file
$ git add .
$ git status
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: app.exe
$ git restore app.exe
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
app.exe
nothing added to commit but untracked files present (use "git add" to track)
$ echo “*.exe” > .gitignore
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitignore
从前面的例子可以看出,创建一个. gitignore 文件有助于防止我们的存储库变得不必要的混乱。我们还可以看到。gitignore file 只是一个文件,对它的更改将像任何其他文件更改一样被跟踪。
高级版。吉蒂尔
虽然前面的例子很好,并且提供了很多价值,但通常还不够。我们倾向于对我们的存储库中允许的内容有更详细的计划。
制定诸如“除了文件夹图像之外,我们不允许 png 出现在我们的存储库中”这样的方案并不少见。我们可以用 git ignore 文件做这些事情。
git ignore 文件包含一个从上到下应用的模式列表。我们可以给行加上前缀!,使它们成为包含而不是排除。
因此,为了获得前面的场景,我们可以使用下面的.gitignore文件。请注意,git ignore 文件中以#开头的行将被忽略。
# Example .gitignore
# Exclude all pngs
*.png
# Include pngs in img/
!img/*.png
BUILDING A .GITIGNORE FILE
以下一组命令在命令行中构建了一个 git ignore 文件,该文件不允许将 png 文件添加到存储库中,除非它们位于 images 文件夹中。
首先,我们注意到存储库中有两个 png 文件,一个在根目录中,一个在 images 文件夹中。当我们添加根时。),两个 png 文件都是临时的。
$ ls
file.png img/ README.md
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: file.png
new file: img/file.png
$ git restore .
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
file.png
img/file.png
nothing added to commit but untracked files present (use "git add" to track)
在将我们的 stage 恢复到存储库中的状态之后,我们完全忽略存储库中的 png 文件。当我们转移根目录时,不会转移任何 png 文件。
$ echo *.png > .gitignore
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitignore
$ git restore .
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
nothing added to commit but untracked files present (use "git add" to track)
既然我们已经再次恢复到基本状态,我们可以使用!作为模式的前缀。在这种情况下,我们允许图像文件夹中的 png。
$ echo !img/*.png >> .gitignore
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitignore
new file: img/file.png
这也可以通过单独的。gitignore file 文件夹中只包含这一行的文件!*.png。这将具有这样的效果,即无论存储库相信什么,在这个文件夹和下面的 png 文件都是允许的。因此,我们可以在目录结构中放置任意数量的 git ignore 文件。与 Git 的许多其他方面一样,这增加了复杂性,我们应该在根之外添加 ignore 文件。
忽略全局
在 shell 语言中,有一个 globbing 的概念,这是一种模糊通配符扩展。一个*代表任何一个名字。但是我们也可以使用序列**来表示任意嵌套。这使得我们可以说“我们不希望 png 文件在我们的存储库中,除非它们在一个名为 images 的文件夹中,不管那个文件夹在哪里”。在您希望允许存储库中有 png 文件,但又担心人们会不小心添加他们随意放置的 png 文件的情况下,这是非常有用的。如果 png 被放在一个名为 images 的文件夹中,那么它就是允许的,这迫使开发人员在将 png 添加到存储库时要更加慎重,同时不要过度限制。
以下示例从上一个练习开始,然后扩展到其他几个位置。
GLOB PATTERNS IN GIT IGNORE
本练习假设我们有与上一个练习相同的 git ignore 文件,也就是说,git ignore 文件拒绝 png,除了存储库根目录中的文件夹 img/之外。这意味着我们有一些不同的图像文件夹,它们的内容是不允许的。
我们从上一个练习中的设置开始,然后向 gitignore 文件添加一个通配符模式。
$ echo ‘img/*.png’ >> .gitignore
$ git add .
$ git status
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitignore
new file: img/file.png
new file: subfoldimg/file.png
$ git restore .
我们现在添加了一个通配符模式,允许任何子文件夹在名为 images 的子文件夹中包含 png。这允许我们更广泛地允许异常,而不用在一个文件夹一个文件夹的层次上做所有的事情。
$ echo '!img/*.png' >> .gitignore
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitignore
new file: img/file.png
new file: subfoldimg/file.png
new file: subfolder/subfoldimg/file.png
我们现在添加了一个更广泛的模式,只要 png 在一个名为 img/的文件夹中,这种情况发生在目录树中的多远都没有关系。该子句比本练习中的第一个子句更宽泛,因此我们可以返回并从 Git ignore 文件中删除行img/*.png,因为它被刚刚添加的行所覆盖。
前面的例子非常具体地展示了我们如何创建精细的方案,这些方案将给予我们对进入存储库的内容的细粒度控制。它还表明,即使是像前面这样简单的例子,它也可能很快变得复杂。
我强烈建议不要创建太复杂的 gitignore 文件,它们会很快变得看起来很神奇,而且当开发人员使用他们的版本控制系统时,他们不会很清楚发生了什么。如果可能的话,这当然是要避免的。
去吧卡塔
为了更好地支持本章的学习目标,我建议您阅读以下 Git 表格:
-
基本-提交
-
基本阶段
-
改进
-
忽视
摘要
在这一章中,我们已经介绍了这个阶段,以及如何利用它来创建漂亮的原子提交。我们还讨论了提交消息以及如何写好它们。更重要的是,我们讨论了一些不同的策略来决定您希望您的提交消息遵循什么模式。我们谈到了使用 amend 重做提交的最简单的方法。最后,我们讨论了如何使用 Git ignore 文件来避免存储库中的意外文件。有了这些知识,我们就可以大胆尝试,创造完美提交的悠久历史。
三、线性历史
Git 以其轻量级分支而闻名。它们在创建和合并方面都非常高效。作为开发人员,它们使用起来也相对简单。任何经历过看似无法解决的合并冲突的人都会质疑这种说法。
简而言之,分支是我们如何管理我们的源代码生命周期,我们如何管理我们的代码库的不同版本,以及我们如何隔离我们的变更集以促进原子变更和协作。
分支看起来有点复杂,但是我会尽我所能给你正确的词汇和心智模型,让你能够轻松地使用分支。
在这一章中,我们将涵盖线性历史,也就是说,具有完整的单字符串提交链的存储库。首先,我们将介绍 Git 分支模型的基本构件。然后,我们将展示它们如何以可视化的方式交互,并通过 Git 存储库中的任务来总结这一点。
分支基础
当我们想到树枝时,会想到两件事。一是发散。它甚至在自然语言中表现出来。“分支的道路”指的是分成多个方向的道路。其次,我们对分支的直觉是有一定长度的——而不仅仅是一个点。就版本控制而言,当我们有独立的提交链时,我们会期望我们有分支。这种直觉是人们学习 Git 时许多困惑的根源。在图 3-1 中,我们可以看到这些概念是如何映射到实际树的绘图中的。
图 3-1
我们的直觉表现出来了;树枝有长度,代表分叉点
在 Git 中,分支只不过是对单个提交的引用。这是本书中最重要的一句话,所以我要重复一遍。在 Git 中,分支只不过是对单个提交的引用。这意味着,当我们提到源代码的一个分支时,我们通常指的是该分支上的最新提交以及在它之前的提交。但是从技术上来说,分支只是指向最新的提交,而“分支上”的其余提交是通过跟随来自该提交的父指针而得到的。这也意味着分支的存在不一定需要任何分歧。我们可以有两个指向相同提交的分支,因此两个分支是相同的,没有任何差异。
该布局如图 3-2 所示。我们有一串提交和指向这串提交的三个分支。分支 A 和 B 指向相同的提交,而分支 C 指向不同的提交。请注意,虽然 C 不同于 A 和 B,但并不存在分歧。c 仅仅是 A 和 b 的前缀,这在我们讨论合并时会变得很重要。
图 3-2
线性历史中的三个分支。A 和 B 指向同一个提交,而 C 指向 A 和 B 的前任
保持头脑清醒
如前所述,我们可以在同一个存储库中拥有多个分支,甚至指向同一个提交。这使得我们目前在哪个分支上工作变得不明显。在 Git 中,当前活动的分支被称为签出。Git 使用一个名为 HEAD 的文件来跟踪当前签出的内容。
在图 3-3 中,你可以看到当我们创建提交时,头指针是如何引用移动的分支指针的。
图 3-3
树枝是如何移动的
头指着的东西定义了两件事。首先,HEAD 指向一个分支,也就是说,当我们进行更多提交时移动的分支。第二,当我们使用 git status 这样的命令时,我们的工作区和 stage 要与提交进行比较。
提交到您的分支机构
从上一章,我们知道了如何创建提交。我们现在已经讨论了分支指针和头指针。这意味着我们现在准备在主分支上创建一些提交。需要注意的重要一点是,当我们提交时,HEAD 指针不会改变。它一直指向主分支,主分支指向的内容会发生变化。
以下练习将使用 git 命令完成图 3-3 中的场景。
COMMITTING
在下面的练习中,我们将首先看到我们的历史与图 3-3 的历史相匹配。然后我们将提交并看到我们的历史现在与图 3-3 的下半部分相匹配。这个练习可以在本书的源代码中找到。
$ git log --oneline
f157eed (HEAD -> A, B) 4
3652176 3
5cbbdc1 (C) 2
6856025 1
$ echo 5 > README.md
$ git commit -am "5"
[A 933a6c5] 5
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
933a6c5 (HEAD -> A) 5
f157eed (B) 4
3652176 3
5cbbdc1 (C) 2
6856025 1
正如在下面的练习中可以看到的,我们在创建提交时移动了一个分支,但是头部保持不变。
签出以前的版本
到目前为止,我们只关心创建提交。我们没有积极地探索历史。在这一节中,我将向您展示如何在您的工作区中切换版本。Git 的一个关键特性是速度快。这也意味着使您的工作空间类似于您的代码库的不同版本是一项微不足道的任务。由于 Git 是分布式的,我们在本地表示了整个存储库,这也允许这些操作发生,即使我们离线。
我们使用命令“git checkout <target>”将特定的修订放入我们的工作区。因为目标检验可以获取任何最终导致提交的内容。最常见的是,我们使用分支、标记或提交 sha。Git 结帐是一个两步的过程。首先,它将头指针移动到特定的修订版。然后,它获取该修订中的内容,并将其移动到工作区中,使工作区看起来像该修订。如果 Git 不能以安全的方式做到这一点,它将中止签出。这意味着如果您的工作被签出覆盖,Git 将不会完成操作。Git 也不会清理任何未被跟踪的文件。如果我们使用一个标签或者一个提交 sha 作为 checkout 命令的目标,我们将会以一个分离的 HEAD 状态结束。这听起来比实际更危险。这仅仅意味着我们目前没有跟踪任何分支,并且可能会丢失我们在这一点上所做的工作,因为我们没有提交任何分支。然而,没有理由为此担心。稍后,我们将一起解决这个问题。
在下面的练习中,我们将看到如何使用 git checkout 命令在不同版本的存储库之间切换。我们将检查单独的提交,查看工作空间是如何变化的,并返回到最近的版本。
CHECKING OUT DIFFERENT VERSIONS
这个练习从上一个练习的结束状态开始。
$ git log --oneline --decorate
7f1c255 (HEAD -> A) 5
f157eed (B) 4
3652176 3
5cbbdc1 (C) 2
6856025 1
$ cat README.md
5
$ git checkout 4
error: pathspec '4' did not match any file(s) known to git.
$ git checkout B
Switched to branch 'B'
$ cat README.md
4
$ git log --oneline --decorate
f157eed (HEAD -> B) 4
3652176 3
5cbbdc1 (C) 2
6856025 1
$ git checkout C
Switched to branch 'C'
$ cat README.md
2
$ git checkout A
Switched to branch 'A'
$ cat README.md
5
$ echo "Important information" > README.md
$ git checkout B
error: Your local changes to the following files would be overwritten by checkout:
README.md
Please commit your changes or stash them before you switch branches.
Aborting
请注意,当我们试图检出一些会以不安全的方式覆盖我们工作空间中的更改的内容时,Git 会阻止该操作,并建议可能的操作。
当你完成前面的练习时,注意每个操作有多快。它几乎不需要任何时间。虽然这是一个小而琐碎的存储库,但是在非常大的存储库上也可以看到类似的性能。简单地说,Git 不需要与服务器通信这一事实是一个很大的优势,即使假设网络连接和服务器负载处于最佳状态。
看到不同版本之间的差异
在上一节中,我们看到了如何将代码库的任何版本实例化到我们的工作空间中。有了这些知识,找出我们的存储库的两个版本之间的区别的一个常见方法是在磁盘上有相同存储库的两个副本,在不同的文件夹中检查不同的版本,并比较它们。这既可以通过手动调查感兴趣的领域来完成,也可以使用工具来显示差异。这不是地道的饭桶。它还容易出错,并可能导致繁琐的返工,因为您需要反复检查哪个版本在哪个文件夹中,并试图找出哪个文件要复制到哪里。
Git 用 diff 命令解决了这个问题。diff 命令显示了两次提交之间的区别。该命令接受两次提交,或者对提交的引用作为参数。如果只给出一个参数,则假定 HEAD 是第一个参数。该命令如下所示:git diff ,git diff master release-1.0 就是一个例子。这将显示提交主机引用的提交和 1.0 版引用的提交之间的内容差异。
Note
git diff 的参数顺序很重要。如果改变参数的顺序,一个方向的文件创建就变成了文件删除。添加的 20 行变成删除的 20 行。当您试图找出变更集中的内容时,这可能会导致混乱。
我对 diff 的直觉是,我将 Git 指向两个不同的提交,它会告诉我从一个提交到另一个提交需要做什么。这当然也可以看作是两者之间发生的事情。在清单 3-1 中,显示了这一点,以及参数顺序对 diff 的影响。
$ git diff C A
diff --git a/README.md b/README.md
index 0cfbf08..7edff8 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-2
+5
$ git diff C A
diff --git b/README.md a/README.md
index 7edff8..0cfbf08 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-5
+2
Listing 3-1A diff and the impact of the order of the arguments. Here, we need to delete a 2 and add a 5 or, in the other direction, delete a 5 and add a 2
diff 命令并不关注提交之间的历史记录,不管它是否有分歧。Git 只是告诉您作为参数传递的提交所代表的两个工作区之间的区别。
有时,补丁输出可能有点难以解析——尤其是长行中的小变化。这有时可以用标志--word-diff来帮助,它将在一行中内联更改,而不是作为两个单独的行。这可以在清单 3-2 中看到。
Normal Diff
<Navbar bg="success" variant="dark">
- <Navbar.Brand href={window.location.host}>Cultooling</Navbar.Brand>
+ <Navbar.Brand href={homeUrl()}>Cultooling</Navbar.Brand>
<Nav className="mr-auto">
- <Nav.Link href={window.location.host}>Home</Nav.Link>
+ <Nav.Link href={homeUrl()}>Home</Nav.Link>
</Nav>
</Navbar>
With --word-diff
<Navbar bg="success" variant="dark">
<Navbar.Brand
[-href={window.location.host}>Cultooling</Navbar.Brand>-]{+href={homeUrl()}>Cultooling</Navbar.Brand>+}
<Nav className="mr-auto">
<Nav.Link
[-href={window.location.host}>Home</Nav.Link>-]{+href={homeUrl()}>Home</Nav.Link>+}
</Nav>
</Navbar>
Listing 3-2Showing how it is much easier to see what the changes are using the --word-diff flag. This can vary from use case to use case
我们还可以使用不带参数或带标志--staged的 diff 命令来查看我们的工作区、阶段和存储库之间的差异。这两个命令是强大的工具,可以帮助您更加谨慎地进行提交。
去吧卡塔
为了支持本章的学习目标,我建议你去解决以下 Git 卡塔:
-
分离的头部。
-
再次执行基本提交并注意正在进行的分支特定的事情可能是有用的。
摘要
在这一章中,我们首先介绍了如何使用一个简单的分支,包括头指针来跟踪我们当前已经签出的内容。我们还创建了一些提交,并看到我们的分支指针随着我们的操作而移动。随后,我们用 checkout 命令浏览了我们的历史,并用一些 diff 魔术完成,向我们展示了在历史的两个点之间到底发生了什么。学完这一章后,你应该对使用线性分支历史感到舒服了。
四、复杂分支
在上一章中,我们看了线性历史。这对于琐碎的存储库来说可能是好的,但是如果我们有信心使用分支工作,它将几乎不会引入开销,所以我们可以运用分支的力量,甚至对于我们最简单的项目。
在 Git 中与分支积极合作有很多好处。我们将在下一章讨论与多个开发人员的合作,但是即使对于一个单独的开发人员,也有来自分支的成功。它们主要源于这样一个事实,即我们可以使用分支来隔离我们的工作。当我们隔离我们的工作时,我们可以减轻一些多任务的成本。通过隔离我们在一个分支上的工作,我们总是可以创建一个新的分支,如果一个紧急的任务需要被开发的话。我们可以安全地在一个分支上运行实验,并且只有当它以一种有利的方式出现时才整合我们的实验。如前所述,分支是围绕 Git 如何工作产生混乱的一个重要原因。这是非常不幸的,因为它们是获得 Git 全部价值和理解许多概念的关键,包括使用远程存储库和除了最简单的协作方案之外的所有概念。
在这一章中,我们将着重于围绕多个分支获得一个健康的心智模型,并获得足够的实践经验,使你能够使用和推理分支。
创建分支
我们已经介绍过,分支是提交的指针。具体地说,这意味着分支是存储库中的一个文件,包含分支所指向的 sha。这可以在清单 4-1 中看到。
$ cat .git/refs/heads/master
5355b7b7f01b6d69c1ae94b428f54952139eb2f8
$ git log --oneline --decorate -n 1
5355b7b (HEAD -> master, origin/master, origin/HEAD) [Chapter 7] Add aliases exercise
Listing 4-1A branch is a file containing the sha of the commit it points to
我们可以使用命令 git branch 来操作和列出分支。谈到远程分支时有一些微妙之处,但是我们将在下一章中讨论这些。当我们使用不带参数的命令时,我们列出(本地)分支。
我们还使用 branch 命令创建分支。我们用两个参数调用:git branch <branch-name> <commit>。举个例子:git branch my-branch master。这将在存储库中创建一个分支。它将被称为my-branch,并指向与主服务器相同的提交。这可以在图 4-1 中看到。
图 4-1
从引用创建分支
现在我们已经创建了一个分支,我们可以在不同的分支上做一些工作。根据 HEAD 当前指向的内容,将在适当的位置创建新的提交,并更新当前签出的分支以指向新的提交。这可以从图 4-2 中看出。
图 4-2
在分支上创建提交将添加提交并更新分支指针
既然我们已经看到了在分支上创建提交时的样子,我们就为分支的下一步做好了准备
使用多个分支
在 Git 中,没有任何分支的工作真的没有任何意义,我们默认总是在一个分支上工作:主分支。但真正的力量来自于对多个分支的玩弄。使用多个分支时有两个主要任务。一是让我们的工作在不同的分支上分开。我们之前已经讨论过了。另一部分是将多个分支上的变更放入同一个分支。这通常被称为合并。有多种方法可以做到这一点。在这一章中,我们将讨论合并和重置基础。
从概念上讲,当我们想要合并两个分支时,我们创建一个新的 commit,它包含来自两个分支的联合变更集。这是通过找到分支分叉的点并连接两个变更集来实现的。这可以从图 4-3 中看出。
图 4-3
一个普通的合并,合并分别指向 C 和 E 的分支
在变更集兼容的情况下,Git 将为我们处理一切。如果变更集不兼容,或者 Git 无法合并它们,我们将会陷入合并冲突。我们将在本章后面讨论这些。在我工作过的大多数代码库中,合并冲突并不常见。
合并
合并是我们的语言妨碍我们理解 Git 的另一个地方。我们都在谈论分支的抽象合并,不管我们打算如何做,我们都在谈论命令“git merge”。
使用 merge 命令的常见方式是“git merge branch”形式,它将把分支中的变更集合并到当前签出的分支中,例如,git merge feature-123。还有其他选择,但我喜欢这种工作方式,因为我们只改变我们所在的分支,这很好,因为它导致相对较少的问题。这种融合就是图 4-3 被创造出来的原因。
快进合并
快进合并是 Git 中最简单的合并形式。不幸的是,人们对它们的工作方式也有一些误解。这一节将有希望让您处于喜欢快速合并的状态。
当您要合并的分支之间没有分歧时,就会发生快进合并。当一个分支是另一个分支的延续时,就会出现这种情况。在图 4-4 中,我们可以看到特征分支线性领先于主分支的场景。为了合并特性中的变化,我们需要做的就是将主分支指针移动到提交特性点。因为主分支中包含的所有变更已经是特征分支的一部分。
图 4-4
执行快进合并不会导致任何新的提交,而是一个简单的操作
这也意味着任何冲突都不可能进行快速合并。出于这个原因,快进合并可以被认为是安全的。
Note
一些工作流使用 Git 特性,其中创建新的提交来标记分支的合并。这将创建一个没有变更集的合并提交,以标记此时分支已被合并。这是通过命令 git merge - no-ff 完成的。
FAST FORWARD
在本练习中,我们将只从master分支开始。它有两个提交。我们将创建一个名为feature的分支,创建一个 commit,并将其合并到 master 中。这个练习可以在练习文件夹中找到,名为chapter4/fast-forward/。
$ git log --oneline --decorate
fa8d7db (HEAD -> master) second commit
35b6a68 Initial Commit
$ git checkout -b feature
Switched to a new branch 'feature'
$ git add 1.txt
$ git commit -m "Adding file1"
[feature 4b346fe] Adding file1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1.txt
$ git log --oneline --decorate
4b346fe (HEAD -> feature) Adding file1
fa8d7db (master) second commit
35b6a68 Initial Commit
此时,功能分支包含不在主服务器上的提交,但是主服务器不包含也不能从功能分支到达的任何内容。
$ git checkout master
Switched to branch 'master'
$ git merge feature
Updating fa8d7db..4b346fe
Fast-forward
1.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1.txt
Git 告诉我们它正在进行快进,并从哪个提交开始移动指针。
$ git log --oneline
4b346fe (HEAD -> master, feature) Adding file1
fa8d7db second commit
35b6a68 Initial Commit
如果我们将其与快进合并之前的日志语句中的一个进行比较,我们可以看到提交 ID 是相同的。这意味着没有创建新的提交,并且更改纯粹是分支更新。
从前面的练习中可以看出,默认情况下,快进合并不会导致新的提交。这意味着这种类型的合并是一个非常快速的操作,因为它只是一个简单的两步过程:将更新的 sha 写入分支文件,然后签出该修订版的工作区。
三向合并
在上一节中,我们讨论了琐碎的或快速前进的合并,其中没有分歧,也没有冲突的可能性。在本节中,我们将讨论简单合并或三向合并。当我们正在合并的两个分支都包含只在一个分支上的工作时,就会出现这种情况。这种差异是完全自然的,在大多数情况下,当多个开发人员在一个源代码库上合作时,都会出现这种差异。通常,当我们在我们的特性分支上开发时,一些其他开发人员已经向主分支交付了一些变更。因此,我们从主分支分支出来的点不再是主分支上的最新提交。由于提交表示工作区的特定状态,我们需要创建一个新的提交,它包含在获取两个变更集之后工作区的状态。在图 4-5 中,您可以看到这在合并前后在 Git 图上的样子。在下一个练习中,我们将介绍它在磁盘上的外观。
图 4-5
合并两个分支会创建新的提交并更新分支指针
三向合并之所以这样命名,是因为在合并中涉及三个点——两个结束状态以及两个分支离开的点。我们分别将它们命名为源、目标和合并库。这可以在图 4-6 中看到。
图 4-6
三向合并的不同组件:源、目标和合并基础
Git 使用合并基础来确定不同的变更集,并计算它们是否重叠,从而不能被 Git 自动合并。结果将是提交,并且接收分支将被更新。当我们在一个方向上完成了三向合并,如果我们在另一个方向上进行合并,它将始终是一个快进合并。
THREE-WAY MERGE
在本练习中,我们有两个内容不同的分支需要合并。我们将首先把来自master的内容合并到feature中。然后,我们将主更新到特征分支。这是一个常见的工作流程,因为您可以在交付给主模块之前,首先测试您的特征分支中的最终状态。这个练习的资源库可以在chapter4/three-way-merge/的练习中找到。
$ git log --all --graph --oneline
* d03b0bd (HEAD -> feature) Add feature.txt
| * 390d440 (master) Add master.txt
|/
* ea2b9f5 second commit
* f90da57 Initial Commit
我们看到有两个分叉。
$ git merge master
Merge made by the 'recursive' strategy.
master.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master.txt
当我们将来自master的变更合并到feature分支时,使用三向合并来解决合并问题。它使用了递归策略,这是一个我们可以放心忽略的实现细节。
$ git log --all --graph --oneline
* ddeeef9 (HEAD -> feature) Merge branch 'master' into feature
|\
| * 390d440 (master) Add master.txt
* | d03b0bd Add feature.txt
|/
* ea2b9f5 second commit
* f90da57 Initial Commit
三向合并导致了新的提交ddeeef9。请注意,主分支仍然指向与以前相同的提交。
$ git checkout master
Switched to branch 'master'
$ git merge feature
Updating 390d440..ddeeef9
Fast-forward
feature.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 feature.txt
现在我们在另一个方向合并分支,我们得到一个快速前进的合并。这是真的,因为从master可到达的所有内容也是从feature可到达的,因此 Git 认为这个合并已经解决了。许多工作流只允许在master上快进合并,这就是如何实现的。
$ git log --all --graph --oneline
* ddeeef9 (HEAD -> master, feature) Merge branch 'master' into feature
|\
| * 390d440 Add master.txt
* | d03b0bd Add feature.txt
|/
* ea2b9f5 second commit
* f90da57 Initial Commit
在前面的代码中,我们遍历了三路合并,并注意到在另一个方向重复三路合并会导致快进合并。
前面的练习经历了快乐之路的场景。当我们的合并很简单时,Git 可以很容易地自动解决它们,我们感觉很强大。不幸的是,Git 并不总是能够为我们解决合并问题。我们将在下一节讨论这一点。
合并冲突
Git 可能无法确定合并分支的结果。在这种情况下,Git 将要求用户解决合并问题,并继续这个过程。这种情况称为合并冲突。Git 将进入提示符状态,并将文件标记为处于冲突状态。清单 4-2 通过一个状态命令展示了这一点。
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: mergesort.py
no changes added to commit (use "git add" and/or "git commit -a")
Listing 4-2Git status shows that we are in a state of an unresolved merge conflict and instructs as to what our next steps are
我可以解释如何解决合并冲突的最简单的方法是,您需要使工作区看起来像您想要的合并,然后告诉 Git 您已经完成了。Git 在冲突的文件中输出所谓的标记。这可以在清单 4-3 中看到。
$ cat mergesort.py
from heapq import merge
def merge_sort2(m):
"""Sort list, using two part merge sort"""
if len(m) <= 1:
return m
# Determine the pivot point
middle = len(m) // 2
# Split the list at the pivot
<<<<<<< HEAD
left = m[:middle]
right = m[middle:]
=======
right = m[middle:]
left = m[:middle]
>>>>>>> Mergesort-Impl
<Rest of file truncated>
Listing 4-3Merge markers in a file show origin of different changes
如果您遇到复杂的合并冲突,通常使用外部合并工具(如 meld 或 kdiff)会有所帮助。在正常情况下,必须合并冲突很容易解决,可以简单地在您的普通编辑器中处理。编辑器,比如 Visual Studio 代码,理解 Git 放在文件中的标记,这使得解决合并冲突变得更加容易。
同一文件中可以有多个合并冲突。Git 查看更小的块,找出文件版本之间的相似之处。这使得处理合并冲突变得更加容易,因为您不必一次决定整个文件,而是可以分解成更小的片段进行比较。
MERGE CONFLICT
在本练习中,我们将经历与上一个练习相同的情况,只是分叉的分支会有不兼容的变化。这将导致合并冲突,我们将解决这一冲突。这个练习可以在chapter4/merge-conflict/下的例子中找到。
$ ls
0.txt master.txt
$ cat master.txt
feature
$ git log --oneline --decorate --graph --all
* 6ce4209 (HEAD -> feature) Add feature.txt
| * c301b9a (master) Add master.txt
|/
* f237b8b second commit
* 7e48076 Initial Commit
$ git checkout master
Switched to branch 'master'
$ cat master.txt
master
现在,我们在仓库里找到了方向。两个分支已经分开。每个都添加了内容不同的文件master.txt。
$ git merge feature
Auto-merging master.txt
CONFLICT (add/add): Merge conflict in master.txt
Automatic merge failed; fix conflicts and then commit the result.
在我们启动合并后,Git 检测到合并冲突并暂停合并,提示我们解决合并。
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both added: master.txt
no changes added to commit (use "git add" and/or "git commit -a")
使用git status向我们展示哪里有问题,让我们知道 Git 无法合并文件master.txt。
$ cat master.txt
<<<<<<< HEAD
master
=======
feature
>>>>>>> feature
Git 在 master.txt 中放置了显示不同变更集的合并标记,这表明当前状态是包含 master 的文件,引入的变更是包含 feature 的文件。
$ echo master > master.txt
$ git add master.txt
warning: LF will be replaced by CRLF in master.txt.
The file will have its original line endings in your working directory.
大多数情况下,我们希望在编辑器或合并工具中完成合并,但在这种情况下,我只需选择我想要的状态。请注意,此状态可以是解决方案之一,也可以是它们的某种组合。这就是 Git 需要人工干预的原因——它不知道我们的源的语义。我们使用add将文件标记为处于已解析状态。
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
$ git commit
[master 3be77eb] Merge branch 'feature'
$ git log --oneline --decorate --graph --all
* 3be77eb (HEAD -> master) Merge branch 'feature'
|\
| * 6ce4209 (feature) Add feature.txt
* | c301b9a Add master.txt
|/
* f237b8b second commit
* 7e48076 Initial Commit
解决了合并冲突后,我们看到我们处于与快乐之路三向合并类似的情况。我们只需要在前进的道路上帮助 Git 一点点。
从这个练习中可以看出,解决合并冲突并不是一项令人畏惧的任务。然而,在复杂的场景中,当使用我们不熟悉的代码库时,这可能会很困难。
重定…的基准
三向合并的替代方法是重定基数。与三向合并不同,三向合并通过合并两个分支来创建表示工作区的新提交,而 rebase 直观地移动了提交。这在技术上是错误的,但是我们暂时保留直觉。当我们将我们的分支放在另一个分支之上时,直觉上我们将提交移动到我们的分支上,并将它们应用到目标分支之上。这可以在图 4-7 中看到。
图 4-7
重定基础与合并。从 A 开始,B 是将母版合并到特征的结果,而 C 是将特征重新基于母版的结果
我们使用git rebase <target>命令在<target>之上重设HEAD的基础。假设特性被签出,我们将编写git rebase master在主特性的基础上重新构建特性分支。这可以从图 4-7 (c)中看出。
REBASE EXERCISE
在本练习中,我们从与三路合并练习相同的情况开始,但是我们不是合并分支,而是在master的顶部重设feature的基础。这个库可以在练习文件夹中找到,名为chapter4/rebase/。
$ git log --oneline --graph --all
* b188294 (HEAD -> feature) Add feature.txt
| * 8cab888 (master) Add master.txt
|/
* 6fb6ffc second commit
* 2a97e8c Initial Commit
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add feature.txt
$ git log --oneline --graph --all
* 449abd2 (HEAD -> feature) Add feature.txt
* 8cab888 (master) Add master.txt
* 6fb6ffc second commit
* 2a97e8c Initial Commit
重定基数的结果与合并的结果有一个巨大的区别。也就是说,我们没有增加提交的数量,并且降低了 Git 图的复杂性。特别是,当您在开发代码时更新您的分支以包含来自 master 的最新内容时,这是一种很好的工作方式。请注意,该功能指向一个新的提交 sha。
$ git show b18829
commit b1882942ed4722828d595e3428fbac75522bb587
Author: Johan Abildskov <randomsort@gmail.com>
Date: Mon May 4 09:34:52 2020 +0200
Add feature.txt
diff --git a/feature.txt b/feature.txt
new file mode 100644
index 0000000..e69de29
在这里,我们使用show来查看特性先前指向的提交仍然存在,因此我们可以安全地从 rebase 中恢复。
Note
虽然我们对 rebase 的直觉是我们移动了一个分支,但事实并非如此。新的提交是在合并基础之上进行的,而旧的提交没有任何对它们的引用。因此,在垃圾收集发生之前,它们可以被恢复。
对于重定基数或合并的情况有许多不同的意见。对此我有几点看法。首先,无论谁交付给定的变更集,整个团队的工作方式都会产生一致的历史,这一点很关键。这最有可能意味着每个人都 rebases 或每个人都合并。团队正在开发的工作流也可能带来影响。然而,如果工作流指示您是否可以从技术的角度使用合并或 rebases,它可能需要被查看,并且您需要重新评估它是否是一种合理的工作方式。
第二,如果你不在一个共享的分支上工作,你应该总是改变基础。这将使您的历史保持干净,并将您的提交很好地捆绑在一起,以实现简洁的交付。这也使你在交付之前更容易操纵你的本地历史,我们将在后面的章节中讨论。由于重定基础改变了提交 sha,所以重定公共分支的基础被认为是不好的做法。但是,您可能正在自己的公共分支上工作。它可以被发布以从持续集成系统中获得构建,或者从同行那里获得反馈。在这种情况下,您不应该停止对您自己的分支,而是对 public 分支进行重新基准化。
标签
到目前为止,在这一章中,我们已经介绍了分支,以及它们是如何轻量级和易于移动的。对于更加静态的提交,命名引用有许多用途。在 Git 中,我们有标签来提供这种功能。标签是对提交的引用。通常,标签被用来标记我们的源代码的发布版本,所以我们有一个产生我们的软件的任何给定版本的源代码的命名引用。
有两种类型的标签,轻量级的和带注释的。轻量级标签就像分支,只是它们是静态的。这意味着它们只是对提交的引用,没有附加信息。带注释的标签是 Git 对象数据库中的完整对象,接收消息并提供附加信息。通过在 tag 命令中添加-a、-s 或-m 来创建带注释的提交。tag 命令看起来像这样:git tag <target>对于轻量级标签。例如,git tag v1.6.2 a233b将创建一个指向提交的轻量级标签,前缀为 a233b。
如果我们省略目标,标签将在 HEAD 处创建。
TAGGING
在本练习中,我们将进入一个简单的存储库,添加一些标签并研究它们。本练习的知识库可在chapter4/tags/中找到。
$ git tag
首先,我们注意到没有标签。这与 flow log 命令的输出一致。
$ git log --oneline --all
f203381 (HEAD -> feature) Add feature.txt
0a664dc (master) Add master.txt
810eb22 second commit
0cae311 Initial Commit
现在,我们使用 sha 810eb22 在提交时创建一个标记。我们使用提交的唯一前缀。
$ git tag v1.0 810eb
现在,当我们列出所有标记时,这些标记都会显示出来,并作为日志上的参考。
$ git tag
v1.0
$ git log --oneline --decorate --graph --all
* f203381 (HEAD -> feature) Add feature.txt
| * 0a664dc (master) Add master.txt
|/
* 810eb22 (tag: v1.0) second commit
* 0cae311 Initial Commit
之前的提交是直接使用提交 sha 进行的。在下面,我们重复相同的流程,但是不使用提交,而是从引用创建一个标签。
$ git tag v2.0 master
$ git tag
v1.0
v2.0
$ git log --oneline --decorate --graph --all
* f203381 (HEAD -> feature) Add feature.txt
| * 0a664dc (tag: v2.0, master) Add master.txt
|/
* 810eb22 (tag: v1.0) second commit
* 0cae311 Initial Commit
前面的标签是轻量级标签,是纯粹的引用。我们可以创建完整的标签对象,例如,将消息附加到标签上。
$ git tag v3.0 feature -m "pre-release"
创建标记后,我们可以看到标记和被标记的提交的完整信息。将这与轻量级标签上的相同信息进行对比。
$ git show v3.0
tag v3.0
Tagger: Johan Abildskov <randomsort@gmail.com>
Date: Mon May 4 10:04:34 2020 +0200
pre-release
commit f203381f79576e69f4de2a75cd6289ea635f3543 (HEAD -> feature, tag: v3.0)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Mon May 4 10:02:12 2020 +0200
Add feature.txt
diff --git a/feature.txt b/feature.txt
new file mode 100644
index 0000000..e69de29
$ git show v1.0
commit 810eb22a50a1bd94facd9917531295ddddd27bb7 (tag: v1.0)
Author: Johan Abildskov <randomsort@gmail.com>
Date: Mon May 4 10:02:11 2020 +0200
second commit
diff --git a/0.txt b/0.txt
index 303ff98..36db9be 100644
--- a/0.txt
+++ b/0.txt
@@ -1 +1,2 @@
first file
+\n additional content
正如我们在这个练习中所看到的,标签可以用来标记我们历史中具有某种意义的地方。
分离头
如果你在开始阅读这本书之前有过任何 Git 经验,很可能你已经发现自己处于一种超然的头脑状态,很可能它吓到了你。我知道,因为至少我花了一些时间才没有让我觉得自己做了不该做的事。
头部分离是完全正常的情况,很容易补救。分离的头仅仅意味着头指向提交而不是分支。这样做的结果是,在分离 head 情况下创建的提交没有任何指向它们的引用。这可能会使它们从 git 日志中消失,被垃圾收集,或者变得不必要的难以恢复。在分离的 HEAD 中结束的两种最常见的方法是显式地检查提交或检查标记。图 4-8 给出了一个例子。
图 4-8
分离的头部,带有悬空的提交
如果结束一个分离的头的情况的目的是简单地看代码,看在那个时间点上库的状态是什么,没有问题,并且我们可以停留在分离的头的状态直到我们准备好返回我们正在工作的分支。如果我们想做出改变,我们最好创建一个分支;这可以在签出时使用标志-b 很容易地完成,它将在我们签出的目标上创建一个分支。这个看起来像git checkout -b <branch-name> <target>。如果我们想在标签v1.2.7处创建一个名为 bugfix 的分支,我们使用命令git checkout -b bugfix v1.2.7。
DETACHED HEAD
在这个练习中,我们将把自己置于分离的头部状态,并从中恢复过来。本练习的资源库可以在示例中找到,名称为chapter4/detached-head/。
$ git log --oneline --decorate --graph --all
* adfcb1d (HEAD -> feature) Add feature.txt
| * ca3e69b (tag: v1.0, master) Add master.txt
|/
* 66d6ce7 second commit
* 66d93b9 Initial Commit
我们检查与主分支指向同一个分支的标签。
$ git checkout v1.0
Note: checking out 'v1.0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at ca3e69b... Add master.txt
前面的文字墙是分离的头感到危险的主要原因。注意,即使有指向我们签出的提交的引用,HEAD 并不指向它们,而是直接指向提交。
$ git log --oneline --decorate --graph --all
* adfcb1d (feature) Add feature.txt
| * ca3e69b (HEAD, tag: v1.0, master) Add master.txt
|/
* 66d6ce7 second commit
* 66d93b9 Initial Commit
$ git checkout -b new-branch
Switched to a new branch 'new-branch'
请注意,我们只是在 HEAD 处创建并签出了一个分支。根据我们的用例,我们可以检查主分支并从那里继续。
$ git log --oneline --decorate --graph --all
* adfcb1d (feature) Add feature.txt
| * ca3e69b (HEAD -> new-branch, tag: v1.0, master) Add master.txt
|/
* 66d6ce7 second commit
* 66d93b9 Initial Commit
从下面的练习可以看出,没有理由害怕脱离的头部,很容易恢复。
去吧卡塔
为了支持本章的学习目标,我建议您完成以下表格:
-
基本分支
-
三路合并
-
合并-冲突
-
合并-合并排序
-
Rebase-branch
-
Git-tag
-
分离头
摘要
在这一章中,我们谈到了 Git 中的分支以及它们是如何工作的。我们讨论了不同类型的合并,并将合并与 rebases 进行了对比。我们逐步解决了合并冲突。我们以一个简短的描述结束了这一章,描述了我们如何使用标签来标记我们代码库中的有趣的点。最后,我们缩小了分离头部的情况。
现在我们已经有了分支的基础,我们可以继续使用 Git 进行协作了。