Git-版本控制秘籍第二版-全-
Git 版本控制秘籍第二版(全)
原文:
annas-archive.org/md5/48eca3c961675b9b653a871b89c68e69
译者:飞龙
前言
Git 正日益成为现代软件开发中源代码管理(SCM)的事实标准。
Git 最初由 Linus Torvalds 开发,作为 Linux 内核的 SCM 系统,用以取代专有的 SCM 工具 BitKeeper。此后,Git 已征服了大部分开源世界,并且被许多组织用于他们的私有/专有项目。
本书旨在为你提供日常 Git 使用的实用配方。这些配方可以直接使用,也可以作为灵感。本书通过实用配方和深入的解释,覆盖了 Git 数据模型,使你更深入地理解 Git 的内部工作原理。本书将展示以下主题:
-
使用历史记录。Git 会将所有历史记录存储在本地。你可以浏览历史、查看历史、查找某个分支上的最后一次提交等。
-
使用分支时,通过选项和策略有效地进行推送、拉取和合并。
-
在 Git 仓库中存储和提取额外的元数据。
-
灾难恢复:本地和全局。
Git 版本控制秘籍 为你提供了精确的、逐步的指引,涵盖了各种常见和不常见的 Git 操作。本书通过提供常见问题的解决方案、实用的技巧与窍门,并深入阐明这些方法的原理与作用,使你的 Git 日常工作变得更加轻松。
本书适合谁阅读
本书面向开发者、专业的构建/发布经理和 DevOps 从业者,提供了 Git 下一个层次的实用指南。从 Git 数据模型开始,逐步深入到分支管理、元数据与钩子等内容,本书通过易于阅读的配方结构,使从简单的日常用例到高级仓库管理的过渡变得平滑。本书适合目标读者群体阅读和理解,你需要具备基本的 GNU/Linux 工具和 Shell/Bash 脚本的知识,才能充分利用本书。
本书涵盖了哪些内容
第一章,导航 Git,展示了 Git 如何存储文件和提交。示例将直观地展示数据模型,以及如何使用简单的命令浏览历史记录和数据库。
第二章,配置,展示了 Git 中可以配置的内容、如何设置配置目标、不同的配置级别以及一些有用的配置目标。
第三章,分支、合并与选项,将让你对分支和推送/拉取目标的选项有更深入的了解。本章还展示了不同的合并策略,以及如何记录合并解决方案的一些技巧。
第四章,定期并交互式地进行变基及其他使用场景,向您展示了如何使用变基来替代合并,以及其他许多变基的使用场景,如发布前清理历史记录和测试单个提交。
第五章,在您的代码库中存储附加信息,带您了解 Git 笔记。它将向您展示如何将附加信息与提交绑定,并如何再次使用和查看这些信息。
第六章,从代码库中提取数据,向您展示了如何从代码库中提取统计信息和其他元数据。
第七章,通过 Git 钩子、别名和脚本提升您的日常工作,包含一系列配方,帮助您自动化繁琐的日常工作。
第八章,从错误中恢复,带您走过几个恢复场景,从本地撤销,到“我的旧提交在哪里”,再到全局恢复场景。
第九章,代码库维护,是一本关于代码库维护和管理的配方集,从强制垃圾回收、过度拆分和合并代码库,到完全重写历史记录。
第十章,修补和离线共享,向您展示如何在离线状态下使用 Git 工作,并通过除推送和拉取外的其他方式共享工作。
第十一章,技巧与窍门,是一本关于各种主题的配方集,从简单的技巧到在提示符中显示当前分支,再到高级 Git 工具,如 bisect 和 stash。
第十二章,Git 提供者、集成和客户端,介绍了最大的 Git 托管网站 GitHub。此外,本章将讨论如何集成 Jenkins 进行自动化构建和测试。
为了充分利用本书的内容
要按照本书中的配方操作并重现结果,您需要一台计算机,最好运行*nix 操作系统。您需要安装 Git,建议安装 Git 2.x 版本或更高版本。
如果您是 Windows 用户,我们推荐使用 Git Extensions 包,它包含了这两者。
图形化和文本(Bash)Git 界面。后者是本书中的配方所必需的。
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/GitVersionControlCookbookSecondEdition_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 账号。例如:“最新的提交是 3061dc6 Adds Java version of 'hello world'
提交。”
任何命令行输入或输出会按如下方式编写:
$ git checkout master && git reset --hard b14a939
粗体:表示新术语、重要单词或屏幕上显示的词语。例如,菜单或对话框中的词语会像这样显示在文本中。这里有一个例子:“从管理面板中选择系统信息。”
警告或重要的注释会像这样显示。
提示和技巧会像这样显示。
章节
在本书中,你将发现多个常见的标题(准备就绪,如何操作...,如何运作...,还有更多...,以及另请参见)。
为了提供清晰的操作指引,使用以下这些部分:
准备就绪
本节将告诉你在食谱中可以期待什么,并描述如何设置任何所需的软件或前期设置。
如何操作……
本节包含遵循食谱所需的步骤。
如何运作……
本节通常包含对上一节内容发生情况的详细解释。
还有更多……
本节包含关于食谱的附加信息,帮助你更好地理解食谱内容。
另请参见
本节提供指向其他有用信息的链接,以帮助你完成本食谱。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过电子邮件 feedback@packtpub.com
并在邮件主题中提及书名。如果你对本书的任何方面有疑问,请通过 questions@packtpub.com
联系我们。
勘误表:尽管我们已尽一切努力确保内容的准确性,但错误仍然会发生。如果你在本书中发现错误,我们将非常感谢你向我们报告。请访问 www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并填写相关信息。
盗版:如果你在互联网上发现我们作品的任何非法复制品,我们将非常感谢你提供其位置地址或网站名称。请通过 copyright@packtpub.com
联系我们,并附上相关链接。
如果你有兴趣成为作者:如果你在某个领域拥有专业知识,并且有兴趣撰写或为书籍贡献内容,请访问 authors.packtpub.com。
书评
请留下评论。一旦你阅读并使用了本书,为什么不在你购买书籍的站点上留下评论呢?潜在读者可以通过你的公正意见做出购买决策,Packt 也能了解你对我们产品的看法,作者也能看到你对他们书籍的反馈。谢谢!
如需了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:导航 Git
本章将涵盖以下内容:
-
Git 的对象
-
三个阶段
-
查看有向无环图(DAG)
-
提取已修复的问题
-
获取已更改文件的列表
-
使用 gitk 查看历史
-
查找历史中的提交
-
搜索历史代码
介绍
本章我们将深入了解 Git 的数据模型。我们将学习 Git 如何引用其对象以及历史如何记录。我们将学习如何导航历史,从查找提交消息中的特定文本片段,到引入代码中的特定字符串。
Git 的数据模型与其他常见的版本控制系统(VCSs)在数据处理方式上有所不同。传统上,VCS 会将数据存储为初始文件,接着是每个新版本文件的补丁列表:
Git 的不同之处在于:Git 不像常规的文件和补丁列表那样,而是记录了 Git 跟踪的所有文件及其相对于仓库根目录的路径的快照——也就是说,Git 在文件系统树中跟踪的文件。Git 中的每个提交都记录了完整的树状态。如果文件在提交之间没有变化,Git 不会重新存储该文件,而是存储该文件的链接。如下图所示,你可以看到每个提交/版本后文件的状态。
这就是 Git 与大多数其他版本控制系统不同之处,在接下来的章节中,我们将探讨这种强大模型的一些好处。
Git 引用文件和目录的方式直接内建于数据模型中。简而言之,Git 的数据模型可以通过下图总结:
commit
对象指向根树。根树指向子树和文件。
分支和标签指向一个 commit
对象,而 HEAD
对象指向当前检出的分支。因此,每个提交的完整树状态和快照由根树标识。
Git 的对象
现在,既然你知道 Git 将每次提交存储为完整的树状态或快照,让我们仔细看看存储库中的 Git 对象存储。
Git 的对象存储是一个键值存储,键是对象的 ID,值是对象本身。键是对象的 SHA-1 哈希值,包含一些额外的信息,如大小。Git 中有四种类型的对象,还有分支(虽然不是对象,但很重要)以及特殊的 HEAD
指针,它指向当前检出的分支/提交。Git 中的四种对象类型如下:
-
文件,或者在 Git 上下文中也叫做 blobs
-
目录,或在 Git 上下文中的树
-
提交
-
标签
我们将从查看我们刚刚克隆的仓库中的最新 commit
对象开始,记住,特殊的 HEAD
指针指向当前检出的分支。
准备开始
要查看 Git 数据库中的对象,我们首先需要一个需要检查的仓库。对于这个实例,我们将克隆一个位于以下位置的示例仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition.git
$ cd Git-Version-Control-Cookbook-Second-Edition
现在,您已准备好查看数据库中的对象。我们将首先查看 commit
对象,然后是 tree
,文件,最后是分支和标签。
如何做...
让我们更仔细地看看仓库中对象的 Git 存储情况。
commit
对象
Git 的特殊 HEAD
对象始终指向当前的快照/提交,因此我们可以将其作为目标来查看我们想要检查的提交:
$ git cat-file -p HEAD
tree 34fa038544bcd9aed660c08320214bafff94150b
parent 5c662c018efced42ca5e9cce709787c40a849f34
author John Doe <john.doe@example.com> 1386933960 +0100
committer John Doe <john.doe@example.com> 1386941455 +0100
这是提交消息的主题行。它应该后跟一个空行,然后是正文,正文是这段文本。在这里,您可以使用多个段落来解释您的提交。它就像一封电子邮件,有主题和正文,目的是吸引人们的注意力。
带有 -p
选项的 cat-file
命令会打印命令行中给出的对象;在这种情况下,HEAD
指向 master
,master
进一步指向该分支上的最新提交。
现在,我们可以看到 commit
对象,它由根 tree
(tree
)、父 commit
对象的 ID(parent
)、作者和时间戳信息(author
)、提交者和时间戳信息(committer
)以及提交消息组成。
tree
对象
要查看 tree
对象,我们可以对该 tree
运行相同的命令,但目标是该 tree
的 ID(34fa038544bcd9aed660c08320214bafff94150b
):
$ git cat-file -p 34fa038544bcd9aed660c08320214bafff94150b
100644 blob f21dc2804e888fee6014d7e5b1ceee533b222c15 README.md
040000 tree abc267d04fb803760b75be7e665d3d69eeed32f8 a_sub_directory
100644 blob b50f80ac4d0a36780f9c0636f43472962154a11a another-file.txt
100644 blob 92f046f17079aa82c924a9acf28d623fcb6ca727 cat-me.txt
100644 blob bb2fe940924c65b4a1cefcbdbe88c74d39eb23cd hello_world.c
我们还可以通过指定git cat-file -p HEAD^{tree}
来指定想要查看 HEAD
所指向提交的 tree
对象,这将与之前的命令给出相同的结果。特殊符号HEAD^{tree}
表示从给定的引用中,HEAD
会递归地解引用该引用所指向的对象,直到找到一个 tree
对象。
第一个 tree
对象是从 master
分支指向的提交中找到的根 tree
对象,而 HEAD
也指向该分支。该符号的一般形式是 <rev>^<type>
,它将递归查找 <rev>
中的第一个 <type>
对象。
从tree
对象中,我们可以看到它包含的内容:文件类型/权限、类型(tree
/blob
)、ID 和路径名:
类型/****权限 | 类型 | ID/SHA-1 | 路径名 |
---|---|---|---|
100644 | blob |
f21dc2804e888fee6014 d7e5b1ceee533b222c15 |
README.md |
040000 | tree |
abc267d04fb803760b75 be7e665d3d69eeed32f8 |
a_sub_directory |
100644 | blob |
b50f80ac4d0a36780f9c 0636f43472962154a11a |
another-file.txt |
100644 | blob |
92f046f17079aa82c924 a9acf28d623fcb6ca727 |
cat-me.txt |
100644 | blob |
bb2fe940924c65b4a1ce fcbdbe88c74d39eb23cd |
hello-world.c |
blob
对象
现在,我们可以查看 blob
(文件)对象。我们可以使用相同的命令,通过将 blob
的 ID 作为目标来查看 cat-me.txt
文件:
$ git cat-file -p 92f046f17079aa82c924a9acf28d623fcb6ca727
文件的内容是 cat-me.txt
。
Not really that exciting, huh?
这只是文件的内容,我们也可以通过运行普通的cat cat-me.txt
命令来获取。因此,这些对象是相互关联的,blob 到树,树到其他树,根树到commit
对象,所有的连接都通过对象的 SHA-1 标识符来实现。
分支对象
branch
对象并不像其他 Git 对象;你不能像其他对象一样通过cat-file
命令打印它(如果你指定-p
的漂亮打印选项,你只会得到它指向的commit
对象),如下代码所示:
$ git cat-file master
usage: git cat-file (-t|-s|-e|-p|<type>|--textconv) <object>
or: git cat-file (--batch|--batch-check) < <list_of_objects>
<type> can be one of: blob, tree, commit, tag.
...
$ git cat-file -p master
tree 34fa038544bcd9aed660c08320214bafff94150b
parent a90d1906337a6d75f1dc32da647931f932500d83
...
相反,我们可以查看.git
文件夹中的分支,该文件夹存储着整个 Git 仓库。如果我们打开文本文件.git/refs/heads/master
,就能看到master
分支指向的提交 ID。我们可以使用cat
命令查看,方法如下:
$ cat .git/refs/heads/master
13dcada077e446d3a05ea9cdbc8ecc261a94e42d
我们可以通过运行git log -1
来验证这是否是最新的提交:
$ git log -1
commit 34acc370b4d6ae53f051255680feaefaf7f7850d (HEAD -> master, origin/master, origin/HEAD)
Author: John Doe <john.doe@example.com>
Date: Fri Dec 13 12:26:00 2013 +0100
This is the subject line of the commit message
...
我们还可以使用cat
命令查看.git/HEAD
文件,确认HEAD
指向的是活动分支:
$ cat .git/HEAD
ref: refs/heads/master
branch
对象只是一个指向提交的指针,通过其 SHA-1 哈希值来标识。
标签对象
最后需要分析的对象是tag
对象。tag
有三种不同的类型:轻量标签(仅是一个label
)、注释标签和签名标签。在示例仓库中,有两个注释标签:
$ git tag
v0.1
v1.0
我们来仔细看看v1.0
标签:
$ git cat-file -p v1.0
object f55f7383b57ad7c11cf56a7c55a8d738af4741ce
type commit
tag v1.0
tagger John Doe <john.doe@example.com> 1526017989 +0200
We got the hello world C program merged, let's call that a release 1.0
如你所见,标签由一个对象组成——在这种情况下,是master
分支上的最新提交——对象的类型(提交、blob 和树都可以被标记)、标签名称、标签创建者和时间戳,最后是标签信息。
它是如何工作的...
Git 命令git cat-file -p
会打印作为输入的对象。通常,它不用于日常的 Git 命令中,但它在调查对象如何相互关联时非常有用。
我们还可以通过使用 Git 命令git hash-object
重新哈希来验证git cat-file
的输出;例如,如果我们想验证HEAD
上的commit
对象(34acc370b4d6ae53f051255680feaefaf7f7850d
),可以运行以下命令:
$ git cat-file -p HEAD | git hash-object -t commit --stdin 13dcada077e446d3a05ea9cdbc8ecc261a94e42d
如果你看到的提交哈希与HEAD
指向的哈希相同,可以通过git log -1
来验证它是否正确。
还有更多内容...
查看 Git 数据库中的对象有很多种方法。git ls-tree
命令可以轻松显示树和子树的内容,而git show
则可以以不同的方式显示 Git 对象。
三个阶段
我们已经看过 Git 中的不同对象,那么我们如何创建它们呢?在这个示例中,我们将看到如何在仓库中创建blob
、tree
和commit
对象。我们还将学习创建提交的三个阶段。
准备就绪
我们将使用上一个示例中的相同仓库Git-Version-Control-Cookbook-Second-Edition
:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition.git
$ cd Git-Version-Control-Cookbook-Second-Edition
如何操作...
- 首先,我们对文件进行一个小修改,然后检查
git status
:
$ echo "Another line" >> another-file.txt
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: another-file.txt
no changes added to commit (use "git add" and/or "git commit -a")
当然,这只是告诉我们我们修改了another-file.txt
,并且我们需要使用git add
将其暂存。
- 让我们添加
another-file.txt
文件并再次运行git status
:
$ git add another-file.txt
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: another-file.txt
现在,文件已经准备好提交,就像你可能之前看到的那样。但add
命令到底发生了什么呢?一般来说,add
命令会将文件从工作目录移动到暂存区;然而,这并不是唯一发生的事情,尽管你看不到。当文件被移到暂存区时,文件的 SHA-1 哈希值会被创建,并且blob
对象会被写入 Git 的数据库。每次添加文件时都会发生这种情况,但如果文件没有发生变化,意味着它已经存储在数据库中。刚开始时,这看起来数据库会迅速增长,但实际情况并非如此。垃圾回收机制会定期启动,压缩并清理数据库,只保留需要的对象。
- 我们可以再次编辑文件并运行
git status
:
$ echo 'Whoops almost forgot this' >> another-file.txt
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: another-file.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: another-file.txt
现在,文件同时出现在待提交更改
和未暂存的更改
部分。刚开始看起来有点奇怪,但当然是有原因的。当我们第一次添加文件时,它的内容被哈希并存储在 Git 的数据库中。第二次对文件所做的更改尚未被哈希并写入数据库;它仅存在于工作目录中。因此,文件会同时出现在待提交更改
和未暂存的更改
部分;第一次的更改已准备好提交,第二次的更改则没有。我们也来添加第二次更改:
$ git add another-file.txt
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: another-file.txt
- 现在,我们对文件所做的所有更改都已经准备好提交,我们可以记录一次提交:
$ git commit -m 'Another change to another file'
[master 99fac83] Another change to another file
1 file changed, 2 insertions(+)
它是如何工作的……
正如我们之前学到的,add
命令会创建blob
、tree
和commit
对象;不过,当我们运行commit
命令时,它们也会被创建。我们可以使用cat-file
命令查看这些对象,正如我们在前面的方法中所看到的:
$ git cat-file -p HEAD
tree 162201200b5223d48ea8267940c8090b23cbfb60
parent 13dcada077e446d3a05ea9cdbc8ecc261a94e42d
author John Doe <john.doe@example.com> 1524163792 +0200
committer John Doe <john.doe@example.com> 1524163792 +0200
对另一个文件进行了更改。
提交中的root-tree
对象如下所示:
$ git cat-file -p HEAD^{tree}
100644 blob f21dc2804e888fee6014d7e5b1ceee533b222c15 README.md
040000 tree abc267d04fb803760b75be7e665d3d69eeed32f8 a_sub_directory
100644 blob 35d31106c5d6fdb38c6b1a6fb43a90b183011a4b another-file.txt
100644 blob 92f046f17079aa82c924a9acf28d623fcb6ca727 cat-me.txt
100644 blob bb2fe940924c65b4a1cefcbdbe88c74d39eb23cd hello_world.c
从前面的方法中我们知道,根树的 SHA-1 值是34fa038544bcd9aed660c08320214bafff94150b
,another-file.txt
文件的 SHA-1 值是b50f80ac4d0a36780f9c0636f43472962154a11a
,并且如预期的那样,它们在我们更新another-file.txt
文件时发生了变化。在我们创建提交之前,我们添加了同一个文件another-file.txt
两次,将更改记录到版本库的历史中。我们还了解到,add
命令会在调用时创建一个blob
对象。因此,在 Git 数据库中,第一次将文件添加到暂存区时,应该已经存在一个类似于another-file.txt
内容的对象。我们可以使用git fsck
命令检查悬空对象——即没有被其他对象或引用所指向的对象:
$ git fsck --dangling
Checking object directories: 100% (256/256), done.
dangling blob ad46f2da274ed6c79a16577571a604d3281cd6d9
让我们使用以下命令检查blob
对象的内容:
$ git cat-file -p ad46f2da274ed6c79a16577571a604d3281cd6d9
This is just another file
Another line
如预期的那样,当我们第一次将another-file.txt
文件添加到暂存区时,blob
对象的内容与文件内容相似。
以下图示描述了树的各个阶段及用于在各阶段之间移动的命令:
另请参见
如需更多关于 cat-file
和 fsck
命令的示例和信息,请查阅 Git 文档:git-scm.com/docs/git-cat-file
和 git-scm.com/docs/git-fsck
。
查看 DAG
Git 中的历史记录由 commit
对象构成;随着开发的推进,分支被创建和合并,历史记录将形成一个有向无环图(DAG),因为 Git 将提交与其父提交关联的方式。DAG 使得通过提交轻松查看项目的开发进度。
请注意,以下图示中的箭头是依赖箭头,这意味着每个提交指向它的父提交,因此箭头的方向与时间流逝的正常方向相反:
示例仓库的图形,带有简化的提交 ID
你可以通过使用 git log
命令查看 Git 中的历史记录(DAG)。也有一些可视化 Git 工具可以图形化地显示历史记录。本节将展示 git log
的一些功能。
准备工作
我们将使用上一节中的示例仓库,并确保 master 分支指向 34acc37
:
$ git checkout master && git reset --hard 34acc37
在前面的命令中,我们只使用提交 ID 的前七个字符(34acc37
);只要所使用的简化 ID 在仓库中是唯一的,这样做是没问题的。
如何操作...
- 查看历史的最简单方法是使用
git log
命令;这将以逆时间顺序显示历史记录。输出通过less
分页显示,还可以进一步限制显示的内容,例如,通过提供要显示的提交数:
$ git log -3
- 这将显示以下结果:
commit 34acc370b4d6ae53f051255680feaefaf7f7850d
Author: John Doe <john.doe@example.com>
Date: Fri Dec 13 12:26:00 2013 +0100
This is the subject line of the commit message.
It should be followed by a blank line then the body, which is this text. Here
you can have multiple paragraphs etc. and explain your commit. It's like an
email with subject and body, so try to get people's attention in the subject
commit a90d1906337a6d75f1dc32da647931f932500d83
Author: John Doe <john.doe@example.com>
Date: Fri Dec 13 12:17:42 2013 +0100
Instructions for compiling hello_world.c
commit 485884efd6ac68cc7b58c643036acd3cd208d5c8
Merge: 44f1e05 0806a8b
Author: John Doe <john.doe@example.com>
Date: Fri Dec 13 12:14:49 2013 +0100
Merge branch 'feature/1'
Adds a hello world C program.
通过运行 git config --global color.ui auto
来在 Git 输出中启用颜色。
- 默认情况下,
git log
会打印提交、作者的姓名和电子邮件 ID、时间戳以及提交信息。然而,这些信息并不是非常直观,尤其是当你想查看分支和合并时。要显示这些信息并限制其他一些数据,你可以使用以下选项与git log
一起使用:
$ git log --decorate --graph --oneline --all
- 前面的命令将每行显示一个提交(
--oneline
),并用简化的提交 ID 和提交信息主题进行标识。提交之间将绘制一条图形,表示它们的依赖关系(--graph
)。--decorate
选项会在简化的提交 ID 后显示分支名称,--all
选项则显示所有分支,而不仅仅是当前分支:
$ git log --decorate --graph --oneline --all
* 34acc37 (HEAD, tag: v1.0, origin/master, origin/HEAD, master) This is the sub...
* a90d190 Instructions for compiling hello_world.c
* 485884e Merge branch 'feature/1'
...
然而,这个输出不会显示时间戳或作者信息,因为 --oneline
选项的格式化方式。
- 幸运的是,
log
命令使我们能够创建自己的输出格式。因此,我们可以制作一个类似于之前的历史视图。颜色使用%C<color-name>text-be-colored%Creset
语法,并结合作者和时间戳信息以及一些颜色来漂亮地显示它:
$ git log --all --graph \
--pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset'
- 写起来有点麻烦,但幸运的是,它可以作为别名来简化,你只需要写一次:
git config --global alias.graph "log --all --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset'"
现在,你只需调用git graph
来显示历史记录,正如你之前看到的那样。
它是如何工作的...
Git 通过跟踪给定提交的父 ID(哈希值)来遍历 DAG。传递给git log
的选项可以以不同方式格式化输出;这可以服务于多个目的——例如,像之前看到的那样,以漂亮的图形方式显示历史、分支和标签,或者提取仓库历史中的特定信息,供脚本使用等。
提取已修复的问题
创建发布版本的常见用例是生成发布说明,其中包括修复的漏洞等内容。一个好的实践是,在提交信息中写明该提交是否修复了某个漏洞。更好的做法是采用标准化的方式来进行标记——例如,在提交信息的最后部分添加一行"Fixes-bug: "
,后面跟上漏洞标识符。这使得我们能够轻松编译出修复的漏洞列表用于发布说明。JGit 项目就是一个很好的例子,他们的提交信息中的漏洞标识符是简单的"Bug: "
字符串,后面跟着漏洞 ID。
这个方法将向你展示如何将git log
的输出限制为仅列出自上次发布(标签)以来的、包含漏洞修复的提交。
准备工作
使用以下命令行克隆 JGit 仓库:
$ git clone https://git.eclipse.org/r/jgit/jgit
$ cd jgit
如果你想要和这个示例完全一样的输出,请将master
分支重置为b14a93971837610156e815ae2eee3baaa5b7a44b
:
$ git checkout master && git reset --hard b14a939
如何做…
现在,你已经准备好查看提交日志,查找描述已修复漏洞的提交信息。
- 首先,让我们将日志限制为只查看上一个标签(发布)以来的历史记录。为了找到上一个标签,我们可以使用
git describe
:
$ git describe
v3.1.0.201310021548-r-96-gb14a939
上面的输出告诉我们三件事:
-
-
上一个标签是
v3.1.0.201310021548-r
-
自该标签以来的提交数量是
96
-
当前的提交简写形式是
b14a939
-
现在,日志可以从HEAD
解析到v3.1.0.201310021548-r
。但是仅仅运行git log 3.1.0.201310021548-r..HEAD
将会返回所有 96 个提交,而我们只需要包含"Bug: xxxxxx"
的提交信息来生成发布说明。xxxxxx
是漏洞的标识符,将是一个数字。我们可以使用--grep
选项和git log
来达到此目的,形成代码片段git log --grep "Bug: "
。这将返回所有提交信息中包含"Bug: "
的提交;现在我们只需要将它格式化成可以用于发布说明的内容。
- 现在,让我们假设我们希望发布说明的格式如以下模板所示:
Commit-id: Commit subject
Fixes-bug: xxx
- 到目前为止,我们的命令行如下:
$ git log --grep "Bug: " v3.1.0.201310021548-r..HEAD
这给出了所有修复 bug 的提交,但我们可以使用 --pretty
选项将其格式化为易于解析的格式。
- 首先,我们将打印缩写的提交 ID(
%h
),后面是我们选择的分隔符(|
),然后是提交主题(%s
,提交消息的第一行),再加上一个新行(%n
),以及正文(%b
):
--pretty="%h|%s%n%b"
当然,输出需要被解析,但这在常规的 Linux 工具如 grep
和 sed
中很容易实现。
- 首先,我们只需要包含
"|"
或"Bug: "
的行:
grep -E "\||Bug: "
- 然后,我们可以使用
sed
替换这些:
sed -e 's/|/: /' -e 's/Bug:/Fixes-bug:/'
- 整个命令如下所示:
$ git log --grep "Bug: " v3.1.0.201310021548-r..HEAD --pretty="%h|%s%n%b" | grep -E "\||Bug: " | sed -e 's/|/: /' -e 's/Bug:/Fixes-bug:/'
- 前面一组命令给出了以下输出:
f86a488: Implement rebase.autostash
Fixes-bug: 422951
7026658: CLI status should support --porcelain
Fixes-bug: 419968
e0502eb: More helpful InvalidPathException messages (include reason)
Fixes-bug: 413915
f4dae20: Fix IgnoreRule#isMatch returning wrong result due to missing reset
Fixes-bug: 423039
7dc8a4f: Fix exception on conflicts with recursive merge
Fixes-bug: 419641
99608f0: Fix broken symbolic links on Cygwin.
Fixes-bug: 419494
...
现在,如果必要的话,我们可以从 bug 跟踪器中提取 bug 信息,并将上述代码放入发布说明中。
工作原理...
首先,我们将 git log
命令限制为仅显示我们感兴趣的提交范围,然后进一步通过在提交消息中过滤 "Bug: "
字符串来限制输出。我们将字符串漂亮地打印出来,以便我们可以轻松地将其格式化为我们需要的发布说明样式,并最终使用 grep
和 sed
找到 "Bug: "
并将其替换为 "Fixes-bug: "
,以完全匹配发布说明的样式。
还有更多...
如果我们只想从提交消息中提取 bug ID 而不关心提交 ID,我们可以在 git log
命令之后使用 grep
,仍然将日志限制为最后一个标签:
$ git log v3.1.0.201310021548-r..HEAD | grep "Bug: "
如果我们只想获取提交 ID 和它们的主题,但不想获取实际的 bug ID,我们可以结合 git log
的 --oneline
功能和 --grep
选项使用:
$ git log --grep "Bug: " --oneline v3.1.0.201310021548-r..HEAD
获取已更改文件的列表
正如我们在前一篇文章中看到的,从历史记录中提取到的固定问题列表,也可以轻松地提取出自上次发布以来已更改的所有文件列表。文件可以进一步过滤,以查找已添加、已删除、已修改等文件。
准备工作
与前一篇文章中看到的相同仓库和 HEAD
位置(HEAD
指向 b14a939
)将被使用。发布版本也相同,即 v3.1.0.201310021548-r
。
如何操作...
以下命令列出自上次发布(v3.1.0.201310021548-r
)以来已更改的所有文件:
$ git diff --name-only v3.1.0.201310021548-r..HEAD
org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.target
org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
...
工作原理...
git diff
命令与前一篇文章中 git log
所做的相同的修订范围操作。通过指定 --name-only
,Git 将仅将在指定范围内的提交更改的文件路径作为输出。
还有更多...
命令的输出可以进一步过滤:如果我们只想显示自上次提交以来在仓库中已删除的文件,我们可以使用 git diff
的 --diff-filter
开关:
$ git diff --name-only --diff-filter=D v3.1.0.201310021548-r..HEAD
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SampleDataRepositoryTestCase.java
org.eclipse.jgit.packaging/org.eclipse.jgit.target/org.eclipse.jgit.target.target
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GCTest.java
还有关于已添加 (A
)、复制 (C
)、删除 (D
)、修改 (M
)、重命名 (R
) 等文件的开关。
另请参见
如需更多信息,请通过运行git help diff
命令访问帮助页面。
使用 gitk 查看历史
我们之前已经看过如何通过使用git log
查看历史(DAG 图)并将其可视化。然而,随着历史的增长,终端中显示的历史可能会变得有些难以导航。幸运的是,Git 中有许多图形化工具,其中一个是 gitk,它支持多个平台(Linux、Mac 和 Windows)。
本示例将向你展示如何开始使用 gitk。
准备工作
确保你已安装gitk
:
$ which gitk
/usr/local/bin/gitk
如果什么都没有显示出来,那说明你的系统没有安装 gitk,或者至少它不在你的$PATH
中。
切换到Git-Version-Control-Cookbook-Second-Edition
仓库,该仓库包含对象和 DAG 示例。确保已检出 master 分支并指向13dcad
:
$ git checkout master && git reset --hard 13dcad
如何操作…
在仓库中,运行gitk --all &
以启动gitk
界面。你还可以指定提交范围或分支,就像你使用git log
时一样(或提供--all
查看所有提交):
$ gitk --all &
Gitk 展示了仓库的提交历史:
它是如何工作的…
Gitk 解析每个提交及其附加的对象信息,提供一个易于使用的图形化信息界面,显示历史图、作者和每个提交的时间戳。底部则是选中某个提交后的结果,包括提交信息和每个修改文件的补丁内容。此外,修改过的文件列表会显示在右侧。
虽然 gitk 非常轻量且速度很快,但它是一个功能非常强大的工具。用户点击历史视图中的提交、分支或标签后,会出现多个上下文菜单。你可以创建和删除分支、撤销和挑选提交、diff
选定的提交等,功能非常多。
还有更多…
在界面中,你可以执行查找和搜索操作。查找会遍历历史,而搜索会遍历 gitk 下半部分显示的信息,查找当前高亮的提交。
查找历史中的提交
你在前面的示例中已经看过如何过滤git log
的输出,只列出提交信息中包含“Bug:
”字符串的提交。在本例中,我们将使用相同的技术来查找整个历史中的特定提交。
准备工作
同样,我们将使用 JGit 仓库,尝试查找与“Performance
”关键字相关的提交。在本示例中,我们将浏览整个历史,因此不需要让 master 分支指向特定的提交。
如何操作…
如我们之前所做的,我们可以使用--grep
选项在提交信息中查找特定的字符串。在本示例中,我们查看整个历史并搜索所有提交信息中包含“Performance
”的提交:
$ git log --grep "Performance" --oneline --all
e3f19a529 Performance improvement on writing a large index
83ad74b6b SHA-1: collision detection support
48e245fc6 RefTreeDatabase: Ref database using refs/txn/committed
087b5051f Skip redundant 'OR-reuse' step in tip commit bitmap setup
9613b04d8 Merge "Performance fixes in DateRevQueue"
84afea917 Performance fixes in DateRevQueue
7cad0adc7 DHT: Remove per-process ChunkCache
d9b224aeb Delete DiffPerformanceTest
e7a3e590e Reuse DiffPerformanceTest support code to validate algorithms
fb1c7b136 Wait for JIT optimization before measuring diff performance
它是如何工作的…
在这个示例中,我们特别要求 Git 考虑历史中的所有提交,通过提供--all
开关。Git 会遍历 DAG(有向无环图),并检查提交信息中是否包含"Performance"
字符串。为了更方便地查看结果,--oneline
开关被用来限制输出仅显示提交信息的主题。希望这样,所需的提交记录可以从这个更简短的提交列表中被识别出来。
注意,搜索是区分大小写的——如果我们搜索的是 "performance"
(全小写),提交列表将会完全不同:
$ git log --grep "performance" --oneline --all
d7deda98d Skip ignored directories in FileTreeIterator
5a87d5040 Teach UploadPack "include-tag" in "fetch"
7d9246f16 RawParseUtils#lineMap: Simplify by using null sentinel internally
4bfc6c2ae Significantly speed up FileTreeIterator on Windows
4644d15bc GC: Replace Files methods with File alternatives
d3021788d Use bitmaps for non-commit reachability checks
6b1e3c58b Run auto GC in the background
db7761025 Pack refs/tags/ with refs/heads/
30eb6423a Add GC_REST PackSource to better order DFS packs
... more output
还有更多...
我们也可以使用 gitk 的查找功能来查找相同的提交记录。打开 gitk 并使用 --all
开关,在查找字段中输入 Performance
,然后按 Enter。这会在历史视图中突出显示相关的提交记录,你可以通过按 Shift + 上箭头、Shift + 下箭头,或使用查找字段旁边的按钮,来导航到前一个或下一个结果。你仍然能够在视图中看到整个历史记录,并且匹配的提交会被高亮显示:
在历史代码中查找
有时候,仅列出提交信息是不够的。你可能想知道哪些提交修改了特定的方法或变量。使用git log
也可以做到这一点。例如,你可以对一个字符串、变量或方法进行搜索,git log
会列出包含该字符串的提交记录,无论是添加还是删除了该字符串。通过这种方式,你可以轻松地获取该段代码的完整提交上下文。
准备工作
再次,我们将使用 JGit 仓库,master 分支指向 b14a939
:
$ git checkout master && git reset --hard b14a939
如何操作...
我们希望找到所有修改了包含 "isOutdated"
方法的行的提交记录。同样,我们将每个提交记录显示为一行;之后我们可以逐一检查:
$ git log -G"isOutdated" --oneline
f32b861 JGit 3.0: move internal classes into an internal subpackage
c9e4a78 Add isOutdated method to DirCache
797ebba Add support for getting the system wide configuration
ad5238d Move FileRepository to storage.file.FileRepository
4c14b76 Make lib.Repository abstract and lib.FileRepository its implementation
c9c57d3 Rename Repository 'config' as 'repoConfig'
5c780b3 Fix unit tests using MockSystemReader with user configuation
cc905e7 Make Repository.getConfig aware of changed config
我们可以看到,八个提交包含了涉及 "isOutdated"
字符串的补丁。
它是如何工作的...
Git 会查看历史(DAG),检查每个提交中父提交与当前提交之间的补丁,寻找"isOutdated"
字符串。这种方法非常方便,用来查找某个字符串被引入或删除的时间点,并获取该时刻的完整上下文和提交记录。
还有更多...
-G
选项与 git log
一起使用时,会查找补丁中包含的新增或删除的行,这些行与给定的字符串匹配。然而,这些行也可能是由于某些其他重构或变量/方法重命名而被添加或删除的。git log
还有另一个选项 -S
,它将以类似 -G
选项的方式检查补丁文本中的差异,但只会匹配那些指定字符串出现次数发生变化的提交记录——也就是说,某行被添加或删除,而不是同时添加和删除。
让我们看看 -S
选项的输出:
$ git log -S"isOutdated" --oneline
f32b861 JGit 3.0: move internal classes into an internal subpackage
c9e4a78 Add isOutdated method to DirCache
797ebba Add support for getting the system wide configuration
ad5238d Move FileRepository to storage.file.FileRepository
4c14b76 Make lib.Repository abstract and lib.FileRepository its implementation
5c780b3 Fix unit tests using MockSystemReader with user configuation
cc905e7 Make Repository.getConfig aware of changed config
搜索匹配七个提交,而使用 -G
选项的搜索则匹配了八个提交。不同之处在于,在第一个列表中,只有使用 -G
选项才能找到 ID 为 c9c57d3
的提交。仔细查看此提交显示,由于另一个对象的重命名,仅触及了 isOutdated
字符串,这就是在使用 -S
选项时将其从匹配提交列表中过滤掉的原因。我们可以使用 git show
命令查看提交的内容,并使用 grep -C4
仅限制输出到搜索字符串前后的四行内容:
$ git show c9c57d3 | grep -C4 "isOutdated"
@@ -417,14 +417,14 @@ public FileBasedConfig getConfig() {
throw new RuntimeException(e);
}
}
- if (config.isOutdated()) {
+ if (repoConfig.isOutdated()) {
try {
- loadConfig();
+ loadRepoConfig();
} catch (IOException e) {
第二章:配置
在本章中,我们将涵盖以下内容:
-
配置目标
-
查询现有配置
-
模板
-
.git
目录模板 -
一些配置示例
-
Git 别名
-
示例化的 refspec
介绍
Git 在开发者的日常工作中扮演着基本且至关重要的角色,但它也非常复杂且高度可配置。本章将概述最重要的可用选项,并提供正确的工具,以便学习和浏览各种配置标志和字段,从而根据个人需求定制你的 Git 使用体验。
配置目标
在本节中,我们将查看可以配置的不同层级。这些层级如下:
-
SYSTEM
:此层级是全系统范围的,可以在/etc/gitconfig
中找到。 -
GLOBAL
:此层级是用户的全局配置,可以在~/.gitconfig
中找到。 -
LOCAL
:此层级仅限于当前仓库,并可以在.git/config
中找到。
准备就绪
我们将在此示例中使用 jgit
仓库;克隆它,或者使用你在 第一章,《导航 Git》中已经克隆的版本,如以下命令所示:
$ git clone https://git.eclipse.org/r/jgit/jgit
$ cd jgit
如何操作...
在上一章中,我们看到如何使用命令 git config --list
列出配置项。这个列表实际上是由 Git 提供的三个不同层级的配置构成的:全系统配置 SYSTEM
,用户的全局配置 GLOBAL
,以及本地仓库配置 LOCAL
。
- 对于每个配置层级,我们都可以查询现有配置。在默认安装 Git 扩展的 Windows 系统中,不同的配置层级大致如下:
$ git config --list --system
core.symlinks=false
core.autocrlf=true
color.diff=auto
color.status=auto
color.branch=auto
color.interactive=true
pack.packsizelimit=2g
help.format=html
http.sslcainfo=/bin/curl-ca-bundle.crt
sendemail.smtpserver=/bin/msmtp.exe
diff.astextplain.textconv=astextplain
rebase.autosquash=true
# list the global configuration
$ git config --list --global
merge.tool=kdiff3
mergetool.kdiff3.path=C:/Program Files (x86)/KDiff3/kdiff3.exe
diff.guitool=kdiff3
difftool.kdiff3.path=C:/Program Files (x86)/KDiff3/kdiff3.exe
core.editor="C:/Program Files (x86)/GitExtensions/GitExtensions.exe" fileeditor
core.autocrlf=true
credential.helper=!"C:/Program Files (x86)/GitExtensions/GitCredentialWinStore/git-credential-winst
ore.exe"
user.name=John Doe
user.email=john.doe@example.com
# list the configuration for this repository
$ git config --list --local
core.repositoryformatversion=0
core.filemode=false
core.bare=false
core.logallrefupdates=true
core.symlinks=false
core.ignorecase=true
core.hidedotfiles=dotGitOnly
remote.origin.url=https://git.eclipse.org/r/jgit/jgit
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
- 我们还可以通过使用以下命令查询单个键,并将作用范围限制在三层中的一个:
$ git config --global user.email
john.doe@example.com
- 我们可以为当前仓库设置不同的用户电子邮件地址:
$ git config --local user.email john@example.com
现在,列出 GLOBAL
层级的 user.email
将返回 john.doe@example.com
,列出 LOCAL
层级将返回 john@example.com
,而在不指定层级的情况下列出 user.email
将返回当前仓库操作中使用的有效值;在这种情况下,LOCAL
层级的值 john@example.com
。当需要有效值时,它将优先使用该值。如果在不同的层级中指定了两个或更多相同键的值,则较低层级的值会优先。Git 在需要配置值时,首先会查看 LOCAL
配置。如果在此未找到,则查询 GLOBAL
配置。如果在 GLOBAL
配置中也未找到,则使用 SYSTEM
配置。
如果这些都无效,则会使用 Git 中的默认值。
在上面的示例中,user.email
在 GLOBAL
和 LOCAL
层级中都有指定。因此,将使用 LOCAL
层级的值。
工作原理...
查询三层配置时,简单地返回配置文件的内容;/etc/gitconfig
用于系统范围配置,~/.gitconfig
用于用户特定配置,.git/config
用于仓库特定配置。如果未指定配置层,则返回的值为有效值。
还有更多内容...
除了通过键值在命令行中设置所有配置值外,你还可以直接编辑配置文件来设置它们。打开你喜欢的编辑器中的配置文件,设置你需要的配置,或者使用内置的git config -e
命令在 Git 配置的编辑器中直接编辑配置。你可以通过更改$EDITOR
环境变量或使用core.editor
配置目标来设置你喜欢的编辑器,例如:
$ git config --global core.editor vim
查询现有配置
在这个例子中,我们将看看如何查询现有的配置并设置配置值。
准备工作
我们将再次使用jgit
,通过以下命令来操作:
$ cd jgit
如何操作...
你可以使用git config
查询你的本地和全局 Git 配置。在这一部分,我们将展示几个例子。
- 要查看当前 Git 仓库的所有有效配置,请运行以下命令:
$ git config --list
user.name=John Doe
user.email=john.doe@example.com
core.repositoryformatversion=0
core.filemode=false
core.bare=false
core.logallrefupdates=true
remote.origin.url=https://git.eclipse.org/r/jgit/jgit
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
之前的输出当然会反映运行命令的用户。输出中的名字和电子邮件将反映你的设置,而不是John Doe
。
- 如果我们只对单个配置项感兴趣,可以通过其
section.key
或section.subsection.key
来查询:
$ git config user.name
John Doe
$ git config remote.origin.url
https://git.eclipse.org/r/jgit/jgit
它是如何工作的...
Git 的配置存储在纯文本文件中,类似于键值存储。你可以通过键来设置/查询并获取值。以下是基于文本的配置文件示例(来自jgit
仓库):
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
[remote "origin"]
url = https://git.eclipse.org/r/jgit/jgit
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
还有更多内容...
设置配置值也很简单。你可以使用与查询配置时相同的语法,只不过需要在值后加上一个参数。要在LOCAL
层设置一个新的电子邮件地址,我们可以执行以下命令:
git config user.email john.doe@example.com
LOCAL
层是默认层,如果没有其他指定。如果值中需要空格,你可以像配置姓名时那样用引号将字符串括起来:
git config user.name "John Doe"
你甚至可以设置自己的配置,这对核心 Git 没有任何影响,但在脚本编写/构建等方面会很有用:
$ git config my.own.config "Whatever I need"
列出该值:
$ git config my.own.config
Whatever I need
删除/取消配置项也非常容易:
$ git config --unset my.own.config
列出该值:
$ git config my.own.config
模板
在这个例子中,我们将看到如何创建一个模板提交信息,这将在创建提交时显示在编辑器中。该模板仅适用于本地用户,而不会与仓库一起分发。
准备工作
在这个例子中,我们将使用第一章中的示例仓库,Navigating Git:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition.git
$ cd Git-Version-Control-Cookbook-Second-Edition
我们将使用以下命令作为提交信息的模板:
Short description of commit
Longer explanation of the motivation for the change Fixes-Bug: Enter bug-id or delete line
Implements-Requirement: Enter requirement-id or delete line
将提交信息模板保存在$HOME/.gitcommitmsg.txt
。文件名不是固定的,你可以选择任何你喜欢的文件名。
如何操作...
- 为了让 Git 知道我们新的提交信息模板,我们可以设置配置变量
commit.template
,指向我们刚刚创建的包含该模板的文件;我们将全局设置它,以便适用于我们所有的仓库:
$ git config --global commit.template $HOME/.gitcommitmsg.txt
- 现在,我们可以尝试修改一个文件,添加它,并创建一个提交。这将打开我们首选的编辑器,并预加载提交信息模板:
$ git commit
Short description of commit
Longer explanation of the motivation for the change
Fixes-Bug: Enter bug-id or delete line
Implements-Requirement: Enter requirement-id or delete line
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: another-file.txt
#
~
~
".git/COMMIT_EDITMSG" 13 lines, 396 characters
- 现在我们可以根据我们的提交编辑信息并保存,以完成提交。
它是如何工作的...
当commit.template
被设置时,Git 会将模板文件的内容作为所有提交信息的起点。如果你有提交信息的策略,这非常方便,因为它大大增加了遵循该策略的机会。你甚至可以为不同的仓库设置不同的模板,因为你可以在本地级别设置配置。
一个 .git 目录模板
有时候,单单拥有全局配置是不够的。你还需要触发脚本(也称为 Git 钩子)、排除文件等的执行。可以通过设置模板选项git init
来实现这一点。它可以作为命令行选项传递给git clone
和git init
,或者作为环境变量$GIT_TEMPLATE_DIR
,或者作为配置选项init.templatedir
。默认为/usr/share/git-core/templates
。模板选项通过将模板目录中的文件复制到.git ($GIT_DIR)
文件夹中来工作。默认目录包含示例钩子和一些建议的排除模式。在下面的示例中,我们将看到如何设置一个新的模板目录,并添加提交信息钩子和排除文件。
准备工作
首先,我们将创建模板目录。我们可以使用任何我们想要的名称,这里我们使用~/.git_template
,如以下命令所示:
$ mkdir ~/.git_template
现在,我们需要在目录中填充一些模板文件。可以是一个钩子文件或排除文件。我们将创建一个钩子文件和一个排除文件。钩子文件位于.git/hooks/name-of-hook
,排除文件位于.git/info/exclude
。创建所需的两个目录,hooks
和info
,如以下命令所示:
$ mkdir ~/.git_template/{hooks,info}
为了保留默认模板目录(Git 安装提供的)中的示例钩子,我们将默认模板目录中的文件复制到新目录中。当我们使用新创建的模板目录时,它会覆盖默认目录。所以,将默认文件复制到我们的模板目录中会确保,除了我们的特定更改外,模板目录与默认目录相似,如以下命令所示:
$ cd ~/.git_template/hooks
$ cp /usr/share/git-core/templates/hooks/* .
我们将使用commit-msg
钩子作为示例钩子:
#!/bin/sh
MSG_FILE="$1"
echo "\nHi from the template commit-msg hook" >> $MSG_FILE
这个钩子非常简单,它只会将Hi from the template commit-msg hook
添加到提交信息的末尾。将其保存为commit-msg
文件到~/.git_template/hooks
目录,并通过以下命令使其可执行:
chmod +x ~/.git_template/hooks/commit-msg
现在提交信息钩子已完成,我们还将向示例中添加一个排除文件。排除文件的作用类似于.gitignore
文件,但它不会被仓库跟踪。
我们将创建一个排除文件,排除所有*.txt
文件,如下所示:
$ echo "*.txt" > ~/.git_template/info/exclude
现在,我们的模板目录已准备就绪,可以使用了。
如何做...
- 我们的模板目录已准备好,并且可以按照前面描述的方式,作为命令行选项、环境变量,或者像这个例子一样,作为配置来设置:
$ git config --global init.templatedir ~/.git_template
- 现在,所有使用
init
或clone
创建的 Git 仓库将拥有模板目录的默认文件。我们可以通过创建一个新仓库来测试它是否有效,方法如下:
$ git init template-example
$ cd template-example
- 让我们尝试创建一个
.txt
文件,并查看git status
告诉我们什么。它应该会被模板目录中的排除文件忽略:
$ echo "this is the readme file" > README.txt
$ git status
排除文件生效了!你可以自己添加文件扩展名,或者直接留空并继续使用.gitignore
文件。
- 为了测试
commit-msg
钩子是否有效,我们来尝试创建一个提交。首先,我们需要一个文件来提交。接下来,我们按如下方式创建并提交它:
$ echo "something to commit" > somefile
$ git add somefile
$ git commit -m "Committed something"
- 现在,我们可以通过
git log
来查看历史记录:
$ git log -1
commit 1f7d63d7e08e96dda3da63eadc17f35132d24064
Author: John Doe <john.doe@example.com>
Date: Mon Jan 6 20:14:21 2014 +0100
Committed something
Hi from the template commit-msg hook
它是如何工作的...
当 Git 创建一个新仓库时,无论是通过init
还是clone
,它会在创建目录结构时将template
目录中的文件(默认位置是/usr/share/git-core/templates
)复制到新仓库中。模板目录可以通过命令行参数、环境变量或配置选项来定义。如果没有指定,将使用默认的模板目录(与 Git 安装一起分发)。通过将配置设置为--global
选项,所定义的模板目录将适用于所有用户(新的)仓库。这是一个很好的方式来在仓库之间分发相同的钩子,但它也有一些缺点。由于模板目录中的文件仅复制到 Git 仓库,因此对模板目录的更新不会影响现有的仓库。这可以通过在每个现有仓库中运行git init
来重新初始化仓库来解决,但这可能相当繁琐。另外,模板目录可能会强制某些仓库应用钩子,而你并不希望这样做。这个问题可以通过简单地删除该仓库中.git/hooks
目录里的钩子文件来轻松解决。
另请参阅
欲了解更多关于 Git 钩子的信息,请参考第七章,使用 Git 钩子、别名和脚本提升你的日常工作效率。
一些配置示例
核心 Git 系统中有配置目标。在本节中,我们将更详细地查看一些在日常工作中可能有用的配置目标。
我们将查看以下三个不同的配置区域:
-
重基与合并设置
-
对象过期
-
自动更正
准备就绪
在本练习中,我们将设置一些配置。我们将使用第一章《Navigating Git》中的数据模型仓库:
$ cd Git-Version-Control-Cookbook-Second-Edition
如何操作...
让我们仔细看看前面提到的配置区域。
重基与合并设置
默认情况下,当执行git pull
时,如果本地分支的历史与远程分支发生分歧,Git 将创建一个合并提交。然而,为了避免所有这些合并提交,可以配置仓库,使其在执行git pull
时默认使用重基(rebase)而不是合并。与此选项相关的几个配置目标如下:
pull.rebase
:当此配置设置为true
时,在执行git pull
时,会将当前分支拉取并重基到获取的分支上。也可以设置为preserve
,以便在重基时不会压平本地的合并提交,方法是将--preserve-merges
传递给git rebase
。默认值为false
,即该配置未设置。要在本地仓库中设置此选项,请运行以下命令:
$ git config pull.rebase true
-
branch.autosetuprebase
:当此配置设置为always
时,通过<git branch
或git checkout
创建的任何新分支,如果跟踪其他分支,将会设置为拉取时进行重基(而非合并)。有效的选项如下:-
never
:此设置用于拉取时默认执行重基(rebase)。 -
local
:此设置用于本地跟踪分支执行拉取时重基(rebase)。 -
remote
:此设置用于远程跟踪分支执行拉取时重基(rebase)。 -
always
:此设置用于所有被跟踪的分支执行拉取时重基(rebase)。 -
要为所有新分支设置此选项,无论其跟踪远程还是本地分支,请运行以下命令:
-
$ git config branch.autosetuprebase always
branch.<name>.rebase
:当此配置设置为true
时,仅适用于<name>
分支,指示 Git 在执行git pull
时进行重基。它也可以设置为preserve
,以便在执行git pull
时不会压平本地的合并提交。默认情况下,任何分支都未设置此配置。要将仓库中的feature/2
分支设置为默认重基,而非合并,可以运行以下命令:
$ git config branch.feature/2.rebase true
对象过期
默认情况下,Git 将在未引用的对象上执行垃圾回收,并清理reflog
中超过 90 天的条目。为了让某个对象被引用,必须有某些内容指向它;如树、提交、标签、分支,或者一些内部 Git 记录,例如stash
或reflog
。有三个设置可以用来更改这个时间,如下所示:
-
gc.reflogexpire
:这是了解分支历史在reflog
中保存时间的一般设置。默认时间为 90 天。该设置是一个时间长度,例如10 days
、6 months
,也可以通过值never
完全禁用。此设置可以通过在配置中提供模式来匹配refs
模式。gc.<pattern>.reflogexpire
:例如,此模式可以是/refs/remotes/*
,并且过期设置仅适用于这些 refs。 -
gc.reflogexpireunreachable
:此设置控制不属于当前分支历史的reflog
条目在仓库中的可用时间。默认值为30 days
,与前一个选项类似,它表示一个时间长度,或者设置为never
以关闭该设置。此设置可以像前一个选项一样,设置为匹配refs
模式。 -
gc.pruneexpire
:此选项告知git gc
修剪比设定时间更久的对象。默认值为2.weeks.ago
,并且值可以表示为相对日期,例如3.months.ago
。若要禁用宽限期,可以使用now
作为值。要仅在远程分支上设置非默认的过期日期,可以使用以下命令:
$ git config gc./refs/remote/*.reflogexpire never
$ git config gc./refs/remote/*.reflogexpireunreachable "2 months"
- 我们还可以设置一个日期,以便
git gc
更早地修剪对象:
$ git config gc.pruneexpire 3.days.ago
自动更正
当你因为键入错误而看到如下消息时,这个配置非常有用:
$ git statis
git: 'statis' is not a git command. See 'git --help'.
Did you mean this?
status
通过设置help.autocorrect
配置,你可以控制 Git 在意外输入错误时的行为。默认值为0
,表示列出与输入相似的可选项(例如输入statis
时,显示status
)。负值表示立即执行相应的命令。正值表示在执行命令之前等待指定的十分之一秒(0.1 秒),因此在这段时间内可以取消命令。如果可以从输入的文本中推断出多个命令,则什么也不会发生。将值设置为半秒,给你一些时间来取消错误的命令,如下所示:
$ git config help.autocorrect 5
$ git statis
WARNING: You called a Git command named 'statis', which does not exist.
Continuing under the assumption that you meant 'status'
in 0.5 seconds automatically...
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: another-file.txt
#
它的工作原理...
设置配置目标会改变 Git 的行为。前面的示例描述了几种常用的方法,使 Git 的行为不同于默认设置。在更改配置时,你应确保完全理解该配置的作用。因此,可以通过使用git help config
来查看 Git 配置帮助页面。
还有更多...
Git 中有许多可用的配置目标。你可以运行git help config
,所有配置项将显示并在几页中解释。
Git 别名
别名是配置长且/或复杂的 Git 命令以表示简短实用命令的好方法。别名只是别名部分下的一个配置项。通常配置为--global
,使其在任何地方都适用。
准备就绪
在这个示例中,我们将使用jgit
仓库,这也是在第一章《导航 Git》中使用的,master
分支指向b14a93971837610156e815ae2eee3baaa5b7a44b
。可以使用第一章《导航 Git》中的克隆,也可以重新克隆该仓库,如下所示:
$ git clone https://git.eclipse.org/r/jgit/jgit
$ cd jgit
$ git checkout master && git reset --hard b14a939
如何做到这一点...
- 首先,我们将创建一些简单的别名,然后创建几个更特殊的别名,最后创建几个使用外部命令的别名。我们可以为每次需要切换分支时创建一个别名,而不是每次都输入
git checkout
,并命名为git co
。我们可以对git branch
、git commit
和git status
做相同的操作,如下所示:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
- 现在,尝试在
jgit
仓库中运行git st
,如下所示:
$ git st
# On branch master
nothing to commit, working directory clean
alias
方法对于创建你认为 Git 缺失的命令也很有用。一个常见的 Git 别名是unstage
,它用于将文件从暂存区移出,如下所示:
$ git config --global alias.unstage 'reset HEAD --'
尝试编辑jgit
仓库根目录中的README.md
文件并将其添加到根目录中。
- 现在,
git status/git st
应该显示如下内容:
$ git st
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.md
#
- 让我们尝试取消暂存
README.md
,然后查看git st
,如下所示:
$ git unstage README.md
Unstaged changes after reset:
M README.md
$ git st
# On branch master
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: README.md
#
no changes added to commit (use "git add" and/or "git commit -a")
- 别名的一个常见用例是以特定方式格式化 Git 的历史记录。假设你希望在提交时显示每个文件的新增和删除行数,并附带一些常见的提交数据。为此,我们可以创建以下别名,这样每次就不必输入所有内容:
$ git config --global alias.ll "log --pretty=format:"%C(yellow)%h%Cred%d %Creset%s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --numstat"
- 现在,我们可以在终端中执行
git ll
命令并获得一个漂亮的统计输出,如下所示:
$ git ll
b14a939 (HEAD, master) Prepare 3.3.0-SNAPSHOT builds (8 days ago) <Matthias Sohn>
6 6 org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
1 1 org.eclipse.jgit.ant.test/pom.xml
3 3 org.eclipse.jgit.ant/META-INF/MANIFEST.MF
1 1 org.eclipse.jgit.ant/pom.xml
4 4 org.eclipse.jgit.archive/META-INF/MANIFEST.MF
2 2 org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
1 1 org.eclipse.jgit.archive/pom.xml
6 6 org.eclipse.jgit.console/META-INF/MANIFEST.MF
1 1 org.eclipse.jgit.console/pom.xml
12 12 org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
...
- 也可以使用外部命令代替 Git 命令。因此,小的 Shell 脚本等可以被嵌入。要使用外部命令创建
alias
方法,别名必须以感叹号!
开头。当解决 rebase 或 merge 冲突时,可以使用这些示例。在~/.gitconfig
文件的[alias]
下,添加以下内容:
editconflicted = "!f() {git ls-files --unmerged | cut -f2 | sort -u ; }; $EDITOR 'f'"
这将调出你配置的$EDITOR
,并显示由于合并/rebase 而处于冲突状态的所有文件。这使得你可以快速修复冲突并继续合并/rebase。
- 在
jgit
仓库中,我们可以在较早的时间点创建两个分支并合并这两个分支:
$ git branch A 03f78fc
$ git branch B 9891497
$ git checkout A
Switched to branch 'A'
$ git merge B
现在,你会看到这个操作无法执行合并,你可以运行git st
来检查很多处于冲突状态的文件的状态,both modified
。要打开并编辑所有冲突文件,我们可以运行git editconflicted
。这将使用$EDITOR
打开文件。如果你的环境变量未设置,可以使用EDITOR=<your-favorite-editor>
来设置它。
在这个示例中,我们实际上并不解决冲突。只需检查别名是否有效,然后准备好下一个别名。
- 现在,我们已经解决了所有的合并冲突,接下来是添加所有这些文件,在合并结束之前。幸运的是,我们可以创建一个
alias
方法来帮助我们实现,如下所示:
addconflicted = "!f() { git ls-files --unmerged | cut -f2 | sort -u ; }; git add 'f'"
- 现在,我们可以运行
git addconflicted
。稍后,git status
会告诉我们所有冲突的文件都已添加:
$ git st
On branch A
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: org.eclipse.jgit.console/META-INF/MANIFEST.MF
modified: org.eclipse.jgit.console/pom.xml
modified: org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
modified: org.eclipse.jgit.http.server/pom.xml
modified: org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
modified: org.eclipse.jgit.http.test/pom.xml
...
Now we can conclude the merge with git commit:
$ git commit
[A 94344ae] Merge branch 'B' into A
它是如何工作的...
Git 只是运行别名所代表的命令。这对长的 Git 命令或那些难以记住具体如何写的 Git 命令非常方便。现在,你只需要记住别名,并且可以随时查看配置文件来查找它。
还有更多...
创建一种 Git 别名的另一种方法是创建一个 shell 脚本,并将文件保存为git-<你的别名>
。使文件具有可执行权限,并将其放置在你的$PATH
中。现在,你只需通过从命令行运行git<你的别名>
即可运行该文件。
示例中的 refspec
尽管refspec
并不是想到 Git 配置时第一个想到的内容,但它实际上是非常接近的。在许多 Git 命令中都会使用refspec
,但通常是隐式使用的,即refspec
是从配置文件中获取的。如果你不记得设置过refspec
配置,可能是对的,但如果你克隆了仓库或添加了远程仓库,那么.git/config
文件中会有一个类似以下内容的部分(这是针对jgit
仓库的):
[remote "origin"]
url = https://git.eclipse.org/r/jgit/jgit
fetch = +refs/heads/*:refs/remotes/origin/*
fetch 行包含与此仓库相关的已配置refspec
。
准备工作
在这个例子中,我们将使用jgit
仓库作为我们的服务器仓库,但我们需要将其克隆到一个裸仓库中,以便可以推送。你不能推送到非裸仓库的已检出分支,因为这可能会覆盖工作区和索引。
从jgit
仓库创建一个裸仓库,并创建一个新的 Git 仓库,在其中我们可以操作refspec
,如下所示:
$ git clone --bare https://git.eclipse.org/r/jgit/jgit jgit-bare.git
$ git init refspec-tests
Initialized empty Git repository in /Users/john.doe/refspec-tests/.git/
$ cd refspec-tests
$ git remote add origin ../jgit-bare.git
我们还需要更改某些分支的分支名称,以匹配命名空间的示例;以下命令将stable-xxx
分支重命名为stable/xxx
:
$ for br in $(git branch -a | grep "stable-"); do new=$(echo $br| sed 's/-///'); git branch $new $br; done
在之前的 shell 脚本中,$new
和$br
变量没有放在双引号("
)中,虽然良好的 shell 脚本实践建议这样做。这是可以的,因为这些变量反映的是仓库中分支的名称,而分支名不能包含空格。
如何操作...
- 让我们设置新的仓库,只获取
master
分支。我们通过在配置文件(.git/config
)中更改[remote "origin"]
下的 fetch 行来实现,如下所示:
[remote "origin"]
url = ../jgit-bare.git
fetch = +refs/heads/master:refs/remotes/origin/master
- 现在,我们在执行
git fetch
、git pull
或git remote
更新 origin 时,只会获取master
分支,而不会获取其他分支,如下所示:
$ git pull
remote: Counting objects: 44033, done.
remote: Compressing objects: 100% (6927/6927), done.
remote: Total 44033 (delta 24063), reused 44033 (delta 24063)
Receiving objects: 100% (44033/44033), 9.45 MiB | 5.70 MiB/s, done.
Resolving deltas: 100% (24063/24063), done.
From ../jgit-bare
* [new branch] master -> origin/master
From ../jgit-bare
* [new tag] v0.10.1 -> v0.10.1
* [new tag] v0.11.1 -> v0.11.1
* [new tag] v0.11.3 -> v0.11.3
...
$ git branch -a
* master
remotes/origin/master
- 我们还可以设置一个单独的 refspec,将所有
stable/*
分支获取到本地仓库,具体如下:
[remote "origin"]
url = ../jgit-bare.git
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/stable/*:refs/remotes/origin/stable/*
- 现在,按如下命令在本地获取分支:
$ git fetch
From ../jgit-bare
* [new branch] stable/0.10 -> origin/stable/0.10
* [new branch] stable/0.11 -> origin/stable/0.11
* [new branch] stable/0.12 -> origin/stable/0.12
* [new branch] stable/0.7 -> origin/stable/0.7
* [new branch] stable/0.8 -> origin/stable/0.8
* [new branch] stable/0.9 -> origin/stable/0.9
* [new branch] stable/1.0 -> origin/stable/1.0
* [new branch] stable/1.1 -> origin/stable/1.1
* [new branch] stable/1.2 -> origin/stable/1.2
* [new branch] stable/1.3 -> origin/stable/1.3
* [new branch] stable/2.0 -> origin/stable/2.0
* [new branch] stable/2.1 -> origin/stable/2.1
* [new branch] stable/2.2 -> origin/stable/2.2
* [new branch] stable/2.3 -> origin/stable/2.3
* [new branch] stable/3.0 -> origin/stable/3.0
* [new branch] stable/3.1 -> origin/stable/3.1
* [new branch] stable/3.2 -> origin/stable/3.2
- 我们还可以设置一个推送(push)
refspec
,指定默认推送到哪个分支。让我们创建一个名为develop
的分支并提交一个更改,如以下命令所示:
$ git checkout -b develop
Switched to a new branch 'develop'
$ echo "This is the developer setup, read carefully" > readme-dev.txt
$ git add readme-dev.txt
$ git commit -m "adds readme file for developers"
[develop ccb2f08] adds readme file for developers
1 file changed, 1 insertion(+)
create mode 100644 readme-dev.txt
- 现在,让我们创建一个推送(push)
refspec
,将develop
分支的内容推送到远程integration/master
:
[remote "origin"]
url = ../jgit-bare.git
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/stable/*:refs/remotes/origin/stable/*
push = refs/heads/develop:refs/remotes/origin/integration/master
- 让我们将我们的提交推送到
develop
分支,如下所示:
$ git push
Counting objects: 4, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 345 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To ../jgit-bare.git
* [new branch] develop -> origin/integration/master
由于integration/master
分支在远程端不存在,因此它为我们创建了该分支。
它是如何工作的...
refspec
的格式为<source>:<destination>
。对于拉取(fetch)refspec
,这意味着<source>
是远程端的源,而<destination>
是本地的。对于推送(push)refspec
,<source>
是本地的,<destination>
是远程的。refspec
可以通过+
前缀来表示即使不是快进更新,ref
模式也可以被更新。refspec
模式中不能使用部分通配符,如以下行所示:
fetch = +refs/heads/stable*:refs/remotes/origin/stable*
然而,使用命名空间是可能的。这就是为什么我们必须将stable-xxx
分支重写为stable/xxx
以符合命名空间模式的原因:
fetch = +refs/heads/stable/*:refs/remotes/origin/stable/*
第三章:分支、合并和选项
在本章中,我们将介绍以下几种方法:
-
管理你的本地分支
-
带有远程的分支
-
强制合并提交
-
使用 Git 重用已记录的解决方案(rerere)来合并 Git 冲突
-
计算分支之间的差异
-
孤立分支
介绍
如果你在大公司作为开发者开发一个小型应用,或者你正在尝试理解一个来自 GitHub 的开源项目,你已经在使用 Git 的分支功能了。
大多数时候,你可能只是一直在本地的开发分支或主分支上工作,因此没有太在意其他分支。
在本章中,我们将展示不同类型的分支以及如何使用它们。
管理你的本地分支
假设你只有本地 Git 仓库,并且目前没有分享代码给其他人的计划;不过,你仍然可以轻松地将你在与仓库合作时所获得的知识分享给一个或多个远程仓库。没有远程的本地分支正是这样工作的。如你所见,在示例中,我们正在克隆一个仓库,因此我们有一个远程。
我们从创建几个本地分支开始。
准备工作
使用以下命令克隆 jgit
仓库以进行匹配:
$ git clone https://git.eclipse.org/r/jgit/jgit
$ cd jgit
如何操作...
执行以下步骤来管理你的本地分支:
- 每当你开始修复 Bug 或开发新特性时,都应该创建一个分支。你可以使用以下代码做到这一点:
$ git branch newBugFix
$ git branch
* master
newBugFix
newBugFix
分支指向创建时你所处的当前HEAD
。你可以通过git log -1
查看HEAD
:
$ git log -1 newBugFix --format=format:%H
25fe20b2dbb20cac8aa43c5ad64494ef8ea64ffc
- 如果你想为分支添加描述,可以使用
git branch
命令的--edit-description
选项来做到这一点:
$ git branch --edit-description newBugFix
- 上一个命令会打开一个编辑器,你可以在其中输入描述:
Refactoring the Hydro controller
The hydro controller code is currently horrible needs to be refactored.
- 关闭编辑器后,消息将被保存。
它是如何工作的...
Git 会将信息存储在本地的 git config
文件中;这也意味着你无法将这些信息推送到远程仓库。
要检索分支的描述,你可以使用 git config
命令的 --get
标志:
$ git config --get branch.newBugFix.description
Refactoring the Hydro controller
The hydro controller code is currently horrible and needs to be refactored.
这将在我们在 第七章 中自动化一些任务时非常有帮助,通过 Git Hooks、别名* 和脚本来提升你的日常工作*。
记得在开始工作之前先切换到 newBugFix
分支。这必须通过 Git 的 checkout
命令来完成。如果你着急,也可以通过一个命令来创建并切换到新分支。只需在 checkout
命令中加上 -b
选项。
分支信息会以文件的形式存储在 .git/refs/heads/newBugFix
中:
$ cat .git/refs/heads/newBugFix
25fe20b2dbb20cac8aa43c5ad64494ef8ea64ffc
请注意,这是我们通过 git log
命令获取的相同提交哈希。
还有更多内容...
也许你想从特定的提交哈希值创建特定的分支。你可能首先想到的是检出该提交,然后创建一个分支;然而,使用git branch
命令直接创建分支而不检出提交要简单得多:
- 如果你需要从特定的提交哈希值创建一个分支,可以使用
git branch
命令如下:
$ git branch anotherBugFix 979e346
$ git log -1 anotherBugFix --format=format:%h
979e346
$ git log -1 anotherBugFix --format=format:%H
979e3467112618cc787e161097986212eaaa4533
- 如你所见,当你使用
%h
时,显示的是简化的提交哈希,而当你使用%H
时,显示的是完整的提交哈希。你可以看到,简化的提交哈希与用于创建分支的哈希相同。大多数情况下,你希望立即创建并开始在分支上工作:
$ git checkout -b lastBugFix 979e346
Switched to a new branch 'lastBugFix'
- Git 会在创建分支后立即切换到新分支。可以使用
gitk
验证是否已经切换到lastBugFix
分支,并且另一个BugFix
分支处于相同的提交哈希值:
$ gitk
这可以通过以下截图来展示:
- 不使用 Gitk 时,你也可以在
git branch
命令中添加-v
,甚至可以再加一个-v
:
$ git branch -v
anotherBugFix 979e346 Interactive Rebase: Do actions if
* lastBugFix 979e346 Interactive Rebase: Do actions if
master 25fe20b Add missing package import for jg
newBugFix 25fe20b Add missing package import for jg
- 使用
-v
,你可以查看每个分支的简化提交哈希,而使用-vv
,你还可以看到master
分支跟踪origin/master
分支:
$ git branch -vv
anotherBugFix 979e346 Interactive Rebase: Do actions if e
* lastBugFix 979e346 Interactive Rebase: Do actions if e
master 25fe20b [origin/master] Add missing package
newBugFix 25fe20b Add missing package import for g
带有远程仓库的分支
在某个时刻,很可能你已经克隆了别人的仓库。这意味着你有一个关联的远程仓库。这个远程仓库通常被称为origin
,因为它是源代码的来源。
在使用 Git 和远程仓库时,你将从 Git 中获得一些好处。
我们可以从git status
开始,看看在与远程仓库交互时我们得到的结果。
准备就绪
按照以下步骤操作:
- 我们将首先检查一个跟踪远程分支的本地分支:
$ git checkout -b remoteBugFix --track origin/stable-3.2
Branch remoteBugFix set up to track remote branch stable-3.2 from origin.
Switched to a new branch 'remoteBugFix'
-
上一个命令创建并切换到
remoteBugFix
分支,该分支将跟踪origin/stable-3.2
分支。因此,例如,执行git status
将自动显示你的分支与origin/stable-3.2
的差异,并且它还会显示你的分支HEAD
是否可以快速前移到远程分支的HEAD
。 -
为了提供一个如何执行前一步骤的示例,我们需要做一些手动操作来模拟这种情况。首先,我们找到一个提交:
$ git log -10 origin/stable-3.2 --oneline
f839d383e (HEAD -> remoteBugFix, origin/stable-3.2) Prepare post 3.2.0 builds
699900c30 (tag: v3.2.0.201312181205-r) JGit v3.2.0.201312181205-r
0ff691cdb Revert "Fix for core.autocrlf=input resulting in modified file..."
1def0a125 Fix for core.autocrlf=input resulting in modified file and unsmudge
0ce61caef Canonicalize worktree path in BaseRepositoryBuilder if set via config
be7942f2b Add missing @since tags for new public methods in Config
ea04d2329 Don't use API exception in RebaseTodoLine
3a063a0ed Merge "Fix aborting rebase with detached head" into stable-3.2
e90438c0e Fix aborting rebase with detached head
2e0d17885 Add recursive variant of Config.getNames() methods
- 该命令将列出
stable-3.2
分支的最后 10 个提交,这些提交来自远程的origin
。--oneline
选项将显示简化的提交哈希和提交主题。对于这个例子,我们将使用以下提交:
$ git reset --hard 2e0d178
HEAD is now at 2e0d178 Add recursive variant of Config.getNames() methods
- 这将重置
remoteBugFix
分支到2e0d178
提交哈希值。现在我们已经准备好继续使用 Git 的免费功能,当我们有远程跟踪分支时。
我们正在重置到一个可以从origin/stable-3.2
远程跟踪分支访问的提交;这样做是为了模拟我们已经执行了 Git fetch 并下载了origin/stable-3.2
分支的新提交。
如何操作...
在这里,我们将尝试一些命令,帮助你在有远程跟踪分支时操作:
- 从执行
git status
开始:
$ git status
On branch remoteBugFix
Your branch is behind 'origin/stable-3.2' by 9 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working directory clean
当你有一个跟踪分支并使用git status
时,Git 会非常详细地描述当前状态。
从消息中你可以看到,可以使用git pull
来更新你的本地分支,我们将在下一个示例中尝试这个。消息说它可以进行快进合并。这意味着 Git 可以在不进行合并的情况下推进HEAD
。现在,我们将执行合并操作:
git pull
命令实际上是一个git fetch
命令,接着是一个与远程跟踪分支的git merge
命令。
$ git merge origin/stable-3.2
Updating 2e0d178..f839d38
Fast-forward
.../org/eclipse/jgit/api/RebaseCommandTest.java | 213 +++++++++++
.../src/org/eclipse/jgit/api/RebaseCommand.java | 31 +--
.../jgit/errors/IllegalTodoFileModification.java | 59 ++++++
.../eclipse/jgit/lib/BaseRepositoryBuilder.java | 2 +-
.../src/org/eclipse/jgit/lib/Config.java | 2 +
.../src/org/eclipse/jgit/lib/RebaseTodoLine.java | 16 +-
6 files changed, 266 insertions(+), 57 deletions(-)
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/errors/IllegalTodoFileModification.java
-
从输出中你可以看到,这是一个快进合并,就像 Git 预测的那样。
git status
的输出。
还有更多...
你还可以向现有分支添加远程,这在你意识到其实想要一个远程跟踪分支,但在创建分支时忘记添加跟踪信息时非常有用:
- 从
2e0d17
提交开始创建一个本地分支:
$ git checkout -b remoteBugFix2 2e0d17
Switched to a new branch 'remoteBugFix2'
remoteBugFix2
分支目前只是一个本地分支,没有跟踪信息;要设置跟踪分支,我们需要使用--set-upstream-to
或-u
作为git branch
命令的标志:
$ git branch --set-upstream-to origin/stable-3.2
Branch remoteBugFix2 set up to track remote branch stable-3.2 from origin.
- 从 Git 输出中可以看到,我们现在正在跟踪
origin
的stable-3.2
分支:
$ git status
On branch remoteBugFix2
Your branch is behind 'origin/stable-3.2' by 9 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working directory clean
- 从 Git 输出中你可以看到你领先了九次提交,你可以使用
git pull
来更新分支。记住,git pull
命令实际上是一个git fetch
命令,接着是一个与上游分支的git merge
命令,我们也称之为远程跟踪分支:
$ git pull
Updating 2e0d17885..f839d383e
Fast-forward
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java | 31 ++++++++------
org.eclipse.jgit/src/org/eclipse/jgit/errors/IllegalTodoFileModification.java | 59 +++++++++++++++++++++++++++
org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java | 2 +-
org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java | 2 +
org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java | 16 ++++----
6 files changed, 266 insertions(+), 57 deletions(-)
create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/errors/IllegalTodoFileModification.java
- 从输出中可以看到,分支已被快进到
f839d383e
提交哈希,相当于origin/stable-3.2
。你可以使用git log
验证这一点:
$ git log -1 origin/stable-3.2 --format=format:%h
f839d383e
强制合并提交
在阅读本书之前,你可能已经见过很多关于软件交付链和分支模型的基本示例。很可能你已经尝试过使用不同的策略,但发现没有一种完全支持你的场景,这完全没问题,只要工具能够支持你特定的工作流。
Git 支持几乎所有的工作流。我们经常遇到在合并功能时需要一个合并提交的情况,尽管可以使用快进合并来完成。那些请求合并提交的人通常是为了表明你确实合并了某个功能,并希望将该信息存储在仓库中。
Git 可以快速访问所有的提交信息,因此代码仓库应当作为日志使用,而不仅仅是源代码的备份。
准备工作
从origin/stable-3.1
跟踪的本地分支remoteOldbugFix
开始:
$ git checkout -b remoteOldBugFix --track origin/stable-3.1
Branch remoteOldBugFix set up to track remote branch stable-3.1 from Switched to a new branch 'remoteOldBugFix'
如何操作...
以下步骤将向你展示如何强制进行合并提交:
- 要强制进行合并提交,你需要使用
--no-ff
标志;no-ff表示不进行快进合并。我们还将使用--quiet
标志来最小化输出,并使用--edit
来允许我们编辑提交信息。除非你有合并冲突,否则 Git 会自动为你创建合并提交:
$ git merge origin/stable-3.2 --no-ff --edit --quiet
Auto-merging org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDat
Removing org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GCTe
Auto-merging org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.target
-
提交消息编辑器将会打开,你可以写下提交消息。关闭编辑器后会创建合并提交,完成操作。
-
为了验证这一点,你可以重置回
origin/stable-3.1
并在没有--no-ff
标志的情况下进行合并:
$ git reset --hard remotes/origin/stable-3.1
HEAD is now at da6e87b Prepare post 3.1.0 builds
- 现在,使用以下命令执行合并:
$ git merge origin/stable-3.2 --quiet
- 你可以通过 Gitk 查看差异。以下截图显示了快速前进合并;如你所见,我们的
remoteOldBugFix
分支指向origin/stable-3.2
:
- 下一张截图展示了我们强制 Git 创建的合并提交。我们的分支
remoteOldBugFix
领先于remotes/origin/stable-3.2
,然后我们进行了提交:
还有更多...
尽管大多数分支场景期望你完全合并分支,但在实际环境中,仍然有一些情况,只需要将一个分支中的特定部分合并到另一个分支中。使用--no-commit
选项,Git 将会执行合并并在提交前停止,允许你在提交之前修改并添加文件到合并提交中。
例如,我们一直在处理这样的项目:字符串版本在功能分支中更新了,但在主分支中没有更新。因此,自动合并到主分支时,将会替换主分支当前使用的版本字符串,而这在本例中并非我们的意图。在接下来的示例中,我们将使用一个简单的 Git 仓库,里面有几个提交和文件:
- 首先,检查出一个本地的
remotePartlyMerge
分支,该分支跟踪origin/release/1.0
:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model $ git checkout -b remotePartlyMerge --track origin/release/1.0 Branch remotePartlyMerge set up to track remote branch release/1.0 from origin.
Switched to a new branch 'remotePartlyMerge'
- 然后,为了创建合并并允许你决定哪些内容将成为提交的一部分,你可以使用
--no-commit
选项:
$ git merge origin/master --no-ff --no-commit
Automatic merge went well; stopped before committing as requested
-
再次强调,Git 提供了非常详细的信息;从输出中可以看到,一切正常,并且 Git 在提交之前按照要求停止了。接下来,假设我们不希望
LICENSE
文件成为合并提交的一部分。为了实现这一点,我们使用
git reset <path>
命令重置了目录:
$ git reset LICENSE
- 从输出中你可以看到,重置后有未暂存的更改;这正是我们想要的。你可以通过运行
git status
查看有哪些未暂存的更改。现在,我们将完成合并:
$ git commit -m "Merging without LICENSE"
[remotePartlyMerge f138175] Merging without LICENSE
- 合并提交已完成。如果现在运行
git status
命令,你仍然会看到工作区中有未暂存的更改。为了验证结果是否如预期,我们可以使用git diff
计算差异,显示文件与origin/master
分支上的文件一致,排除LICENSE
文件:
$ git diff origin/master !(LICENSE)
diff
没有任何输出,这是预期的结果。我们告诉diff
命令将当前的HEAD
提交与分支origin/master
进行diff
,并且我们不关心LICENSE
中的差异。
如果你没有指定HEAD
,你将会与当前的WA
进行diff
,并且由于有未暂存的更改,diff
命令会产生很多输出。
使用 Git 重用已记录的解决方案(rerere)来合并 Git 冲突
在处理功能分支时,你可能喜欢每天合并一次,甚至更频繁。但是,当你在长期存在的功能分支上工作时,你会发现自己处于一个不断出现相同冲突的局面。
在这里,你可以使用git rerere
,它代表重用记录的解决方案。git rerere
默认是未启用的,但可以通过以下命令启用:
$ git config rerere.enabled true
你可以通过将--global
添加到git config
命令来全局配置它。
怎么做...
执行以下步骤来合并已知的冲突:
- 在
jgit
仓库文件夹中,首先检出一个跟踪origin/stable-2.2
的分支:
$ git checkout -b rerereExample --track origin/stable-2.2
- 现在,将 maven-compiler-plugin 版本更改为个性化的版本,比如 2.5.2,因为这是
pom.xml
中的第 211 行。如果你运行git diff
,你应该会看到类似下面的结果:
$ git diff
diff --git a/pom.xml b/pom.xml
index 085e00f..d5aec17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -208,7 +208,7 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
- <version>2.5.1</version>
+ <version>2.5.2</version>
</plugin>
<plugin>
- 现在添加文件并创建一个提交:
$ git add pom.xml
$ git commit -m "Update maven-compiler-plugin to 2.5.2"
[rerereExample d474848] Update maven-compiler-plugin to 2.5.2
1 file changed, 1 insertion(+), 1 deletion(-)
- 将当前提交存储在一个名为
rerereExample2
的备份分支中:
$ git branch rerereExample2
在这里,git branch rerereExample2
只是将当前提交存储为一个分支,因为我们需要用它来做第二个 rerere 示例。
- 现在,我们需要执行第一次合并,这次合并将会在自动合并时失败。然后我们可以解决这个问题。解决后,我们可以复用合并的解决方案,以后遇到相同的问题时可以再次使用:
$ git merge --no-ff v3.0.2.201309041250-rc2
A lot of output ...
Automatic merge failed; fix conflicts and then commit the result.
- 由于我们启用了
git rerere
,我们可以使用git rerere status
来查看哪些文件或路径将被记录:
$ git rerere status
pom.xml
- 编辑
pom.xml
文件(大约在第 229 行),解决合并冲突,使其得到如下所示的diff
输出。你需要删除包含 3.1 的行和合并标记:
合并标记是以<<<<<<
、>>>>>>
或======
开头的行;这些行标记了 Git 无法自动合并的地方。
$ git diff v3.0.2.201309041250-rc2 pom.xml
diff --git a/pom.xml b/pom.xml
index 60cb0c8..faa7618 100644
--- a/pom.xml
+++ b/pom.xml
@@ -226,7 +226,7 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
- <version>3.1</version>
+ <version>2.5.2</version>
</plugin>
<plugin>
- 使用
git add
将pom.xml
添加到暂存区,然后运行git commit
来完成合并,标记合并为完成:
$ git commit
Recorded resolution for 'pom.xml'.
[rerereExample 9b8725f] Merge tag 'v3.0.2.201309041250-rc2' into rerereExample
-
注意 Git 记录的
pom.xml
输出的解决方案;如果没有启用git rerere
,这里是不会出现的。Git 已经记录了这个特定合并冲突的解决方案,并且会记录如何解决这个冲突。现在,我们可以尝试将更改进行rebase
到另一个分支。 -
从我们的仓库中检出
rerereExample2
分支:
$ git checkout rerereExample2
Switched to branch 'rerereExample2'
- 尝试将你的更改基于
origin/stable-3.2
分支进行rebase
:
$ git rebase origin/stable-3.2
First, rewinding head to replay your work on top of it...
Applying: Update maven-compiler-plugin to 2.5.2
Using index info to reconstruct a base tree...
M pom.xml
Falling back to patching base and 3-way merge...
Auto-merging pom.xml
CONFLICT (content): Merge conflict in pom.xml
Resolved 'pom.xml' using previous resolution.
error: Failed to merge in the changes.
Patch failed at 0001 Update maven-compiler-plugin to 2.5.2
The copy of the patch that failed is found in: .git/rebase-apply/patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
- 你应该注意到以下输出:
CONFLICT (content): Merge conflict in pom.xml
Resolved 'pom.xml' using previous resolution
- 由于合并冲突发生在
pom.xml
中,Git 可以自动解决该文件中的冲突。你可以通过打开文件并看到没有合并标记来清楚地看到这一点,因为 Git 已应用记录的解决方案。完成合并后,添加pom.xml
并继续rebase
:
$ git add pom.xml
$ git rebase --continue
Applying: Update maven-compiler-plugin to 2.5.2
- 启动 Gitk 以查看提交是否已经在
origin/stable-3.2
分支之上进行了rebase
:
你可以尝试相同的场景进行合并,Git 将自动为你合并该文件。
还有更多...
当你经常合并不同的分支时,如果你不确定某个特定错误修复属于哪个分支,实际上很容易找出:
- 你需要找到一个提交,然后使用
git branch
命令的--contains
标志获取这个信息:
$ git branch --contains 699900c308
anotherBugFix
lastBugFix
master
newBugFix
remoteBugFix
remoteBugFix2
remoteOldbugFix
* rerereExample2
- 上述命令列出了所有包含特定提交的分支。如果省略提交参数(
8e2886897
),Git 会检查HEAD
。例如,检出rerereExample2
分支并执行以下命令,你会看到该提交只出现在该分支上:
$ git checkout rerereExample2
Switched to branch 'rerereExample2'
$ git branch -a --contains
* rerereExample2
-a
选项表示你希望检查所有的远程分支。如果省略该选项,它只会检查本地分支。
然而,正如你所看到的,我们的提交不在任何远程分支上,因为该提交刚刚在本地创建,并且尚未推送到任何远程仓库。
你可以在使用 git branch -a --contains
命令时使用标签、分支名称或提交哈希。
- 让我们尝试查看
v2.3.0.201302130906
标签存在的分支:
$ git branch -a --contains v2.3.0.201302130906
anotherBugFix
lastBugFix
master
newBugFix
remoteBugFix
remoteBugFix2
remoteOldbugFix
remotePartlyMerge
* rerereExample2
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/origin/stable-2.3
remotes/origin/stable-3.0
remotes/origin/stable-3.1
remotes/origin/stable-3.2
... and many more
该标签可以在许多分支中找到。
计算分支之间的差异
在合并之前,检查分支之间的差异可以提供有价值的信息。
一个常规的 git diff
可以显示两个分支之间的所有差异信息,但它可能非常冗长,也许你只关心某一个文件。因此,你不需要查看长的统一差异。
准备开始
首先,我们决定想要查看差异的两个分支、标签或提交。然后,为了列出这两个分支之间有变化的文件,你可以使用 --name-only
标志。
如何操作...
执行以下步骤以查看分支之间的差异:
- 将
origin/stable-3.1
与origin/stable-3.2
分支进行差异比较:
$ git diff --name-only origin/stable-3.1 origin/stable-3.2 org.eclipse.jgit/src/org/eclipse/jgit/transport/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetch
More output..
-
我们按照这个模式构建命令,即
git diff [options] <commit> <commit> <path>
。然后,我们可以在查看分支之间的差异时,专注于我们关心的部分。这对于负责源代码某一子集的人非常有用,你只需查看该区域的差异。 -
让我们尝试在两个分支之间执行相同的差异比较,但这次我们将对整个分支进行比较,而不仅仅是一个子目录;不过,我们只想显示分支之间被删除或添加的文件。通过使用
--diff-filter=DA
和--name-status
选项可以做到这一点。--name-status
选项只会显示文件名和变化类型,--diff-filter=DA
选项只会显示删除和添加的文件:
$ git diff --name-status --diff-filter=DA origin/stable-3.1 origin/stable-3.2
A org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target
A org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
A org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java
A org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
A org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
A org.eclipse.jgit.test/tst/org/eclipse/jgit/api/Sets.java
D org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GCTest.java
More output..
-
这显示了在从
origin/stable-3.1
到origin/stable-3.2
的过程中被添加和删除的文件。 -
如果我们交换分支,如以下命令所示,我们将得到相反的结果:
$ git diff --name-status --diff-filter=DA origin/stable-3.2 origin/stable-3.1
D org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target
D org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
D org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java
D org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
D org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
D org.eclipse.jgit.test/tst/org/eclipse/jgit/api/Sets.java
A org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GCTest.java
More output..
请注意,字母 A 和 D 交换了位置,因为现在我们想知道如果从 origin/stable-3.2
切换到 origin/stable-3.1
会发生什么。
孤立分支
现在你已经熟悉了 Git 的数据模型——有向无环图(DAG)。你已经看到对象有一个父对象。当你创建一个新分支时,提交就是它的父对象。然而,在某些情况下,拥有一个没有父分支的分支是非常有用的。
一个例子是,当你有两个独立的代码库,但出于某种原因,现在想要将它们合并到一个库中。一种方法是直接复制文件并将其添加到其中一个仓库,但缺点是你会丢失历史记录。第二种方法是使用孤立分支,它可以帮助你将一个仓库的内容拉取到另一个仓库中。
准备工作
其实创建孤立分支并不难。使用--orphan
标志与checkout
命令就可以实现。执行方法如下:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition.git
$ cd Git-Version-Control-Cookbook-Second-Edition $ git checkout --orphan fresh-start
Switched to a new branch 'fresh-start'
如何操作...
- 现在我们有一个没有父分支的分支。你可以通过检查提交日志来验证这一点,如下所示:
$ git log
fatal: your current branch 'fresh-start' does not have any commits yet
全新开始
并不意味着你从头开始。已经添加到仓库中的文件和目录仍然存在:
$ ls
README.md a_sub_directory another-file.txt cat-me.txt hello_world.c
$ git status
On branch fresh-start
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
new file: a_sub_directory/readme
new file: another-file.txt
new file: cat-me.txt
new file: hello_world.c
- 如果你需要一个全新的开始,可以删除文件(但记得不要删除
.git
),方法如下:
$ git rm --cached README.md a_sub_directory/readme another-file.txt cat-me.txt hello_world.c
$ rm -rf README.md a_sub_directory another-file.txt cat-me.txt hello_world.c
$ git status
On branch fresh-start
No commits yet
nothing to commit (create/copy files and use "git add" to track)
- 你有一个没有文件和提交的分支。此外,该分支与
master
分支没有任何提交历史记录。你可以通过git remote add
和git fetch
将另一个仓库添加到该分支并拉取所有提交记录。但我们将通过简单地添加一个文本文件来说明这一点,如下所示:
$ echo "This is from an orphan branch." > orphan.txt
$ git add orphan.txt
$ git commit -m "Orphan"
提交是你可以通过git log
命令验证的历史记录中的唯一内容。如果你将另一个仓库拉取到该分支中,你将看到所有的提交记录,最重要的是,你将拥有该仓库历史的副本。
- 一旦你在孤立分支上完成提交,就该将它们合并到主分支上了。然而,你的第一次尝试会失败。例如,检查如下内容:
$ git checkout master
$ git merge fresh-start
fatal: refusing to merge unrelated histories
- 正如你所见,孤立分支与主分支没有共享历史记录,git 将不允许你合并该分支。这不应该让你感到意外,因为孤立分支的基本特征就是如此。不过,你仍然可以通过允许合并不相关的历史记录来合并孤立分支:
$ git merge fresh-start --allow-unrelated-histories
$ git log -3
commit aa804347c728552f7ce9298a83ab646148078dab (HEAD -> master)
Merge: 13dcada 45d1798
Author: John Doe <john.doe@example.com>
Date: Fri May 11 08:57:45 2018 +0200
Merge branch 'fresh-start'
commit 45d179838f8f9f8fd64c6c7bf96147e09ceadbc2 (fresh-start)
Author: John Doe <john.doe@example.com>
Date: Fri May 11 08:57:22 2018 +0200
Orphan
commit 13dcada077e446d3a05ea9cdbc8ecc261a94e42d (origin/master, origin/HEAD)
Author: John Doe <john.doe@example.com>
Date: Fri Dec 13 12:26:00 2013 +0100
This is the subject line of the commit message
... and more output
虽然你不太可能每天都使用孤立分支,但当你需要重组代码库时,它是一个很强大的功能,值得掌握。
还有更多内容...
Git 的帮助文件中有更多选项。只需运行git merge --help
或git branch --help
即可查看其他可用的选项。
第四章:定期和交互式变基,及其他使用场景
在本章中,我们将介绍以下操作:
-
将提交变基到另一个分支
-
继续处理合并冲突的变基操作
-
交互式变基选定提交
-
使用交互式变基压缩提交
-
使用变基更改提交的作者
-
自动压缩提交
介绍
变基是一个非常强大的 Git 功能。希望你之前已经使用过它;如果没有,你也许听说过它。变基正如它的字面意思。如果你有一个基于提交B
的提交A
,那么将A
变基到C
,最终结果就是提交A
基于提交C
。
正如你将在本章的不同示例中看到的那样,情况并不总是那么简单。
将提交变基到另一个分支
首先,我们将执行一个非常简单的变基操作,在这个过程中,我们将引入一个新文件,提交该文件,对其进行修改,然后再次提交,这样我们最终会得到两个新的提交。
准备工作
在开始之前,我们需要一个可以操作的仓库。你可以使用之前克隆的jgit
仓库,但为了得到与示例尽可能相同的输出,你可以克隆jgit
仓库。
jgit
仓库可以通过以下方式克隆:
$ git clone https://git.eclipse.org/r/jgit/jgit chapter4
$ cd chapter4
如何操作...
我们首先创建一个本地分支,然后通过以下步骤进行两个提交;这些提交是我们希望变基到另一个分支上的:
- 检出一个新的分支
rebaseExample
,它跟踪origin/stable-3.1
:
$ git checkout -b rebaseExample --track origin/stable-3.1
Branch rebaseExample set up to track remote branch stable-3.1 from origin.
Switched to a new branch 'rebaseExample'
- 在
rebaseExample
分支上做两个提交,如下所示:
$ echo "My Fishtank
Gravel, water, plants
Fish, pump, skeleton" > fishtank.txt
$ git add fishtank.txt
$ git commit -m "My brand new fishtank"
[rebaseExample 4b2c2ec] My brand new fishtank
1 file changed, 4 insertions(+)
create mode 100644 fishtank.txt
$ echo "mosquitos" >> fishtank.txt
$ git add fishtank.txt
$ git commit -m "Feeding my fish"
[rebaseExample 2132d88] Feeding my fish
1 file changed, 1 insertion(+)
- 然后,我们将把更改变基到
origin/stable-3.2
分支上:
$ git rebase origin/stable-3.2
First, rewinding head to replay your work on top of it...
Applying: My brand new fishtank
Applying: Feed the fish
工作原理...
当你执行git rebase
时,Git 首先会查找当前HEAD
分支和你希望变基的目标分支的公共祖先。当 Git 找到merge-base
时,它会查找那些在目标分支中不可用的提交。Git 会逐个应用这些提交。
继续处理合并冲突的变基操作
当你将一个提交或分支变基到另一个HEAD
上时,你可能最终会遇到冲突。
如果发生冲突,系统会要求你解决合并冲突,然后使用git rebase --continue
继续变基操作。
如何操作...
我们将创建一个提交,将相同的fishtank.txt
文件添加到origin/stable-3.1
分支上;然后,我们将尝试将它变基到我们在将提交变基到另一个分支部分中创建的rebaseExample
分支上:
- 检出名为
rebaseExample2
的分支,它跟踪origin/stable-3.1
:
$ git checkout -b rebaseExample2 --track origin/stable-3.1
Checking out files: 100% (212/212), done.
Branch rebaseExample2 set up to track remote branch stable-3.1 from origin.
Switched to a new branch 'rebaseExample2'
- 在分支上进行提交:
$ echo "My Fishtank
Pirateship, Oister shell
Coconut shell
">fishtank.txt
$ git add fishtank.txt
$ git commit -m "My brand new fishtank"
[rebaseExample2 39811d6] My brand new fishtank
1 file changed, 4 insertions(+)
create mode 100644 fishtank.txt
- 尝试将分支变基到
rebaseExample
分支上:
$ git rebase rebaseExample
First, rewinding head to replay your work on top of it...
Applying: My brand new fishtank
Using index info to reconstruct a base tree...
<stdin>:12: new blank line at EOF.
+
warning: 1 line adds whitespace errors.
Falling back to patching base and 3-way merge...
Auto-merging fishtank.txt
CONFLICT (add/add): Merge conflict in fishtank.txt
Failed to merge in the changes.
Patch failed at 0001 My brand new fishtank
The copy of the patch that failed is found in:
/Users/JohnDoe/repos/chapter4/.git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
- 你可以在你喜欢的编辑器中解决冲突。然后,使用
git add
将文件添加到索引,并继续进行变基操作。
$ git add fishtank.txt
$ git rebase --continue
Applying: My brand new fishtank
- 现在,我们可以使用
gitk
检查我们的更改是否已经变基到rebaseExample
分支上,如下图所示:
工作原理...
正如我们在第一个例子中所学到的,Git 会应用那些在你重新基准的目标分支中不存在的提交。在我们的例子中,只有我们自己创建的提交存在于rebaseExample2
分支上。
还有更多内容…
你可能在失败的重新基准输出中注意到,你有两个额外的选项来处理提交。
当你解决了这个问题后,运行git rebase --continue
。如果你希望跳过这个补丁,可以运行git rebase --skip
。要检出原始分支并停止重新基准,运行git rebase --abort
。
我们的第一个额外选项是完全忽略这个补丁并跳过它;你可以使用git rebase --skip
来实现。在我们的例子中,这将导致我们的分支快进到rebaseExample
分支。因此,我们的两个分支将指向相同的提交哈希值。
第二个选项是中止重新基准。如果我们选择这样做,我们将返回到重新基准开始之前的分支状态。可以使用git rebase --abort
来实现。
交互式地重新基准选定的提交
当你在开发新特性并从旧版本分支出来时,你可能希望将此分支重新基准到最新的发布版本。当查看特性分支上的提交列表时,你可能会发现一些提交不适用于新的发布版本。在这种情况下,当你想要将分支重新基准到新版本时,你需要删除一些提交。这可以通过交互式重新基准来实现,Git 会给你选择你希望重新基准的提交的选项。
准备工作
要开始这个例子,你需要检查之前创建的分支rebaseExample
;如果你没有这个分支,可以按照将提交重新基准到另一个分支部分的步骤操作,并使用以下命令:
$ git checkout rebaseExample
Switched to branch 'rebaseExample'
Your branch is ahead of 'origin/stable-3.1' by 109 commits.
(use "git push" to publish your local commits)
请注意,因为我们正在跟踪origin/stable-3.1
,所以 Git 检出会告诉我们与该分支相比,我们超前了多少。
如何操作…
我们将尝试将当前分支rebaseExample
重新基准到origin/stable-3.1
分支,通过执行以下步骤。记住,Git 会应用那些在我们重新基准的目标分支上不存在的提交;因此,在这种情况下,会有很多提交:
- 使用以下命令将分支重新基准到
origin/stable-3.1
:
$ git rebase --interactive origin/stable-3.1
- 你现在看到的是所有将被重新基准到
origin/stable-3.1
分支上的提交列表。这些提交是origin/stable-3.1
和rebaseExample
分支之间的所有提交。提交将从上到下应用,因此提交会按相反的顺序列出——至少与 Git 中通常看到的顺序相反。这其实是有道理的。提交左侧有pick
关键字,接着是简短的提交哈希值,最后是提交的标题。
如果你向下滚动到页面底部,你会看到类似以下内容的列表:
pick 43405e6 My brand new fishtank
pick 08d0906 Feed the fish
# Rebase da6e87b..08d0906 onto da6e87b
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
所以,如果我们只希望我们的fishtank
提交基于origin/stable-3.1
分支,那么我们应该移除所有提交,除了我们这两个提交。
- 移除底部两个提交之外的所有行;暂时保持
pick
作为关键字。保存文件并关闭编辑器,Git 会给出以下信息:
Successfully rebased and updated refs/heads/rebaseExample.
- 现在,使用
gitk
检查我们是否达到了预期的效果。下图展示了我们两个fishtank
提交位于origin/stable-3.1
分支之上。接下来的截图就是我们预期的效果:
还有更多...
事实上,使用一个简单的 Git 命令也可以实现相同的效果。我们已经将origin/stable-3.2
分支的提交变基到rebaseExample
分支,并将其合并到origin/stable-3.2
分支。这也可以通过以下方式实现:
$ git rebase --onto origin/stable-3.1 origin/stable-3.2 rebaseExample
First, rewinding head to replay your work on top of it...
Applying: My brand new fishtank
Applying: Feed the fish
--onto origin/stable-3.2
标志告诉 Git 变基到origin/stable-3.2
,并且它必须从origin/stable-3.1
分支到rebaseExample
分支。因此,最终我们会得到rebaseExample
分支位于origin/stable-3.1
分支上,依此类推。下图展示了变基前的状态,其中我们有两个提交位于origin/stable-3.2
之上,而变基后的状态是我们的提交位于origin/stable-3.1
之上,正如我们所期望的那样:
使用交互式变基来压缩提交
当我在本地分支上工作时,我喜欢以小增量提交,每次提交都带上一些关于我所做工作的评论;然而,由于这些提交不能构建或通过任何测试要求,我无法逐个提交它们进行审查和验证。我必须在我的分支中将它们合并,但如果要逐一挑选我的修复,就必须挑选两倍数量的提交,这样做不太方便。
我们可以做的是变基并将提交压缩为一个提交,或者至少减少提交数量。
准备就绪
要开始这个示例,我们需要一个新的分支,即rebaseExample3
,它跟踪origin/stable-3.1
。使用以下命令创建该分支:
$ git checkout -b rebaseExample3 --track origin/stable-3.1
Branch rebaseExample3 set up to track remote branch stable-3.1 from origin.
Switched to a new branch 'rebaseExample3'
如何操作...
为了真正展示这个 Git 特性,我们将从origin/stable-3.1
分支的六个提交开始。这是为了模拟我们刚刚在rebaseExample3
分支上创建了六个提交;为此,请执行以下步骤:
- 找到一个介于
origin/stable-3.1
和origin/stable-3.2
之间的提交,并按逆序列出提交。或者,您可以滚动到底部,找到我们将使用的提交,如下所示:
$ git log origin/stable-3.1..origin/stable-3.2 --oneline --reverse
8a51c44 Do not close ArchiveOutputStream on error
3467e86 Revert "Close unfinished archive entries on error"
f045a68 Added the git-describe implementation
0be59ab Merge "Added the git-describe implementation"
fdc80f7 Merge branch 'stable-3.1'
7995d87 Prepare 3.2.0-SNAPSHOT builds
5218f7b Propagate IOException where possible when getting refs.
- 将
rebaseExample3
分支重置为5218f7b
提交;这将模拟在origin/stable-3.1
分支上有六个提交。可以通过如下命令检查 Git 的状态来验证:
$ git reset --hard 5218f7b
HEAD is now at 5218f7b Propagate IOException where possible when getting refs.
$ git status
On branch rebaseExample3
Your branch is ahead of 'origin/stable-3.1' by 6 commits.
(use "git push" to publish your local commits)
nothing to commit, working directory clean
- 现在,我们在
origin/stable-3.1
分支上有了这六个提交,我们希望将这些提交压缩成两个不同的提交。只需运行git rebase --interactive
即可完成。请注意,我们没有指定要 rebase 到哪个分支,因为在创建分支时我们已经使用--track
设置了跟踪分支。
为继续操作,让我们执行如下的 rebase 命令:
$ git rebase --interactive
pick 8a51c44 Do not close ArchiveOutputStream on error
pick f045a68 Added the git-describe implementation
pick 7995d87 Prepare 3.2.0-SNAPSHOT builds
pick 5218f7b Propagate IOException where possible when getting refs.
- 编辑器将打开,你将看到四个提交,而不是你预期的六个。这是因为通常情况下,rebase 会拒绝将合并的提交作为 rebase 的一部分。虽然你可以使用
--preserve-merges
标志,但根据 Git 帮助文档,这并不推荐。
根据 Git 帮助文档,--preserve-merges
并不是忽略合并,而是尝试重新创建它们。--preserve-merges
标志内部使用--interactive
机制,但通常不建议将其与--interactive
选项显式结合使用,除非你知道自己在做什么(参见下面代码段中的 bug)。
- 编辑文件,使其看起来如下所示:
pick 8a51c44 Do not close ArchiveOutputStream on error
squash f045a68 Added the git-describe implementation
pick 7995d87 Prepare 3.2.0-SNAPSHOT builds
squash 5218f7b Propagate IOException where possible when getting refs.
- 记住,提交是按与 Git 日志相反的顺序列出的。所以,在压缩提交时,我们将提交合并到我们标记为
pick
的提交中。当你关闭编辑器时,Git 将从上到下开始 rebase。首先,应用8a51c44
,然后将f045a68
压缩到8a51c44
提交中。这将打开提交信息编辑器,其中包含两个提交信息。你可以编辑提交信息,但现在我们只是关闭编辑器,以完成这两个提交的 rebase 和压缩操作。编辑器将再次打开,完成将5218f7b
压缩到7995d87
的操作。使用gitk
验证结果。
以下截图符合预期;现在,我们只在origin/stable-3.1
分支上有两个提交:
- 如果查看
HEAD
提交的提交信息,你会看到它包含了两个提交的信息,如以下命令所示。这是因为我们决定在进行更改时不更改提交信息:
$ git log -1
commit 9c96a651ff881c7d7c5a3974fa7a19a9c264d0a0
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:40:22 2013 +0200
Prepare 3.2.0-SNAPSHOT builds
Change-Id: Iac6cf7a5bb6146ee3fe38abe8020fc3fc4217584
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Propagate IOException where possible when getting refs.
Currently, Repository.getAllRefs() and Repository.getTags()
silently
ignores an IOException and instead returns an empty map. Repository
is a public API and as such cannot be changed until the next major
revision change. Where possible, update the internal jgit APIs to
use the RefDatabase directly, since it propagates the error.
Change-Id: I4e4537d8bd0fa772f388262684c5c4ca1929dc4c
还有更多内容...
现在我们已经压缩了两个提交,但我们本可以在编辑 rebase 的待办列表时使用其他关键字。
我们将尝试修复功能(fixup),它的工作方式类似于 squash 功能,通过执行以下步骤;唯一的区别是,Git 将使用pick
关键字选择提交的提交信息:
- 从重置回我们的起点开始:
$ git reset --hard 5218f7b
HEAD is now at 5218f7b Propagate IOException where possible when getting refs.
$ git status
On branch rebaseExample3
Your branch is ahead of 'origin/stable-3.1' by 6 commits.
(use "git push" to publish your local commits)
nothing to commit, working directory clean
- 如你所见,我们已经回到了起点,即我们比
origin/stable-3.1
分支多了六个提交。现在我们可以尝试 fixup 功能。启动交互式 rebase 并根据以下输出更改文件。注意,你可以使用f
代替fixup
:
$ git rebase --interactive
pick 8a51c44 Do not close ArchiveOutputStream on error
f f045a68 Added the git-describe implementation
pick 7995d87 Prepare 3.2.0-SNAPSHOT builds
f 5218f7b Propagate IOException where possible when getting refs.
- 一旦你关闭编辑器,你将看到 Git 显示变基的进度。正如预期的那样,提交信息编辑器不会打开。Git 会将变基操作合并到
origin/stable-3.1
分支的两个提交上。使用git status
命令,你可以确认你现在有两个提交:
$ git status
On branch rebaseExample3
Your branch is ahead of 'origin/stable-3.1' by 2 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
- 另一个不同点是,我们用
fixup
标记的两个提交的提交信息已消失。所以,如果你将其与之前的示例进行比较,差异非常明显;差异可以通过以下命令查看:
$ git log -1
commit c5bc5cc9e0956575cc3c30c3be4aecab19980e4d
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:40:22 2013 +0200
Prepare 3.2.0-SNAPSHOT builds
Change-Id: Iac6cf7a5bb6146ee3fe38abe8020fc3fc4217584
Signed-off-by: Matthias Sohn matthias.sohn@sap.com
- 最后,我们还可以确认我们仍然拥有相同的源代码,但提交已发生变化。这可以通过使用以下命令,将此提交与通过
5218f7b
创建的提交进行比较来完成:
$ git diff 5218f7b
正如预测的那样,git diff
没有输出,所以我们仍然保留着相同的源代码。
这个检查也可以在之前的示例中进行。
使用变基更改提交的作者
在开始一个新项目时,常常会忘记为该项目设置作者姓名和电子邮件地址。因此,你可能会在本地分支中看到一些使用错误的用户名和/或电子邮件 ID 进行提交的记录。
准备工作
在我们开始之前,我们需要一个分支,像 Git 中一样,创建一个名为resetAuthorRebase
的分支,并使其跟踪origin/master
。可以使用以下命令来完成:
$ git checkout -b resetAuthorRebase -t origin/master
Branch resetAuthorRebase set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'resetAuthorRebase'
如何进行...
现在,我们想要将所有从origin/stable-3.2
到我们的HEAD
(即master
)的提交作者进行更改。这只是一个示例;你通常不会需要更改已经发布到远程仓库的提交作者。
你可以通过使用git commit --amend --reset-author
来更改HEAD
提交的作者;然而,这只会更改HEAD
的作者,并保持其余提交不变。我们将从更改HEAD
提交的作者开始,然后通过执行以下步骤验证为什么这样做是错误的:
- 如下所示更改
HEAD
提交的作者:
$ git commit --amend --reset-author
[resetAuthorRebase b0b2836] Update Kepler target platform to use Kepler SR2 orbit R-build
1 file changed, 1 insertion(+), 1 deletion(-)
- 使用 Git log 命令验证你是否已经更改了作者信息:
$ git log --format='format:%h %an <%ae>' origin/stable-3.2..HEAD
b0b2836 John Doe <john.doe@example.com>
b9a0621 Matthias Sohn <matthias.sohn@sap.com>
ba15d82 Matthias Sohn matthias.sohn@sap.com
- 我们将列出从
origin/stable-3.2
到HEAD
的所有提交,并定义一个格式,%h
代表简短的提交哈希值,%an
代表作者的名字,%ae
代表作者的电子邮件地址。从输出中你可以看到,我现在是HEAD
提交的作者,但我们真正想要的是更改所有提交的作者。为此,我们将基于origin/stable-3.2
分支进行变基;然后,对于每个提交,我们将停止并修正作者。Git 可以通过--exec
选项帮助我们完成大部分工作,具体命令如下:
$ git rebase --interactive --exec "git commit --amend --reset-author" origin/stable-3.2
pick b14a939 Prepare 3.3.0-SNAPSHOT builds
exec git commit --amend --reset-author
pick f2abbd0 archive: Prepend a specified prefix to all entry filenames
exec git commit --amend --reset-author
-
如你所见,Git 为你打开了变基的待办列表,在每个提交之间,你都可以看到
exec
关键字和我们在命令行中指定的命令。如果有需要,你还可以在提交之间添加更多exec
行。关闭编辑器将开始变基操作。 -
如你所见,这个过程并不理想,因为每次都会打开提交消息编辑器,你必须关闭编辑器以让 Git 继续变基。要停止变基,请清空提交消息编辑器,Git 将返回到命令行;然后,你可以使用
git rebase --abort
,如下所示:
Executing: git commit --amend --reset-author
Aborting commit due to empty commit message.
Execution failed: git commit --amend --reset-author
You can fix the problem, and then run
git rebase --continue
$ git rebase --abort
为了实现我们真正想要的,你可以为 git commit
添加 --reuse-message
选项;这将重用你指定提交的提交消息。我们想要使用 HEAD
的消息,因为我们将其修改到 HEAD
提交。所以,再试一次,如下所示:
$ git rebase --interactive --exec "git commit --amend --reset-author --reuse-message=HEAD" origin/stable-3.2
Executing: git commit --amend --reset-author --reuse-message=HEAD
[detached HEAD 0cd3e87] Prepare 3.3.0-SNAPSHOT builds
51 files changed, 291 insertions(+), 291 deletions(-)
rewrite org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF (62%)
rewrite org.eclipse.jgit.junit/META-INF/MANIFEST.MF (73%)
rewrite org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF (61%)
rewrite org.eclipse.jgit.test/META-INF/MANIFEST.MF (76%)
rewrite org.eclipse.jgit.ui/META-INF/MANIFEST.MF (67%)
Executing: git commit --amend --reset-author --reuse-message=HEAD
[detached HEAD faaf25e] archive: Prepend a specified prefix to all entry filenames
5 files changed, 115 insertions(+), 1 deletion(-)
Executing: git commit --amend --reset-author --reuse-message=HEAD
[detached HEAD cfd743e] [CLI] Add option --millis / -m to debug-show-dir-cache c
Command
Successfully rebased and updated refs/heads/resetAuthorRebase.
- Git 提供了一个输出,表示操作成功;然而,为了验证这一点,你可以执行之前的 Git 日志命令,你应该看到所有提交的电子邮件地址都已经改变,如下所示:
$ git log --format='format:%h %an <%ae>' origin/stable-3.2..HEAD
9b10ff9 John Doe <john.doe@example.com>
d8f0ada John Doe <john.doe@example.com>
53df2b7 John Doe <john.doe@example.com>
它是如何工作的...
它按你预期的方式工作!有一点需要记住:当使用 exec
选项时,Git 会检查工作区中未暂存和已暂存的更改。请看以下命令行:
exec echo rainy_day > weather_main.txt
exec echo sunny_day > weather_california.txt
如果你像前面的命令那样有一行,第一次 exec
将被执行,接着你会在工作区有一个未暂存的更改。Git 会抱怨,你必须解决这个问题才能继续下一个 exec
。
所以,如果你想做类似的事情,你必须创建一行执行所有你想做的操作的 exec
。除此之外,变基功能相当简单;它只是尝试按变基待办列表中指定的顺序应用更改。Git 只会应用列表中指定的更改,因此,如果你移除其中的一些,它们将不会被应用。这是一种清理功能分支中不需要的提交的方法,比如调试过程中生成的提交。
自动合并提交
当我使用 Git 时,我经常为一个单独的 bug 修复创建许多提交,但在将修改推送到远程仓库时,我更喜欢——并且推荐——将 bug 修复作为一个提交来交付。这可以通过交互式变基来实现,但由于这应该是一个常见的工作流程,Git 提供了一个内置功能,叫做 autosquash,它可以帮助你将提交合并在一起。
准备工作
在我们开始这个操作之前,我们将从 origin/master
创建一个分支,以便准备将提交添加到我们的修复中。
我们从这样的例子开始:
$ git checkout -b readme_update_developer --track origin/master
Branch readme_update_developer set up to track remote branch master from origin.
Switched to a new branch 'readme_update_developer'
如何操作...
检查分支后,我们将创建我们想要合并其他提交的第一个提交。我们需要使用此提交的缩写提交哈希值,按照以下步骤自动创建其他将合并到此提交的提交:
- 从将一些文本写入
README.md
开始:
$ echo "More information for developers" >> README.md
- 这将为开发者向
README.md
添加更多信息;使用 Git 状态验证文件是否已更改,如下所示:
$ git status
On branch readme_update_developer
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: README.md
no changes added to commit (use "git add" and/or "git commit -a")
- 现在,我们想要添加并提交这些更改。我们可以使用
commit
命令,并加上-a
标志,这将把任何未暂存的更改加入提交,正如以下命令所示:
$ git commit -a -m "Updating information for developers"
[readme_update_developer d539645] Updating information for developers
1 file changed, 1 insertion(+)
-
创建提交后,请记住简短的提交哈希;我们已经在命令输出中以粗体标出了它。该简短哈希在你的环境中会有所不同,一旦完成练习,你应该会有自己的简短哈希。
-
为了继续,我们将向分支添加三个提交,并希望将其中的两个与第一个提交进行 squash,正如以下命令所示:
$ echo "even More information for developers" >> README.md
$ git commit -a --squash=d539645 --no-edit
[readme_update_developer d62922d] squash! Updating information for developers
1 file changed, 1 insertion(+)
- 这是第一个提交。注意我们为什么需要存储第一个提交的简短哈希——我们将其与
git commit
的--squash
选项一起使用。该选项会使用指定的提交主题来创建提交,同时会在主题开头加上squash!
,表示在执行 rebase 时 Git 应该 squash 该提交。现在,创建第二个提交,正如以下命令所示:
$ echo "even More information for developers" >> README.md
$ git commit -a --squash=d539645 --no-edit
[readme_update_developer 7d6194d] squash! Updating information for developers
1 file changed, 1 insertion(+)
- 我们添加了两个提交,并希望将它们与第一个提交一起 squash。在提交时,我还使用了
--no-edit
选项;该选项会跳过打开提交信息编辑器。如果不使用这个标志,编辑器将照常打开。不同之处在于,提交主题已经设置好,你只需要填写提交信息。现在,我们将创建最后一个提交,我们不希望将这个提交进行 squash:
$ echo "Adding configuration information" >> README.md
$ git commit -a -m "Updating information on configuration"
[readme_update_developer fd07857] Updating information on configuration
1 file changed, 1 insertion(+)
- 我们添加了最后一个提交,它与我们添加的前三个提交没有任何关系。这就是我们为什么没有使用
--squash
选项的原因。现在,我们可以使用git rebase -i
将这些提交进行 squash:
$ git rebase -i
- 你会在配置的提交编辑器中看到 rebase 的待办事项列表。我们原本期望 Git 会为我们想要 sqush 的提交配置 squash,正如以下命令所示:
pick d539645 Updating information for developers
pick d62922d squash! Updating information for developers
pick 7d6194d squash! Updating information for developers
pick fd07857 Updating information on configuration
- 你可以看到,Git 已经将
squash
插入到两个提交的主题中,但除此之外,我们没有得到预期的结果。Git 要求你在git rebase -i
命令中指定--autosquash
。关闭编辑器后,Git 将执行 rebase 操作,并给出以下输出:
Successfully rebased and updated refs/heads/readme_update_developer.
- 让我们再试一次使用
--autosquash
,看看 rebase 的待办事项列表会发生什么:
$ git rebase -i --autosquash
pick d539645 Updating information for developers
squash d62922d squash! Updating information for developers
squash 7d6194d squash! Updating information for developers
pick fd07857 Updating information on configuration
-
现在,rebase 的待办事项列表看起来更符合我们的预期。Git 已经预先配置了待办事项列表,显示出它将会 squash 哪些提交,保留哪些提交。
-
现在关闭待办事项列表将开始执行 rebase,但我们不想这样做(下一步会展示我们真正想要的结果)。如果你清空待办事项列表(删除所有行),然后保存并关闭编辑器,rebase 将会被中止。这正是我们想要的。输出将如下所示:
Nothing to do
- 我们真正想要做的只是运行
git rebase -i
,Git 会默认使用--autosquash
。这可以通过git config rebase.autosquash true
来实现;试试看,然后运行git rebase -i
:
$ git config rebase.autosquash true
$ git rebase -i
- 变基的待办事项列表弹出,我们得到了预期的结果,如下所示:
pick d539645 Updating information for developers
squash d62922d squash! Updating information for developers
squash 7d6194d squash! Updating information for developers
pick fd07857 Updating information on configuration
- 现在关闭编辑器并允许变基开始。编辑器打开后,你可以更改合并消息的提交消息,如下命令所示:
# This is a combination of 3 commits.
# The first commit's message is:
Updating information for developers
# This is the 2nd commit message:
squash! Updating information for developers
# This is the 3rd commit message:
squash! Updating information for developers
- 修改消息并关闭编辑器;Git 会继续进行变基操作并以以下消息结束:
[detached HEAD baadd53] Updating information for developers
1 file changed, 3 insertions(+)
Successfully rebased and updated refs/heads/readme_update_developer.
Verify the commit log with git log -3
$ git log -3
commit 6d83d44645e330d0081d3679aca49cd9bc20c891
Author: John Doe <john.doe@example.com>
Date: Wed May 21 10:52:03 2014 +0200
Updating information on configuration
commit baadd53018df2f6f3cdf88d024c3b9db16e526cf
Author: John Doe <john.doe@example.com>
Date: Wed May 21 10:25:43 2014 +0200
Updating information for developers
commit 6d724dcd3355f09e3450e417cf173fcafaee9e08
Author: Shawn Pearce <spearce@spearce.org>
Date: Sat Apr 26 10:40:30 2014 -0700
- 正如预期的那样,我们现在在
origin/master
提交之上有两个提交。
希望这能帮助你在进行更改并提交时,但仍想将代码作为一个提交来交付。
还有更多...
如果你想避免像在 自动压缩提交 配方的第 17 步那样打开提交消息编辑器,可以使用 --fixup=d539645
。这将使用第一个提交的提交消息,完全忽略其他提交中编写的消息。
第五章:在你的仓库中存储附加信息
在本章中,我们将涵盖以下内容:
-
添加你的第一个 Git 备注
-
按类别分离备注
-
从远程仓库获取备注
-
将 Git 备注推送到远程仓库
-
在仓库中标记提交
介绍
Git 在许多方面都非常强大。Git 最强大的功能之一是它有不可变的历史记录。这非常强大,因为没有人可以在 Git 的历史中偷偷插入任何东西,所有克隆了该仓库的人都会注意到这一点。这也给开发者带来了一些挑战,因为有些开发者希望在提交发布后修改提交消息。在许多其他版本控制系统中这是可能的,但由于 Git 有不可变历史记录,它提供了 Git 备注。Git 备注本质上是 Git 中的一个额外的refs/notes/commits
引用。在这里,你可以在提交中添加额外的信息,这些信息可以在运行git log
命令时显示出来。你还可以将备注推送到远程仓库,以便其他人能够获取这些备注。
添加你的第一个 Git 备注
我们将为已发布的代码添加一些额外的信息。如果我们在实际的提交中做这件事,我们会看到提交哈希发生变化。
准备就绪
在开始之前,我们需要一个仓库来进行操作;你可以使用之前克隆的jgit
,但为了获得几乎完全相同的输出,你可以按如下方式克隆jgit
仓库:
$ git clone https://git.eclipse.org/r/jgit/jgit chapter5
$ cd chapter5
如何做...
我们首先创建一个本地分支notesMessage
,并跟踪origin/stable-3.2
。
然后,我们将尝试修改提交消息,并看到提交哈希发生变化:
- 切换到跟踪
origin/stable-3.2
的notesMessage
分支:
$ git checkout -b notesMessage --track origin/stable-3.2
Branch notesMessage set up to track remote branch stable-3.2 from origin.
Switched to a new branch 'notesMessage'
- 列出分支的
HEAD
提交哈希:
$ git log -1
commit f839d383e6fbbda26729db7fd57fc917fa47db44
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Dec 18 21:16:13 2013 +0100
Prepare post 3.2.0 builds
Change-Id: Ie2bfdee0c492e3d61d92acb04c5bef641f5f132f
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
- 通过使用
git commit --amend
修改提交消息,然后,在Change-Id:
行上方添加一行Update MANIFEST files
:
$ git commit --amend
- 现在,我们再次列出提交,看到提交哈希已经变化:
$ git log -1
commit 5ccc9c90d29badb1bd860d29860715e0becd3d7b
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Dec 18 21:16:13 2013 +0100
Prepare post 3.2.0 builds
Update MANIFEST files
Change-Id: Ie2bfdee0c492e3d61d92acb04c5bef641f5f132f
Signed-off-by: Matthias Sohn matthias.sohn@sap.com
- 注意到提交部分已经从
f839d383e6fbbda26729db7fd57fc917fa47db44
变化为9fcaa153c4afc6ee95572a58ddfa297f60b7e1cf
,因为提交是由提交中的内容、提交的父级和提交消息派生出来的。所以,在更新提交消息时,提交哈希会发生变化。由于我们已经改变了HEAD
提交的内容,我们不再基于origin/stable-3.2
分支的HEAD
提交。这一点在gitk
和git status
中可以看到:
$ git status
On branch notesMessage
Your branch and 'origin/stable-3.2' have diverged,
and have 1 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working directory clean
- 从输出中可以看到,我们的分支已经与
origin/stable-3.2
发生了分歧;这一点也可以从gitk
中看到。注意,我们可以通过gitk
指定想要查看的分支和提交。在这种情况下,我们希望查看origin/stable-3.2
和HEAD
:
$ gitk origin/stable-3.2 HEAD
以下是此操作的截图:
-
为了避免这种情况,我们可以在提交消息中添加一个备注。
让我们先将分支重置为
origin/stable-3.2
,然后为提交添加一条笔记:
$ git reset --hard origin/stable-3.2
HEAD is now at f839d38 Prepare post 3.2.0 builds
- 现在,添加与之前相同的消息,但仅作为笔记:
$ git notes add -m "Update MANIFEST files"
- 我们已经通过使用
-m
标志和消息,直接从命令行添加了笔记,而不调用编辑器。现在,当运行git log
时,日志将可见:
$ git log -1
commit f839d383e6fbbda26729db7fd57fc917fa47db44
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Dec 18 21:16:13 2013 +0100
Prepare post 3.2.0 builds
Change-Id: Ie2bfdee0c492e3d61d92acb04c5bef641f5f132f
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes:
Update MANIFEST files
- 从日志输出中可以看到,我们有一个
Notes:
部分,显示了我们的笔记。尽管它不会像--amend
选项那样直接将笔记添加到提交信息中,但我们仍然可以看到我们对提交信息的关键添加。我们可以通过git status
验证,确保我们没有再偏离:
$ git status
On branch notesMessage
Your branch is up-to-date with 'origin/stable-3.2'.
nothing to commit, working directory clean
还有更多内容...
所以,你已经为提交添加了笔记,现在你想进一步添加信息。你可能会认为只需再次添加笔记并补充更多内容。实际上并非如此。你有选项可以附加、编辑或强制创建笔记:
- 首先尝试再次添加笔记并附加更多信息:
$ git notes add -m "Update MANIFESTS files for next version"
error: Cannot add notes. Found existing notes for object f839d383e6fbbda26729db7
fd57fc917fa47db44\. Use '-f' to overwrite existing notes
- 正如预期的那样,我们不能直接添加笔记,但可以使用
-f
标志来进行操作:
$ git notes add -f -m "Update MANIFESTS files for next version"
Overwriting existing notes for object f839d383e6fbbda26729db7fd57fc917fa47db44
- 由于
-f
标志,Git 会覆盖现有的笔记。你也可以使用--force
,效果相同。用git log
验证它:
$ git log -1
commit f839d383e6fbbda26729db7fd57fc917fa47db44
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Dec 18 21:16:13 2013 +0100
Prepare post 3.2.0 builds
Change-Id: Ie2bfdee0c492e3d61d92acb04c5bef641f5f132f
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes:
Update MANIFESTS files for next version
- 你也可以使用
git notes append
来附加当前笔记:
$ git notes append -m "Verified by John Doe"
- 除非出现错误,否则此操作不会产生输出,但你可以通过再次使用
git log
来验证。为了保持输出简洁,我们使用了--oneline
。这将显示提交的最简输出。但要显示笔记,我们必须添加--notes
,它会在输出中显示提交的笔记:
$ git log -1 --notes --oneline
f839d38 Prepare post 3.2.0 builds
Notes:
Update MANIFESTS files for next version
Verified by John Doe
- 从输出中可以看到,我们已经将行附加到笔记中。如果你尝试使用
edit
选项,你会发现只有在使用-m
标志时才能使用此选项。这是有道理的,因为你应该编辑笔记,而不是覆盖或附加到已经创建的笔记:
$ git notes edit -m "John Doe"
The -m/-F/-c/-C options have been deprecated for the 'edit' subcommand.
Please use 'git notes add -f -m/-F/-c/-C' instead.
- 换句话说,Git 会拒绝编辑笔记,并提到其他的操作方法。
如果不带任何参数使用 git notes add
和 git notes edit
命令,它们将执行相同的操作,即打开配置的编辑器并允许你为提交编写笔记。
按类别分隔笔记
正如我们在前面的示例中看到的,我们可以将笔记添加到提交中;然而,在某些情况下,将信息按类别存储是有意义的,例如 featureImplemented
、defect
和 alsoCherryPick
。正如本章开头简要解释的那样,笔记存储在 refs/notes/commits
中,但我们可以添加多个引用,以便轻松地对笔记的不同范围进行排序和列出。
准备工作
为了开始这个示例,我们需要一个新的分支来跟踪 origin/stable-3.1
分支;我们将分支命名为 notesReferences
,并使用以下命令创建并切换到该分支:
$ git checkout -b notesReferences --track origin/stable-3.1
Branch notesReferences set up to track remote branch stable-3.1 from origin.
Switched to a new branch 'notesReferences'
如何操作...
假设我们修复了一个缺陷,并在发布之前尽一切可能确保提交的质量。尽管如此,我们还是不得不为相同的缺陷做出另一个修复。
因此,我们希望向refs/notes/alsoCherryPick
引用添加一个注释,该注释应表示如果你选择性地拾取这个提交,你还应该选择性地拾取其他提交,因为它们修复了相同的缺陷。
在这个示例中,我们将找到提交,并在多个注释引用规格中为提交添加一些额外的信息:
- 首先列出分支上的前 10 个提交,这样我们就有一些内容可以复制和粘贴:
$ git log -10 --oneline
da6e87b Prepare post 3.1.0 builds
16ca725 JGit v3.1.0.201310021548-r
c6aba99 Fix order of commits in rebase todo file header
5a2a222 Prepare post 3.1.0 RC1 builds
6f0681e JGit v3.1.0.201309270735-rc1
a065a06 Attempt to fix graph layout when new heads are introduced
b4f07df Prepare re-signing pgm's ueberjar to avoid SecurityException
aa4bbc6 Use full branch name when getting ref in BranchTrackingStatus
570bba5 Ignore bitmap indexes that do not match the pack checksum
801aac5 Merge branch 'stable-3.0'
- 为
da6e87bc3
提交添加注释:
$ git notes add -m "test note"
- 现在,要为
b4f07df
提交在alsoCherryPick
引用中添加注释,我们必须在git notes
中使用--ref
选项。这个选项必须在add
选项之前指定:
$ git notes --ref alsoCherryPick add -m "570bba5" b4f07df
- 没有输出表示添加注释成功。现在我们已经有了一个注释,我们应该能够通过单个
git log -1
命令列出它。但事实并非如此。实际上,你需要指定要从特定引用中列出注释。这可以通过git log
的--notes=alsoCherryPick
选项来完成:
$ git log -1 b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a --notes=alsoCherryPick
commit b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Tue Sep 24 09:11:47 2013 +0200
Prepare re-signing pgm's ueberjar to avoid SecurityException
More output...
Change-Id: Ia302e68a4b2a9399cb18025274574e31d3d3e407
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes (alsoCherryPick):
570bba5
- 如你从输出中看到的,Git 显示了
alsoCherryPick
注释。Git 默认将注释添加到refs/notes/commits
,但我们已经明确指定要显示alsoCherryPick
。如果你能够默认显示alsoCherryPick
注释的引用,而无需使用--notes=alsoCherryPick
,那会很好。可以通过如下配置 Git 来实现:
$ git config notes.displayRef "refs/notes/alsoCherryPick"
- 通过配置此选项,你告诉 Git 始终列出这些注释。但默认的注释呢?我们是否覆盖了列出默认
refs/notes/commits
注释的配置?我们可以通过git log -1
检查是否仍然显示测试注释:
$ git log -1
commit da6e87bc373c54c1cda8ed563f41f65df52bacbf
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:22:08 2013 +0200
Prepare post 3.1.0 builds
Change-Id: I306a3d40c6ddb88a16d17f09a60e3d19b0716962
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes:
test note
- 不,我们并没有覆盖设置来列出默认的注释引用。既然我们知道可以有任意数量的
notes.displayRef
配置,我们应该在我们的仓库中添加所有我们想要使用的引用。在某些情况下,最好直接添加refs/notes/*
。这将配置 Git 显示所有注释:
$ git config notes.displayRef 'refs/notes/*'
- 如果我们现在在
refs/notes/defect
中添加另一个注释,我们应该能够在使用git log
时列出它,而不需要指定我们想要列出哪个注释引用。我们正在为已经在alsoCherryPick
引用中有注释的提交添加注释:
$ git notes --ref defect add -m "Bug:24435" b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
- 现在,用
git log
列出提交:
$ git log -1 b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
commit b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Tue Sep 24 09:11:47 2013 +0200
Prepare re-signing pgm's ueberjar to avoid SecurityException
See http://dev.eclipse.org/mhonarc/lists/jgit-dev/msg02277.html
Change-Id: Ia302e68a4b2a9399cb18025274574e31d3d3e407
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes (alsoCherryPick):
570bba5
Notes (defect):
Bug:24435
- Git 显示了两个注释,这正是我们预期的结果。
它是如何工作的…
我们一直在讨论refs/notes/alsoCherryPick
引用等等。如你所知,我们将远程分支称为引用,例如refs/remotes/origin/stable-3.2
,但是本地分支也有引用,例如refs/heads/develop
,例如。
由于你可以创建一个从特定引用开始的分支,因此你应该能够创建一个从refs/notes/alsoCherrypick
引用开始的分支:
- 创建一个从
refs/notes/alsoCherryPick
开始的分支,并切换到该分支:
$ git checkout -b myNotes notes/alsoCherryPick
Switched to a new branch 'myNotes'
myNotes
分支现在指向refs/notes/alsoCherryPick
上的HEAD
。列出分支上的文件会显示一个包含我们已添加注释的提交哈希的文件:
$ ls
b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
- 显示文件内容将展示我们用作注释文本的文字:
$ cat b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
570bba5
- 如你所见,我们为
b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
添加的注释570bba5
已经存在于文件中。如果我们有更长的消息,这条消息也会在这里显示。
从远程仓库获取笔记
到目前为止,我们一直在自己的本地仓库中创建笔记,这样做没问题。但如果我们想要分享这些笔记,就必须确保能够推送它们。我们还希望能够从远程仓库中获取其他人的笔记。不幸的是,这并不像看起来那么简单。
准备就绪
在开始之前,我们需要从已有的本地克隆中再克隆一个。这是为了展示 Git 使用git notes
的推送和获取机制:
- 首先检查主分支:
$ git checkout master
Checking out files: 100% (1529/1529), done.
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
- 现在,创建一个所有
stable-3.1
分支的本地分支:
$ git branch stable-3.1 origin/stable-3.1
Branch stable-3.1 set up to track remote branch stable-3.1 from origin.
- 我们检查所有这些分支,因为我们想克隆这个仓库,并且默认情况下,所有
refs/heads/*
分支都会被克隆。因此,当我们克隆chapter5
目录时,你会看到我们只获得了执行git branch
命令时看到的分支:
$ git branch
* master
myNotes
notesMessage
notesReference
stable-3.1
- 现在,向上跳转到一个目录,以便可以从
chapter5
目录创建新的克隆:
$ cd ..
$ git clone ./chapter5 shareNotes
Cloning into 'shareNotes'...
done.
- 现在,进入
shareNotes
目录并运行git branch -a
,你会看到我们唯一的远程分支是我们在chapter5
目录中作为本地分支检查出来的分支。完成这一步后,我们准备好提取一些笔记:
$ cd shareNotes
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/origin/myNotes
remotes/origin/notesMessage
remotes/origin/notesReference
remotes/origin/stable-3.1
- 正如预测的那样,列表与
chapter5
目录中的 Git 分支输出相匹配。
怎么做...
我们现在已经准备好推送和获取笔记的设置。挑战在于,Git 默认并不设置为获取和推送笔记,因此你通常看不到其他人的笔记:
- 我们首先展示没有在克隆过程中接收到笔记的情况:
$ git log -1 b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a --notes=alsoCherryPick
warning: notes ref refs/notes/alsoCherryPick is invalid
commit b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Tue Sep 24 09:11:47 2013 +0200
Prepare re-signing pgm's ueberjar to avoid SecurityException
- 正如预期的那样,输出没有显示笔记,第一行明确说明了原因。在
chapter5
目录中,我们会看到该笔记。为了使笔记可以被提取,我们需要创建一个新的提取规则配置;它需要类似于refs/heads
的提取规则。请查看git config
中的配置:
$ git config --get remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*
- 这表明我们正在将
refs/heads
提取到refs/remotes/origin
引用中,但我们还想做的是将refs/notes/*
提取到refs/notes/*
:
$ git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*'
- 你现在应该已经配置好了。如果在命令中省略了
--add
选项,你会覆盖当前设置。请验证规则是否已存在:
$ git config --get-all remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*
+refs/notes/*:refs/notes/*
- 现在,尝试获取笔记:
$ git fetch
From /tmp/chapter5
* [new ref] refs/notes/alsoCherryPick -> refs/notes/alsoCherryPick
* [new ref] refs/notes/commits -> refs/notes/commits
* [new ref] refs/notes/defect -> refs/notes/defect
- 正如 Git 输出所示,我们已经接收到了一些新的引用。所以,让我们检查一下现在是否已经有了提交的笔记:
$ git log -1 b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a --notes=alsoCherryPick
commit b4f07df357fccdff891df2a4fa5c5bd9e83b4a4a
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Tue Sep 24 09:11:47 2013 +0200
Prepare re-signing pgm's ueberjar to avoid SecurityException
More output...
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes (alsoCherryPick):
570bba5
- 现在,我们的仓库中已经有了笔记,这正是我们所期望的。
它是如何工作的...
我们已经成功获取了注释。之所以能够工作,是因为我们获取注释的方式。默认情况下,Git 被配置为将 refs/heads/*
获取到 refs/remotes/origin/*
。这样,我们可以轻松地跟踪远程和本地的内容。我们本地仓库中的分支位于 refs/heads/*
,这些分支也会在执行 git branch
时列出。
对于注释,我们需要将 refs/notes/*
获取到 refs/notes/*
,因为我们想从服务器获取注释,并在 git show
、git log
和 git notes
等 Git 命令中使用它们。
将 Git 注释推送到远程仓库
我们已经成功地尝试从远程仓库获取注释,但那你的注释呢?你如何将它们推送到服务器?这必须像推送其他引用(例如分支和提交)一样,通过推送命令来完成。
如何操作...
在我们可以将注释从 shareNotes
仓库推送之前,我们需要创建一个注释,因为我们现在的注释都已经在远程仓库中。此时远程仓库是 chapter5
目录:
- 你已经找到了一个想要添加注释的提交,并且想将注释添加到
verified
引用:
$ git notes --ref verified add -m "Verified by john.doe@example.com" 871ee53b52a
- 现在我们已经添加了注释,可以通过
git log
命令列出它:
$ git log --notes=verified -1 871ee53b52a
commit 871ee53b52a7e7f6a0fe600a054ec78f8e4bff5a
Author: Robin Rosenberg <robin.rosenberg@dewire.com>
Date: Sun Feb 2 23:26:34 2014 +0100
Reset internal state canonical length in WorkingTreeIterator when moving
Bug: 426514
Change-Id: Ifb75a4fa12291aeeece3dda129a65f0c1fd5e0eb
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes (verified):
Verified by john.doe@example.com
- 正如预期的那样,我们可以看到注释。如果你看不到注释,可能是因为在执行
git log
命令时漏掉了--notes=verified
,因为我们尚未将verified
配置为notes.displayRef
。要推送注释,我们必须使用git push
命令,因为 Git 中的默认推送规则是将分支推送到refs/heads/<branchname>
。
所以,如果我们仅仅尝试将注释推送到远程,什么也不会发生:
$ git push
Everything up-to-date
- 你可能会看到关于
git push.default
未配置的警告;你可以在这些示例中安全地忽略此警告。重要的是,Git 显示一切都是最新的。但是我们知道我们已经为一个提交创建了 Git 注释。所以,要推送这些注释,我们需要将注释引用推送到远程的注释引用。这可以按如下方式完成:
$ git push origin refs/notes/*
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 294 bytes | 294.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /Users/kneth/tmp/./chapter5
* [new branch] refs/notes/verified -> refs/notes/verified
- 现在,发生了一些变化;我们在远程仓库上有了一个新的分支,名为
refs/notes/verified
。这是因为我们将注释推送到了远程仓库。为了验证这一点,我们可以进入chapter5
目录,查看871ee53b52a
提交是否有 Git 注释:
$ cd ../chapter5/
$ git log --notes=verified -1 871ee53b52a
commit 871ee53b52a7e7f6a0fe600a054ec78f8e4bff5a
Author: Robin Rosenberg <robin.rosenberg@dewire.com>
Date: Sun Feb 2 23:26:34 2014 +0100
Reset internal state canonical length in WorkingTreeIterator when moving
Bug: 426514
Change-Id: Ifb75a4fa12291aeeece3dda129a65f0c1fd5e0eb
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Notes (verified):
Verified by john.doe@example.com
- 如预期的那样,我们可以在此目录中看到注释。
还有更多内容...
由于 Git 注释不像普通分支那样工作,所以当你尝试协作时,将它们推来推去可能会有些麻烦。因为你不能像处理其他分支一样,轻松地获取和合并 Git 注释分支,因此明确的建议是建立一些工具来添加这些注释,这样就只有一个服务器来添加注释。
一个简单但有价值的备注可以是关于 Jenkins 构建和测试的信息。这在你需要重新打开一个缺陷时非常有价值。这样你可以在仓库中查看哪个测试是在提交哈希上执行的。
在仓库中标记提交
如果你正在使用 Git 发布软件,你必然会涉及标签。标签描述了仓库中的不同软件发布版本。标签有两种类型,轻量标签和注释标签。轻量标签与分支非常相似,因为它只是一个命名的引用,比如refs/tags/version123
。它指向你标记的提交的提交哈希;而如果它是一个分支,则会是refs/heads/version123
。不同之处在于,当你在分支上工作并提交时,分支会向前移动。而标签始终指向相同的提交哈希。我们稍后会讨论注释标签。
准备工作
在我们开始之前,你必须进入chapter5
目录,这是我们最初克隆的地方。
本章内容。
我们应该从标记距离origin/stable-2.3
十次提交并且不是合并的提交开始。为了找到那个提交,我们将使用git log
命令。
对于git log
命令,我们使用了--no-merges
选项,它会显示只有一个父提交的提交。我们之前使用过的--oneline
选项告诉 Git 将输出限制为每个提交一行。此外,-11
选项显示了最近的 11 个提交(比最新的提交早 10 个)。
按如下方式找到提交:
$ git checkout stable-2.3
$ git log -11 --no-merges --oneline
49ec6c1 Prepare 2.3.2-SNAPSHOT builds
63dcece JGit v2.3.1.201302201838-r
3b41fcb Accept Change-Id even if footer contains not well-formed entries
5d7b722 Fix false positives in hashing used by PathFilterGroup
9a5f4b4 Prepare post 2.3.0.201302130906 builds
19d6cad JGit v2.3.0.201302130906
3f8ac55 Replace explicit version by property where possible
1c4ee41 Add better documentation to DirCacheCheckout
e9cf705 Prepare post 2.3rc1 builds
ea060dd JGit v2.3.0.201302060400-rc1
60d538f Add getConflictingNames to RefDatabase
如何操作...
现在我们已经找到了60d538f
提交,我们应该将其标记为轻量标签:
- 使用
git tag
命令为发布指定有意义的名称:
$ git tag 'v2.3.0.201302061315rc1' ea060dd
- 由于没有输出,操作成功。要查看标签是否可用,请使用
git tag
命令:
$ git tag -l "v2.3.0.2*"
v2.3.0.201302061315rc1
v2.3.0.201302130906
- 我们使用
git tag
命令并带上-l
选项,因为我们想列出标签,而不是标记当前的HEAD
。一些仓库有很多标签;为了防止列表变得过长,你可以指定要列出的标签,并使用*
通配符,就像我们之前做的那样。我们的标签是可用的,但它实际上只是说我们在仓库中有一个名为v2.3.0.201302061315rc1
的标签,如果你使用git show v2.3.0.201302061315rc1
,你会看到输出与git show ea060dd
相同:
$ git show v2.3.0.201302061315rc1
commit ea060dd8e74ab588ca55a4fb3ff15dd17343aa88
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Feb 6 13:15:01 2013 +0100
JGit v2.3.0.201302060400-rc1
Change-Id: Id1f1d174375f7399cee4c2eb23368d4dbb4c384a
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.a
... More output
$ git show ea060dd
commit ea060dd8e74ab588ca55a4fb3ff15dd17343aa88
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Feb 6 13:15:01 2013 +0100
JGit v2.3.0.201302060400-rc1
Change-Id: Id1f1d174375f7399cee4c2eb23368d4dbb4c384a
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.a
... More output
- 输出中还会有大量的文件差异信息,但输出内容完全相同。因此,为了添加更多信息,我们应该使用注释标签。注释标签是你需要在标签中添加一些信息的标签。要创建注释标签,我们使用
git tag
命令的--annotate
选项:
$ git tag --annotate -m "Release Maturity rate 97%" 'v2.3.0.201409022257rc2' 1c4ee41
-m
标志与--message
相同,因为我们希望给标签添加一条消息。如果你不使用-m
标志,Git 会打开配置的编辑器,你可以在标签的注释中写入完整的发布说明。我们可以使用git show
来检查标签信息:
$ git show 'v2.3.0.201409022257rc2'
tag v2.3.0.201409022257rc2
Tagger: John Doe <john.doe@example.com>
Date: Sun Feb 9 22:58:28 2014 +0100
Release Maturity rate 97%
commit 1c4ee41dc093266c19d4452879afe5c0f7f387f4
Author: Christian Halstrick christian.halstrick@sap.com
... More output
- 我们实际上可以看到标签名称和我们通过
-m
标志添加的信息。使用轻量级标签时,我们在输出中看不到任何关于标签的信息。实际上,当我们使用git show
查看轻量级标签时,甚至看不到标签名称。
还有更多内容...
标签非常强大,因为它们可以为仓库添加有价值的信息,并且由于标签应被视为仓库中的正式发布版本,因此在使用时应非常小心。
自然,你可以将标签推送到远程区域,仓库的贡献者会获取这些标签。这是你需要小心的地方。在传统的版本控制系统中,你可以回到过去并直接更改发布版本,而这些传统系统都基于集中式服务器,你必须连接才能工作,因此更改发布版本并不会造成太大问题,因为并不是很多人使用这个版本或者已经下载了这个版本。但在 Git 中则不同。如果你修改了已经推送的标签,使其指向另一个提交哈希值,那么那些已经获取了该标签的开发者将无法获得新的标签,除非他们在本地删除该标签:
- 为了证明不获取新标签的危险,我们将尝试删除一个标签并重新创建它,使其指向另一个提交哈希值:
$ git tag -d v1.3.0.201202121842-rc4
Deleted tag 'v1.3.0.201202121842-rc4' (was d1e8804)
-
现在我们已删除该标签,准备重新创建标签:
指向
HEAD
:
$ git tag -a -m "Local created tag" v1.3.0.201202121842-rc4
- 我们已经重新创建了标签,它指向
HEAD
,因为我们在命令末尾没有指定提交哈希值。现在,执行git fetch
,看看是否能从远程仓库获取覆盖的标签:
$ git fetch
-
由于没有输出,标签可能没有被覆盖。让我们通过以下方式验证:
git show
:
$ git show v1.3.0.201202121842-rc4
tag v1.3.0.201202121842-rc4
Tagger: John Doe <john.doe@example.com>
Date: Wed May 2 16:27:25 2018 +0200
Local created tag
commit 1c4ee41dc093266c19d4452879afe5c0f7f387f4
- 如你从输出中看到的,它仍然是我们本地创建的标签。要再次从远程获取标签,我们需要删除本地标签并执行
git fetch
。要删除标签,你需要使用-d
标志:
$ git tag -d v1.3.0.201202121842-rc4
Deleted tag 'v1.3.0.201202121842-rc4' (was 28be24b)
$ git fetch
From https://git.eclipse.org/r/jgit/jgit
* [new tag] v1.3.0.201202121842-rc4 -> v1.3.0.201202121842-rc4
-
如你所见,Git 已经从服务器重新获取了标签。我们可以验证:
使用
git show
:
$ git show v1.3.0.201202121842-rc4
tag v1.3.0.201202121842-rc4
Tagger: Matthias Sohn <matthias.sohn@sap.com>
Date: Mon Feb 13 00:57:56 2012 +0100
JGit 1.3.0.201202121842-rc4
-----BEGIN PGP SIGNATURE-----
Version: GnuPG/MacGPG2 v2.0.14 (Darwin)
iF4EABEIAAYFAk84UhMACgkQWwXM3hQMKHbwewD/VD62MWCVfLCYUIEz20C4Iywx
4OOl5TedaLFwIOS55HcA/ipDh6NWFvJdWK3Enm2krjegUNmd9zXT+0pNjtlJ+Pyi
=LRoe
-----END PGP SIGNATURE-----
commit 53917539f822afa12caaa55db8f57c29570532f3
- 所以,如你所见,我们再次得到了正确的标签,但这也应当是一个警告。一旦你将标签推送到远程仓库,你就不应再更改它,因为从仓库拉取的开发者可能永远不会知道更改,除非他们重新克隆仓库或删除本地标签并重新拉取。
在这一章中,我们学习了如何为提交添加标签并添加注释。这些都是在提交已经提交并发布到共享仓库后,存储附加信息的强大方法。但在你实际发布提交之前,你有机会为提交添加最有价值的信息。提交信息是你必须指定你在做什么,及有时也说明你为什么这样做的地方。
如果你正在解决一个 bug,你应该列出 bug 的 ID;如果你使用了一种特殊的方法来解决问题,建议你描述一下为什么使用了这个很棒的技巧来解决问题。这样,当人们回顾你的提交时,他们也能了解为什么做出不同的决策。
第六章:从仓库中提取数据
本章将介绍以下内容:
-
提取顶级贡献者
-
查找源代码树中的瓶颈
-
使用
grep
查找提交信息 -
发布内容
-
查找最近一段时间内仓库中的成就
介绍
无论你在大公司还是小公司工作,保护和维护数据始终非常重要,它会为你追踪大量信息;这只是一个提取数据的问题。部分数据是你或其他开发者在填写提交信息时自动加入到系统中的——例如,修复的 bug 的详细信息来自于 bug 跟踪系统。
这些数据不仅对管理有用,还可以用于增加更多时间来重构 C 文件,在这些文件中几乎所有的 bug 都已修复。
提取顶级贡献者
Git 提供了一些内置的统计数据,你可以即时获取。git log
命令有不同的选项,比如--numstat
,它将显示自每次提交以来每个文件的添加行数和删除行数。然而,要在仓库中找到顶级提交者,我们只需使用git shortlog
命令。
准备工作
在本书中的所有示例中,我们使用的是jgit
仓库;你可以选择克隆它,或者使用你可能已经拥有的克隆之一。
如下克隆jgit
仓库:
$ git clone https://git.eclipse.org/r/jgit/jgit chapter6
$ cd chapter6
如何操作...
shortlog
Git 命令非常简单,没有太多可用的选项或标志。它可以显示日志,但以精简版显示,然后可以按如下方式总结给我们:
- 开始时,用
shortlog
显示最后五次提交。我们可以使用-5
来限制输出量:
$ git shortlog -5
Jonathan Nieder (1):
Update commons-compress to 1.6
Matthias Sohn (2):
Update com.jcraft.jsch to 0.1.50 in Kepler target platform
Update target platforms to use latest orbit build
SATO taichi (1):
Add git checkout --orphan implementation
Stefan Lay (1):
Fix fast forward rebase with rebase.autostash=true
- 如你所见,输出结果与
git log
的输出非常不同。你可以自己尝试运行git log -5
。括号中的数字是该提交者的提交次数。在名字和数字下面是提交的标题。请注意,没有显示提交的哈希值。仅凭这五次提交找出顶级提交者很容易,但如果你尝试运行不带-5
的git shortlog
,就很难找到这个人。为了排序并找到顶级提交者,我们可以使用-n
或--numbered
选项来排序输出;顶级提交者会排在最上面:
$ git shortlog -5 --numbered
Matthias Sohn (2):
Update com.jcraft.jsch to 0.1.50 in Kepler target platform
Update target platforms to use latest orbit build
Jonathan Nieder (1):
Update commons-compress to 1.6
SATO taichi (1):
Add git checkout --orphan implementation
Stefan Lay (1):
Fix fast forward rebase with rebase.autostash=true
- 如你所见,输出结果已很好地排序。如果我们不关心提交的主题,可以使用
-s
或--summary
来仅显示每个开发者的提交计数,如下所示:
$ git shortlog -5 --numbered --summary
2 Matthias Sohn
1 Jonathan Nieder
1 SATO taichi
1 Stefan Lay
- 最后,我们得到了我们想要的,除了我们没有提交者的电子邮件地址;这个选项也可以使用
-e
或--email
。这将显示提交者的电子邮件地址。这一次,我们将尝试在整个仓库上运行。当前,我们只列出了 HEAD 提交的信息。为了列出整个仓库的信息,我们需要在命令的末尾添加--all
,以便对所有分支执行命令,如下所示:
$ git shortlog --numbered --summary --email --all
765 Shawn O. Pearce <spearce@spearce.org>
399 Matthias Sohn <matthias.sohn@sap.com>
360 Robin Rosenberg <robin.rosenberg@dewire.com>
181 Chris Aniszczyk <caniszczyk@gmail.com>
172 Shawn Pearce <spearce@spearce.org>
160 Christian Halstrick <christian.halstrick@sap.com>
114 Robin Stocker <robin@nibor.org>
- 所以,现在这是列表;我们知道谁提交了最多的代码,但这个图像可能有些偏差,因为最大的提交者可能恰好是项目的创建者,可能并没有积极地参与仓库的贡献。因此,要列出过去六个月的主要提交者,我们可以在
git shortlog
命令中添加--since="6 months ago"
,如下所示:
$ git shortlog --numbered --summary --email --all --since="6 months ago"
73 Matthias Sohn <matthias.sohn@sap.com>
15 Robin Stocker <robin@nibor.org>
14 Robin Rosenberg <robin.rosenberg@dewire.com>
13 Shawn Pearce <sop@google.com>
12 Stefan Lay <stefan.lay@sap.com>
8 Christian Halstrick <christian.halstrick@sap.com>
7 Colby Ranger <cranger@google.com>
- 如你所见,自从仓库开始以来,图片已经发生了变化。
你可以使用 "n 周前", "n 天前", "n 个月前", "n 小时前" 等来指定时间段。你也可以使用具体日期,如 "2013 年 10 月 1 日"
。
你还可以使用 --until
选项列出某个月份的主要提交者,你可以指定希望列出提交记录的截止日期。可以按如下方式执行:
$ git shortlog --numbered --summary --email --all --since="30 september 2013" --until="1 november 2013"
15 Matthias Sohn <matthias.sohn@sap.com>
4 Kaloyan Raev <kaloyan.r@zend.com>
4 Robin Rosenberg <robin.rosenberg@dewire.com>
3 Colby Ranger <cranger@google.com>
2 Robin Stocker <robin@nibor.org>
1 Christian Halstrick <christian.halstrick@sap.com>
1 Michael Nelson <michael.nelson@tasktop.com>
1 Rüdiger Herrmann <ruediger.herrmann@gmx.de>
1 Tobias Pfeifer <to.pfeifer@web.de>
1 Tomasz Zarna <tomasz.zarna@tasktop.com>
- 如你所见,我们得到了一份新的列表,看来 Matthias 是主要贡献者,至少相比于初步结果来说。通过收集自仓库初始化以来每个月的数据,这类信息还可以用来可视化仓库中责任的转变。
还有更多……
在处理代码时,通常很有用的一点是知道在你需要修复软件中的问题时,应该找谁,尤其是在你不熟悉的领域。所以,找出你正在修改的文件或文件的代码负责人是很有帮助的。显而易见的原因是为了获取代码方面的输入,同时也知道应该找谁进行代码审查。你同样可以使用 git shortlog
来搞清楚这一点。你也可以在文件上使用这个命令:
- 为此,我们只需将文件添加到
git shortlog
命令的末尾:
$ git shortlog --numbered --summary --email ./pom.xml
86 Matthias Sohn <matthias.sohn@sap.com>
21 Shawn O. Pearce <spearce@spearce.org>
4 Chris Aniszczyk <caniszczyk@gmail.com>
4 Jonathan Nieder <jrn@google.com>
3 Igor Fedorenko <igor@ifedorenko.com>
3 Kevin Sawicki <kevin@github.com>
2 Colby Ranger <cranger@google.com>
- 至于
pom.xml
,我们也有一个主要提交者。由于git log
的所有选项也可以用于shortlog
,我们同样可以对一个目录使用它,如下所示:
$ git shortlog --numbered --summary --email org.eclipse.jgit.lfs.server.test
35 Matthias Sohn <matthias.sohn@sap.com>
20 David Pursehouse <david.pursehouse@gmail.com>
4 Markus Duft <markus.duft@ssi-schaefer.com>
2 Saša Živkov <sasa.zivkov@sap.com>
1 David Ostrovsky <david@ostrovsky.org>
1 Mat Booth <mat.booth@redhat.com>
1 Karsten Thoms <karsten.thoms@itemis.de>
- 如你所见,要知道在 Git 中哪些文件或目录该找谁是相当简单的。
在源代码树中查找瓶颈
通常,开发团队知道源代码树中的瓶颈在哪里,但说服管理层需要资源来重写部分代码可能是一个挑战。然而,使用 Git,提取此类数据相对简单。
准备工作
先通过以下方式查看 stable-3.1 版本:
$ git checkout stable-3.1
Branch stable-3.1 set up to track remote branch stable-3.1 from origin.
Switched to a new branch 'stable-3.1'
如何做……
我们希望从列出某个提交的统计信息开始,然后可以将示例扩展到更大范围的提交:
- 我们将使用的第一个选项是
git log
的--dirstat
,如下所示:
$ git log -1 --dirstat
commit da6e87bc373c54c1cda8ed563f41f65df52bacbf
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:22:08 2013 +0200
Prepare post 3.1.0 builds
Change-Id: I306a3d40c6ddb88a16d17f09a60e3d19b0716962
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
5.0% org.eclipse.jgit.http.server/META-INF/
6.9% org.eclipse.jgit.http.test/META-INF/
3.3% org.eclipse.jgit.java7.test/META-INF/
4.3% org.eclipse.jgit.junit.http/META-INF/
6.6% org.eclipse.jgit.junit/META-INF/
5.5% org.eclipse.jgit.packaging/
5.9% org.eclipse.jgit.pgm.test/META-INF/
13.7% org.eclipse.jgit.pgm/META-INF/
15.4% org.eclipse.jgit.test/META-INF/
3.7% org.eclipse.jgit.ui/META-INF/
13.1% org.eclipse.jgit/META-INF/
--dirstat
选项显示了在提交中哪些目录发生了变化,并且这些变化与其他目录相比有多少。默认设置是计算在提交中添加或删除的行数。因此,代码的重新排列可能不会算作任何变化,因为行数可能是相同的。你可以通过使用--dirstat=lines
来稍微补偿这一点。这个选项会逐行查看每个文件,查看它们与之前的版本相比是否发生了变化,如下所示:
$ git log -1 --dirstat=lines
commit da6e87bc373c54c1cda8ed563f41f65df52bacbf
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:22:08 2013 +0200
Prepare post 3.1.0 builds
Change-Id: I306a3d40c6ddb88a16d17f09a60e3d19b0716962
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
4.8% org.eclipse.jgit.http.server/META-INF/
6.5% org.eclipse.jgit.http.test/META-INF/
3.2% org.eclipse.jgit.java7.test/META-INF/
4.0% org.eclipse.jgit.junit.http/META-INF/
6.1% org.eclipse.jgit.junit/META-INF/
6.9% org.eclipse.jgit.packaging/
5.7% org.eclipse.jgit.pgm.test/META-INF/
13.0% org.eclipse.jgit.pgm/META-INF/
14.6% org.eclipse.jgit.test/META-INF/
3.6% org.eclipse.jgit.ui/META-INF/
13.8% org.eclipse.jgit/META-INF/
- 这也会产生略微不同的结果。如果你想限制输出仅显示某个百分比以上的目录,我们可以按如下方式限制输出:
$ git log -1 --dirstat=lines,10
commit da6e87bc373c54c1cda8ed563f41f65df52bacbf
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:22:08 2013 +0200
Prepare post 3.1.0 builds
Change-Id: I306a3d40c6ddb88a16d17f09a60e3d19b0716962
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
13.0% org.eclipse.jgit.pgm/META-INF/
14.6% org.eclipse.jgit.test/META-INF/
13.8% org.eclipse.jgit/META-INF/
- 通过在
--dirstat=lines
命令中添加10
,我们要求 Git 只显示发生了 10%或更多变化的目录;你可以根据需要使用任何数字。默认情况下,Git 不统计子目录中的变化,只统计目录中的文件变化。因此,在下图中,只有文件 A1的变化被算作变化;对于目录 A1和文件 B1,它们的变化被算作目录 A2的变化:
-
为了累积这些数据,我们可以在
--dirstat=lines,10
命令中添加cumulative
,这将累积所有变化并计算出百分比。请注意,由于计算方式的不同,百分比可能会超过 100:
$ git log -1 --dirstat=files,10,cumulative
commit da6e87bc373c54c1cda8ed563f41f65df52bacbf
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Thu Oct 3 17:22:08 2013 +0200
Prepare post 3.1.0 builds
Change-Id: I306a3d40c6ddb88a16d17f09a60e3d19b0716962
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
31.3% org.eclipse.jgit.packaging/
- 如你所见,输出与我们之前看到的稍有不同。通过使用
git log --dirstat
,你可以获得一些关于仓库的情况显现出来的信息。显然,你也可以在两个版本之间或两个提交哈希之间做这件事。让我们尝试一下,但这次我们不使用git log
,而是使用git diff
,因为 Git 会显示两个版本之间的累积diff
,而git log
会显示两个版本之间每个提交的dirstat
:
$ git diff origin/stable-3.1..origin/stable-3.2 --dirstat
4.0% org.eclipse.jgit.packaging/org.eclipse.jgit.target/
3.9% org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/
4.1% org.eclipse.jgit.pgm/
20.7% org.eclipse.jgit.test/tst/org/eclipse/jgit/api/
21.3% org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/
5.2% org.eclipse.jgit.test/tst/org/eclipse/jgit/
14.5% org.eclipse.jgit/src/org/eclipse/jgit/api/
6.5% org.eclipse.jgit/src/org/eclipse/jgit/lib/
3.9% org.eclipse.jgit/src/org/eclipse/jgit/transport/
4.6% org.eclipse.jgit/src/org/eclipse/jgit/
- 所以,在
origin/stable-3.1
和origin/stable-3.2
分支之间,我们可以看到哪些目录的变化百分比最高。然后,我们可以通过--stat
或--numstat
进一步深入查看该目录,并再次使用git diff
。我们还会使用--relative="org.eclipse.jgit.test/tst/org/eclipse/"
,这将显示相对于org.eclipse.jgit.test/tst/org/eclipse/
的文件路径,这样在控制台上显示会更清晰。你可以尝试在不使用以下选项的情况下进行此操作:
$ git diff --pretty origin/stable-3.1..origin/stable-3.2 --numstat --relative="org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/" org.eclipse.jgit.test/
tst/org/eclipse/jgit/internal/
4 2 storage/file/FileRepositoryBuilderTest.java
8 1 storage/file/FileSnapshotTest.java
0 741 storage/file/GCTest.java
162 0 storage/file/GcBasicPackingTest.java
119 0 storage/file/GcBranchPrunedTest.java
119 0 storage/file/GcConcurrentTest.java
85 0 storage/file/GcDirCacheSavesObjectsTest.jav
104 0 storage/file/GcKeepFilesTest.java
180 0 storage/file/GcPackRefsTest.java
120 0 storage/file/GcPruneNonReferencedTest.java
146 0 storage/file/GcReflogTest.java
78 0 storage/file/GcTagTest.java
113 0 storage/file/GcTestCase.java
- 第一个数字是添加的行数,第二个数字是两分支之间删除的行数。
还有更多...
我们已经使用了git log
、git diff
和git shortlog
来查找关于仓库的信息,但这些命令有许多选项可以帮助我们找到源代码中的瓶颈。
如果我们想找到提交次数最多的文件,而这些文件不一定是行数增加或删除最多的文件,我们可以使用git log
:
-
我们可以在
origin/stable-3.1
和origin/stable-3.2
分支之间使用git log
,列出每次提交中更改的所有文件。然后,我们只需要使用一些 Bash 工具对结果进行排序和统计,如下所示:
$ git log origin/stable-3.1..origin/stable-3.2 --format=format: --name-only
org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
org.eclipse.jgit.ant.test/pom.xml
- 首先,我们只是执行命令,而没有使用 Bash 工具。从大量输出中可以看到,你只会看到文件名,其他什么都看不见。这是由于所使用的选项。
--format=format:
选项告诉 Git 不显示任何与提交信息相关的信息,而--name-only
则告诉 Git 只列出每次提交的文件。现在,我们要做的就是统计它们:
$ git log origin/stable-3.1..origin/stable-3.2 --format=format: --name-only | sed '/^$/d' | sort | uniq -c | sort -r | head -10
12 se.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
12 est/tst/org/eclipse/jgit/api/RebaseCommandTest.java
9 org.eclipse.jgit/META-INF/MANIFEST.MF
7 org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
7 org.eclipse.jgit.packaging/pom.xml
6 pom.xml
6 pse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
6 org.eclipse.jgit.test/META-INF/MANIFEST.MF
6 org/eclipse/jgit/pgm/internal/CLIText.properties
6 org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
- 现在,我们有了两个版本之间前十个文件的列表,但在进一步操作之前,我们先回顾一下我们所做的。我们获取了文件列表,使用
sed '/^$/d'
移除了输出中的空行。之后,我们用sort
对文件列表进行了排序。接着,我们使用uniq -c
,它会统计每个文件出现的次数,并将数量加到输出中。最后,我们使用sort -r
进行倒序排序,并使用head 10
只显示前十个结果。从这里开始,我们应该列出两个分支之间所有更改了最上面文件的提交,方法如下:
$ git log origin/stable-3.1..origin/stable-3.2 \\
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
commit e90438c0e867bd105334b75df3a6d640ef8dab01
Author: Stefan Lay <stefan.lay@sap.com>
Date: Tue Dec 10 15:54:48 2013 +0100
Fix aborting rebase with detached head
Bug: 423670
Change-Id: Ia6052867f85d4974c4f60ee5a6c820501e8d2427
commit f86a488e32906593903acb31a93a82bed8d87915
- 通过将文件添加到
git log
命令的末尾,我们将看到两个分支之间的提交。现在,我们要做的就是 grep 出有错误的提交,这样我们就可以告诉我们的经理我们在这个文件中修复了多少个错误。
Grep 提交信息
现在我们知道如何列出和排序我们频繁更改的文件,反之亦然,但我们也感兴趣的是找出我们修复的错误,正在实现的功能,甚至可能是谁签署了代码。所有这些信息通常都在提交信息中。有些公司有政策,要求你在提交信息中提到一个错误、一个功能或其他引用。通过在提交信息中包含这些信息,生成漂亮的发布说明也会变得容易得多。
准备好
由于在这些例子中我们大部分时间都在 grep Git 数据库,我们其实不需要检出任何东西或处于某个特定的提交。所以,如果你仍然在 chapter6
文件夹中,我们可以继续。
如何操作...
让我们看看仓库中有多少提交提到了一个错误:
- 首先,我们需要知道提交信息中提到的错误的模式。我通过查看提交信息找到了
jgit
的模式是使用Bug: 6 位数字
;因此,要找到所有这些提交,我们使用git log
的--grep
选项,并可以通过"[Bb][Uu][gG]: [0-9]+"
来 grep:
$ git log --all --grep="^[bB][uU][gG]: [0-9]"
commit 3db6e05e52b24e16fbe93376d3fd8935e5f4fc9b
Author: Stefan Lay <stefan.lay@sap.com>
Date: Wed Jan 15 13:23:49 2014 +0100
Fix fast forward rebase with rebase.autostash=true
The folder .git/rebase-merge was not removed in this case. The
repository was then still in rebase state, but neither abort nor
continue worked.
Bug: 425742
Change-Id: I43cea6c9e5f3cef9d6b15643722fddecb40632d9
- 你应该会看到大量的提交作为输出,但你需要注意的是,所有提交都包含一个指向 bug ID 的引用。那么,grep 做了什么呢?
^[Bb][Uu][gG]:
部分匹配任何大小写组合的 bug。^
字符表示从行首开始。:
字符匹配冒号。接着是[0-9]+
,它将匹配从零到九的任何数字,+
部分表示一个或多个出现。但是正则表达式的内容就到此为止。我们得到了很多输出(这很有价值),但现在我们只想统计提交次数。我们可以通过将其传递给wc -l
来做到这一点(wc -l
用于统计行数):
$ git log --all --oneline --grep="^[bB][uU][gG]: [0-9]+" | wc -l
366
- 在将其传递给
wc
之前,记得使用--oneline
限制每个提交的输出为一行。如你所见,当我写这篇文章时,jgit
参考了366
个 bug,这些 bug 都已经被修复并发布到仓库中。如果你习惯于在其他脚本或编程语言中使用正则表达式,你会发现--grep
并不支持所有内容。你可以通过使用git log
的--extended-regexp
选项来启用更广泛的正则表达式支持;然而,模式仍然需要与--grep
一起使用,如下所示:
$ git log --all --oneline --extended-regexp --grep="^[bB][uU][gG]: [0-9]{6}"
3db6e05 Fix fast forward rebase with rebase.autostash=true
c6194c7 Update com.jcraft.jsch to 0.1.50 in Kepler target platform
1def0a1 Fix for core.autocrlf=input resulting in modified file and unsmudge
0ce61ca Canonicalize worktree path in BaseRepositoryBuilder if set via config
e90438c Fix aborting rebase with detached head
2e0d178 Add recursive variant of Config.getNames() methods
- 我们在前面的示例中使用了它,你可以看到我们得到了相同的提交。我使用了稍微不同的表达式,并且现在将
{6}
替代了+
;{6}
搜索与模式关联的六个出现。在我们的案例中,它是六个数字,因为它紧邻[0-9]
模式。我们可以通过重新计算行数或提交次数来验证,如下所示,使用wc -l
:
$ git log --all --oneline --extended-regexp --grep="^[bB][uU][gG]: [0-9]{6}" | wc -l
366
- 我们得到相同的数字。为了进一步缩小正则表达式,我们可以使用
--regexp-ignore-case
,它将忽略模式的大小写:
$ git log --all --oneline --regexp-ignore-case --extended-regexp --grep="^bug: [0-9]{6}"
3db6e05 Fix fast forward rebase with rebase.autostash=true
c6194c7 Update com.jcraft.jsch to 0.1.50 in Kepler target platform
1def0a1 Fix for core.autocrlf=input resulting in modified file and unsmudge
0ce61ca Canonicalize worktree path in BaseRepositoryBuilder if set via config
e90438c Fix aborting rebase with detached head
2e0d178 Add recursive variant of Config.getNames() methods
- 现在我们得到了完全相同的输出,且不再有
[bB][uU][Gg]
,而只有bug
。
现在你知道如何使用 grep 搜索提交消息中的信息,你可以在提交消息中搜索任何内容,并列出所有匹配正则表达式的提交。
发布内容
在从 Git 提取信息时,生成发布说明是其中一个常见的操作。要生成发布说明,你需要获取从此版本到上一个版本之间的所有有效信息。
我们可以利用之前使用过的一些方法来生成我们想要的数据。
如何操作...
我们首先列出两个标签之间的提交,v2.3.1.201302201838-r
和 v3.0.0.201305080800-m7
,然后基于这些信息进行构建:
- 使用
git log
和v3.0.0.201305080800-m7.. v3.0.0.201305080800-m7
,我们将获得标签之间的提交:
$ git log --oneline v2.3.1.201302201838-r..v3.0.0.201305080800-m7
00108d0 JGit v3.0.0.201305080800-m7
e27993f Add missing @since tags
d7cc6eb Move org.eclipse.jgit.pgm's resource bundle to internal package
75e1bdb Merge "URIish: Allow multiple slashes in paths"
b032623 Remove unused repository field from RevWalk
a626f9f Merge "Require a DiffConfig when creating a FollowFilter"
- 由于这两个标签之间有很多提交,让我们使用
wc -l
来统计它们:
$ git log --oneline v2.3.1.201302201838-r..v3.0.0.201305080800-m7 | wc -l
211
- 在这两个标签之间有
211
次提交。现在,我们将展示发布版本之间最常修改的文件:
$ git log v2.3.1.201302201838-r..v3.0.0.201305080800-m7 --format=format: --name-only | sed '/^$/d' | sort | uniq -c | sort -r | head -10
11 org.eclipse.jgit/src/org/eclipse/jgit/internal/st
10 org.eclipse.jgit/src/org/eclipse/jgit/internal/sto
10 org.eclipse.jgit.pgm/resources/org/eclipse/jgit/p
9 org.eclipse.jgit.test/META-INF/MANIFEST.MF
8 pom.xml
8 org.eclipse.jgit/src/org/eclipse/jgit/storage/pac
8 org.eclipse.jgit/src/org/eclipse/jgit/internal/sto
8 org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLI
7 org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/D
7 org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/D
- 这些信息非常有用,因为我们现在有了对大多数更改所在位置的概览。然后,我们可以找到与 bug 相关的提交,这样我们就可以列出 bug ID:
$ git log --format=format:%h --regexp-ignore-case --extended-regexp --grep="bug: [0-9]{6}" v2.3.1.201302201838-r..v3.0.0.201305080800-m7 | xargs -n1 git log -1 | grep --ignore-case -E "commit [0-9a-f]{40}|bug:"
commit e8f720335f86198d4dc99af10ffb6f52e40ba06f
Bug: 406722
commit f448d62d29acc996a97ffbbdec955d14fde5c254
Bug: 388095
commit 68b378a4b5e08b80c35e6ad91df25b1034c379a3
Bug: 388095
commit 8bd1e86bb74da17f18272a7f2e8b6857c800a2cc
Bug: 405558
commit 37f0e324b5e82f55371ef8adc195d35f7a196c58
Bug: 406722
commit 1080cc5a0d67012c0ef08d9468fbbc9d90b0c238
Bug: 403697
commit 7a42b7fb95ecd2c132b2588e5ede0f1251772b30
Bug: 403282
commit 78fca8a099bd2efc88eb44a0b491dd8aecc222b0
Bug: 405672
commit 4c638be79fde7c34ca0fcaad13d7c4f1d9c5ddd2
Bug: 405672
- 所以,现在我们得到的是一个包含修复的 bug 和对应提交哈希值的不错列表。
它是如何工作的...
我们正在使用一些 Bash 工具来获取已修复 bug 的列表。我将在本节简要解释它们的作用:
-
xargs -n1 git log -1
部分将在从第一个git log
命令来的每个提交上执行git log -1
,命令为git log --format=format:%h --regexp-ignore-case --extended-regexp --grep="bug: [0-9]{6}" v2.3.1.201302201838-r..v3.0.0.201305080800-m7
。 -
grep --ignore-case -E "commit [0-9a-f]{40}|bug:"
部分将忽略正则表达式中的大小写,-E
将启用扩展正则表达式。你可能会发现,很多这些grep
工具的选项和我们在git log
中使用的选项是一样的。这个正则表达式匹配的是提交和由[0-9a-f]
范围表示的 40 个字符,或者是 bug。|
字符表示或。记住,我们现在处于git log -1
的输出中。
我们提取的所有这些信息是编写良好、扎实的发布说明的基础,包含了从一个版本到另一个版本的更改内容。
接下来的自然步骤是查看 bug 跟踪系统,并列出每个错误在提交中被修复的标题。然而,这不是我们在这里讨论的内容,因为它完全取决于你使用的系统。
查找在最近一段时间内在仓库中完成的工作
有时,能够提取在特定时间范围内完成的工作是非常有用的。让我们看看 git log
的多个参数如何帮助完成这个任务。
如何操作...
- 假设我们想知道在过去 30 天内,在我们分析的
jgit
仓库中完成的所有工作:
$ git log --all --since="30 days ago"
commit 6efedb41c6fe3fc6eb88f49afc3e7f481514e806 (HEAD -> master, origin/master, origin/HEAD)
Author: Jonathan Nieder <jrn@google.com>
Date: Wed May 2 15:23:31 2018 -0700
Mark CrissCrossMergeTest as flaky
It often fails on my machine, both in maven and bazel.
This patch marks the test flaky[1] in bazel so that "bazel test" can
run it a few times before declaring failure.
[1] https://docs.bazel.build/versions/master/be/common-definitions.html#test.flaky
Bug: 534285
Change-Id: Ibe5414fefbffe4e8f86af7047608d51cf5df5c47
commit 5f2ddc8ac0528f2fc9776be822568dff3f065670
Merge: b1f8ddfb7 3d89622d4
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Sat May 5 19:44:26 2018 -0400
Merge "Add API filter for "non-API type FileRepository" in tests"
commit 3d89622d4e32eb24c203b71f4cce49e35dff8e09
Author: David Pursehouse <david.pursehouse@gmail.com>
Date: Thu Apr 12 10:53:29 2018 +0900
Add API filter for "non-API type FileRepository" in tests
Change-Id: If805ad4a962e48dd16fbc7eff915fd6539839933
Signed-off-by: David Pursehouse <david.pursehouse@gmail.com>
[...]
在这里,我们使用 --all
来查看所有分支中的提交,而不仅仅是当前分支。我们还使用了如本章前面所示的 --since
。
- 现在,让我们只显示
David Pursehouse
的提交:
$ git log --all --since="30 days ago" --oneline --author="David Pursehouse"
3d89622d4 Add API filter for "non-API type FileRepository" in tests
9fb724f1b RefDatabase: add hasRefs convenience method
4dcf2f93d RefDatabase: Introduce getAllRefs method
57f158632 RefDatabase: Update Javadoc for ALL constant
20d431f79 LargePackedWholeObject#openStream: Suppress resource warning
7575cab53 Upgrade error_prone_core to 2.3.1
cbb2e65db PushProcess: Remove unused import of HashMap
5b0129641 Merge "Push: Ensure ref updates are processed in input order"
e5ba2c9bd DirCache: Use constant from StandardCharsets
ec84767c3 Use Constants.CHARACTER_ENCODING in tests
b0ac5f9c8 LargePackedWholeObject: Refactor to open DfsReader in try-with-resource
045799f2e Merge branch 'stable-4.11'
我们使用了 --author
来指定所需的提交作者,使用 --oneline
来将输出缩减为更易于管理的格式。
如果你在寻找自己的提交,可以将你的名字传递给 --author
,但是如果你在编写脚本或别名并希望它具有可移植性,至少在 Linux 和 macOS 上,你可以使用子进程来自动检索信息:--author=$(git config user.name)
。
- 看起来存在一些合并提交。这些提交并不是描述上个月活动的有用信息,因此我们可以通过
--no-merges
来去除它们:
$ git log --all --since="30 days ago" --oneline --author="David Pursehouse" --no-merges
3d89622d4 Add API filter for "non-API type FileRepository" in tests
9fb724f1b RefDatabase: add hasRefs convenience method
4dcf2f93d RefDatabase: Introduce getAllRefs method
57f158632 RefDatabase: Update Javadoc for ALL constant
20d431f79 LargePackedWholeObject#openStream: Suppress resource warning
7575cab53 Upgrade error_prone_core to 2.3.1
cbb2e65db PushProcess: Remove unused import of HashMap
e5ba2c9bd DirCache: Use constant from StandardCharsets
ec84767c3 Use Constants.CHARACTER_ENCODING in tests
b0ac5f9c8 LargePackedWholeObject: Refactor to open DfsReader in try-with-resource
我们终于得到了所需的信息。这个简单的示例还展示了良好提交信息的重要性,因为它们会让 Git 管理的历史变得更加有用和有价值!
它是如何工作的...
在这个例子中,我们不需要做复杂的操作就能得到所需的结果;我们只是利用了git log
命令及其选项的强大功能。事实上,git log
有将近 200 个不同的参数,并且它的帮助文档,通过git log --help
访问,包含了超过 11,000 个单词!现在你知道下一次没有网络连接的长途飞行时该做什么了!
还有更多…
那么,跨仓库的情况呢?当然,前面的做法可以通过一些脚本进行扩展,从而在一系列仓库上重复操作,但有一个更好且更简单的选项可用,它作为一个第三方应用程序,利用了我们到目前为止探索的相同git log
功能:git-standup
。
它可以通过curl
命令轻松安装:
curl -L https://raw.githubusercontent.com/kamranahmedse/git-standup/master/installer.sh | sudo sh
它的源代码可以在https://github.com/kamranahmedse/git-standup
找到,且具有多个选项,可以使日常或每周的团队会议准备工作变得更加轻松。
git-standup
也可以在单个仓库上运行,当应用到之前的例子时,它的输出如下:
$ git-standup -a "David Pursehouse" -d 30
3d89622d4 - Add API filter for "non-API type FileRepository" in tests (6 days ago) <David Pursehouse>
9fb724f1b - RefDatabase: add hasRefs convenience method (12 days ago) <David Pursehouse>
4dcf2f93d - RefDatabase: Introduce getAllRefs method (2 weeks ago) <David Pursehouse>
57f158632 - RefDatabase: Update Javadoc for ALL constant (2 weeks ago) <David Pursehouse>
20d431f79 - LargePackedWholeObject#openStream: Suppress resource warning (2 weeks ago) <David Pursehouse>
7575cab53 - Upgrade error_prone_core to 2.3.1 (3 weeks ago) <David Pursehouse>
cbb2e65db - PushProcess: Remove unused import of HashMap (4 weeks ago) <David Pursehouse>
e5ba2c9bd - DirCache: Use constant from StandardCharsets (4 weeks ago) <David Pursehouse>
第七章:使用 Git 钩子、别名和脚本增强你的日常工作
在本章中,我们将涵盖以下内容:
-
在提交信息中使用分支描述
-
创建动态提交信息模板
-
在提交信息中使用外部信息
-
防止推送特定的提交
-
配置和使用 Git 别名
-
配置和使用 Git 脚本
-
设置和使用提交模板
介绍
为了在企业环境中高效工作,关于生产的任何代码都有一些前提条件或规则。代码应该能够编译并通过特定的单元测试集。此外,提交信息中还应该包含某些文档内容,例如修复 ID 或实例的引用。这些规则中的大多数可以通过脚本进行自动化。但为什么不把这些规则纳入到流程中呢?在本章中,你将看到一些示例,展示如何在看到提交信息之前将数据从一个位置传输到提交信息中。你还将学习如何验证你是否将代码推送到正确的位置。最后,你将学习如何将脚本添加到 Git 中。
Git 中的钩子是一个在特定事件(如推送、提交或变基)触发时执行的脚本。如果这些脚本以非零值退出,最好取消当前的 Git 操作。你可以在任何 Git 克隆的 .git/hooks
文件夹中找到这些钩子脚本。如果它们的文件扩展名是 .sample
,则表示这些钩子是非激活状态。
在提交信息中使用分支描述
在第三章《分支、合并与选项》中,我们提到过你可以为你的分支设置描述,并且可以通过 git config --get branch.<branchname> description
命令从脚本中获取此信息。在这个例子中,我们将提取这些信息并将其用于提交信息。
我们将使用 prepare-commit-msg
钩子。prepare-commit-msg
钩子会在每次你想要提交时执行,钩子可以设置为在你实际看到提交信息编辑器之前检查任何你想要检查的内容。
准备工作
我们需要一个克隆和一个分支来开始这个练习,因此我们将再次将 jgit
克隆到 chapter7.5
文件夹中,如下所示:
$ git clone https://git.eclipse.org/r/jgit/jgit chapter7.5
Cloning into 'chapter7.5'...
remote: Counting objects: 2170, done
remote: Finding sources: 100% (364/364)
remote: Total 45977 (delta 87), reused 45906 (delta 87)
Receiving objects: 100% (45977/45977), 10.60 MiB | 1.74 MiB/s, done.
Resolving deltas: 100% (24651/24651), done.
Checking connectivity... done.
Checking out files: 100% (1577/1577), done.
检出一个本地的 descriptioInCommit
分支,该分支跟踪 origin/stable-3.2
分支:
$ cd chapter7.5
$ git checkout -b descriptioInCommit --track origin/stable-3.2
Branch descriptioInCommit set up to track remote branch stable-3.2 from origin.
Switched to a new branch 'descriptioInCommit'
如何操作...
我们将从设置本地分支的描述开始。然后,我们将创建一个钩子来提取此信息并将其放入提交信息中。
我们有本地的 descriptioInCommit
分支,我们需要为其设置描述。我们将使用 --edit-description
Git 分支命令为本地分支添加描述。这样会打开描述编辑器,你可以通过以下步骤输入消息:
- 当你执行命令时,描述编辑器将打开,你可以输入消息:
$ git branch --edit-description descriptioInCommit
- 现在,输入以下消息:
Remote agent not connection to server
When the remote agent is trying to connect
it will fail as network services are not up
and running when remote agent tries the first time
- 你应该像编写提交信息一样编写你的分支描述。然后,将描述重复使用在提交信息中是有意义的。现在,我们将验证是否有以下描述的信息:
$ git config --get branch.descriptioInCommit.description
Remote agent not connection to server
When the remote agent is trying to connect
it will fail as network services are not up
and running when remote agent tries the first time
- 如预期的那样,我们得到了所需的输出。现在,我们可以继续创建将获取描述并使用它的钩子。
接下来,我们将检查是否有钩子的描述,如果有,我们将使用该描述作为提交信息。
- 首先,我们将确保能够在期望的位置获取提交信息。实现这一点有多种方法,我们选择了以下方法:打开
.git/hook/prepare-commit-msg
钩子文件,输入以下脚本,并使其可执行(chmod +x
):
#!/bin/bash
BRANCH=$(git branch | grep '*'| sed 's/*//g'| sed 's/ //g')
DESCRIPTION=$(git config --get branch.${BRANCH}.description)
if [ -z "$DESCRIPTION" ]; then
echo "No desc for branch using default template"
else
# replacing # with n
DESCRIPTION=$(echo "$DESCRIPTION" | sed 's/#/\n/g')
# replacing the first \n with \n\n
DESCRIPTION=$(echo "$DESCRIPTION" | sed 's/\n/\n\n/')
# append default commit message
DESCRIPTION=$(echo "$DESCRIPTION" && cat $1)
# and write it all to the commit message
echo "$DESCRIPTION" > $1
fi
- 现在,我们可以尝试创建一个提交,看看提交信息是否按预期显示。使用
git commit --allow-empty
生成一个空提交,同时触发 prepare-commit-msg 钩子:
$ git commit --allow-empty
- 你应该会看到一个带有我们分支描述作为提交信息的编辑器,如下所示:
Remote agent not connection to server
When the remote agent is trying to connect
it will fail as network services are not up
and running when remote agent tries the first time
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch descriptioInCommit
# Your branch is up-to-date with 'origin/stable-3.2'.
#
# Untracked files:
# hen the remote agent is trying to connect
#
- 这正如我们所预期的那样。保存提交信息并关闭编辑器。尝试使用
git log -1
命令来验证我们是否在提交中有以下信息:
$ git log -1
commit 92447c6aac2f6d675f8aa4cb88e5abdfa46c90b0
Author: John Doe <john.doe@example.com>
Date: Sat Mar 15 00:19:35 2014 +0100
Remote agent not connection to server
When the remote agent is trying to connect
it will fail as network services are not up
and running when remote agent tries the first time
- 你应该会得到类似的提交信息,内容与我们的分支描述相同。不过,如果分支描述为空呢?我们的钩子会如何处理?我们可以尝试创建一个名为
noDescriptionBranch
的新分支。使用git checkout
创建它,并按以下命令检查:
$ git checkout -b noDescriptionBranch
Switched to a new branch 'noDescriptionBranch'
- 现在,我们将再创建一个空提交,以查看提交信息是否如下所示:
$ git commit --allow-empty
- 你应该会看到带有默认提交信息文本的提交信息编辑器,如下所示:
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit. #
# On branch noDescriptionBranch
一切都如我们预期的那样。这个脚本可以与下一个练习结合使用,后者将从一个有缺陷的系统中提取内容。
创建动态提交信息模板
可以鼓励开发人员做正确的事,或者可以强迫开发人员做正确的事。然而,最终,开发人员需要花时间进行编码。因此,如果需要良好的提交信息,我们可以使用prepare-commit-msg
钩子来协助开发人员。
在这个例子中,我们将为开发人员创建一个包含工作区状态信息的提交信息。它还会插入一些来自网页的信息。这些信息也可以是 Bugzilla 中的缺陷信息。
准备就绪
为了开始这个练习,我们不会克隆一个仓库,而是创建一个新的仓库。为此,我们将使用git init
,如以下代码所示。你可以使用git init <directory>
在某个地方创建一个新仓库,或者你可以进入一个目录并执行git init
,Git 会为你创建一个仓库。
$ git init chapter7
Initialized empty Git repository in /Users/JohnDoe/repos/chapter7/.git/
$ cd chapter7
如何做到这一点...
我们有我们的chapter7
目录,在这里我们刚刚初始化了我们的仓库。在此目录中,钩子已经可用。只需查看.git/hooks
目录即可。我们将使用prepare-commit-msg
钩子。执行以下步骤:
- 从以下钩子文件夹开始查找:
$ ls .git/hooks/
applypatch-msg.sample pre-applypatch.sample
pre-rebase.sample commit-msg.sample
pre-commit.sample prepare-commit-msg.sample
post-update.sample pre-push.sample
update.sample
- 如您所见,每个钩子文件中都有很多钩子。这里有一个示例脚本,并简要说明了钩子做什么以及何时执行。要启用
prepare-commit-msg
,请按以下代码所示重命名文件:
$ cd .git/hooks/
$ mv prepare-commit-msg.sample prepare-commit-msg
$ cd -
-
在您喜欢的编辑器中打开
prepare-commit-msg
文件。 -
您可以查看文件中的信息,但对于我们的示例,我们将清空文件,以便可以包括脚本。
-
现在,在文件中包含以下命令:
#!/bin/bash
echo "I refuse to commit"
exit 1
-
保存文件。
-
最后,尝试提交某些内容或不提交内容。通常,您不能提交空的内容,但使用
--allow-empty
选项,您可以创建一个空的提交,如下所示:
$ git commit --allow-empty
I refuse to commit
- 正如您所见,我们得到了在
prepare-commit-msg
脚本文件中输入的消息。您可以使用git log -1
命令检查我们是否有提交,方法如下:
$ git log -1
fatal: your current branch 'master' does not have any commits yet
没有提交,我们收到了一个我们以前没有见过的错误消息。消息必须存在,因为到目前为止在这个仓库中还没有提交。在我们进一步更改脚本之前,我们应该知道prepare-commit-msg
钩子会根据情况接收一些参数。第一个参数始终是.git/COMMIT_EDITMSG
,第二个参数可以是 merge、commit、squash 或 template,具体取决于情况。我们可以在脚本中使用这些参数。
- 更改脚本,以便我们可以拒绝修改提交,如下所示:
#!/bin/bash
if [ "$2" == "commit" ]; then
echo "Not allowed to amend"
exit 1
fi
- 现在我们已经更改了脚本,让我们创建一个提交并尝试修改它,如下所示:
$ echo "alot of fish" > fishtank.txt
$ git add fishtank.txt
$ git commit -m "All my fishes are belong to us"
[master (root-commit) f605886] All my fishes are belong to us
1 file changed, 1 insertion(+)
create mode 100644 fishtank.txt
- 现在我们已经有了提交,让我们尝试使用
git commit --amend
来修改它:
$ git commit --amend
Not allowed to amend
- 正如我们所预期的那样,我们没有被允许修改提交。如果我们希望提取一些信息,例如从错误处理系统中提取,我们必须在打开编辑器之前将这些信息放入文件中。所以,我们将再次更改脚本,如下所示:
#!/bin/bash
if [ "$2" == "commit" ]; then
echo "Not allowed to amend"
exit 1
fi
MESSAGE=$(curl -s http://whatthecommit.com/index.txt)
echo "$MESSAGE" > $1
- 这个脚本从
http://www.whatthecommit.com/
下载一个提交消息并将其插入到提交信息中。每次提交时,您都会从网页上获取一条新的消息。让我们使用以下命令试一下:
$ echo "gravel, plants, and food" >>fishtank.txt
$ git add fishtank.txt
$ git commit
- 当提交信息编辑器打开时,您应该看到来自
whatthecommit.com
的消息。关闭编辑器后,使用git log -1
命令验证我们是否已经有了提交,方法如下:
$ git log -1
commit c087f75665bf516af2fe30ef7d8ed1b775bcb97d
Author: John Doe <john.doe@example.com>
Date: Wed Mar 5 21:12:13 2014 +0100
640K ought to be enough for anybody
- 正如预期的那样,我们已经成功完成了提交。显然,这不是为提交者准备的最佳消息。更典型的做法是在提交信息中列出分配给开发者的错误,如下所示:
# You have the following artifacts assigned
# Remove the # to add the artifact ID to the commit message
#[artf23456] Error 02 when using update handler on wlan
#[artf43567] Enable Unicode characters for usernames
#[artf23451] Use stars instead of & when keying pword
- 这样,开发者可以轻松地从 TeamForge 中选择正确的错误 ID,或者在这种情况下,使用其他系统查看提交信息时所需的正确格式的工件 ID。
还有更多内容...
你可以轻松扩展 prepare-commit-msg
钩子的功能,但你应该记住,获取一些信息的等待时间应该是值得的。一个通常很容易检查的事情是工作区是否有修改。
在这里,我们需要在准备提交信息的钩子中使用 git status
命令,并且我们需要预测提交后是否会有修改的文件:
- 要检查这一点,我们需要有一些已暂存的更改和一些未暂存的更改,如下所示:
$ git status
On branch master
nothing to commit, working directory clean
- 现在,修改
fishtank.txt
文件:
$ echo "saltwater" >> fishtank.txt
- 使用
git status --porcelain
检查工作区:
$ git status --porcelain
M fishtank.txt
- 使用
git add
将文件添加到暂存区:
$ git add fishtank.txt
- 现在尝试
git status --porcelain
:
$ git status --porcelain
M fishtank.txt
- 你需要注意的是,第一次使用
--porcelain
选项查看 Git 状态时,M
前面有一个空格。porcelain
选项提供了机器友好的输出,显示 Git 状态下文件的状态。第一个字符表示暂存区的状态,而第二个字符表示工作区的状态。因此,MM fishtank.txt
表示该文件在工作区和暂存区都有修改。所以,如果你再次修改fishtank.txt
,你可以预期如下结果:
$ echo "sharks and oysters" >> fishtank.txt
$ git status --porcelain
MM fishtank.txt
- 如预期的那样,Git 状态的输出为
MM fishtank.txt
。我们可以在钩子中使用这个输出,来判断提交后工作区是否会有未提交的更改。将以下命令添加到prepare-commit-msg
文件中:
for file in $(git status --porcelain)
do
if [ ${file:1:1} ]; then
DIRTY=1
fi
done
if [ "${DIRTY}" ]; then
# -i '' is not needed on Linux
sed -i '' "s/# Please/You have a dirty workarea are you sure you wish to commit ?&/" $1
fi
- 首先,我们使用
git status --porcelain
列出所有已更改的文件。然后,对于每个文件,我们检查是否有第二个字符。如果有第二个字符,那么提交后工作区就会有修改。最后,我们将信息插入到提交信息中,以便开发人员查看。让我们尝试并通过以下命令提交更改:
$ git commit
- 检查是否有类似以下内容的消息。第一行可能会有所不同,因为我们仍然有来自
http://www.whatthecommit.com/
的消息:
somebody keeps erasing my changes.
You have a dirty workarea are you sure you wish to commit ?
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# modified: fishtank.txt
#
# Changes not staged for commit:
# modified: fishtank.txt
#
- 保存文件并关闭编辑器将创建提交。使用
git log -1
验证此操作,如下所示:
$ git log -1
commit 70cad5f7a2c3f6a8a4781da9c7bb21b87886b462
Author: John Doe <john.doe@example.com>
Date: Thu Mar 6 08:25:21 2014 +0100
somebody keeps erasing my changes.
You have a dirty workarea are you sure you wish to commit ?
- 我们得到了预期的信息。有关脏工作区的文本已出现在提交信息中。为了在下一次练习前做一个干净的清理,我们应该将工作区重置为
HEAD
,如下所示:
$ git reset --hard HEAD
HEAD is now at 70cad5f somebody keeps erasing my changes.
现在,只需找出什么最适合你。在提交并可能推送代码到远程分支之前,你是否希望检查任何信息?这可能包括:
-
代码中的样式检查
-
使用 Pylint 检查你的 Python 脚本
-
检查是否有不允许添加到 Git 的文件
这个列表并不详尽;对于世界上每个组织或开发团队,可能还有其他需要添加的内容。然而,这显然是一种方法,可以减少开发者繁琐的手动工作,让他们能够专注于编码。
在提交信息中使用外部信息
提交钩子在你关闭提交信息编辑器时执行。它可以用于操作提交信息或自动审核提交信息,以检查其是否具有特定的格式。
在这个示例中,我们将操作并检查提交信息的内容。
准备就绪
为了开始这个练习,我们只需要创建一个分支并切换到它。我们需要禁用当前的prepare-commit-msg
钩子;可以通过简单地重命名它来实现。现在,我们可以通过以下命令开始处理commit-msg
钩子:
$ git checkout -b commit-msg-example
Switched to a new branch 'commit-msg-example'
$ mv .git/hooks/prepare-commit-msg .git/hooks/prepare-commit-msg.example
如何操作...
在第一个示例中,我们要做的是检查缺陷信息是否正确。无需发布引用不存在的缺陷的提交:
- 我们将从测试
commit-msg
钩子开始。首先,复制当前的钩子文件,然后我们将强制使钩子以非零值退出,从而中止提交的创建:
$ cp .git/hooks/commit-msg.sample .git/hooks/commit-msg
- 现在,在你喜欢的编辑器中打开文件,并将以下行添加到文件中:
#!/bin/bash
echo "you are not allowed to commit"
exit 1
- 现在,我们将尝试进行一次提交,看看会发生什么,具体如下:
$ echo "Frogs, scallops, and coco shell" >> fishtank.txt
$ git add fishtank.txt
$ git commit
- 编辑器将打开,你可以写一个简短的提交信息,然后关闭编辑器。你应该会看到
you are not allowed to commit
的消息,如果你使用git log -1
检查,你会发现没有你刚才写的提交信息,具体如下:
you are not allowed to commit
$ git log -1
commit 70cad5f7a2c3f6a8a4781da9c7bb21b87886b462
Author: John Doe <john.doe@example.com>
Date: Thu Mar 6 08:25:21 2014 +0100
somebody keeps erasing my changes.
You have a dirty workarea are you sure you wish to commit ?
- 如你所见,提交信息钩子在你关闭消息编辑器后执行,而
prepare-commit-msg
钩子在消息编辑器之前执行。为了验证,如果我们在提交信息中有对钩子的正确引用,我们将检查 Jenkins-CI 项目是否有特定的错误。将commit-msg
钩子中的行替换成以下命令:
#!/bin/bash
JIRA_ID=$(cat $1 | grep jenkins | sed 's/jenkins //g')
ISSUE_INFO=$(curl -g "https://issues.jenkins-ci.org/browse/JENKINS-${JIRA_ID}")
if [ -z "${ISSUE_INFO}" ]; then
echo "Jenkins issue ${JIRA_ID} does not exist"
echo "Please try again"
exit 1
else
TITLE=$(curl -g "https://issues.jenkins-ci.org/browse/JENKINS-$JIRA_ID}" | grep -E "<title>.*</title>")
echo "Jenkins issue ${JIRA_ID}"
echo "${TITLE}"
exit 0
fi
- 我们使用 curl 来检索网页,如果网页为空,我们就知道该 ID 不存在。现在,我们应该创建一个提交,看看如果我们输入错误的 ID(如
jenkins 384895
)或者一个存在的 ID(如jenkins 3157
)会发生什么。为此,我们将按如下方式创建提交:
$ echo "more water" >> fishtank.txt
$ git add fishtank.txt
$ git commit
- 在提交信息中,写入类似
Feature cascading...
的提交信息标题。然后,在提交信息的正文中插入jenkins 384895
。这是关键部分,因为钩子将使用该号码在 Jenkins 问题追踪器中查找:
Feature: Cascading...
jenkins 384895
- 你应该得到以下输出:
Jenkins issue 384895 does not exist
Please try again
- 这是我们预期的结果。现在,使用
git status
验证更改是否已经提交:
$ git status
On branch commit-msg-example
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: fishtank.txt
- 现在,我们将再次尝试提交;这次,我们将使用正确的 JIRA ID:
$ git commit
- 输入一个像之前那样的提交信息;这次,确保 Jenkins 问题 ID 是存在的。你可以使用
51444
:
Feature: Cascading...
jenkins 51444
- 保存提交信息后,应该得到如下输出。我们可以通过去除标题 HTML 标签进一步清理它:
<title>[#JENKINS-51444] Maven Parser creates errors during affectedFilesResolving - Jenkins JIRA</title>
[commit-msg-example 3d39ca3] Feature: Cascading...
1 file changed, 2 insertions(+)
- 如你所见,我们可以获取信息并输出。我们也可以将这些信息添加到提交信息中。然后,我们可以更改并将其作为
else
分支插入脚本:
TITLE=$(curl https://issues.jenkins-ci.org/browse/JENKINS-${JIRA_ID} | grep -E "<title>.*</title>")
TITLE=$(echo ${TITLE} | sed 's/^<title>//' | sed 's/<\/title>$//')
echo "${TITLE}" >> $1
echo "Jenkins issue ${JIRA_ID}"
echo "${TITLE}"
exit 0
- 为了测试这一点,我们将再次创建一个提交,并且在信息中需要指定存在的 JIRA ID:
$ echo "Shrimps and mosquitos" >> fishtank.txt
$ git add fishtank.txt
$ git commit
After saving the commit message editor you will get an output similar like this.
Jenkins issue 51444
[JENKINS-51444] Maven Parser creates errors during affectedFilesResolving - Jenkins JIRA
[commit-msg-example 6fa2cb4] Feature: Cascading...
1 file changed, 1 insertion(+)
- 为了验证我们是否在信息中得到了所需的内容,我们将再次使用
git log -1
:
$ git log -1
commit 6fa2cb47989e12b05cd2689aa92244cb244426fc
Author: John Doe <john.doe@example.com>
Date: Thu Mar 6 09:46:18 2014 +0100
Feature: Cascading...
jenkins 51444
[#JENKINS-51444] Maven Parser creates errors during affectedFilesResolving - Jenkins JIRA
正如预期的那样,我们在提交的末尾得到了信息。在这些示例中,如果 JIRA ID 不存在,我们将丢弃提交信息。这对开发者来说有点苛刻。所以,你可以将它与prepare-commit-msg
钩子结合使用。如果commit-msg
停止提交过程,那么临时保存该信息,以便在开发者再次尝试时,prepare-commit-msg
钩子可以使用这个信息。
防止特定提交的推送
预推送钩子会在使用推送命令时触发,并且脚本执行发生在推送之前。因此,我们可以在发现拒绝推送的原因时阻止推送。
其中一个原因可能是你在提交信息中有nopush
文本。
准备就绪
要使用 Git 的预推送钩子,我们需要有一个远程仓库。我们将再次克隆jgit
,如以下所示:
$ git clone https://git.eclipse.org/r/jgit/jgit chapter7.1
Cloning into 'chapter7.1'...
remote: Counting objects: 2429, done
remote: Finding sources: 100% (534/534)
remote: Total 45639 (delta 145), reused 45578 (delta 145)
Receiving objects: 100% (45639/45639), 10.44 MiB | 2.07 MiB/s, done.
Resolving deltas: 100% (24528/24528), done.
Checking connectivity... done.
Checking out files: 100% (1576/1576), done.
如何实现...
我们希望能够推送到远程分支,但不幸的是,Git 会在执行钩子之前通过 HTTPS 尝试对jgit
仓库进行身份验证。因此,我们将从chapter7.1
目录创建一个本地克隆,如下所示。这将使我们的远程变为本地文件夹:
$ git clone --branch master ./chapter7.1/ chapter7.2
Cloning into ' chapter7.2'...
done.
Checking out files: 100% (1576/1576), done.
$ cd chapter7.2 $ git branch
* master
我们正在将chapter7.1
目录克隆到名为chapter7.2
的文件夹中,克隆完成后将检查master
分支。
我们现在想做的是创建一个提交,提交信息中包含nopush
。通过在提交信息中添加这个词,钩子中的代码将自动停止推送。我们将在一个分支上进行此操作。所以,首先,你应该检出一个prepushHook
分支,该分支跟踪origin/master
分支,然后创建一个提交。
当我们设置好预推送提交时,我们将尝试将其推送到远程,具体如下:
- 从创建一个名为
prepushHook
的新分支开始,该分支跟踪origin/master
:
$ git checkout -b prepushHook --track origin/master
Branch prepushHook set up to track remote branch master from origin.
Switched to a new branch 'prepushHook'
- 现在,我们使用
reset
回到一个较早的提交。这并不重要我们回到多远。我们只是选择了一个随机的提交,如下所示:
$ git reset --hard 2e0d178
HEAD is now at 2e0d178 Add recursive variant of Config.getNames() methods
- 现在我们可以创建一个提交。我们将使用
sed
进行简单的内联替换,然后添加pom.xml
并提交:
$ sed -i '' 's/2.9.1/3.0.0/g' pom.xml
$ git add pom.xml
$ git commit -m "Please nopush"
[prepushHook 69d571e] Please nopush
1 file changed, 1 insertion(+), 1 deletion(-)
- 要验证我们是否有包含文本的提交,可以运行
git log -1
:
$ git log -1
commit 1269d14fe0c32971ea33c95126a69ba6c0d52bbf
Author: John Doe <john.doe@example.com>
Date: Thu Mar 6 23:07:54 2014 +0100
Please nopush
- 我们在提交信息中得到了所需的内容。现在,我们只需要准备钩子。我们将从复制示例钩子开始,重命名为实际名称,以便它会在推送时执行:
$ cp .git/hooks/pre-push.sample .git/hooks/pre-push
- 编辑钩子,使其代码如以下代码片段所示:
#!/bin/bash
echo "You are not allowed to push"
exit 1
- 现在我们准备推送了。我们将把当前分支
HEAD
推送到远程的master
分支:
$ git push origin HEAD:refs/heads/master
You are not allowed to push
error: failed to push some refs to '../chapter7.1/'
- 正如预期的那样,钩子正在执行,推送被钩子拒绝。现在,我们可以实现我们想要进行的检查。如果我们在任何提交信息中有
nopush
这个词,我们希望退出。我们可以使用git log --grep
来搜索提交信息中包含nopush
关键词的提交,如下所示的命令:
$ git log --grep "nopush"
commit 51201284a618c2def690c9358a07c1c27bba22d5
Author: John Doe <john.doe@example.com>
Date: Thu Mar 6 23:07:54 2014 +0100
Please nopush
- 我们已经创建了带有
nopush
关键词的新提交。现在,我们将在钩子中执行一个简单的检查,并编辑 pre-push 钩子,使其包含以下内容:
#!/bin/bash
COMMITS=$(git log --grep "nopush")
if [ "$COMMITS" ]; then
echo "You have commit(s) with nopush message"
echo "aborting push"
exit 1
fi
- 现在,我们可以再次尝试推送,看看结果会是什么。我们将尝试将我们的
HEAD
推送到远程origin
的主分支:
$ git push origin HEAD:refs/heads/master
You have commit(s) with nopush message
aborting push
error: failed to push some refs to '/Users/JohnDoe/repos/./chapter7.1/'
正如预期的那样,由于我们在提交中有nopush
信息,系统不允许我们推送。
还有更多...
拥有一个钩子来防止你推送不想推送的提交非常方便。你可以指定任何你想要的关键词。诸如reword
、temp
、nopush
、temporary
或hack
等词语都可以是你希望停止的内容,但有时你可能还是想把它们推送出去。
你可以做的是有一个小检查器,检查特定的词,然后列出提交,并询问你是否仍然想要推送。
如果你将脚本更改为以下片段,钩子将尝试找到包含nopush
关键词的提交并列出它们。如果你希望无论如何推送它们,你可以回答问题,Git 将继续推送:
#!/bin/bash
COMMITS=$(git log --grep "nopush" --format=format:%H)
if [ "$COMMITS" ]; then
exitmaybe=1
fi
if [ $exitmaybe -eq 1 ]; then
while true
do
clear
for commit in $COMMITS
do
echo "$commit has no push in the message"
done
echo "Are you sure you want to push the commit(s) "
read -r REPLY <&1
case $REPLY in
[Yy]* ) break;;
[Nn]* ) exit 1;;
* ) echo "Please answer yes or no.";;
esac
done
fi
再次尝试使用git push
命令,如下所示:
$ git push origin HEAD:refs/heads/master
Commit 70fea355bac0c65fd51f4874d75e65b4a29ad763 has nopush in message
Are you sure you want to push the commit(s)
输入n
并按Enter。然后,预期推送将被中止,并显示以下信息:
error: failed to push some refs to '/Users/JohnDoe/repos/./chapter7.1/'
正如预期的那样,它不会推送。但是,如果你按下 y,Git 将推送到远程。现在使用以下命令尝试一下:
$ git push origin HEAD:refs/heads/master
054c5f78fdc82141e9d73e6b6955c38ff79c8b2e has no push in the message
Are you sure you want to push the commit(s)
y
To /Users/JohnDoe/repos/./chapter7.1/
! [rejected] HEAD -> master (non-fast-forward)
error: failed to push some refs to 'c:/Users/Rasmus/repos/./chapter7.1/'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
正如预期的那样,推送将被尝试,但正如你从输出中看到的,它被远程拒绝了。这是因为我们产生了分歧,推送在主分支的最新提交上不起作用。
因此,通过这个钩子,你可以让生活更轻松些,防止不小心推送你不希望推送的内容。这个示例也考虑了已经发布的提交;因此,如果你选择一个不同的关键词,那么其他提交——不仅仅是本地创建的——也会被脚本考虑进去。
配置和使用 Git 别名
Git 别名,像 Unix 别名一样,是可以在全局或每个仓库中配置的简短命令。它是一种简单的方式来重命名一些 Git 命令,以使用简短的缩写,例如,git checkout
可以是git co
,以此类推。
如何操作...
创建别名非常简单直接。你只需要使用git config
进行配置。
我们将做的是检查一个分支,然后逐一创建它的别名并执行它们,通过执行以下步骤来查看它们的输出:
-
因此,我们将从检查一个名为
gitAlias
的分支开始,该分支跟踪origin/stable-3.2
分支:
$ git checkout -b gitAlias --track origin/stable-3.2
Branch gitAlias set up to track remote branch stable-3.2 from origin.
Switched to a new branch 'gitAlias'
- 完成此操作后,我们可以开始创建一些别名。我们将从以下别名开始,它只会简单地修改你的提交:
$ git config alias.amm 'commit --amend'
- 执行这个别名将会打开提交信息编辑器,里面有来自
HEAD
提交的以下信息:
$ git amm
Prepare post 3.2.0 builds
Change-Id: Ie2bfdee0c492e3d61d92acb04c5bef641f5f132f
Signed-off-by: Matthias Sohn matthias.sohn@sap.com
- 如你所见,使用 Git 别名可以非常简单地加速你日常工作流程的处理。以下命令将只作用于最后 10 次提交,使用
git log
的--oneline
选项:
$ git config alias.lline 'log --oneline -10'
- 使用别名将会得到以下输出:
$ git lline
314a19a Prepare post 3.2.0 builds
699900c JGit v3.2.0.201312181205-r
0ff691c Revert "Fix for core.autocrlf=input resulting in mo
1def0a1 Fix for core.autocrlf=input resulting in modified f
0ce61ca Canonicalize worktree path in BaseRepositoryBuilder
be7942f Add missing @since tags for new public methods ig
ea04d23 Don't use API exception in RebaseTodoLine
3a063a0 Merge "Fix aborting rebase with detached head" into
e90438c Fix aborting rebase with detached head
2e0d178 Add recursive variant of Config.getNames() methods
- 你也可以执行一个简单的 checkout。这样,你可以使用
git co <branch>
来代替 Git 的 checkout。按照如下方式进行配置:
$ git config alias.co checkout
- 你会看到,别名也像普通的 Git 命令一样接受参数。让我们使用以下命令来试试这个别名:
$ git co master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git co gitAlias
Switched to branch 'gitAlias'
Your branch and 'origin/stable-3.2' have diverged,
and have 1 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
- 命令按预期工作。你可能会好奇为什么在再次检出
gitAlias
分支后我们发生了分叉。然后,当我们修改HEAD
提交时,我们发生了分叉。下一个别名是创建一个包含工作区中所有未提交内容的提交,除了未跟踪的文件:
$ git config alias.ca 'commit -a -m "Quick commit"'
- 在我们测试这个别名之前,我们应该创建一个文件并修改它,以展示它的实际作用。你可以按下面的命令创建一个文件:
$ echo "Sharks" > aquarium
$ echo "New HEADERTEXT" > pom.xml
- 要验证你想要的内容,运行
git status
:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: pom.xml
Untracked files:
(use "git add <file>..." to include in what will be committed)
aquarium
no changes added to commit (use "git add" and/or "git commit -a")
- 现在,我们可以使用以下命令来测试这个别名:
$ git ca
[gitAlias ef9739d] Quick commit
1 file changed, 1 insertion(+), 606 deletions(-)
rewrite pom.xml (100%)
- 要验证
aquarium
文件是否是提交的一部分,使用git status
:
Untracked files:
(use "git add <file>..." to include in what will be committed)
aquarium
nothing added to commit but untracked files present (use "git add" to track)
- 你还可以使用
git log -1
来查看我们刚刚创建的提交:
$ git log -1
commit ef9739d0bffe354c75b82f3b785780f5e3832776
Author: John Doe <john.doe@example.com>
Date: Thu Mar 13 00:01:49 2014 +0100
Quick commit
- 输出正如我们所预期的那样。下一个别名稍有不同,因为它将计算仓库中的提交次数,可以使用
wc
(wordcount
)工具来完成此操作。然而,由于这不是一个内置的 Git 工具,我们必须使用感叹号并指定 Git:
$ git config alias.count '!git log --all --oneline | wc -l'
- 让我们试试下面的命令:
$ git count
3008
- 目前,仓库中有
3008
个提交。这也意味着你可以像使用 Git 工具一样,通过创建 Git 别名来执行外部工具;例如,如果你正在使用 Windows、Mac 或 Linux,你可以按如下方式创建一个别名:
$ git config alias.wa '!explorer .' # Windows
$ git config alias.wa '!open .' # MacOS
$ git config alias.wa '!xdg-open .' # Linux
- 这个别名将会打开你当前所在路径的文件资源管理器。下一个别名展示了
HEAD
提交中发生了什么变更。它使用git log
的--name-status
选项来执行:
$ git config alias.gl1 'log -1 --name-status'
- 现在,试试下面的命令:
$ git gl1
commit ef9739d0bffe354c75b82f3b785780f5e3832776
Author: John Doe <john.doe@example.com>
Date: Thu Mar 13 00:01:49 2014 +0100
Quick commit
M pom.xml
- 如你所见,它只是简单地列出了提交和文件,包括文件在提交中的变动。由于别名接受参数,我们实际上可以重复利用这个功能来列出另一个分支的信息。让我们试试下面的命令:
$ git gl1 origin/stable-2.1
commit 54c4eb69acf700fdf80304e9d0827d3ea13cbc6d
Author: Matthias Sohn <matthias.sohn@sap.com>
Date: Wed Sep 19 09:00:33 2012 +0200
Prepare for 2.1 maintenance changes
Change-Id: I436f36a7c6dc86916eb4cde038b27f9fb183465a
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
M org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
M org.eclipse.jgit.ant.test/pom.xml
M org.eclipse.jgit.ant/META-INF/MANIFEST.MF
M org.eclipse.jgit.ant/pom.xml
M org.eclipse.jgit.console/META-INF/MANIFEST.MF
M org.eclipse.jgit.console/pom.xml
M org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
M org.eclipse.jgit.http.server/pom.xml ... more output
如你所见,我们得到了预期的输出。所以,举个例子,如果你一直在为git diff
使用一组特定的选项,那么你可以将其制作成一个别名,以便轻松使用。
它是如何工作的...
这就像是在config
文件中插入文本一样简单。所以,你可以尝试打开.git/config
配置文件,或者你也可以通过git config -list
来列出配置:
$ git config --list | grep alias
alias.amm=commit --amend
alias.lline=log --oneline -10
alias.co=checkout
alias.ca=commit -a -m "Quick commit"
alias.count=!git log --all --oneline | wc -l
alias
特性非常强大,它的理念是让你通过它来缩短那些你经常使用的长命令。你还可以利用这个特性将那些长命令缩短为别名,这样你就能更加频繁和精确地使用命令。如果你将一个长而复杂的 Git 评论设置为别名,你每次运行它时都会按相同的方式操作,而输入长命令则时常容易出错。
配置和使用 Git 脚本
是的,我们有别名,别名的作用就是将简短的命令转换为简洁有用的 Git 命令。然而,当涉及到较长的脚本,它们也是你工作流程的一部分,并且你希望将它们整合进 Git 时,你可以简单地将脚本命名为 git-scriptname
,然后像使用 git scriptname
一样调用它。
如何实现...
有几点需要记住。脚本必须在你的路径中,这样 Git 才能使用它。除此之外,只有想象力才是界限:
- 打开你喜欢的编辑器,并将以下内容插入到文件中:
#!/bin/bash
NUMBEROFCOMMITS=$(git log --all --oneline | wc -l)
while :
WHICHCOMMIT=$(( ( RANDOM % $NUMBEROFCOMMITS ) + 1 ))
COMMITSUBJECT=$(git log --oneline --all -${WHICHCOMMIT} | tail -n1)
COMMITSUBJECT_=$(echo "$COMMITSUBJECT" | cut -b1-60)
do
if [ $RANDOM -lt 14000 ]; then
echo "${COMMITSUBJECT_} PASSED"
elif [ $RANDOM -gt 15000 ]; then
echo "${COMMITSUBJECT_} FAILED"
fi
done
- 将文件保存为
git-likeaboss
。这是一个非常简单的脚本,它将列出随机的提交主题,结果会显示“通过”或“失败”。它会一直运行,直到你按下 Ctrl + C:
$ git likeaboss
5ec4977 Create a MergeResult for deleted/modified PASSED
fcc3349 Add reflog message to TagCommand PASSED
591998c Do not allow non-ff-rebase if there are ed PASSED
0d7dd66 Make sure not to overwrite untracked notfil PASSED
5218f7b Propagate IOException where possible where FAILED
f5fe2dc Teach PackWriter how to reuse an existing s FAILED
- 注意,你也可以使用 tab 补全这些命令,Git 会在你稍微拼写错误时考虑它们,具体如下:
$ git likeboss
git: 'likeboss' is not a git command. See 'git --help'.
Did you mean this?
likeaboss
显然,这个脚本本身在日常工作中并没有太大用处,但我们希望你能理解我们要表达的意思。所有脚本都围绕软件交付链展开,你可以将它们命名为 Git,因为它们是 Git 的一部分。这使得记住哪些脚本适用于你的工作变得更加容易。
无论是 Git 别名还是 Git 脚本,在使用 tab 补全时都会作为 Git 命令显示出来。输入 git <tab> <tab>
以查看可能的 Git 命令列表。
设置和使用提交模板
在本章中,我们一直在使用动态模板,但 Git 也有静态提交模板的选项。静态模板本质上只是一个配置好的文本文件。使用模板非常简单直接。
准备工作
首先,我们需要一个模板。这个模板必须是一个你知道位置的文本文件。创建一个包含以下内容的文件:
#subject no more than 74 characters please
#BugFix id in the following formats
#artf [123456]
#PCP [AN12354365478]
#Bug: 123456
#Descriptive text about what you have done
#Also why you have chosen to do in that way as
#this will make it easier for reviewers and other
#developers.
这是我们提供的一个简单的提交信息模板。你可能会发现有其他模板倾向于将 bug 放在标题或提交信息的底部。将 bug 放在顶部的原因是,人们往往不会阅读文本中的重要部分!这里重要的是格式化外部系统引用的部分。如果我们正确地处理了这些引用,我们也能自动更新缺陷系统。将文件保存为 ~/committemplate
。
如何实现...
我们将配置我们新创建的模板,然后进行一次提交,使用这个模板。
要配置模板,我们需要使用git config commit.template <pathtofile>
来设置它,一旦设置完成,我们就可以尝试创建一次提交,看看它是如何工作的:
- 从以下配置模板开始:
$ git config commit.template ~/committemplate
- 现在列出
config
文件以查看它是否已被设置:
$ git config --list | grep template
commit.template=/Users/JohnDoe/committemplate
- 正如我们预料的那样,配置成功了。模板,就像任何其他配置一样,可以通过
git config --global
在全局级别设置,或者通过不使用--global
选项在本地仓库级别设置。我们仅为这个仓库配置了提交模板。让我们尝试进行一次提交:
$ git commit --allow-empty
- 现在,提交信息编辑器应该已打开,你应该在提交信息编辑器中看到我们的模板:
#subject no more than 74 characters please
#BugFix id in the following formats
#artf [123456]
#PCP [AN12354365478]
#Bug: 123456
#Descriptive text about what you have done
#Also why you have chosen to do in that way as
#this will make it easier for reviewers and other
#developers.
就是这么简单。
在本章中,我们已经看到了如何在提交信息中存在特定单词时防止推送。我们还看到了如何在提交时动态创建适用于你或其他开发人员的有效提交信息。
我们接着展示了如何通过添加简短的脚本或别名将功能集成到你自己的 Git 中,这些脚本或别名都会通过 Git 执行。希望这些信息能帮助你更加聪明地工作,而不是更加辛苦。
第八章:从错误中恢复
在本章中,我们将介绍以下几种做法:
-
撤销 – 完全移除一个提交
-
撤销 – 移除一个提交并保留文件的更改
-
撤销 – 移除一个提交并保留暂存区的更改
-
撤销 – 处理脏工作区
-
重做 – 使用新更改重新创建最新提交
-
还原 – 撤销提交所引入的更改
-
撤销合并
-
使用 git reflog 查看过去的 Git 操作
-
使用 git fsck 查找丢失的更改
介绍
在 Git 中可以通过 git push 上下文来纠正错误(如果错误在分享或发布更改之前发现,可以不暴露它们)。如果错误已经推送,仍然可以撤销引入错误的提交所做的更改。
我们将查看 reflog
命令,了解如何使用它以及 git fsck
恢复丢失的信息。
在 Git 核心中没有 git undo 命令,原因之一是对需要撤销的内容存在歧义,例如,最后一个提交,已添加的文件。如果你想撤销最后一个提交,应该怎么做?是否应该删除该提交对文件做出的更改?比如,你是直接回滚到最后一个已知的良好提交,还是保留更改以便进行更好的提交?提交信息是否应该仅仅重新措辞?在本章中,我们将根据要实现的目标,探索多种撤销提交的方式。我们将探讨四种撤销提交的方法:
-
撤销一切,完全移除最后一个提交,仿佛它从未发生过
-
撤销提交并取消暂存文件;这将我们带回到开始添加文件之前的状态
-
撤销提交,但保留文件在暂存区或暂存区域,这样我们可以进行一些小的修改,然后完成提交
-
在脏工作区中撤销提交
本章中的 undo
和 redo
命令是针对已经发布的提交进行的操作。通常,你不应该对已经发布的公共仓库中的提交执行撤销和重做操作,因为这会重写历史。然而,在接下来的示例中,我们将使用示例仓库并对已发布的提交执行操作,以便每个人都有相同的体验。
撤销 – 完全移除一个提交
在这个例子中,我们将学习如何撤销一个提交,仿佛它从未发生过。我们将学习如何使用 reset 命令有效地丢弃提交,从而将我们的分支重置到期望的状态。
准备工作
在这个例子中,我们将使用 Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
仓库,克隆该仓库,并将工作目录切换到克隆的仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-
Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
如何操作...
首先,我们将尝试撤销仓库中的最新提交,仿佛它从未发生过:
- 我们将确保工作目录干净,没有文件处于修改状态,也没有文件被添加到暂存区:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
- 同时,检查一下我们工作树中的内容:
$ ls
HelloWorld.java Makefile hello_world.c
- 如果一切正常,我们将检查日志以查看仓库的历史。我们将使用
--oneline
选项来限制输出:
$ git log --oneline
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 最后一条提交是
3061dc6 Adds Java version of 'hello world'
提交。我们现在将撤销该提交,就像它从未发生过一样,历史中将不再显示它:
$ git reset --hard HEAD^
HEAD is now at 9c7532f Fixes compiler warnings
- 查看日志、状态和文件系统,这样你就可以看到实际发生了什么:
$ git log --oneline
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working directory clean
$ ls
hello_world.c
- 提交现在已经消失,连同它引入的所有更改(
Makefile
和HelloWorld.java
)。
在git status
命令的最后输出中,你可以看到我们的主分支落后于origin/master
一个提交。这与我们在章节开头提到的类似,因为我们正在移除和撤销已发布的提交。此外,如前所述,你应该只在尚未共享的提交上执行撤销和重做(git reset
)操作。在这里,我们仅展示已发布的提交,以便于示例的重现。
它是如何工作的...
实际上,我们只是将主分支的指针指向前一个提交的HEAD,也就是说,指向HEAD的第一个父提交。现在,分支将指向9c7532f,而不是我们移除的提交35b29ae。这在下面的图示中展示:
上述图示还表明,原始的3061dc6提交仍然存在于仓库中,但主分支的新提交将从9c7532f开始;3061dc6提交被称为悬挂提交。
你应该仅对尚未共享(推送)的提交执行此撤销操作,因为在撤销或重置后创建的新提交会形成一条新的历史,与原始的仓库历史分叉。
当执行重置命令时,Git 会查看HEAD指向的提交,并从中找到父提交。当前分支、主分支和HEAD指针会被重置到父提交,暂存区和工作树也会被重置。
撤销 – 删除一个提交并保留文件的更改
与其执行硬重置并丢失该提交引入的所有更改,不如执行重置,使更改保留在工作目录中。
准备工作
我们再次使用 hello world 仓库的示例。如果你已经克隆过仓库,可以重新设置主分支,或者新克隆一个仓库。
你可以按如下方式创建一个新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
你可以按如下方式重置现有的克隆:
$ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
如何操作...
- 首先,我们将检查工作树中的文件是否有任何更改(仅为便于示例的清晰度)以及仓库的历史:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
$ git log --oneline
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 现在,我们将撤销该提交并保留引入工作树的更改:
$ git reset --mixed HEAD^
$ git log --oneline
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Untracked files:
(use "git add <file>..." to include in what will be committed)
HelloWorld.java
Makefile
nothing added to commit but untracked files present (use "git add" to track)
我们可以看到我们的提交已被撤销,但文件的更改保留在工作树中,因此可以继续进行工作,以便创建一个正确的提交。
它是如何工作的...
从指向HEAD的提交的父提交开始,Git 将重置分支指针和HEAD以指向父提交。暂存区被重置,但工作树保持了重置之前的状态,因此受撤销
提交影响的文件将处于修改状态。下图展示了这一过程:
默认情况下,--mixed
选项是git reset
的行为,因此可以省略:git reset HEAD^
撤销 – 删除提交并保留暂存区中的更改
当然,也可以撤销提交,但保留索引或暂存区中文件的更改,以便随时可以重新创建提交,例如进行一些微小的修改。
准备工作
我们仍然会使用 hello world 代码库的示例。如果您已经克隆了代码库,请进行一次新的克隆,或者重置主分支。
按以下步骤创建新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git $ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按以下步骤重置现有的克隆:
$ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
如何操作...
- 检查是否有文件处于修改状态并查看日志:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
$ git log --oneline
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 现在,我们可以撤销提交,同时保留索引中的更改:
$ git reset --soft HEAD^
$ git log --oneline
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: HelloWorld.java
new file: Makefile
现在,您可以对需要的文件进行微小(或重大)更改,将它们添加到暂存区,并创建一个新的提交。
工作原理...
再次,Git 将重置分支指针和HEAD以指向前一个提交。但是,使用--soft
选项时,索引和工作目录不会被重置,即它们的状态与我们创建现在被撤销的提交之前相同。
以下图示展示了撤销前后的 Git 状态:
撤销 – 处理脏区
在前面的示例中,我们假设工作树是干净的,即没有跟踪文件处于修改状态。但这并不总是情况,如果执行硬重置,那些修改过的文件的更改将会丢失。幸运的是,Git 提供了一个智能的方式来快速将东西放置到一边,以便稍后可以使用git stash
命令检索。
准备工作
再次以 hello world 代码库为例。如果您已经克隆了代码库,请进行一次新的克隆,或者重置主分支。
我们可以按以下步骤创建新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git $ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按以下步骤重置现有的克隆:
$ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
我们还需要确保一些文件处于工作状态,因此我们将hello_world.c
更改为以下内容:
#include <stdio.h>
void say_hello(void) {
printf("hello, worldn");
}
int main(void){
say_hello();
return 0;
}
如何操作...
为了避免在撤销提交时意外删除工作树中的任何修改,您可以使用 git status
命令查看工作目录的当前状态(如我们之前所见)。如果有修改且您希望保留它们,您可以在撤销提交之前将它们存入暂存区,然后稍后取回。Git 提供了一个暂存命令,可以将未完成的修改存放起来,因此可以在不丢失工作的情况下快速切换上下文。暂存功能在第十一章,技巧与窍门中有进一步描述。目前,您可以将暂存命令看作是一个堆栈,可以将修改放入其中,然后稍后取出。
在工作目录中,hello_world.c
文件已修改到前述状态,我们可以尝试对 HEAD
提交进行硬重置,在重置前先将修改存入暂存区,稍后再应用这些修改:
- 首先,检查历史记录:
$ git log --oneline
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 然后,检查状态:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: hello_world.c
no changes added to commit (use "git add" and/or "git commit -a")
- 正如预期的那样,
hello_world.c
文件处于修改状态;所以,将其存入暂存区,检查状态,然后执行重置:
$ git stash
Saved working directory and index state WIP on master: 3061dc6 Adds Java version of 'hello world'
HEAD is now at 3061dc6 Adds Java version of 'hello world'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
$ git reset --hard HEAD^
HEAD is now at 9c7532f Fixes compiler warnings
$ git log --oneline
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 重置完成,我们已经删除了想要删除的提交。现在,让我们恢复存入暂存区的修改并检查文件:
$ git stash pop
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: hello_world.c
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (e56b68a1f5a0f72afcfd064ec13eefcda7a175ca)
$ cat hello_world.c
#include <stdio.h>
void say_hello(void) {
printf("hello, worldn");
}
int main(void){
say_hello();
return 0;
}
所以,文件恢复到了重置前的状态,我们也删除了不需要的提交。
它是如何工作的……
重置命令的工作原理与前面示例中的解释相同,但与暂存命令结合使用时,形成了一种非常有用的工具,即使您已经开始做其他工作,也能纠正错误。暂存命令通过保存工作目录和暂存区的当前状态来工作。然后,它会将工作目录恢复到干净的状态。
重新做——使用新修改重新创建最新提交
与撤销类似,重做也可以有多种含义。在这个上下文中,重做一个提交意味着几乎相同地重新创建一个提交,具有与之前提交相同的父提交,但内容和/或提交信息不同。如果您刚刚创建了一个提交,但可能忘记在提交之前将必要的文件添加到暂存区,或者需要重新编写提交信息,这非常有用。
准备工作
再次,我们将使用 hello world 仓库。创建该仓库的新克隆,或者如果您已经克隆过,重置主分支。
我们可以按如下方式创建一个新的克隆:
$ git https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按如下方式重置现有的克隆:
$ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
如何操作……
假设我们需要重新做最新的提交,因为我们需要重新编写提交信息以包含对问题追踪器的引用。
- 首先查看最新的提交,并确保工作目录是干净的:
$ git log -1
commit 3061dc6cf7aeb2f8cb3dee651290bfea85cb4392
Author: John Doe <john.doe@example.com>
Date: Sun Mar 9 14:12:45 2014 +0100
Adds Java version of 'hello world'
Also includes a makefile
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
- 现在,我们可以重新做提交,并使用
git commit --amend
命令更新提交信息。这将打开默认编辑器,我们可以在提交信息中添加对问题追踪器的引用(Fixes: RD-31415
):
$ git commit --amend
Adds Java version of 'hello world'
Also includes a makefile
Fixes: RD-31415
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author: John Doe <john.doe@example.com>
#
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
# new file: HelloWorld.java
# new file: Makefile
#
~
~
[master 75a41a2] Adds Java version of 'hello world'
Author: John Doe <john.doe@example.com>
2 files changed, 19 insertions(+)
create mode 100644 HelloWorld.java
create mode 100644 Makefile
- 现在,重新检查日志,看看是否一切正常:
$ git log -1
commit 75a41a2f550325234a2f5f3ba41d35867910c09c
Author: John Doe <john.doe@example.com> Date: Sun Mar 9 14:12:45 2014 +0100 Adds Java version of 'hello world' Also includes a makefile Fixes: RD-31415
- 我们可以看到提交信息已经更改,但我们无法从日志输出中验证该提交的父提交是否与原始提交相同,以及其他信息,就像我们在第一次提交时看到的那样。为了检查这一点,我们可以使用在第一章中学到的
git cat-file
命令,Git 导航。首先,让我们看看原始提交是什么样的:
$ git cat-file -p 3061dc6
tree d3abe70c50450a4d6d70f391fcbda1a4609d151f
parent 9c7532f5e788b8805ffd419fcf2a071c78493b23
author John Doe <john.doe@example.com> 1394370765 +0100
committer John Doe <john.doe@example.com> 1394569447 +0100 Adds Java version of 'hello world' Also includes a makefile
父提交是b8c39bb35c4c0b00b6cfb4e0f27354279fb28866
,根树是d3abe70c50450a4d6d70f391fcbda1a4609d151f
。
- 让我们检查一下新提交中的数据:
$ git cat-file -p HEAD
tree d3abe70c50450a4d6d70f391fcbda1a4609d151f
parent 9c7532f5e788b8805ffd419fcf2a071c78493b23
author John Doe <john.doe@example.com> 1394370765 +0100
committer John Doe <john.doe@example.com> 1394655225 +0100
Adds Java version of 'hello world'
Also includes a makefile
Fixes: RD-31415
父提交是相同的,即9c7532f5e788b8805ffd419fcf2a071c78493b23
,根树也是相同的,即d3abe70c50450a4d6d70f391fcbda1a4609d151f
。这是我们预期的结果,因为我们只更改了提交信息。如果我们将一些更改添加到暂存区并执行git commit --amend
,我们将把这些更改包含在提交中,并且根树的 SHA1 ID 会发生变化,但父提交 ID 仍然相同。
它是如何工作的……
--amend
选项大致相当于执行git reset --soft HEAD^
,然后修复需要的文件并将它们添加到暂存区。接着,我们会运行git commit
并重新使用上一个提交的提交信息(git commit -c ORIG_HEAD
)。
还有更多……
我们也可以使用--amend
方法将遗漏的文件添加到最新的提交中。假设你需要将README.md
文件添加到最新的提交中,以便使文档保持最新,但你已经创建了提交,尽管尚未推送。
然后,你将文件添加到索引中,就像在开始制作新提交时一样。你可以通过git status
检查,确认只有README.md
文件被添加:
$ git add README.md
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README.md
现在,你可以通过git commit --amend
修改最新的提交。此命令将把索引中的文件包含在新的提交中,像上一个示例一样,你可以在需要时修改提交信息。在这个示例中不需要修改,所以我们将传递--no-edit
选项给命令:
$ git commit --amend --no-edit
[master f09457e] Adds Java version of 'hello world'
Author: John Doe <john.doe@example.com>
3 files changed, 20 insertions(+)
create mode 100644 HelloWorld.java
create mode 100644 Makefile
create mode 100644 README.md
你可以从提交命令的输出中看到,三个文件发生了变化,其中README.md
是其中之一。
你还可以通过提交的--amend
命令重置作者信息(姓名、邮箱和时间戳)。只需传递--reset-author
选项,Git 将创建一个新的时间戳,并从配置或环境中读取作者信息,而不是使用旧提交对象中的信息。
撤销 – 撤消提交所引入的更改
撤销可以用于撤销已发布(已推送)历史中的提交,而使用修改或重置选项则无法做到这一点,因为那样会重写历史。
撤销通过应用由目标提交引入的反补丁来工作。默认情况下,撤销将创建一个新提交,并附上描述已撤销提交的提交信息。
准备开始
再次,我们将使用 hello world 仓库。重新克隆该仓库,或者如果已经克隆过,重置 master 分支。
我们可以按如下方式创建一个新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按如下方式重置现有的克隆:
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook $ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
如何操作...
- 首先,我们将列出仓库中的提交:
$ git log --oneline
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
- 我们将撤销第二个提交
9c7532f
:
$ git revert 9c7532f
Revert "Fixes compiler warnings"
This reverts commit 9c7532f5e788b8805ffd419fcf2a071c78493b23.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
# modified: hello_world.c
#
~
~
~
"~/john.doe/packt/repos/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook/.git/COMMIT_EDITMSG" 12L, 359C [master 9b94515] Revert "Fixes compiler warnings" 1 file changed, 1 insertion(+), 5 deletions(-)
- 当我们查看日志时,我们可以看到已经创建了一个新的提交:
$ git log --oneline
9b94515 Revert "Fixes compiler warnings"
3061dc6 Adds Java version of 'hello world'
9c7532f Fixes compiler warnings
5b5d692 Initial commit, K&R hello world
如果我们想更仔细地检查发生了什么,可以使用 git show
来查看这两个提交。
它是如何工作的...
git revert
命令将相应提交的反补丁应用到当前的 HEAD
指针。它将生成一个带有反补丁的新提交,并附带描述被撤销提交的提交消息。
还有更多...
可以在一次撤销操作中撤销多个提交,例如,git revert master~6..master~2
将撤销从 master 分支底部第六个提交到 master 分支底部第三个提交(包括这两个提交)。
也可以在撤销时不创建提交;将 -n
选项传递给 git revert
将应用所需的补丁,但仅应用于工作区和暂存区。
撤销合并
合并提交在撤销时是一个特殊情况。为了能够撤销合并提交,您必须指定希望保留的合并父分支。然而,当您撤销合并提交时,应该记住,尽管撤销将撤销对文件的更改,但它不会撤销历史记录。这意味着,当您撤销合并提交时,您声明将不保留合并引入的任何更改到目标分支。
这样做的效果是,来自其他分支的后续合并将只带入不是被撤销合并提交的祖先的提交更改。
在这个示例中,我们将学习如何撤销合并提交,并且我们将学习如何在撤销合并提交后重新合并分支,合并所有的更改。
准备工作
再次,我们将使用 hello world 仓库。重新克隆该仓库,或者如果已经克隆过,重置 master 分支。
我们可以按如下方式创建一个新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按如下方式重置现有的克隆:
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook $ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
在这个示例中,我们还需要使用仓库中的其他分支,因此需要在本地创建它们:
$ git branch -f feature/p-lang origin/feature/p-lang
Branch feature/p-lang set up to track remote branch feature/p-lang from origin.
$ git checkout develop
Switched to branch 'develop'
Your branch is up-to-date with 'origin/develop'.
$ git reset --hard origin/develop
HEAD is now at a95abc6 Adds Groovy hello world
如何操作...
在 develop 分支上,我们刚刚检查了存在一个合并提交,该合并提交引入了以字母 P 开头的编程语言中的 hello world 程序。
不幸的是,Perl 版本无法运行:
$ perl hello_world.pl
Can't find string terminator '"' anywhere before EOF at hello_world.pl line 3.
以下步骤将帮助您撤销一个合并:
- 让我们来看一下历史记录,查看最新的五个提交,并找到合并提交:
$ git log --oneline --graph -5
* a95abc6 Adds Groovy hello world
* 5ae3beb Merge branch 'feature/p-lang' into develop
|
| * 7b29bc3 php version added
| * 9944417 Adds perl hello_world script
* | ed9af38 Hello world shell script
|/
我们正在寻找的提交是5ae3beb Merge branch 'feature/p-lang' into develop
;此提交将 hello world 的 Perl 和 PHP 版本添加到 develop 分支。我们希望 Perl 版本的修复在 feature 分支上进行,修复完成后再合并到 develop。为了保持develop
的稳定性,我们需要撤销引入有问题的 Perl 版本的合并提交。在执行合并之前,让我们先看看HEAD
的内容:
$ git ls-tree --abbrev HEAD
100644 blob 28f40d8 helloWorld.groovy
100644 blob 881ef55 hello_world.c
100644 blob 5dd01c1 hello_world.php
100755 blob ae06973 hello_world.pl
100755 blob f3d7a14 hello_world.py
100755 blob 9f3f770 hello_world.sh
- 撤销合并,保留第一个父节点的历史:
$ git revert -m 1 5ae3beb
[develop e043b95] Revert "Merge branch 'feature/p-lang' into develop"
2 files changed, 4 deletions(-)
delete mode 100644 hello_world.php
delete mode 100755 hello_world.pl
- 让我们看看我们新
HEAD
状态的内容:
$ git ls-tree --abbrev HEAD
100644 blob 28f40d8 helloWorld.groovy
100644 blob 881ef55 hello_world.c
100755 blob f3d7a14 hello_world.py
100755 blob 9f3f770 hello_world.sh
合并中引入的 Perl 和 PHP 文件已经消失,因此撤销操作完成了它的工作。
它是如何工作的...
撤销命令将获取你想撤销的提交所引入的补丁,并将反向/反补丁应用到工作树。如果一切顺利,即没有冲突,将会生成一个新的提交。在撤销合并提交时,只有主线(-m
选项)中引入的更改会被保留,合并另一方引入的所有更改都会被撤销。
还有更多内容...
尽管撤销合并提交很容易,但如果以后你想再次合并该分支,你可能会遇到问题,因为合并中的问题尚未解决。在撤销合并提交时,你实际上告诉 Git,你不希望在该分支中包含另一个分支所引入的任何更改。因此,当你再次尝试合并该分支时,你只会得到那些不是撤销合并提交的祖先提交中的更改。
我们将通过再次尝试将feature/p-lang
分支与 develop 分支合并来实际演示这一过程:
$ git merge --no-edit feature/p-lang
CONFLICT (modify/delete): hello_world.pl deleted in HEAD and modified in feature/p-lang. Version feature/p-lang of hello_world.pl left in tree.
Automatic merge failed; fix conflicts and then commit the result.
我们只需添加hello_world.pl
就可以解决冲突:
$ git add hello_world.pl
$ git commit
[develop 2804731] Merge branch 'feature/p-lang' into develop
让我们检查一下树,看看一切是否正常:
$ git ls-tree --abbrev HEAD
100644 blob 28f40d8 helloWorld.groovy
100644 blob 881ef55 hello_world.c
100755 blob 6611b8e hello_world.pl
100755 blob f3d7a14 hello_world.py
100755 blob 9f3f770 hello_world.sh
hello_world.php
文件缺失,但这很有意义,因为引入它的更改已经在撤销合并提交中被撤销。
要执行正确的重新合并,我们首先必须撤销撤销合并提交;这可能看起来有些奇怪,但这是将撤销前的更改重新纳入我们树中的方法。然后,我们可以再次合并该分支,最终会得到我们合并分支所引入的所有更改。然而,我们首先需要通过硬重置丢弃我们刚刚做的合并提交:
$ git reset --hard HEAD^
HEAD is now at c46deed Revert "Merge branch 'feature/p-lang' into develop"
现在,我们可以撤销撤销合并并重新合并该分支:
$ git revert HEAD
[develop 9950c9e] Revert "Revert "Merge branch 'feature/p-lang' into develop""
2 files changed, 4 insertions(+)
create mode 100644 hello_world.php
create mode 100755 hello_world.pl
$ git merge feature/p-lang
Merge made by the 'recursive' strategy.
hello_world.pl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
让我们检查一下树,看看 Perl 和 PHP 文件,并查看 Perl 文件是否已修复:
$ git ls-tree --abbrev HEAD
100644 blob 28f40d8 helloWorld.groovy
100644 blob 881ef55 hello_world.c
100644 blob 5dd01c1 hello_world.php
100755 blob 6611b8e hello_world.pl
100755 blob f3d7a14 hello_world.py
100755 blob 9f3f770 hello_world.sh
$ perl hello_world.pl
Hello, world!
另见
欲了解有关撤销合并的更多信息,请参阅以下文章:
使用 git reflog 查看过去的 Git 操作
reflog
命令存储了 Git 中更新分支尖端的相关信息,普通的 git log
命令显示从 HEAD
开始的祖先链,而 reflog
命令显示 HEAD
在仓库中指向的内容。这是你在仓库中的历史,告诉你如何在分支之间移动、创建提交和重置等等。基本上,任何使 HEAD
指向新内容的操作都会被记录在 reflog
中。这意味着,通过查看 reflog
命令,你可以找到那些没有被任何分支或其他提交指向的丢失提交。因此,reflog
命令是寻找丢失提交的一个很好的起点。
准备工作
再次,我们将使用 hello world 仓库。如果你进行一个新的克隆,确保运行本章的脚本,这样 reflog
命令中就会有一些条目。
脚本可以在本书的主页上找到。如果你只是在执行完本章的操作后,将主分支重置为 origin/master
,那么一切就准备好了。
我们可以按如下方式创建一个新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按如下方式重置现有的克隆:
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook $ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
如何操作...
- 让我们尝试运行
reflog
命令,并限制只显示最新的七个条目:
$ git reflog -7
3061dc6 HEAD@{0}: checkout: moving from develop to master
d557284 HEAD@{1}: merge feature/p-lang: Merge made by the 'recursive' strategy.
9950c9e HEAD@{2}: revert: Revert "Revert "Merge branch 'feature/p-lang' into develop""
c46deed HEAD@{3}: reset: moving to HEAD^
2804731 HEAD@{4}: commit (merge): Merge branch 'feature/p-lang' into develop
c46deed HEAD@{5}: revert: Revert "Merge branch 'feature/p-lang' into develop"
a95abc6 HEAD@{6}: checkout: moving from master to develop
在你的仓库中,由于示例中生成的提交会有略微不同的内容,具体来说是你的用户名和电子邮件地址,因此提交的 SHA-1 哈希值会有所不同,但顺序应该大致相同。
我们可以看到在上一个示例中通过回退、提交和重置所执行的操作。我们可以看到我们放弃的合并提交 2804731
。由于之前的合并及其回退,它没有合并我们想要的所有更改。
- 我们可以使用
git show
更详细地查看该提交:
$ git show 2804731
commit 2804731c3abc4824cdab66dc7567bed4cddde0d3
Merge: c46deed 32fa2cd
Author: John Doe <john.doe@example.com>
Date: Thu Mar 13 23:20:21 2014 +0100
Merge branch 'feature/p-lang' into develop
Conflicts:
hello_world.pl
确实,这正是我们在前一个示例中选择放弃的提交。我们也可以像在前一个示例中那样查看提交的树形结构,检查它们是否相同:
$ git ls-tree --abbrev 2804731
100644 blob 28f40d8 helloWorld.groovy
100644 blob 881ef55 hello_world.c
100755 blob 6611b8e hello_world.pl
100755 blob f3d7a14 hello_world.py
100755 blob 9f3f770 hello_world.sh
从这里开始,有多种方式可以恢复这些更改。你可以检查提交并创建一个分支;这样,你将有一个指针,方便你再次找到它。你也可以使用 git checkout
检出特定的文件 —— path/to/file SHA-1
,或者使用 git show
或 git cat-file
命令查看文件。
它是如何工作的...
对于仓库中每次HEAD
指针的移动,Git 会存储指向的提交以及到达该提交的操作。这个操作可以是提交、检出、重置、回退、合并、变基等。信息是本地存储在仓库中的,不会在推送、拉取和克隆时共享。如果你知道你在寻找的内容以及大致的时间点,使用 reflog
命令查找丢失的提交是相当简单的。如果你有大量的 reflog 历史、许多提交、切换分支等,那么由于 HEAD
的多次更新带来的噪音,查找就变得困难。reflog
命令的输出可能会有很多选项,其中一些选项也可以传递给普通的 git log
命令。
使用 git fsck 找回丢失的更改
Git 中还有一个工具可以帮助你找回丢失的提交,甚至是 blob(文件),它就是 git fsck
。fsck
命令会测试对象数据库,验证对象的 SHA-1 ID 以及它们之间的连接。这个命令还可以用来查找那些从任何命名引用中不可达的对象,因为它会测试数据库中所有的对象,这些对象位于 .git/objects
文件夹中。
准备工作
再次,我们将使用 hello world 仓库。如果你做一个新的克隆,确保运行本章的脚本(04_undo_dirty.sh
),这样 git fsck
就会有一些对象可以处理。脚本可以在书的主页找到。如果你在执行完本章其他操作后只重置主分支,一切就绪。
我们可以按照以下方式创建新的克隆:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook.git
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook
我们可以按照以下方式重置现有的克隆:
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_cookbook $ git checkout master
$ git reset --hard origin/master
HEAD is now at 3061dc6 Adds Java version of 'hello world'
怎么做...
- 让我们在数据库中查找不可达的对象:
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
unreachable commit 147240ad0297f85c9ca3ed513906d4b75209e83d
unreachable blob b16cf63ab66605f9505c17c5affd88b34c9150ce
unreachable commit 4c3b1e10d8876cd507bcf2072c85cc474f7fb93b
如果你在自己的计算机上执行这个示例,对象的 ID(SHA-1 哈希值)将不会相同,因为提交者、作者和时间戳会不同。
- 我们找到了两个提交和一个 blob。让我们仔细看看每一个;先看这个 blob:
$ git show b16cf63ab66605f9505c17c5affd88b34c9150ce
#include <stdio.h>
void say_hello(void) {
printf("hello, worldn");
}
int main(void){
say_hello();
return 0;
}
所以,blob 是示例中的 hello_world.c
文件,它在重置提交之前将你的更改存放在暂存区。在这里,我们将文件暂存,执行了重置操作,并从暂存区恢复文件,但我们实际上并没有执行提交。然而,stash
命令确实将文件添加到了数据库中,这样它就可以再次找到,直到垃圾回收启动,或者如果文件被历史中的提交引用,它将永远存在。
- 让我们更仔细地看看这两个提交:
$ git show 147240ad0297f85c9ca3ed513906d4b75209e83d
commit 147240ad0297f85c9ca3ed513906d4b75209e83d
Merge: 3061dc6 4c3b1e1
Author: John Doe <john.doe@example.com>
Date: Thu Mar 13 23:19:37 2014 +0100
WIP on master: 3061dc6 Adds Java version of 'hello world'
diff --cc hello_world.c
index 881ef55,881ef55..b16cf63
--- a/hello_world.c
+++ b/hello_world.c
@@@ -1,7 -1,7 +1,10 @@@
#include <stdio.h>
--int main(void){
++void say_hello(void) {
printf("hello, worldn");
++}
++int main(void){
++ say_hello();
return 0;
--}
++}
$ git show 4c3b1e10d8876cd507bcf2072c85cc474f7fb93b
commit 4c3b1e10d8876cd507bcf2072c85cc474f7fb93b
Author: John Doe <john.doe@example.com>
Date: Thu Mar 13 23:19:37 2014 +0100
index on master: 3061dc6 Adds Java version of 'hello world'
这两个提交实际上是我们在前一个示例中将更改暂存时所做的提交。stash
命令会创建一个提交对象,其中包含暂存区的内容,并且会有一个合并提交,合并 HEAD
和与暂存区内容相关的提交(仅跟踪文件)。正如我们在前一个示例中恢复了暂存的更改,我们不再有任何引用指向之前的提交,因此它们通过 git fsck
被找到了。
它是如何工作的……
git fsck
命令会测试 .git/objects
文件夹中找到的所有对象。当给定 --unreachable
选项时,它会报告那些无法从其他引用中访问到的对象;引用可以是分支、标签、提交、树、reflog
或者已经被暂存的更改。
第九章:仓库维护
在本章中,我们将涵盖以下方案:
-
修剪远程分支
-
手动运行垃圾回收
-
关闭自动垃圾回收
-
拆分仓库
-
重写历史——修改单个文件
-
创建仓库备份作为镜像仓库
-
快速“如何做”子模块
-
子树合并
-
子模块与子树合并
介绍
在本章中,我们将探讨用于仓库维护的各种工具。我们将学习如何轻松删除已从远程仓库删除的本地仓库中的分支。我们还将了解如何触发垃圾回收以及如何关闭它。我们将查看如何使用 filter-branch
命令拆分一个仓库,并了解如何使用相同的命令重写仓库的历史。最后,我们将简要了解如何将其他 Git 项目作为子项目集成到 Git 仓库中,采用子模块功能或子树策略。
修剪远程分支
通常,一个由 Git 跟踪的软件项目的开发是在功能分支上进行的,随着时间的推移,越来越多的功能分支被合并到主干中。通常,这些功能分支会在主仓库(origin)中被删除。然而,分支在所有克隆中不会自动删除,只有在执行拉取(fetch)和拉取请求(pull request)时。必须明确告诉 Git 从本地仓库中删除那些已从 origin 仓库删除的分支。
准备工作
首先,我们将设置两个仓库,并使用其中一个作为另一个的远程仓库。我们将使用 Git-Version-Control-Second-Edition_hello_world_flow_model
仓库,但首先我们会克隆一个仓库到本地裸仓库:
$ git clone --bare https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git hello_world_flow_model_remote Cloning into bare repository 'hello_world_flow_model_remote'...
remote: Counting objects: 51, done.
remote: Total 51 (delta 0), reused 0 (delta 0), pack-reused 51
Unpacking objects: 100% (51/51), done.
接下来,我们将把新克隆的仓库克隆到一个本地仓库,并附带工作目录:
$ git clone hello_world_flow_model_remote hello_world_flow_model
现在,让我们删除裸仓库中的几个已合并的功能分支:
$ cd hello_world_flow_model_remote
$ git branch -D feature/continents
$ git branch -D feature/printing
$ git branch -D release/1.0
$ cd ..
最后,切换到工作副本目录并确保 develop
分支已被检出:
$ cd hello_world_flow_model
$ git checkout develop
$ git reset --hard origin/develop
如何操作...
- 首先,使用以下命令列出所有分支:
$ git branch -a
* develop
remotes/origin/HEAD -> origin/develop
remotes/origin/develop
remotes/origin/feature/cities
remotes/origin/feature/continents
remotes/origin/feature/printing
remotes/origin/master
remotes/origin/release/1.0
- 让我们尝试执行拉取或拉取请求,看看是否发生了任何变化,使用以下命令:
$ git fetch
$ git pull
Already up to date.
$ git branch -a
* develop
remotes/origin/HEAD -> origin/develop
remotes/origin/develop
remotes/origin/feature/cities
remotes/origin/feature/continents
remotes/origin/feature/printing
remotes/origin/master
remotes/origin/release/1.0
- 即使远程仓库中的分支已被删除,分支仍然存在于本地仓库中。我们需要明确告诉 Git 删除那些已经从远程仓库删除的分支,使用以下命令:
$ git fetch --prune
x [deleted] (none) -> origin/feature/continents
x [deleted] (none) -> origin/feature/printing
x [deleted] (none) -> origin/release/1.0
$ git branch -a
* develop
remotes/origin/HEAD -> origin/develop
remotes/origin/develop
remotes/origin/feature/cities
remotes/origin/master
这些分支现在也已从我们的本地仓库中删除。
它是如何工作的...
Git 会检查远程或 origin 命名空间下的远程跟踪分支,并移除那些在远程仓库中已不存在的分支。
还有更多...
有几种方法可以从 Git 中删除已经从主分支删除的分支。我们可以在更新本地仓库时进行操作,正如我们在使用git fetch --prune
时所看到的,也可以使用git pull --prune
。甚至可以使用git remote prune origin
命令来执行。这将删除远程仓库中已不再存在的分支,但不会更新仓库中的远程跟踪分支。
手动运行垃圾回收
在日常使用 Git 时,你可能会注意到某些命令有时会触发 Git 执行垃圾回收,并将松散的对象打包成一个包文件(Git 的对象存储)。垃圾回收和松散对象的打包也可以通过执行git gc
命令手动触发。如果你有很多松散对象,触发git gc
会非常有用。松散对象可以是例如一个 blob、一个树对象或一个提交对象。正如我们在第一章 Git 导航 中看到的,blob-
、tree-
和commit
对象会在我们添加文件并创建提交时被添加到 Git 的数据库中。这些对象最初作为不可达的对象存储在 Git 的对象存储中,以单独的文件形式存放在.git/objects
文件夹中。最终,或者通过手动请求,Git 会将这些松散对象打包成包文件,这样可以减少磁盘使用。添加大量文件到 Git 后,很多对象会变成松散的对象,例如,当你开始一个新项目或频繁执行添加和提交时。运行垃圾回收将确保松散的对象被打包,并且任何没有被任何引用或对象指向的对象会被删除。当你删除了某些分支或提交,并希望确保它们所引用的对象也被删除时,这一操作尤其有用。
让我们看看如何触发垃圾回收,并从数据库中移除一些对象。
准备工作
首先,我们需要一个仓库来执行垃圾回收操作。我们将使用与之前示例相同的仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git
$ cd hello_world_flow_model
$ git checkout develop
$ git reset --hard origin/develop
如何执行...
- 首先,我们将检查仓库中未打包的对象;我们可以使用
count-objects
命令来执行此操作:
$ git count-objects
51 objects, 204 kilobytes
- 我们还将检查不可达的对象,这些对象是无法通过任何引用(标签、分支或其他对象)访问的对象。这些不可达的对象将在垃圾回收运行时被删除。我们还将使用以下命令检查
.git
目录的大小:
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
$ du -sh .git
292K .git # Linux - 1K = 1024 bytes
300K .git # MacOS - 1K = 1000 bytes
- 目前没有不可达的对象。这是因为我们刚刚克隆了仓库,并未实际进行操作。如果我们删除远程仓库的源(origin),远程分支(
remotes/origin/*
)将会被删除,且我们会失去对某些对象的引用;这些对象在运行fsck
时会显示为不可达,并且可以被垃圾回收:
$ git remote rm origin
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
unreachable commit 127c621039928c5d99e4221564091a5bf317dc27
unreachable commit 472a3dd2fda0c15c9f7998a98f6140c4a3ce4816
unreachable blob e26174ff5c0a3436454d0833f921943f0fc78070
unreachable tree f03964e50809d5a0a9d35c208001b141ac36d997
unreachable commit f336166c7812337b83f4e62c269deca8ccfa3675
- 我们可以看到,由于远程仓库被删除,导致一些不可达的对象。让我们尝试手动触发垃圾回收:
$ git gc
Counting objects: 46, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (44/44), done.
Writing objects: 100% (46/46), done.
Total 46 (delta 18), reused 0 (delta 0)
- 如果我们现在检查仓库,我们将看到以下内容:
$ git count-objects
5 objects, 20 kilobytes
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (46/46), done.
unreachable commit 127c621039928c5d99e4221564091a5bf317dc27
unreachable commit 472a3dd2fda0c15c9f7998a98f6140c4a3ce4816
unreachable blob e26174ff5c0a3436454d0833f921943f0fc78070
unreachable tree f03964e50809d5a0a9d35c208001b141ac36d997
unreachable commit f336166c7812337b83f4e62c269deca8ccfa3675
$ du -sh .git
120K .git # Linux
124K .git # MacOS
- 对象的数量变小了。Git 已将这些对象打包到存储在
.git/objects/pack
文件夹中的包文件中。仓库的大小也变小了,因为 Git 对包文件中的对象进行了压缩和优化。然而,仍然有一些无法访问的对象残留。这是因为对象只有在它们比gc.pruneexpire
配置选项指定的时间更久远时才会被删除,而该选项的默认值为两周(config value: 2.weeks.ago
)。我们可以通过运行--prune=now
选项来覆盖默认或配置的选项:
$ git gc --prune=now
Counting objects: 46, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (26/26), done.
Writing objects: 100% (46/46), done.
Total 46 (delta 18), reused 46 (delta 18)
- 调查该仓库会产生以下输出:
$ git count-objects
0 objects, 0 kilobytes
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (46/46), done.
$ du -sh .git
100K .git # Linux
104K .git # MacOS
无法访问的对象已被删除,仓库中没有松散的对象,且由于对象已被删除,仓库的大小也变小了。
其工作原理...
git gc
命令通过压缩文件修订版本和删除没有引用的对象来优化仓库。这些对象可以是提交(commits)等。在一个被废弃(删除)的分支上,git add
的调用、通过 git commit --amend
丢弃/重做的提交或其他命令可能会留下对象。对象在创建时默认已经通过 zlib
压缩,并且在移入包文件(pack file)时,Git 确保只存储必要的更改。例如,如果你只更改了一个大文件中的一行,将整个文件再次存储到包文件中会浪费一些空间。相反,Git 会将最新的文件作为整体存储在包文件中,只存储旧版本的增量(delta)。这非常聪明,因为你更可能需要的是文件的最新版本,并且 Git 不需要为此进行增量计算。这看起来似乎与第一章中学到的 Git 存储快照而非增量的说法相矛盾。可是,记住快照是如何生成的。Git 对文件内容进行哈希处理,生成 tree
和 commit
对象,而提交对象(commit object)描述了完整的树状态,并通过 root-tree sha-1
哈希来表示。存储对象到包文件中不会影响树状态的计算。当你检查早期版本的提交时,Git 会确保 sha-1 哈希值与所请求的分支、提交或标签匹配。
关闭自动垃圾回收
自动触发垃圾回收的功能可以关闭,这样它就不会自动运行,除非手动触发。这在你搜索丢失的提交或文件时非常有用,可以确保在搜索过程中(运行 Git 命令时)不会被垃圾回收。
准备工作
我们将再次使用 Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model
仓库作为本例:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git
Cloning into 'Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model'...
remote: Reusing existing pack: 51, done.
remote: Total 51 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (51/51), done.
Checking connectivity... done.
$ cd Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model
$ git checkout develop
Already on 'develop'
Your branch is up-to-date with 'origin/develop'.
$ git reset --hard origin/develop
HEAD is now at 2269dcf Merge branch 'release/1.0' into develop
如何操作...
- 为了关闭自动垃圾回收的触发,我们需要将
gc.auto
配置设置为 0。首先,我们将检查现有设置,然后可以使用以下命令设置并验证配置:
$ git config gc.auto # exit code is 1 when not set
$ echo $?
1
$ git config gc.auto 0
$ git config gc.auto
0
- 现在我们可以尝试使用
git gc
命令并加上--auto
标志,因为它将在从其他命令触发时被自动调用:
$ git gc --auto
- 正如预期的那样,什么也没发生,因为配置禁用了自动垃圾回收。不过,我们仍然可以手动触发它(不带
--auto
标志):
$ git gc
Counting objects: 51, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (49/49), done.
Writing objects: 100% (51/51), done.
Total 51 (delta 23), reused 0 (delta 0)
拆分一个仓库
有时,一个由 Git 跟踪的项目并不是一个逻辑上的单一项目,而是由多个项目组成的。这可能是完全有意为之,没什么问题,但也可能存在这样的情况,仓库中跟踪的项目确实应该分属于两个不同的仓库。你可以想象一个项目,其中代码库不断增长,在某个时刻,某个子项目可能会成为一个独立的项目。通过拆分包含该子项目的子文件夹和/或文件,可以实现这个目标,同时保留触及这些文件和/或文件夹的完整提交历史。
准备中
在这个示例中,我们将使用 JGit 仓库,这样我们就有一些历史记录可以筛选。我们拆分的子文件夹并不是真正的项目,而是作为此练习的示例。
- 首先,克隆 JGit 仓库,并使用以下命令为远程分支创建本地分支:
$ git clone https://git.eclipse.org/r/jgit/jgit
Cloning into 'jgit'...
remote: Counting objects: 98, done
remote: Total 95247 (delta 0), reused 95247 (delta 0)
Receiving objects: 100% (95247/95247), 41.25 MiB | 1.91 MiB/s, done.
Resolving deltas: 100% (41334/41334), done.
$ cd jgit
$ git checkout master
Already on 'master'
Your branch is up-to-date with 'origin/master'.
- 将当前分支的名称保存到一个名为
current
的变量中:
$ current=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD)
- 在接下来的步骤中,我们从仓库中的所有远程分支创建本地分支:
$ for br in $(git branch -a | grep -v $current | grep remotes | grep -v HEAD);
do
git branch ${br##*/} $br;
done
Branch stable-0.10 set up to track remote branch stable-0.10 from origin.
Branch stable-0.11 set up to track remote branch stable-0.11 from origin.
Branch stable-0.12 set up to track remote branch stable-0.12 from origin.
...
首先,我们过滤分支。从所有分支(git branch -a
)中,我们排除那些在名称中包含 $current
变量的分支(grep -v $current
)。然后,我们仅包括那些匹配远程仓库的分支(grep remotes
)。最后,我们排除所有带有 HEAD
的分支(grep -v HEAD
)。对于每个分支($br
),我们创建一个本地分支,分支名取自该分支全名中最后一个 "/
" 后的部分(git branch ${br##*/} $br
)。例如,remotes/origin/stable-0.10
这个分支变成了本地分支 stable-0.10
。
- 现在,我们将准备一个简短的脚本,该脚本会删除除输入的 Git 索引中的内容之外的所有内容。将以下内容保存为
clean-tree
文件,放在包含 JGit 仓库的文件夹中(不是仓库本身):
#!/bin/bash
# Clean the tree for unwanted dirs and files
# $1 Files and dirs to keep
clean-tree () {
# Remove everything but $1 from the git index/staging area
for f in $(git ls-files | grep -v -E "$1" | grep -o -E "^[^/\"]+" | sort -u); do
git rm -rq --cached --ignore-unmatch $f
done
}
clean-tree $1
这个简短的脚本过滤所有当前在暂存区中的文件(git ls-files
),排除与输入匹配的文件(grep -v -E "$1"
)。它只列出文件的 name/path
的第一部分,直到遇到第一个 "/
"(grep -o -E "^[^/\"]"
),最后按唯一条目进行排序(sort -u
)。剩余列表中的条目($f
)将从 Git 暂存区中移除(git rm -rq --cached --ignore-unmatch $f
)。--cached
选项告诉 Git 从暂存区中删除文件,--ignore-unmatch
告诉 Git 如果文件在暂存区中不存在时不要报错。-rq
选项分别表示递归和安静模式。
暂存区包含 Git 在最后一次快照(提交)中跟踪的所有文件,以及你通过 git add
添加的文件(修改或新增)。然而,当你运行 git status
时,你只会看到最新提交和暂存区之间的差异,以及工作树和暂存区之间的差异。
- 使用以下命令使文件具有可执行权限:
$ chmod +x clean-tree
- 现在我们准备好将仓库的一个子部分拆分出来。
如何操作...
- 首先,我们需要决定哪些文件夹和文件需要保留在新的仓库中;我们会删除仓库中的所有内容,除了那些需要保留的文件。我们会将需要保留的文件和文件夹以
|
分隔,存储在一个字符串中,以便将其作为正则表达式传递给grep
,如下所示的命令:
keep="org.eclipse.jgit.http|LICENSE|.gitignore|README.md|.gitattributes"
- 现在我们准备好开始转换仓库了。我们将使用
git filter-branch
命令,它可以重写整个仓库的历史;这正是我们需要完成此任务的工具。
始终记得确保在运行 git filter-branch
前备份好即将操作的仓库,以防出现意外。
- 我们将使用
--index-filter
选项来过滤分支。该选项允许我们在每个提交记录之前重写索引或暂存区,我们将使用之前创建的clean-tree
脚本来完成这一操作。我们还将使用cat
作为tag-name-filter
来保留标签。我们将在所有分支上执行重写,并记得使用清理脚本的绝对路径:
$ git filter-branch --prune-empty --index-filter "\"/absolute/path/to/clean-tree\" \"$keep\"" --tag-name-filter cat -- --all
...
Rewrite 720734983bae056955bec3b36cc7e3847a0bb46a (13/3051)
Rewrite 6e1571d5b9269ec79eadad0dbd5916508a4fee82 (23/3051)
Rewrite 2bfe561f269afdd7f4772f8ebf34e5e25884942b (37/3051)
Rewrite 2086fdaedd5e71621470865c34ad075d2668af99 (60/3051)
...
- 重写过程需要一些时间,因为所有的提交都需要被处理。重写完成后,我们可以看到所有内容都被删除了,除了我们想要保留的文件和文件夹:
$ git ls-tree --abbrev HEAD
100644 blob f57840b7e .gitattributes
100644 blob 3679a3365 .gitignore
100644 blob 1b85c6466 LICENSE
100644 blob 54133e1d3 README.md
040000 tree 2edd8e193 org.eclipse.jgit.http.apache
040000 tree cda583881 org.eclipse.jgit.http.server
040000 tree daace995c org.eclipse.jgit.http.test
- 清理工作还没有完成。
git filter-branch
会将所有原始引用、分支和标签保存到仓库的refs/original
命名空间中。经过验证,新历史看起来没问题后,我们可以删除原始的refs
,因为这些引用指向的对象不在我们当前的历史中,并且占用了大量磁盘空间。我们将删除所有原始引用,并运行垃圾回收器来清理仓库中的旧对象:
$ du -sh .git
53M .git # MacOS
- 删除原始引用
refs/original
,并使用git gc
删除旧对象,如下所示的命令:
$ git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --prune=now
Counting objects: 96863, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (28811/28811), done.
Writing objects: 100% (96863/96863), done.
Total 96863 (delta 42589), reused 94395 (delta 41334)
- 检查垃圾回收后的仓库大小:
$ du -sh .git
44M .git # MacOS
- 仓库现在已清除所有旧对象,大小已减小,并且我们列出的需要保留的文件和目录的历史得以保留。
它是如何工作的...
git filter-branch
命令根据在重写仓库时需要做的操作有不同的过滤选项。在这个例子中,我们只需要从仓库中移除文件和文件夹;index-filter
非常实用,它允许我们在将提交记录到数据库之前重写索引,而无需实际检出磁盘上的树,从而节省了大量的磁盘 I/O。然后,我们使用之前准备好的clean-tree
脚本来从索引中移除不需要的文件和文件夹。首先,我们列出索引的内容并过滤出我们想保留的文件和文件夹。然后,我们使用以下命令从索引中移除剩余的文件和文件夹($f
):
git rm -rq --cached --ignore-unmatch $f
--cached
选项告诉 Git 从文件中移除索引,-rq
选项告诉它递归地移除(r)并保持安静(q)。最后,使用--ignore-unmatch
选项,这样即使git rm
尝试移除索引中没有的文件,也不会导致错误退出。
还有更多内容...
git filter-branch
有许多其他过滤器,最常用的过滤器及其使用场景如下:
-
env-filter
:此过滤器用于修改记录提交时的环境,特别适用于重写作者和提交者的信息。 -
tree-filter
:tree-filter
用于重写树结构。如果需要在树中添加或修改文件,例如移除仓库中的敏感数据,它非常有用。 -
msg-filter
:此过滤器用于更新提交信息。 -
subdirectory-filter
:如果你想将一个子目录提取到新的仓库并保留该子目录的历史记录,可以使用此过滤器。该子目录将成为新仓库的根目录。
重写历史 – 更改单个文件
在这个例子中,我们将展示如何使用 Git 的filter-branch
命令在整个仓库历史中移除敏感数据。
准备开始
为了简化操作,我们将使用一个非常简单的示例仓库。它包含几个文件,其中之一是.credentials
,它包含一个用户名和密码。首先克隆仓库并切换到该目录,如下所示:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_Remove-Credentials.git
$ cd Git-Version-Control-Cookbook-Second-Edition_Remove-Credentials
在继续操作之前,可以使用ls
查看仓库内容,并使用git log
查看历史记录。
如何操作...
- 由于我们需要在重写仓库历史时修改文件,所以我们将使用
tree-filter
选项来过滤分支。.credentials
文件如下所示:
username = foobar
password = verysecret
- 我们需要做的就是删除文件中每行等号后面的内容。我们可以使用以下
sed
命令来实现:
sed -i '' -e 's/^\(.*=\).*$/\1/'
- 现在我们可以使用以下命令运行过滤分支:
$ git filter-branch --prune-empty --tree-filter "test -f .credentials && sed -i '' -e 's/^\(.*=\).*$/\1/' .credentials || true" -- --all
- 如果我们现在查看文件,可以看到用户名和密码已经消失:
$ cat .credentials
username =
password =
- 如我们在之前的示例中所见,我们仍然需要在
filter-branch
后进行清理,删除原始引用,过期reflog
,并触发垃圾回收。但此时,你可以比较仓库的内容和提交历史。
它是如何工作的...
对于仓库中的每个提交,Git 会检查该提交的内容并运行 tree-filter
。如果过滤器失败并返回非零退出码,filter-branch
会失败。因此,记得处理 tree-filter
可能失败的情况非常重要。这也是为什么之前的 tree-filter
会检查 .credentials
文件是否存在,如果存在,则运行 sed
命令,否则返回 true
以继续执行 filter-branch
。
创建仓库的镜像仓库备份
尽管 Git 是分布式的,每个克隆实际上都是一个备份,但在备份 Git 仓库时,仍然有一些技巧可以派上用场。一个普通的 Git 仓库会有它跟踪的文件的工作副本,以及该仓库的完整历史记录,这些都保存在该仓库的 .git
文件夹中。服务器上的仓库,通常是你推送和拉取的仓库,通常是裸仓库。裸仓库是没有工作副本的仓库。大致上,它就只是一个普通仓库的 .git
文件夹。镜像仓库几乎与裸仓库相同,唯一的不同是它会获取所有位于 refs/*
下的引用,而裸仓库只会获取位于 refs/heads/*
下的引用。接下来,我们将更详细地看一下普通、裸和镜像克隆的 JGit 仓库。
准备工作
我们将从创建 JGit 仓库的三个克隆开始:一个普通的、一个裸的和一个镜像的克隆。当我们创建第一个克隆时,可以将其作为其他克隆的参考仓库。通过这种方式,我们可以共享数据库中的对象,而不需要将相同的数据传输三遍:
$ git clone https://git.eclipse.org/r/jgit/jgit
$ git clone --reference jgit --bare https://git.eclipse.org/r/jgit/jgit
$ git clone --mirror --reference jgit https://git.eclipse.org/r/jgit/jgit jgit.mirror
如何操作...
- 普通仓库与裸仓库或镜像仓库之间的一个区别是,裸仓库中没有远程分支。所有分支都是本地创建的。我们可以通过使用
git branch
命令列出这三个仓库中的分支,来看这一点:
$ cd jgit
$ git branch
* master
$ cd ../jgit.git # or cd ../jgit.mirror
$ git branch
* master
stable-0.10
stable-0.11
stable-0.12
...
- 为了查看裸仓库和镜像仓库之间的区别,我们需要列出不同的 refspec 获取和不同的
refs
命名空间。列出镜像仓库 (jgit.mirror
) 中的 origin 的获取refspec
:
$ cd ../jgit.mirror
$ git config remote.origin.fetch
+refs/*:refs/*
- 列出镜像仓库中的不同
refs
命名空间:
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u
refs/cache-automerge
refs/changes
refs/heads
refs/meta
refs/notes
refs/tags
- 裸仓库 (
jgit.git
) 的 origin 配置中没有明确的refspec
获取。当未找到配置条目时,Git 会使用默认的refspec
获取,就像在普通仓库中一样。我们可以使用以下命令检查 origin 的远程 URL:
$ cd ../jgit.git
$ git config remote.origin.url
https://git.eclipse.org/r/jgit/jgit
- 使用以下命令列出裸仓库中的不同
refs
命名空间,看看有什么不同:
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u
refs/heads
refs/tags
- 最后,我们可以列出普通仓库 (
jgit
) 的refspec
拉取和refs
命名空间:
$ cd ../jgit
$ git config remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u
refs/heads
refs/remotes
refs/tags
- 镜像仓库有四个在普通或裸仓库中找不到的 ref 命名空间:
refs-cache-automerge
、changes
、meta
和notes
。普通仓库是唯一一个拥有refs/remote
命名空间的仓库。
它的工作原理...
普通和裸仓库非常相似,只有镜像仓库显得与众不同。这是因为镜像仓库上的 refspec
拉取 +refs/*:refs/*
,它将从远程获取所有 refs
而不仅仅是普通仓库(和裸仓库)所做的 refs/heads/*
和 refs/tags/*
。JGit 仓库上许多不同的 ref
命名空间是因为 JGit 仓库由 Gerrit 代码审查管理。它使用不同的命名空间来存储仓库特定内容,例如用于所有提交的代码审查分支的变更分支和代码审查分数元数据。
mirror
仓库是当你想要快速备份 Git 仓库时的理想选择。它确保你包含了一切,而无需比 Git 仓库所在的主机的 Git 访问权限更多的额外访问权限。
还有更多...
GitHub 上的仓库在某些 refs 命名空间中存储额外的信息。如果一个仓库曾经发起过一个拉取请求,该拉取请求将记录在 refs/pull/*
命名空间中。让我们在下面的例子中看看这个:
$ git clone --mirror git@github.com:jenkinsci/extreme-feedback-plugin.git
$ cd extreme-feedback-plugin.git
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u
refs/heads
refs/meta
refs/pull
refs/tags
一个快速的子模块 "如何"
在进行软件项目开发时,有时会遇到需要将另一个项目作为自己项目的一部分的情况。这个其他项目可以是任何东西,从你正在开发的另一个项目到第三方库。尽管需要在一个项目中使用另一个项目,但你希望保持这些项目的分离。Git 为这种项目依赖关系提供了一种机制,称为子模块。基本思想是你可以将另一个 Git 仓库克隆到你的项目中作为子目录,但保持两个仓库的提交记录分离,如下图所示:
准备工作
我们将从克隆一个示例仓库开始,作为超级项目的使用示例:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_Super.git
$ cd Git-Version-Control-Cookbook-Second-Edition_Super
如何做...
- 我们将把子项目
lib_a
作为 Git 子模块添加到超级项目中:
$ git submodule add https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git lib_a
Cloning into 'lib_a'...
remote: Counting objects: 18, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 18 (delta 4), reused 17 (delta 3)
Receiving objects: 100% (18/18), done.
Resolving deltas: 100% (4/4), done.
Checking connectivity... done.
- 让我们使用以下命令检查
git status
的状态:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitmodules
new file: lib_a
- 我们可以更仔细地查看 Git 索引中的两个文件;
.gitmodules
是一个普通文件,所以我们可以使用cat
:
$ cat .gitmodules
[submodule "lib_a"]
path = lib_a
url = https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git
$ git diff --cached lib_a
diff --git a/lib_a b/lib_a
new file mode 160000
index 0000000..0d96e7c
--- /dev/null
+++ b/lib_a
@@ -0,0 +1 @@
+Subproject commit 0d96e7cfc4d4db64002e63af0f7325d33bdaf84f
.gitmodules
文件如上所述,包含有关所有在仓库中注册的子模块的信息。lib_a
文件存储着当子模块被添加到超项目时,子模块HEAD
指向的是哪个提交。每当子模块通过新的提交(本地创建或拉取)进行更新时,超项目在运行git status
时会显示子模块发生了变化。如果子模块的更改可以接受,超项目中的子模块修订版本会通过添加子模块文件并提交到超项目来更新。
- 我们将使用以下命令将子模块
lib_a
更新到开发分支上的最新更改:
$ cd lib_a
$ git checkout develop
Branch develop set up to track remote branch develop from origin by rebasing.
Switched to a new branch 'develop'
$ cd ..
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib_a (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
- 让我们检查一下子模块是否有任何更新:
$ git submodule update
Submodule path 'lib_a': checked out '0d96e7cfc4d4db64002e63af0f7325d33bdaf84f'
- 哎呀!现在我们实际上已经将子模块重置为该子模块文件中描述的状态。我们需要再次切换到子模块,检查开发分支,并这次在超项目中创建一个提交:
$ cd lib_a
$ git status
HEAD detached at 0d96e7c
nothing to commit, working directory clean
$ git checkout develop
Previous HEAD position was 0d96e7c... Fixes book title in README
Switched to branch 'develop'
Your branch is up-to-date with 'origin/develop'.
$ cd ..
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib_a (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
$ git add lib_a
$ git commit -m 'Updated lib_a to newest version'
[master 4d371bb] Updated lib_a to newest version
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 lib_a
注意,默认情况下,子模块处于分离头指针(detached head)状态,这意味着HEAD
直接指向某个提交,而不是分支。你仍然可以编辑子模块并记录提交;但是,如果在超项目中执行子模块更新而没有首先提交新的子模块状态,你的更改可能会很难找到。切记,在切换到子模块工作时,一定要检查或创建一个分支。如果是这样,你只需要再次检出该分支,就能恢复你的更改。从 Git 1.8.2 版本开始,可以使子模块跟踪一个分支,而不是单个提交。Git 1.8.2 版本发布于 2013 年 3 月 13 日,你可以通过运行git --version
来检查你的版本。
- 为了让 Git 跟踪子模块的分支而不是特定的提交,我们需要记录我们想要跟踪的分支名称。这可以在子模块的
.gitmodules
文件中完成;在这里,我们将使用稳定分支:
$ git config -f .gitmodules submodule.lib_a.branch stable
$ cat .gitmodules
[submodule "lib_a"]
path = lib_a
url = https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git
branch = stable
- 我们现在可以添加并提交子模块,然后尝试使用以下命令更新它:
$ git add .gitmodules
$ git commit -m 'Make lib_a module track its stable branch'
[master bf9b9ba] Make lib_a module track its stable branch
1 file changed, 1 insertion(+)
$ git submodule update --remote
Submodule path 'lib_a': checked out '8176a16db21a48a0969e18a51f2c2fb1869418fb'
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib_a (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
子模块仍然处于分离HEAD
状态。然而,当使用git submodule update --remote
更新子模块时,将从子模块的远程仓库拉取更改,并将子模块更新到它正在跟踪的分支的最新提交。我们仍然需要记录一次提交到超项目中,指定子模块的状态。
还有更多...
当你克隆一个包含一个或多个子模块的仓库时,你需要在克隆后显式地拉取它们。我们可以用新创建的子模块仓库尝试这个:
$ git clone super super_clone
Cloning into 'super_clone'...
done.
现在,初始化并更新子模块:
$ cd super_clone
$ git submodule init
Submodule 'lib_a' (https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git) registered for path 'lib_a'
$ git submodule update --remote
Cloning into 'lib_a'...
remote: Counting objects: 18, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 18 (delta 4), reused 17 (delta 3)
Receiving objects: 100% (18/18), done.
Resolving deltas: 100% (4/4), done.
Checking connectivity... done.
Submodule path 'lib_a': checked out '8176a16db21a48a0969e18a51f2c2fb1869418fb'
仓库已准备好进行开发!
克隆仓库时,如果提供--recursive
或--recurse-submodules
选项,子模块可以在克隆后直接初始化和更新。
子树合并
子模块的替代方法是子树合并。子树合并是一种可以在使用 Git 执行合并时采用的策略。当将一个分支(或者如我们在本食谱中所看到的,另一个项目)合并到 Git 仓库的子目录而不是根目录时,这种策略非常有用。使用子树合并策略时,子项目的历史会与超级项目的历史合并,而子项目的历史可以保持清晰,除了那些计划合并到上游的提交。
准备工作
我们将使用与上一个食谱中相同的仓库,并重新克隆超级项目以摆脱子模块设置:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_Super.git
$ cd Git-Version-Control-Cookbook-Second-Edition_Super
如何做...
- 我们将把子项目作为一个新的远程仓库添加,并获取其历史记录:
$ git remote add lib_a https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git
$ git fetch lib_a
warning: no common commits
remote: Reusing existing pack: 18, done.
remote: Total 18 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (18/18), done.
From https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_lib_a.git
* [new branch] develop -> lib_a/develop
* [new branch] master -> lib_a/master
* [new branch] stable -> lib_a/stable
- 现在我们可以创建一个本地分支
lib_a_master
,该分支指向与 lib a 的主分支(lib_a/master
)相同的提交:
$ git checkout -b lib_a_master lib_a/master
Branch lib_a_master set up to track remote branch master from lib_a by rebasing.
Switched to a new branch 'lib_a_master'
- 我们可以使用以下命令检查工作树的内容:
$ ls
README.md a.txt
- 如果我们切换回
master
分支,应该会在目录中看到超级仓库的内容:
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ ls
README.md super.txt
- Git 会正常切换分支并填充工作目录,即使这些分支最初来自两个不同的仓库。现在,我们想将
lib_a
的历史合并到一个子目录中。首先,我们使用ours
策略准备一个合并提交,并确保该提交未完成(我们需要引入所有文件):
$ git merge -s ours --no-commit --allow-unrelated-histories lib_a_master
Automatic merge went well; stopped before committing as requested
简而言之,ours
策略告诉 Git 执行以下操作:合并这个分支,但保持最终的树结构与该分支顶端的树结构相同。所以,分支被合并,但它所引入的所有更改都会被丢弃。在我们之前的命令行中,我们还传递了--no-commit
选项。这个选项阻止 Git 完成合并,但让仓库保持在合并状态。现在我们可以将lib_a
仓库的内容添加到仓库根目录中的lib_a
文件夹。我们使用git read-tree
来确保两个树完全相同,如下所示:
$ git read-tree --prefix=lib_a/ -u lib_a_master
- 我们当前的目录结构如下所示:
$ tree
.
|-- README.md
|-- lib_a
| |-- README.md
| '-- a.txt
'-- super.txt
- 现在是通过以下命令结束我们开始的合并提交的时候了:
$ git commit -m 'Initial add of lib_a project'
[master 5066b7b] Initial add of lib_a project
现在,子项目已添加。接下来,我们将看到如何使用子项目中的新提交更新超级项目,并且如何将超级项目中的提交复制到子项目中。
- 我们需要使用以下命令向超级项目添加并提交一些更改:
$ echo "Lib_a included!" >> super.txt
$ git add super.txt
$ git commit -m "Update super.txt"
[master 83ef9a4] Update super.txt
1 file changed, 1 insertion(+)
- 对子项目进行了更改,并在超级项目中提交:
$ echo "The b file in lib_a" >> lib_a/b.txt
$ git add lib_a/b.txt
$ git commit -m "[LIB_A] Enhance lib_a with b.txt"
[master debe836] [LIB_A] Enhance lib_a with b.txt
1 file changed, 1 insertion(+)
create mode 100644 lib_a/b.txt
当前的历史记录如下图所示:
合并可以在之前的截图中看到,同时还可以看到仓库的两个根提交:原始根提交和来自lib_a
的根提交。
- 现在,我们将学习如何将子项目
lib_a
中做的新提交集成到超级仓库中。通常,我们会通过切换到lib_a_master
分支并执行拉取操作,获取远程仓库中的最新提交。然而,由于我们在本食谱中使用的是示例仓库,master
分支上没有新提交。因此,我们将使用lib_a
的develop
和stable
分支。现在,我们将直接使用lib_a/develop
引用,将develop
分支中的提交集成到lib_a
中,如下所示:
$ git merge -m '[LIB_A] Update lib_a project to latest state' -s subtree lib_a/develop
Merge made by the 'subtree' strategy.
lib_a/a.txt | 2 ++
1 file changed, 2 insertions(+)
现在,我们的master
分支已经更新了来自lib_a/develop
的提交,如下图所示:
- 现在,是时候将我们在
lib_a
目录中所做的提交添加回lib_a
项目了。首先,我们将切换到lib_a_master
分支,并将其与lib_a/develop
合并,以确保尽可能保持最新:
$ git checkout lib_a_master
$ git merge lib_a/develop
Updating 0d96e7c..ab47aca
Fast-forward
a.txt | 2 ++
1 file changed, 2 insertions(+)
- 我们现在准备好将超级项目中的更改与子项目合并。为了避免将超级项目的历史与子项目合并,我们将使用
--squash
选项。这个选项会阻止 Git 完成合并,并且与之前的情况不同,我们也阻止了合并提交记录,它不会使仓库处于合并状态。然而,工作目录和暂存区的状态会像真正的合并一样:
$ git merge --squash -s subtree --no-commit master
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
- 现在,我们可以记录一个包含所有
lib_a
更改的提交到超级项目中:
$ git commit -m 'Enhance lib_a with b.txt'
[lib_a_master 01e45f7] Enhance lib_a with b.txt
1 file changed, 1 insertion(+)
create mode 100644 b.txt
lib_a
仓库的历史如以下截图所示:
- 我们可以将更多来自
lib_a/stable
的更改集成到超级项目中,但首先我们需要更新lib_a_master
分支,以便能够从这里集成它们:
$ git merge lib_a/stable
Merge made by the 'recursive' strategy.
a.txt | 2 ++
1 file changed, 2 insertions(+)
一个新提交已添加到子项目中,如下图所示:
- 最后的任务是将
lib_a_master
上的新提交集成到超级仓库中的master
分支。这与之前的操作一样,通过git merge
使用subtree strategy
选项来完成:
$ git checkout master
$ git merge -s subtree -m '[LIB_A] Update to latest state of lib_a' lib_a_master
Merge made by the 'subtree' strategy.
lib_a/a.txt | 2 ++
1 file changed, 2 insertions(+)
结果历史如以下截图所示:
它是如何工作的...
当使用子树策略时,Git 会确定你试图合并的分支适合你的仓库中的哪个子树。这就是我们使用read-tree
命令添加lib_a
仓库内容的原因,以确保在超级项目中获得与lib_a
项目根树相同的lib_a
目录的 SHA-1 ID。在下面的示例中,SHA-1 ID 在第一条命令中找到。
我们可以通过查找在我们合并子项目的提交中lib_a
树的 SHA-1 来验证这一点:
$ git log -1 | head -1 | awk '{print $2}' 0f10e563c6824402d30380c9f8fbf87769e64e8a $ git ls-tree 0f10e563c6824402d30380c9f8fbf87769e64e8a 100644 blob 456a5df638694a699fff7a7ff31a496630b12d01 README.md 040000 tree 7d66ad11cb22c6d101c7ac9c309f7dce25231394 lib_a 100644 blob c552dead26fdba634c91d35708f1cfc2c4b2a100 super.txt
lib_a/master
上的根树 ID 可以通过以下命令获取:
$ git cat-file -p lib_a/master
tree 7d66ad11cb22c6d101c7ac9c309f7dce25231394
parent a7d76d9114941b9d35dd58e42f33ed7e32a9c134
author John Doe <john.doe@example.com> 1396553189 +0200
committer John Doe <john.doe@example.com> 1396553189 +0200
Fixes book title in README
参见
使用 git subtree
命令是另一种使用子树合并的方式。这在许多 Git 安装中默认未启用,但自 Git 1.7.11 起已随 Git 分发。您可以在以下链接中查看如何安装和使用:
-
要安装,请访问
github.com/git/git/blob/master/contrib/subtree/INSTALL
-
要了解如何使用子树,请访问
github.com/git/git/blob/master/contrib/subtree/git-subtree.txt
如果您是 Homebrew 或 Ubuntu 用户,它们的基本包支持子树。对于 Fedora,您必须安装额外的软件包。
子模块与子树合并
对于一个项目是否应该使用子模块或者子树合并,没有一个简单的答案。选择子模块会给项目中的开发人员增加很大的额外压力,因为他们需要确保子模块和超级项目保持同步。选择通过子树合并添加项目时,对开发人员几乎没有额外的复杂性。然而,仓库维护者需要确保子项目是最新的,并且提交被添加回子项目中。这两种方法都行之有效,且广泛使用,可能只是一个习惯的问题。一个完全不同的解决方案是使用超级项目的构建系统来获取必要的依赖,例如 Maven 或 Gradle。
第十章:补丁和离线共享
在本章中,我们将介绍以下几种方法:
-
创建补丁
-
从分支创建补丁
-
应用补丁
-
发送补丁
-
创建 Git 打包
-
使用 Git 打包
-
从树中创建归档
介绍
由于 Git 的分布式特性以及现有的众多托管选项,当机器通过网络连接时,分享历史记录变得非常容易。如果需要共享历史记录的机器未连接或无法使用支持的传输机制,Git 提供了其他共享历史记录的方法。
Git 提供了一种简便的方法来格式化现有历史记录中的补丁,发送它们到电子邮件,并将其应用到另一个代码库。Git 还提供了一个打包概念,其中一个包含部分历史记录的打包可以作为另一个代码库的远程仓库。最后,Git 提供了一种简单易用的方式来创建一个特定引用的文件夹/子文件夹结构的快照归档。
借助 Git 提供的不同方法,特别是在常规的推送/拉取方法无法使用的情况下,分享代码库之间的历史记录变得更加容易。
创建补丁
在本节中,我们将学习如何从提交中创建补丁。补丁可以通过电子邮件快速共享,或者如果需要应用到离线计算机或类似设备,也可以复制到便携设备(如 USB 闪存、内存卡、外部硬盘等)。补丁是代码审查的有效方法,审查者可以将补丁应用到自己的代码库,调查差异并检查程序。如果审查者认为补丁没有问题,他们可以将其发布(push
)到公共代码库,前提是审查者是该代码库的维护者。如果审查者拒绝该补丁,他们可以简单地将分支重置为原始状态,并通知补丁作者,补丁需要更多的工作才能被接受。
准备工作
在这个示例中,我们将克隆并使用一个新的代码库。该代码库只是一个示例库,用于 Git 命令,并且仅包含一些示例提交:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git $ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing $ git checkout master
操作方法...
让我们查看该代码库的历史记录,使用 gitk
显示如下:
$ git log --graph --all --oneline
* 4bc2b08 (master) Calculate pi with more digits
| * 971ac91 (doc) Adds Documentation folder
| * 2a0c8d6 Add build information
| * 9d00fcc Update readme
| | * 583225a (HEAD -> develop) Adds functionality to prime-test a range of numbers
| | * f6c5713 Adds Makefile for easy building
| | * d00ffc0 Move print functionality of is_prime
| |/
|/|
* | 6e46ff8 Adds checker for prime number
|/
* 8bddff2 Adds math, pi calculation
* 6de7cef Offline sharing, patch, bundle and archive
该代码库中有三个分支:master
、develop
和 doc
。它们之间的差异是通过一个或多个提交来体现的。在 master
分支上,我们现在可以为该分支的最新提交创建一个补丁文件,并将其存储在 latest-commit
文件夹中,如下所示:
$ git format-patch -1 -o latest-commit
latest-commit/0001-Calculate-pi-with-more-digits.patch
如果我们查看由 patch
命令创建的文件,我们将看到以下内容:
$ cat latest-commit/0001-Calculate-pi-with-more-digits.patch From 4bc2b08517141c2b84ae76ccaab3a380c19de8a6 Mon Sep 17 00:00:00 2001
From: John Doe <john.doe@example.com>
Date: Thu, 10 Apr 2014 09:19:29 +0200
Subject: [PATCH] Calculate pi with more digits
Dik T. Winter style
Build: gcc -Wall another_pi.c -o pi
Run: ./pi
---
another_pi.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 another_pi.c
$ diff --git a/another_pi.c b/another_pi.c
new file mode 100644
index 0000000..86df41b
--- /dev/null
+++ b/another_pi.c
@@ -0,0 +1,21 @@
+/* Pi with 800 digits
+ * Dik T. Winter style, but modified sligthly
+ * https://crypto.stanford.edu/pbc/notes/pi/code.html
+ */
+ #include <stdio.h>
+
+void another_pi (void) {
+ printf("800 digits of pi:\n");
+ int a=10000, b=0, c=2800, d=0, e=0, f[2801], g=0;
+ for ( ;b-c; )f[b++]=a/5;
+ for (;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)
+ for (b=c; d+=f[b]*a, f[b]=d%--g,d/=g--,--b; d*=b);
+
+ printf("\n");
+}
+
+int main (void){
+ another_pi();
+
+ return 0;
+}
--
2.14.0
上面的代码片段是生成的补丁文件的内容。它包含了类似邮件的头部,包含From
、Date
和Subject
字段,正文是提交信息,之后是三个破折号(---
)后面的实际补丁,最后是两个破折号(--
),以及用于生成补丁的 Git 版本。git format-patch
生成的补丁采用UNIX邮箱格式,但带有一个固定的时间戳,用于标识它来自git format-patch
而非真实邮箱。你可以在sha-1
ID 后的第一行看到时间戳:Mon Sep 17 00:00:00 2001。
它是如何工作的...
生成补丁时,Git 将HEAD
处的提交与其父提交进行diff
,并将此diff
作为补丁。-1
选项告诉 Git 只为最后一个提交生成补丁,而-o latest-commit
告诉 Git 将补丁存储在latest-commit
文件夹中。如果该文件夹不存在,将会创建它。
还有更多内容...
如果你想为多个提交创建补丁,比如最后的三个提交,只需传递-3
给git format-patch
,而不是-1
。
将最近的三个提交格式化为latest-commits
文件夹中的补丁:
$ git format-patch -3 -o latest-commits latest-commits/0001-Adds-math-pi-calculation.patch latest-commits/0002-Adds-checker-for-prime-number.patch latest-commits/0003-Calculate-pi-with-more-digits.patch $ ls -1 latest-commits 0001-Adds-math-pi-calculation.patch 0002-Adds-checker-for-prime-number.patch 0003-Calculate-pi-with-more-digits.patch
从分支创建补丁
你可以通过在运行format-patch
命令时指定目标分支来创建补丁,而无需计算需要为之生成补丁的提交数量。
准备工作
我们将使用与前一个示例相同的仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git $ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing
确保我们已经检查出develop
分支:
$ git checkout develop
如何操作...
我们假设已经在develop
分支上进行了一些提交。现在,我们需要将这些提交格式化为补丁,以便发送给仓库维护者或将它们带到另一台机器上。
让我们来看一下develop
分支上不在master
分支上的提交:
$ git log --oneline master..develop 583225a (HEAD -> develop) Adds functionality to prime-test a range of numbers
f6c5713 Adds Makefile for easy building
d00ffc0 Move print functionality of is_prime
现在,我们将不再运行git format-patch -3
来生成这三个提交的补丁,而是告诉 Git 为所有不在master
分支上的提交生成补丁:
$ git format-patch -o not-on-master master not-on-master/0001-Move-print-functionality-of-is_prime.patch not-on-master/0002-Adds-Makefile-for-easy-building.patch not-on-master/0003-Adds-functionality-to-prime-test-a-range-of-numbers.patch
它是如何工作的...
Git 会列出develop
分支中没有在master
分支上的提交,就像我们在创建补丁之前所做的一样,并为这些提交生成补丁。我们可以检查not-on-master
文件夹的内容,这个文件夹是我们指定的输出文件夹(-o
),并验证它是否包含预期的补丁:
$ ls -1 not-on-master 0001-Move-print-functionality-of-is_prime.patch 0002-Adds-Makefile-for-easy-building.patch 0003-Adds-functionality-to-prime-test-a-range-of-numbers.patch
还有更多内容...
git format-patch
命令除了-<n>
选项用于指定要生成补丁的提交数量外,还有很多其他选项,比如-o <dir>
用于指定目标目录。以下是一些有用的选项:
-
-s
、--signoff
:在补丁文件的提交信息中添加一行Signed-off-by
,其中包含提交者的名字。当向仓库维护者发送补丁时,通常需要这一行。当补丁发送到 Linux 内核邮件列表或 Git 邮件列表时,这一行是接受补丁的必要条件。 -
-n
、--numbered
:将补丁在主题行中编号为[PATCH n/m]
。 -
--suffix=.<sfx>
:设置补丁的后缀;它可以为空,且不必以点号开头。 -
-q
,--quiet
:在生成补丁时抑制打印补丁文件名。 -
--stdout
:将所有提交输出到标准输出,而不是创建文件。
应用补丁
现在我们知道如何从提交中创建补丁了,是时候学习如何应用它们了。
准备工作
我们将使用之前示例中的仓库,以及生成的补丁,如下所示:
$ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing
$ git checkout master
$ ls -1a
.
..
.git
Makefile
README.md
another_pi.c
latest-commit
math.c
not-on-master
如何操作...
首先,我们将切换到 develop
分支,并在第一个示例中应用从 master
分支生成的补丁(0001-Calculate-pi-with-more-digits.patch
)。
我们使用 Git am
命令来应用补丁;am
是 apply from mailbox
的缩写:
$ git checkout develop
Your branch is up-to-date with 'origin/develop'.
$ git am latest-commit/0001-Calculate-pi-with-more-digits.patch
Applying: Adds functionality to prime-test a range of numbers
error: patch failed: math.c:47
error: math.c: patch does not apply
Patch failed at 0001 Adds functionality to prime-test a range of numbers
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
我们可以解决第 47 行的冲突(删除一个空行),然后继续:
$ git add math.c
$ git am --continue
Applying: Adds functionality to prime-test a range of numbers
我们还可以将 master
分支应用于从 develop
分支生成的一系列补丁,如下所示:
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git am not-on-master/*
Applying: Move print functionality of is_prime
Applying: Adds Makefile for easy building
Applying: Adds functionality to prime-test a range of numbers
它是如何工作的...
git am
命令获取输入中指定的邮件箱文件,并将文件中的补丁应用到需要的文件上。然后,使用补丁中的提交信息和作者信息记录一个提交。提交的提交者身份将是执行 git am
命令的人的身份。我们可以通过 git log
查看作者和提交者信息,但需要传递 --pretty=fuller
选项才能同时查看提交者信息:
$ git log -1 --pretty=fuller
commit 45e49d0c4fcd44b73e11d61e025a62ab2655e42d (HEAD -> master)
Author: John Doe <john.doe@example.com>
AuthorDate: Wed Apr 9 21:50:18 2014 +0200
Commit: John Doe <john.doe@example.com>
CommitDate: Sun Jun 3 21:58:46 2018 +0200
Adds functionality to prime-test a range of numbers
还有更多...
git am
命令会应用指定文件中的补丁并记录提交到仓库。然而,如果你只想将补丁应用到工作树或暂存区,而不记录提交,可以使用 git apply
命令。
我们可以再次尝试将来自 master
分支的补丁应用到 develop
分支;我们只需要先重置 develop
分支:
$ git checkout develop
Switched to branch 'develop'
Your branch is ahead of 'origin/develop' by 1 commit.
(use "git push" to publish your local commits)
$ git reset --hard origin/develop
HEAD is now at c131c8b Adds functionality to prime-test a range of numbers
$ git apply latest-commit/0001-Calculate-pi-with-more-digits.patch
我们可以使用 status
命令检查仓库的状态:
$ git status
On branch develop
Your branch is up-to-date with 'origin/develop'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
another_pi.c
latest-commit/
not-on-master/
nothing added to commit but untracked files present (use "git add" to track)
我们成功地将补丁应用到工作树中。我们还可以使用 --index
选项将其应用到暂存区和工作树,或者仅使用 --cached
选项将其应用到暂存区。
发送补丁
在之前的示例中,你看到了如何创建和应用补丁。当然,你可以直接将这些补丁文件附加到电子邮件中,但 Git 提供了一种直接通过电子邮件发送补丁的方法,即 git send-email
命令。该命令需要一些设置,但如何配置依赖于你的邮件和 SMTP 设置。你可以在 Git 帮助页面找到通用指南,或者访问:git-scm.com/docs/git-send-email
.
准备工作
我们将设置与之前示例相同的仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git
$ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing
如何操作...
首先,我们将发送与第一个示例中创建的补丁相同的补丁。我们将使用在 Git 配置中指定的电子邮件地址将其发送给自己。让我们再次使用git format-patch
创建补丁并使用git send-email
发送:
$ git format-patch -1 -o latest-commit
latest-commit/0001-Calculate-pi-with-more-digits.patch
将 Git 配置中的邮箱地址保存到一个变量中,如下所示:
$ emailaddr=$(git config user.email)
使用--to
和--from
字段中的电子邮件地址发送补丁:
$ git send-email --to $emailaddr --from $emailaddr latest-commit/0001-Calculate-pi-with-more-digits.patch
latest-commit/0001-Calculate-pi-with-more-digits.patch
(mbox) Adding cc: John Doe <john.doe@example.com> from line 'From: John Doe <john.doe@example.com>'
OK. Log says:
Server: smtp.gmail.com
MAIL FROM:<john.doe@example.com>
RCPT TO:<john.doe@example.com>
From: john.doe@example.com
To: john.doe.example.com
Subject: [PATCH] Calculate pi with more digits
Date: Mon, 14 Apr 2014 09:00:11 +0200
Message-Id: <1397458811-13755-1-git-send-email-john.doe@example.com>
X-Mailer: git-send-email 1.9.1
检查您的电子邮件会显示收件箱中有一封新邮件。
它是如何工作的...
如我们在前面的示例中看到的,git format-patch
以 Unix mbox 格式创建补丁文件,因此只需要一点额外的工作,就可以让 Git 将补丁作为电子邮件发送。使用git send-email
发送电子邮件时,请确保您的邮件用户代理(MUA)不会在补丁文件中断行、将制表符替换为空格等。您可以通过将补丁发送给自己并检查它是否能够顺利应用到您的代码库来轻松测试这一点。
还有更多...
send-email
命令当然可以一次发送多个补丁。如果指定的是一个目录而不是单个补丁文件,该目录中的所有补丁都会被发送。我们甚至不需要在发送之前生成补丁文件;只需要指定我们希望发送的修订范围,就像为format-patch
命令指定范围一样。然后,Git 会动态生成补丁并发送。当我们以这种方式发送一系列补丁时,最好创建一个求职信,对随后的补丁系列做一些解释。可以通过将--cover-letter
选项传递给send-email
命令来创建求职信。我们将尝试发送develop
分支上的提交补丁,因为它是从master
分支分出的(与第二个示例中的补丁相同),如下所示:
$ git checkout develop
Switched to branch 'develop'
Your branch is up-to-date with 'origin/develop'.
$ git send-email --to john.doe@example.com --from
john.doe@example.com --cover-letter --annotate origin/master
/tmp/path/for/patches/0000-cover-letter.patch
/tmp/path/for/patches/0001-Move-print-functionality-of-is_prime.patch
/tmp/path/for/patches/0002-Adds-Makefile-for-easy-building.patch
/tmp/path/for/patches/0003-Adds-functionality-to-prime-test-a-range-of-numbers.patch
(mbox) Adding cc: John Doe <john.doe@example.com> from line 'From: John Doe <john.doe@example.com>'
OK. Log says:
Server: smtp.gmail.com
MAIL FROM:<john.doe@example.com>
RCPT TO:<john.doe@exmample.com>
From: john.doe@example.com
To: john.doe@example.com
Subject: [PATCH 0/3] Cover Letter describing the patch series
Date: Sat, 14 Jun 2014 23:35:14 +0200
Message-Id: <1397459884-13953-1-git-send-email-john.doe@example.com>
X-Mailer: git-send-email 1.9.1
...
我们可以检查我们的邮箱收件箱,看到我们发送的四封邮件:求职信和三个补丁。
在发送补丁之前,求职信已填写,并且默认情况下,主题行中会有[PATCH 0/3]
(如果发送三个补丁)。仅包含默认模板主题和正文的求职信不会作为默认发送。在本章附带的脚本中,git send-email
命令调用了--force
和--confirm=never
选项。这样做是为了脚本自动化,即使求职信没有从默认值更改,也强制 Git 发送邮件。您可以尝试删除这些选项,加入--annotate
选项,并再次运行脚本。然后,您应该能够在发送邮件之前编辑包含补丁的求职信和电子邮件。
创建 Git 捆绑包
共享仓库历史记录的另一种方法是使用git bundle
命令。Git 捆绑包是一系列可以作为远程仓库使用的提交,但不包括仓库的完整历史记录。
准备工作
我们将使用一个全新的offline-sharing
仓库克隆,如下所示:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git
$ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing $ git checkout master
如何操作...
首先,我们将创建一个根打包,如以下命令所示,这样打包中的历史就形成了一个完整的历史,并且初始提交也被包含在内:
$ git bundle create myrepo.bundle master
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 1.88 KiB | 0 bytes/s, done.
Total 12 (delta 1), reused 0 (delta 0)
我们可以使用 git bundle verify
验证打包内容:
$ git bundle verify myrepo.bundle
The bundle contains this ref:
1e42a2dfa3a377d412efd27a77b973c75935c62a refs/heads/master
The bundle records a complete history.
myrepo.bundle is okay
为了方便记住我们作为最新提交包含在打包中的提交,我们创建一个指向该提交的tag
;该提交也被master
分支指向:
$ git tag bundleForOtherRepo master
我们已经创建了包含仓库历史初始提交的根打包。现在我们可以创建第二个打包,包含从我们刚创建的标签到 develop
分支尖端的历史。请注意,在以下命令中,我们使用相同的打包文件名 myrepo.bundle
,这将覆盖旧的打包文件:
$ git bundle create myrepo.bundle bundleForOtherRepo..develop
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.47 KiB | 0 bytes/s, done.
Total 9 (delta 2), reused 0 (delta 0)
刚创建打包文件后立即覆盖它可能看起来很奇怪,但给打包文件使用相同的名称是有一定道理的。正如你将在下一个示例中看到的那样,在使用打包文件时,你将其作为远程添加到你的仓库,URL 就是打包文件的路径。第一次这样做时使用的是根打包文件和 URL。打包文件的文件路径将作为远程仓库的 URL 存储。因此,下一次你需要更新仓库时,只需覆盖打包文件并执行 fetch
操作即可。
如果我们验证打包内容,可以看到在目标仓库中需要先存在哪个提交,才能使用该打包:
$ git bundle verify myrepo.bundle
The bundle contains this ref:
c131c8bb2bf8254e46c013bfb33f4a61f9d4b40e refs/heads/develop
The bundle requires this ref:
ead7de45a504ee19cece26daf45d0184296f3fec
myrepo.bundle is okay
我们可以检查历史,看到 ead7de4
提交是 develop
分支分出的地方,所以这个提交作为我们刚创建的打包的基础是合理的:
$ gitk master develop
上述命令给出了以下输出:
工作原理...
bundle
命令创建一个包含指定提交范围历史的二进制文件。当创建一个不包含仓库初始提交的提交范围的打包(例如,bundleForOtherRepo..develop
)时,重要的是要确保该范围与打算使用该打包的仓库中的历史相匹配。
使用 Git 打包
在上一个示例中,我们看到了如何从现有历史中创建包含指定历史范围的打包。现在,我们将学习如何使用这些打包,既可以创建一个新仓库,也可以为现有仓库添加历史。
准备工作
我们将使用与上一个示例相同的仓库和方法来创建打包,但我们将在本示例中重新创建它们,以便能够逐个使用。首先,我们将准备仓库和第一个打包,如以下命令所示:
$ rm -rf offline-sharing
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git $ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing
$ git checkout master
Branch master set up to track remote branch master from origin by rebasing.
Switched to a new branch 'master'
$ git bundle create myrepo.bundle master
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 1.88 KiB | 0 bytes/s, done. Total 12 (delta 1), reused 0 (delta 0)
$ git tag bundleForOtherRepo master
如何操作...
现在,让我们从我们刚创建的打包文件创建一个新仓库。我们可以使用 git clone
命令,并通过指定远程仓库的 URL 作为打包路径来实现这一点。我们将在以下代码片段中看到如何操作:
$ cd ..
$ git clone -b master Git-Version-Control-Cookbook-Second-Edition_offline-sharing/myrepo.bundle offline-other
Cloning into 'offline-other'...
Receiving objects: 100% (12/12), done.
Resolving deltas: 100% (1/1), done.
Checking connectivity... done.
新的仓库已在offline-other
文件夹中创建。让我们使用以下命令检查该仓库的历史记录:
$ cd offline-other
$ git log --oneline --decorate --all
1e42a2d (HEAD, origin/master, master) Calculate pi with more digits
ead7de4 Adds checker for prime number
337bfd0 Adds math, pi calculation
7229805 Offline sharing, patch, bundle and archive
该仓库如预期包含原始仓库中master
分支的所有历史记录。现在我们可以创建第二个捆绑包,跟前面的示例一样,它包含从我们创建的标签(bundleForOtherRepo
)到develop
分支顶端的历史记录:
$ cd ..
$ cd Git-Version-Cookbook-Second-Edition_offline-sharing
$ git bundle create myrepo.bundle bundleForOtherRepo..develop
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.47 KiB | 0 bytes/s, done.
Total 9 (delta 2), reused 0 (delta 0)
$ git bundle verify myrepo.bundle
The bundle contains this ref:
c131c8bb2bf8254e46c013bfb33f4a61f9d4b40e refs/heads/develop
The bundle requires this ref:
ead7de45a504ee19cece26daf45d0184296f3fec
myrepo.bundle is okay
正如我们在前面的示例中看到的那样,捆绑包要求ead7de45a504ee19cece26daf45d0184296f3fec
提交已经存在于我们将使用该捆绑包的仓库中。让我们通过以下命令检查我们从第一个捆绑包创建的仓库是否包含此提交:
$ cd ..
$ cd offline-other
$ git show -s ead7de45a504ee19cece26daf45d0184296f3fec
commit ead7de45a504ee19cece26daf45d0184296f3fec
Author: John Doe <john.doe@example.com>
Date: Wed Apr 9 21:28:51 2014 +0200
Adds checker for prime number
提交存在。现在我们可以使用新的捆绑包文件,它与我们创建的第一个捆绑包具有相同的文件名和路径。我们可以在offline-other
仓库中使用git fetch
命令,如下所示:
$ git fetch
Receiving objects: 100% (9/9), done.
Resolving deltas: 100% (2/2), done.
From /path/to/repo/offline-sharing/myrepo.bundle
* [new branch] develop -> origin/develop
现在我们可以checkout develop
,并验证develop
和master
分支的历史记录是否与原始仓库中的一致:
$ git checkout develop
Branch develop set up to track remote branch develop from origin by rebasing.
Switched to a new branch 'develop'
$ gitk --all
上一个命令输出如下:
还有更多...
捆绑包对于在无法使用正常传输机制的机器上更新仓库历史记录非常有用,这些机器之间可能没有网络连接,防火墙规则等。当然,除了 Git 捆绑包外,还有其他方法可以将历史记录传输到远程机器。也可以使用 U 盘上的裸仓库,甚至可以将普通的补丁应用到仓库。Git 捆绑包的优点是,你不必每次更新远程时都将整个历史记录写入裸仓库,而只需要写入缺失的历史部分。
从树形结构创建归档
有时候,获取某个特定提交指定的目录结构快照而没有相应历史记录是很有用的。当然,这可以通过检出特定的提交后删除/省略.git
文件夹来创建归档。但使用 Git,有一种更好的方法来做到这一点,它是内建的,因此可以从特定提交或引用创建归档。当使用 Git 创建归档时,你还确保归档仅包含 Git 跟踪的文件,而不会包含工作目录中可能存在的任何未跟踪文件或文件夹。
准备就绪
我们将使用本章前面示例中使用的相同的offline-sharing
仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_offline-sharing.git
$ cd Git-Version-Control-Cookbook-Second-Edition_offline-sharing
如何执行...
我们将首先创建master
分支上最新提交的目录结构归档。默认情况下,offline-sharing
仓库是检出的develop
分支,因此我们将使用origin/master
来指定归档的引用:
$ git archive --prefix=offline/ -o offline.zip origin/master
--prefix
选项会将指定的前缀添加到归档中的每个文件前,实际上是为仓库中的文件添加一个 offline
目录作为根目录,而 -o
选项告诉 Git 将归档文件创建为 offline.zip
,当然,它会以 ZIP 格式进行压缩。我们可以检查 ZIP 归档,查看文件是否包含以下内容:
$ unzip -l offline.zip
Archive: offline.zip
1e42a2dfa3a377d412efd27a77b973c75935c62a
Length Date Time Name
-------- ---- ---- ----
0 04-10-14 09:19 offline/
162 04-10-14 09:19 offline/README.md
485 04-10-14 09:19 offline/another_pi.c
672 04-10-14 09:19 offline/math.c
-------- -------
1319 4 files
如果我们查看 Git 仓库中的 origin/master
提交,可以看到文件是相同的;-l
选项告诉 Git 指定每个文件的大小,如下所示:
$ git ls-tree -l origin/master
100644 blob c79cad47938a25888a699142ab3cdf764dc99193 162 README.md
100644 blob 86df41b3a8bbfb588e57c7b27742cf312ab3a12a 485 another_pi.c
100644 blob d393b41eb14561e583f1b049db716e35cef326c3 672 math.c
还有更多...
archive
命令也可以用于为仓库的子目录创建归档。我们可以在仓库的 doc
分支上使用此命令,将 Documentation
文件夹的内容打包成 ZIP 文件:
$ git archive --prefix=docs/ -o docs.zip origin/doc:Documentation
我们可以列出 ZIP 文件和 origin/doc
中的 Documentation
树,如下所示:
$ unzip -l docs.zip
Archive: docs.zip
Length Date Time Name
-------- ---- ---- ----
0 04-13-14 21:14 docs/
99 04-13-14 21:14 docs/README.md
152 04-13-14 21:14 docs/build.md
-------- -------
251 3 files
$ git ls-tree -l origin/doc:Documentation
100644 blob b65b4fc78c0e39b3ff8ea549b7430654d413159f 99 README.md
100644 blob f91777f3e600db73c3ee7b05ea1b7d42efde8881 152 build.md
除了 ZIP 格式外,归档还有其他格式选项,例如 tar
、tar.gz
等。可以使用 --format=<format>
选项指定格式,或者通过 -o
选项将格式作为输出文件名的后缀。以下两个命令将产生相同的输出文件:
$ git archive --format=tar.gz HEAD > offline.tar.gz
$ git archive -o offline.tar.gz HEAD
如果传递一个提交/标签 ID 或树 ID 作为标识符,Git 归档命令的行为会有所不同。如果给定了提交或标签 ID,该 ID 会被存储在 TAR 格式的全局扩展 pax 头中,或者作为文件评论存储在 ZIP 格式中。如果只给定了树 ID,则不会存储额外的信息。实际上,你可以在之前的示例中看到这一点,第一 ID 是作为分支引用的。由于该分支指向一个提交,该提交的 ID 被写为文件的注释,我们实际上可以在归档列出的输出中看到它:
$ unzip -l offline.zip
Archive: offline.zip
1e42a2dfa3a377d412efd27a77b973c75935c62a
Length Date Time Name
-------- ---- ---- ----
0 04-10-14 09:19 offline/
162 04-10-14 09:19 offline/README.md
485 04-10-14 09:19 offline/another_pi.c
672 04-10-14 09:19 offline/math.c
-------- -------
1319 4 files
在第二个示例中,我们也传递了一个分支作为参考,此外,我们指定了 Documentation
文件夹作为我们希望从中创建归档的子文件夹。这相当于将树的 ID 传递给归档命令,因此,归档中不会存储额外的信息。
第十一章:提示与技巧
本章中,我们将介绍以下内容:
-
使用 git stash
-
保存和应用存储的更改
-
使用 git bisect 进行调试
-
使用 blame 命令
-
在提示符中为 UI 着色
-
自动补全
-
带有状态信息的 Bash 提示符
-
更多别名
-
交互式添加
-
使用 Git gui 进行交互式添加
-
忽略文件
-
显示和清理被忽略的文件
介绍
在本章中,你会发现一些在日常 Git 工作中非常有用的提示和技巧;从在重要任务被打断时存储更改,到通过 bisect
和 blame
高效调试,再到在提示符中查看颜色和状态信息。我们还将了解别名,如何选择要包含在提交中的行,从而创建干净的提交,最后,我们将介绍如何忽略文件。
使用 git stash
在这个例子中,我们将探索 git stash
命令,并学习如何使用它快速将未提交的更改存储起来,并在需要时重新获取。这在你被紧急任务打断时很有用,而此时你还不准备提交当前工作目录中的工作。使用 git stash
命令,你可以保存当前工作目录(有或没有暂存区)的状态,并将工作树恢复到干净的状态。
准备工作
在这个例子中,我们将使用 Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
仓库。我们将使用 master
分支,但在尝试 stash
命令之前,我们需要在工作目录和暂存区中创建一些更改,方法如下:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks $ git checkout master
对 foo
进行一些更改并将它们添加到暂存区,方法如下:
$ echo "Just another unfinished line" >> foo
$ git add foo
对 bar
进行一些更改并创建一个新文件:
$ echo "Another line" >> bar
$ echo "Some content" > new_file
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: foo
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: bar
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file
我们可以看到有一个文件被添加到暂存区,foo
,一个文件被修改,bar
,以及一个未跟踪的文件在工作区中,new_file
。
如何操作…
在我们仓库的前述状态下,我们可以将更改存储起来,以便去处理其他事务。基本命令会将暂存区的更改和对已跟踪文件的更改存储起来。它会将未跟踪的文件留在工作目录中:
$ git stash
Saved working directory and index state WIP on master: d611f06 Update foo and bar
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file
nothing added to commit but untracked files present (use "git add" to track)
现在我们可以处理其他事务,创建并提交这些更改。我们将更改 foo
文件的第一行,并创建一个包含此更改的提交:
# MacOS (BSD sed):
$ sed -i '' 's/First line/This is the very first line of the foo file/' foo
# Linux (GNU sed):
$ sed 's/First line/This is the very first line of the foo file/' foo $ git add foo $ git commit -m "Update foo" [master fa4b595] Update foo 1 file changed, 1 insertion(+), 1 deletion(-)
我们可以使用 git stash list
命令查看当前存储的更改:
$ git stash list
stash@{0}: WIP on master: b6dabd7 Update foo and bar
要获取我们之前存储的更改,可以从 stash
栈中弹出它们,方法如下:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file
nothing added to commit but untracked files present (use "git add" to track)
$ git stash pop
Auto-merging foo
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: bar
modified: foo
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (733703568b7dcf2a0d5e4db5957d351417bcd793)
现在,存储的更改可以在工作仓库中再次使用,并且 stash
条目已被删除。请注意,这些更改仅应用于工作目录,尽管在我们创建 stash
时其中一个文件已被暂存。
它是如何工作的…
我们已创建了两个提交:一个是索引提交,一个是工作区提交。在 gitk
中,我们可以看到 stash
创建的提交,以将更改存储起来(gitk stash
),如下图所示:
我们还可以查看我们创建提交后的分支状态(gitk --reflog
),如下图所示:
Git 实际上会在refs/stash
命名空间下创建两个提交。一个提交包含暂存区的内容,这个提交被称为index on master
。另一个提交是工作目录中的进行中的工作,WIP on master
。当 Git 通过创建提交来暂存更改时,它可以使用正常的解析方法将暂存的更改应用回工作目录。这意味着如果在应用 stash 时出现冲突,你需要以常规方式解决冲突。
还有更多内容...
在前面的示例中,我们只看到了stash
命令的基本用法,保存了对未追踪文件和添加到暂存区的更改。也可以在stash
命令中包含未追踪的文件。可以通过--include-untracked
选项来实现。我们可以将foo
添加到暂存区;首先,使它与我们之前创建stash
时的状态相同,然后创建一个包含未追踪文件的stash
:
$ git add foo
$ git stash --include-untracked
Saved working directory and index state WIP on master: 691808e Update foo
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working directory clean
现在,我们可以看到new_file
已从工作目录中消失。它已包含在 stash 中,我们可以通过 Gitk 来检查这一点。它会显示为另一个未追踪文件的提交:
$ gitk master stash
Gitk 显示包含未追踪文件的 stash:
我们还可以确保,在应用stash
后,暂存区中添加的更改会重新添加回暂存区,这样我们就能恢复到暂存更改前的完全相同状态:
$ git stash pop --index
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: foo
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: bar
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file
Dropped refs/stash@{0} (ff331af57406948619b0671dab8b4f39da1e8fa2)
也可以只保存工作目录中的更改,同时保留暂存区中的更改。我们可以仅为已追踪的文件执行此操作,或通过暂存未追踪文件(--include-untracked
)来实现,方法如下:
$ git stash --keep-index --include-untracked
Saved working directory and index state WIP on master: 00dd8f8 Update foo
HEAD is now at 00dd8f8 Update foo
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: foo
保存和应用暂存
在保存工作时,我们可以轻松地将多个状态暂存。但是,默认的暂存名称并不总是很有帮助。在这个示例中,我们将看到如何保存 stash 并为其命名,以便在列出 stash 内容时容易识别。我们还将学习如何应用 stash,而不将其从 stash 列表中删除。
准备开始
我们将使用与前面示例相同的仓库,继续从我们离开时的状态:
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: foo
$ git stash list
stash@{0}: WIP on master: 4447f69 Update foo
如何操作...
要将当前状态保存到一个带有描述的暂存区,以便我们在稍后的时间能够记住,可以使用以下命令:
$ git stash save 'Updates to foo'
Saved working directory and index state On master: Updates to foo
我们的stash
列表现在如下所示:
$ git stash list
stash@{0}: On master: Updates to foo
stash@{1}: WIP on master: 2302181 Update foo
我们可以更改bar
并创建一个新的stash
:
$ echo "Another change" >> bar
$ git stash save 'Made another change to bar'
Saved working directory and index state On master: Made another change to bar
$ git stash list
stash@{0}: On master: Made another change to bar
stash@{1}: On master: Updates to foo
stash@{2}: WIP on master: 2302181 Update foo
我们可以将 stash 应用回工作树(并通过--index
选项应用到暂存区),而不将其从stash
列表中删除:
$ git stash apply 'stash@{1}'
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: foo
no changes added to commit (use "git add" and/or "git commit -a")
$ git stash apply --quiet 'stash@{0}'
$ git stash list
stash@{0}: On master: Made another change to bar
stash@{1}: On master: Updates to foo
stash@{2}: WIP on master: 2302181 Update foo
存储仍然保留在stash
列表中,可以按任何顺序应用,并通过stash@{stash-no}
语法进行引用。--quiet
选项在应用存储后会抑制状态输出。
还有更多内容...
对于使用git stash apply
应用的存储,stash
需要通过git stash drop
删除:
$ git stash drop 'stash@{1}'
Dropped stash@{1} (e634b347d04c13fc0a0d155a3c5893a1d3841fcd)
$ git stash list
stash@{0}: On master: Made another change to bar
stash@{1}: WIP on master: 1676cdb Update foo
通过使用stash apply
将存储保持在stash
列表中,并通过git stash drop
明确删除它们,相比直接使用stash pop
有一些优势。当使用pop
选项时,如果存储能够成功应用,它会自动从列表中删除。但是如果应用失败并进入冲突解决模式,应用的存储不会从列表中删除,而是仍然保留在stash
堆栈中。这样可能会导致后续错误地使用未被删除的存储,因为它被误认为已被移除。通过始终使用git stash apply
和git stash drop
,你可以避免这种情况。
git stash
命令也可以用来将调试信息应用到一个应用程序中。假设你正在进行 bug 寻找,并且在代码中添加了很多调试语句以追踪 bug。与其删除所有这些调试语句,不如将它们保存为 Git 的stash
:
$ git stash save "调试信息存储"
然后,如果以后需要调试语句,你只需应用存储,你就可以准备好进行调试了。
使用 git bisect 进行调试
git bisect
命令是一个极好的工具,用于找出哪个提交导致了仓库中的 bug。这个工具特别有用,尤其是在你查看包含 bug 的长提交列表时。bisect
命令通过执行二分查找来查找引入 bug 的提交,从而尽可能快速地找到这个提交。二分查找法,也叫做折半查找法,是一种搜索方法,算法通过比较给定的关键字与已排序数组的中间值来定位该关键字。在每一步中,如果它们匹配,则返回该位置。否则,算法会根据中间值是否大于或小于关键字,选择继续在中间值左侧或右侧的子数组中查找。在 Git 上下文中,提交历史中的提交列表相当于要测试的数组,而关键字则是测试某个提交时代码是否能成功编译。二分查找算法的时间复杂度为 O(log n)。
准备工作
我们将使用与上一个示例中相同的仓库,但从一个干净的状态开始:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git checkout bug_hunting
bug_hunting
分支自master
分支分出后包含了 23 次提交。我们知道bug_hunting
分支的最新提交包含了这个 bug,并且它是在某个提交中引入的,因为它是从master
分支分出的。这个 bug 是在以下提交中引入的:
commit 83c22a39955ec10ac1a2a5e7e69fe7ca354129af
Author: HAL 9000 <John.Doe@example.com>
Date: Tue May 13 09:53:45 2014 +0200
Bugs...
在map.txt
文件中,位于澳大利亚中部的地方清楚地显示了这个 bug。以下是文件中的片段,展示了这个 bug:
现在,我们只需要某种方式来重现/检测这个错误,以便我们能够测试不同的提交。例如,可以是编译代码、运行测试等。
在这个示例中,我们将创建一个测试脚本来检查代码中的错误(在这个示例中,简单的grep
命令查找oo
应该可以;你可以自己试试看,是否能在map.txt
文件中找到错误):
$ echo "! grep -q oo map.txt" > ../test.sh
$ chmod +x ../test.sh
最好在仓库外创建这个测试脚本,以防止在仓库中的检出、编译等操作之间的相互作用。
如何操作...
要开始二分查找,我们只需输入以下命令:
$ git bisect start
要将当前提交(HEAD -> bug_hunting
)标记为坏,我们输入以下命令:
$ git bisect bad
我们还想将最后一个已知的好提交(master
)标记为好:
$ git bisect good master
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[9d2cd13d4574429dd0dcfeeb90c47a2d43a9b6ef] Build map part 11
这次,发生了点事情。Git 检出了9d2cd13
,它希望我们测试并将其标记为好或坏。它还告诉我们,在此之后还有 11 个修订需要测试,大约需要四个步骤才能完成。这就是二分查找算法的工作原理:每次标记一个提交为好或坏时,Git 会checkout
介于刚标记的提交和当前提交之间的那个相反值的提交。通过这种方式,Git 会快速缩小需要检查的提交数量。它还知道大约需要四步,这很合理,因为剩下 11 个修订,最多需要 *log2
[c45cb51752a4fe41f52d40e0b2873350b95a9d7c] Build map part 16
测试将提交标记为好,然后 Git 会检查下一个需要标记的提交,直到我们找到引入错误的提交:
$ ../test.sh; test $? -eq 0 && git bisect good || git bisect bad
git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[83c22a39955ec10ac1a2a5e7e69fe7ca354129af] Bugs...
$ ../test.sh; test $? -eq 0 && git bisect good || git bisect bad
git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[670ab8c42a6cb1c730c7c4aa0cc26e5cc31c9254] Build map part 13
$ ../test.sh; test $? -eq 0 && git bisect good || git bisect bad
git bisect good
83c22a39955ec10ac1a2a5e7e69fe7ca354129afis the first bad commit
commit 83c22a39955ec10ac1a2a5e7e69fe7ca354129af
Author: HAL 9000 aske.olsson@switch-gears.dk
Date: Tue May 13 09:53:45 2014 +0200
Bugs...
:100644 100644 8a13f6bd858aefb70ea0a7d8f601701339c28bb0 1afeaaa370a2e4656551a6d44053ee0ce5c3a237 M map.txt
四步之后,Git 已经确认`83c22a3`提交是第一个坏提交。我们可以结束`bisect`会话并仔细查看该提交:
$ git bisect reset
Previous HEAD position was 670ab8c... Build map part 13
Switched to branch 'bug_hunting'
Your branch is up-to-date with 'origin/bug_hunting'.
$ git show 83c22a39955ec10ac1a2a5e7e69fe7ca354129af
commit 83c22a39955ec10ac1a2a5e7e69fe7ca354129af
Author: HAL 9000 john.doe@example.com
Date: Tue May 13 09:53:45 2014 +0200
Bugs...
diff --git a/map.txt b/map.txt
index 8a13f6b..1afeaaa 100644
--- a/map.txt
+++ b/map.txt
@@ -34,6 +34,6 @@ Australia:
.-./ |. : :,
/ '-._/ _
/ '
- .' *: Brisbane
- .-' ;
- | |
+ .' __/ *: Brisbane
+ .-' (oo) ;
+ | //||\ |
很明显,这个提交引入了错误。
以下注释截图展示了`bisect`会话中所采取的步骤:

请注意,二分查找算法实际上在第三步就找到了错误的提交,但它需要进一步检查,以确保这个提交不仅仅是错误提交的子提交,确实是引入错误的提交。
# 还有更多...
不需要手动执行所有二分查找步骤,可以通过将一个脚本、makefile 或测试传递给 Git 来自动执行这些步骤。脚本需要在每次提交时退出时返回**零状态**来标记提交为好提交,返回**非零状态**来标记提交为坏提交。我们可以使用在本章开头创建的`test.sh`脚本来实现这一点。首先,我们设置好好提交和坏提交:
$ git bisect start HEAD master
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[9d2cd13d4574429dd0dcfeeb90c47a2d43a9b6ef] Build map part 11
然后,我们告诉 Git 运行`test.sh`脚本,并自动标记提交:
$ git bisect run ../test.sh running ../test.sh Bisecting: 5 revisions left to test after this (roughly 3 steps) [c45cb51752a4fe41f52d40e0b2873350b95a9d7c] Build map part 16 running ../test.sh Bisecting: 2 revisions left to test after this (roughly 2 steps) [83c22a39955ec10ac1a2a5e7e69fe7ca354129af] Bugs... running ../test.sh Bisecting: 0 revisions left to test after this (roughly 1 step) [670ab8c42a6cb1c730c7c4aa0cc26e5cc31c9254] Build map part 13 running ../test.sh 83c22a39955ec10ac1a2a5e7e69fe7ca354129afis the first bad commit commit 83c22a39955ec10ac1a2a5e7e69fe7ca354129af Author: HAL 9000 john.doe@example.com Date: Tue May 13 09:53:45 2014 +0200 Bugs... :100644 100644 8a13f6bd858aefb70ea0a7d8f601701339c28bb0 1afeaaa370a2e4656551a6d44053ee0ce5c3a237 M map.txt bisect run success
Git 找到了相同的提交,我们现在可以退出二分查找会话:
$ git bisect reset
Previous HEAD position was 670ab8c... Build map part 13
Switched to branch 'bug_hunting'
# 使用`blame`命令
`bisect`命令在你不知道代码中哪个地方有 bug,但可以测试它并因此找到引入 bug 的提交时非常有用。如果你已经知道 bug 出现在代码的哪个地方,但想找到引入 bug 的提交,你可以使用`git blame`。`blame`命令会在文件中的每一行旁边标注最近修改该行的提交,帮助你轻松找到提交 ID,并查看提交的完整上下文。
# 准备工作
我们将使用与 bisect 示例中相同的仓库和分支:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks $ git checkout bug_hunting
# 如何操作...
我们知道 bug 出现在`map.txt`的第 37 到第 39 行。为了标注文件中每一行的提交 ID 和作者,我们将对该文件运行`git blame`。我们还可以使用`-L <from>,<to>`选项将搜索限制在特定行,如下图所示:

从输出中可以清楚地看到,由`HAL 9000`提交的 ID 为`83c22a39`的提交引入了这个 bug。
# 还有更多...
即使文件已经重构且代码已被移动,`blame`命令仍然可以使用。通过`-M`选项,`blame`命令可以检测文件中已被移动的行;而使用`-C`选项,Git 可以检测在同一提交中从其他文件移动或复制过来的行。如果使用`-C`选项三次`-CCC`,`blame`命令将能够找到在任何提交中从其他文件复制的行。
# 在提示符中为 UI 着色
默认情况下,Git 在终端中显示信息时没有颜色。然而,显示颜色是 Git 的一项功能,只需配置一下即可启用。
# 准备工作
我们将使用`Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks`仓库:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
# 如何操作...
首先,我们将编辑并添加`foo`:
$ echo "And another line" >> foo
$ git add foo
继续更改`foo`,但不要将其添加到暂存区:
$ echo "Last line ...so far" >> foo
创建一个名为`test`的新文件:
$ touch test
`git status`命令将显示我们的状态:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD
modified: foo
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: foo
Untracked files:
(use "git add
test
我们可以将`color.ui`配置设置为`auto`或`true`,这样在需要时,UI 会显示颜色:
$ git config --global color.ui true
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD
modified: foo
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: foo
Untracked files:
(use "git add
test
# 还有更多...
`color.ui`配置适用于广泛的 Git 命令,包括`diff`、`log`和`branch`。以下是将`color.ui`设置为`true`时的`git log`示例:
$ git log --oneline --decorate --graph
- c111003 (HEAD -> master, origin/master, origin/HEAD) Update foo and bar
- 270e97b Add bar
- 43fd490 Initial commit, adds foo
# 自动补全
Git 内置支持`bash`和`zsh` Shell 的 Git 命令自动补全功能。如果你使用这两种 Shell 中的任何一种,你可以启用自动补全功能,并使用`<tab>`选项来帮助你完成命令。
# 准备工作
通常,自动补全功能会随 Git 安装包一起分发,但在某些平台或发行版上并未默认启用。要启用该功能,我们需要找到与 Git 安装一起分发/安装的`git-completion.bash`文件。
# Linux
对于 Linux 用户,位置可能会根据发行版的不同而有所不同。通常,该文件可以在`/etc/bash_completion.d/git-completion.bash`找到。
# Mac
对于 Mac 用户,通常可以在 `/Library/Developer/CommandLineTools/usr/share/git-core/git-completion.bash` 找到。
如果你是通过 Homebrew 安装的 Git,它可以在 `/usr/local/Cellar/git/2.15.0/etc/bash_completion.d/git-completion.bash` 找到。
# Windows
使用 **Msysgit** 在 Windows 上安装时,Git Bash Shell 已经启用了补全功能。
如果你在系统中找不到该文件,可以从 [`github.com/git/git/blob/master/contrib/completion/git-completion.bash`](https://github.com/git/git/blob/master/contrib/completion/git-completion.bash) 获取最新版本,并将其安装到你的主目录中。
# 如何操作...
要启用补全功能,需要对补全文件运行 `source` 命令,你可以通过将以下几行添加到 `.bashrc` 或 `.zshrc` 文件来实现,具体取决于你的 Shell 和 Git 补全文件的位置:
if [ -f /etc/bash_completion.d/git-completion.bash ]; then
source /etc/bash_completion.d/git-completion.bash
fi
# 工作原理...
现在你已经准备好尝试这个了。切换到一个现有的 Git 仓库,例如 `cookbook-tips-tricks`,然后输入以下命令:
$ git che
checkout cherry cherry-pick
你可以再输入一次 `c<tab>`,命令会自动补全为 `checkout`。但是补全功能不仅仅补全命令,它也能帮助你补全分支名等等。这意味着你可以继续执行 checkout 并输入 `mas<tab>`。你应该能看到输出自动补全为 `master` 分支,除非你在一个有多个以 `mas` 开头的分支的仓库中。
# 还有更多...
补全功能也适用于选项。这在你记不清确切选项,但记得一些内容时非常有用,例如在使用 `git branch` 时:
git branch --
--abbrev= --merged --set-upstream-to=
--color --no-abbrev --track
--contains --no-color --unset-upstream
--edit-description --no-merged --verbose
--list --no-track
# 带有状态信息的 Bash 提示符
Git 提供的另一个酷炫功能是,如果当前工作目录是一个 Git 仓库,提示符会显示状态信息。
# 准备工作
为了使状态信息提示符正常工作,我们还需要加载另一个文件,`git-prompt.sh`,该文件通常与 Git 安装一起分发,并位于与完成文件相同的目录中。
# 如何操作...
在你的 `.bashrc` 或 `.zshrc` 文件中,添加以下代码片段,具体取决于你的 Shell 和 `git-prompt.sh` 文件的位置:
if [ -f /etc/bash_completion.d/git-prompt.sh ]; then
source /etc/bash_completion.d/git-prompt.sh
fi
# 工作原理...
为了使用命令提示符,我们必须修改 `PS1` 变量;通常它设置为类似以下内容:
PS1='u@h:w$ '
上述命令会显示当前用户、一个 `@` 符号、主机名、相对于用户主目录的当前工作目录,最后是 `$` 字符:
john.doe@yggdrasil:~/cookbook-tips-tricks$
我们可以通过将 `$(__git_ps1 " (%s)")` 添加到 `PS1` 变量中,来修改它,使其在工作目录后面添加分支名:
PS1='u@h:w$(__git_ps1 " (%s)") $ '
现在我们的提示符看起来是这样的:
john.doe@yggdrasil:~/cookbook-tips-tricks (master) $
也可以显示工作树、索引等的状态。我们可以通过在 `.bashrc` 文件中导出一些环境变量来启用这些功能,`git-prompt.sh` 会自动获取这些变量。
可以设置以下环境变量:
| **变量** | **值** | **效果** |
| --- | --- | --- |
| `GIT_PS1_SHOWDIRTYSTATE` | 非空 | 显示 `*` 代表未暂存的更改,显示 `+` 代表已暂存的更改。 |
| `GIT_PS1_SHOWSTASHSTATE` | 非空 | 如果有暂存内容,则显示`| **变量** | **值** | **效果** |
| --- | --- | --- |
| `GIT_PS1_SHOWDIRTYSTATE` | 非空 | 显示 `*` 代表未暂存的更改,显示 `+` 代表已暂存的更改。 |
字符。 |
| `GIT_PS1_SHOWUNTRACKEDFILES` | 非空 | 如果仓库中有未跟踪的文件,则显示 `%` 字符。 |
| `GIT_PS1_SHOWUPSTREAM` | autoverbosenamelegacyGitsvn | 自动显示是否落后于(`<`)或领先于(`>`)上游分支。如果分支已经分歧,将显示 `<>`,如果是最新的,则显示 `=`。Verbose 显示落后/领先的提交数量。Name 显示上游的名称。Legacy 是旧版本 Git 的详细信息。Git 比较 `HEAD` 和 `@{upstream}`。SVN 比较 `HEAD` 和 `svn upstream`。 |
| `GIT_PS1_DESCRIBE_STYLE` | containsbranchdescribedefault | 当处于分离的 `HEAD` 状态时,显示额外的信息。Contains 表示相对于更新的注释标签(`v1.6.3.2~35`)。Branch 表示相对于更新的标签或分支(`master~4`)。Describe 表示相对于较旧的注释标签(`v1.6.3.1-13-gdd42c2f`)。Default 是完全匹配的标签。 |
让我们尝试在 `~/.bashrc` 文件中设置一些变量:
export GIT_PS1_SHOWUPSTREAM=auto
export GIT_PS1_SHOWDIRTYSTATE=enabled
PS1='u@h:w$(__git_ps1 " (%s)") $ '
让我们看看 `~/.bashrc` 文件的实际应用:
john.doe@yggdrasil:~ $ cd tips_and_tricks/
john.doe@yggdrasil:~/tips_and_tricks (master=) $ touch test
john.doe@yggdrasil:~/tips_and_tricks (master=) $ git add test
john.doe@yggdrasil:~/tips_and_tricks (master +=) $ echo "Testing" > test
john.doe@yggdrasil:~/tips_and_tricks (master *+=) $ git commit -m "test"
[master 5c66d65] test
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 test
john.doe@yggdrasil:~/tips_and_tricks (master *>) $
使用 `__git_ps1` 选项时,Git 还会在合并、变基、二分搜索等操作时显示信息。这非常有用,很多 `git status` 命令突然变得不再必要,因为您可以直接在提示符中看到这些信息。
# 还有更多内容…
现在的终端没有一些颜色怎么行?`git-prompt.sh` 脚本也支持这个功能。我们需要做的就是将 `GIT_PS1_SHOWCOLORHINTS` 变量设置为非空值,并且不再使用 `PS1`,而是使用 `PROMPT_COMMAND`。让我们修改 `~/.bashrc`:
export GIT_PS1_SHOWUPSTREAM=auto
export GIT_PS1_SHOWDIRTYSTATE=enabled
export GIT_PS1_SHOWCOLORHINTS=enabled
PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "$ "'
如果我们重新执行与之前相同的场景,结果如下:

# 另见
如果您使用的是 `zsh`,或者想尝试一些具有许多功能的新东西,比如自动补全、Git 支持等,您应该看看 `oh-my-zsh` 框架,可以在 `zsh` 上使用,网址:[`github.com/robbyrussell/oh-my-zsh`](https://github.com/robbyrussell/oh-my-zsh)。
# 更多别名
在第二章《配置》中,我们看到了如何创建别名,并且查看了一些例子。在本节中,我们将查看更多有用的别名示例。
# 准备开始
我们将克隆 `cookbook-tips-tricks` 仓库并检出 `aliases` 分支:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git checkout aliases
# 如何实现...
在这里,我们将看到一些别名的示例,每个别名都附带简短的描述和使用示例。这些别名仅适用于本地仓库;使用 `--global` 可以将它们应用于所有仓库。
1. 让我们从一个别名开始,它只显示当前分支:
$ git config alias.b "rev-parse --abbrev-ref HEAD"
$ git b
aliases
1. 要显示带有颜色的紧凑型图形历史视图,以下别名将为您节省许多按键:
$ git config alias.graph "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"
$ git graph origin/conflict aliases
以下截图显示了一个典型的输出,其中提交记录以红色显示,提交者以蓝色显示,等等:

1. 在解决冲突合并时,获取冲突/未合并文件的列表是很有用的:
$ git config alias.unmerged '!git ls-files --unmerged | cut -f2 | sort -u'
1. 我们可以通过合并`origin/conflict`分支来看到之前别名的实际效果:
$ git merge origin/conflict
Auto-merging spaceship.txt
CONFLICT (content): Merge conflict in spaceship.txt
Automatic merge failed; fix conflicts and then commit the result.
1. 首先,检查`git status`的输出:
$ git status
On branch aliases
Your branch is up-to-date with 'origin/aliases'.
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add
both modified: spaceship.txt
no changes added to commit (use "git add" and/or "git commit -a")
1. 我们在输出中看到了未合并的路径。让我们使用`unmerged`别名来获取一个简单的未合并文件列表:
$ git unmerged
spaceship.txt
1. 你可以按如下方式中止合并:
$ git merge --abort
1. 在工作日中,你会多次输入`git status`。添加一个简短的状态命令会非常有帮助:
$ git config alias.st "status"
$ git st
On branch aliases
Your branch is up-to-date with 'origin/aliases'.
nothing to commit, working directory clean
1. 甚至可以定义一个更短的状态命令,显示分支和文件信息:
$ git config alias.s 'status -sb'
1. 要尝试它,首先修改`foo`并创建一个未跟踪的`test`文件:
$ touch test
$ echo testing >> foo
1. 接下来,试试你新的`s`别名:
$ git s
aliases...origin/aliases
M foo
?? test
1. 通常,你只想显示最新的提交及一些统计信息:
$ git config alias.l1 "log -1 --shortstat"
$ git l1
commit a43eaa9b461e811eeb0f18cce67e4465888da333
Author: John Doe john.doe@example.com
Date: Wed May 14 22:46:32 2014 +0200
Better spaceship design
1 file changed, 9 insertions(+), 9 deletions(-)
1. 但有时,你可能需要更多的上下文。以下别名与之前的相同,但会显示五个最新提交的情况(输出未显示):
$ git config alias.l5 "log -5 --decorate --shortstat"
1. 使用以下别名可以显示更改文件的统计信息和颜色化的提交列表:
$ git config alias.ll 'log --pretty=format:"%C(yellow)%h%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset %Cred%d%Creset" --numstat'
$ git ll -5
正如下一张截图所示,提交者用蓝色显示,他们的提交时间用绿色显示,等等:

1. 如果你在多个仓库中工作,记住`upstream/tracking`分支可能会让你感到困惑。以下别名是显示该分支的简写:
$ git config alias.upstream "rev-parse --symbolic-full-name --abbrev-ref=strict HEAD@{u}"
$ git upstream
origin/aliases
1. 你可以使用`details`别名查看 ID/SHA-1(提交、标签、树、blob)的详细信息。虽然你节省了很多按键,但`details`更容易记住:
$ git config alias.details "cat-file -p"
$ git details HEAD
tree bdfdaacbb29934b239db814e599342159c4390dd
parent 8fc1819f157f2c3c25eb973c2a2a412ef3d5517a
author John Doe john.doe@example.com 1400100392 +0200
committer John Doe john.doe@example.com 1400100392 +0200
Better spaceship design
1. 仓库会不断增长,目录树也会变得庞大。你可以使用以下别名显示到达仓库根目录所需的`cd-ups`和`../`的数量,这在 shell 脚本中非常有用:
$ git config alias.root "rev-parse --show-cdup"
$ cd sub/directory/example
$ pwd
/path/to/cookbook-tips-tricks/sub/directory/example
$ git root
../../../
$ cd \((git root)
\) pwd
/path/to/cookbook-tips-tricks
1. 可以使用以下别名轻松查看文件系统中仓库的路径:
$ git config alias.path "rev-parse --show-toplevel"
$ git path
/path/to/cookbook-tips-tricks
1. 如果我们需要放弃索引、工作区中的所有更改,并且可能还需要放弃提交记录,然后将工作区重置为已知状态(提交 ID),但我们不想触及未跟踪的文件,我们只需要一个仓库状态的`ref`,例如`HEAD`:
$ git config alias.abandon "reset --hard"
$ echo "new stuff" >> foo
$ git add foo
$ echo "other stuff" >> bar
$ git s
aliases...origin/aliases
M bar
M foo
?? test
$ git abandon HEAD
$ git s
aliases...origin/aliases
?? test
# 交互式添加
Git 提供的暴露暂存区有时会导致困惑,尤其是在添加一个文件、稍微更改它,然后再添加该文件以便提交第一次添加后的更改时。尽管每次进行小改动后添加文件看起来有点麻烦,但你可以暂存和取消暂存更改,这是一个很大的优势。使用`git add`命令,你甚至可以仅将文件的部分更改添加到暂存区。这在你对文件做了很多修改时尤其方便,例如,你可能希望将更改拆分成修复 bug、重构和功能开发等部分。这个例子将展示你如何轻松做到这一点。
# 准备就绪
再次,我们将使用`Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks` 仓库。克隆它并查看交互式分支:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git checkout interactive
# 如何操作...
首先,我们需要添加一些更改;我们通过重置最新的提交来做到这一点:
$ git reset 'HEAD^'
Unstaged changes after reset:
M liberty.txt
现在我们有一个已修改的文件。要开始交互式添加,我们可以运行`git add -i`或`git add -p`文件名。`-i`选项会显示一个界面,可以逐个交互式地添加处于修改状态的不同文件。`add -p/--patch`选项更简单,直接让你选择要添加的文件的部分内容:
$ git add -p liberty.txt
diff --git a/liberty.txt b/liberty.txt
index 8350a2c..9638930 100644
--- a/liberty.txt
+++ b/liberty.txt
@@ -8,6 +8,13 @@
WW) ,WWW)
7W),WWWW'
'WWWWWW'
-
9---W)
-
,,--WPL=YXW===
-
(P),CY:,I/X'F9P
-
WUT===---/===9)
-
-HP+----Y(C=9W)
-
'9Y3'-'-OWPT-
-
'WWLUIECW (:7L7C7' ,P--=YWFL Y-=:9)UW:L
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]?
Git 会询问你是否想要暂存之前的更改(hunk),同时还会显示很多选项,如果你输入`?`,这些选项可以展开一些:
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk nor any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk nor any of the later hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
有很多选项,但借助帮助文本,它们非常易于理解。让我们添加当前的 hunk,按`y`,然后查看下一个:
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
@@ -17,16 +24,17 @@
7WYW))PW W
7WH)),WC)
7L--/XY)
+DEBUG: Don't include this line...
9+-,KY7)
W9-Y3+7)
W'=9WI7)
,W '-YY)
-
W ::W
-
,T :X)
-
() '9W 'L. ,-
-
(C =:9 '9L ,T
-
() ,,-7) 7WL ,F'
-
() , T9) '9WL ,YF
-
W ::W ,
-
,T :X) ()
-
() '9W 'L. () ,-
-
(C =:9 '9L () ,T
-
() ,,-7) 7WL WW ,F'
-
() , T9) '9WL -- ,YF () '-/(W -==+PE9P7===O) -, 'W, , T+/WX=L-. ,WP+()+3L3,),=WL --==-T- 7) -,YW '-=9WPL+PT-- ':--L/=9WP=-'
Stage this hunk [y,n,q,a,d,/,K,j,J,g,s,e,?]?
哇!看起来那里有一行调试代码,我们不应该添加它。让我们拆分这个 hunk,按`s`:
Stage this hunk [y,n,q,a,d,/,K,j,J,g,s,e,?]? s
Split into 2 hunks.
@@ -17,7 +24,8 @@
7WYW))PW W
7WH)),WC)
7L--/XY)
+DEBUG: Don't include this line...
9+-,KY7)
W9-Y3+7)
W'=9WI7)
,W '-YY)
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]?
我们不希望将输出暂存,使用`n`,但我们会暂存接下来的 hunk 和稍后的所有 hunk,在`a`文件中:
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? n
@@ -20,13 +28,13 @@
9+-,KY7)
W9-Y3+7)
W'=9WI7)
,W '-YY)
-
W ::W
-
,T :X)
-
() '9W 'L. ,-
-
(C =:9 '9L ,T
-
() ,,-7) 7WL ,F'
-
() , T9) '9WL ,YF
-
W ::W ,
-
,T :X) ()
-
() '9W 'L. () ,-
-
(C =:9 '9L () ,T
-
() ,,-7) 7WL WW ,F'
-
() , T9) '9WL -- ,YF () '-/(W -==+PE9P7===O) -, 'W, , T+/WX=L-. ,WP+()+3L3,),=WL --==-T- 7) -,YW '-=9WPL+PT-- ':--L/=9WP=-'
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? a
让我们查看当前状态以及工作目录和暂存区之间的差异:
$ git status
On branch interactive
Your branch is behind 'origin/interactive' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git reset HEAD
modified: liberty.txt
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: liberty.txt
$ git diff
diff --git a/liberty.txt b/liberty.txt
index 035083e..9638930 100644
--- a/liberty.txt
+++ b/liberty.txt
@@ -24,6 +24,7 @@
7WYW))PW W
7WH)),WC)
7L--/XY)
+DEBUG: Don't include this line...
9+-,KY7)
W9-Y3+7)
W'=9WI7)
完美!我们已经暂存了所有更改,除了调试行,因此结果可以提交:
$ git commit -m 'Statue of liberty completed'
[interactive 1ccb885] Statue of liberty completed
1 file changed, 36 insertions(+), 29 deletions(-)
# 还有更多内容...
如前所述,我们也可以使用`git add -i`来交互式地添加文件。如果我们在重置分支后执行此操作,我们将得到以下菜单:
$ git add -i
staged unstaged path
1: unchanged +37/-29 liberty.txt
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>
这八个选项几乎都能做它们所说的事。我们可以选择补丁选项进入补丁菜单,正如我们之前看到的那样,但首先我们必须选择要添加补丁的文件:
What now> p
staged unstaged path
1: unchanged +37/-29 liberty.txt
Patch update>> 1
staged unstaged path
- 1: unchanged +37/-29 liberty.txt
Patch update>>
diff --git a/liberty.txt b/liberty.txt
index 8350a2c..9638930 100644
--- a/liberty.txt
+++ b/liberty.txt
...
一旦我们选择了要添加补丁的文件,它们会在菜单中显示`*`符号。开始打补丁时,只需点击`<return>`。完成后,你将返回菜单,可以退出、审查、恢复等。
# 使用 Git gui 的交互式添加
`git add`的交互式功能非常强大,可以创建仅包含单个逻辑更改的干净提交,即使这些更改最初是作为功能添加和错误修复的混合体编写的。交互式`git add`功能的缺点是,每次只显示一个 hunk,难以全面了解文件中所有的更改。为了更好地了解更改内容,并且仍然能够仅添加选定的 hunks(甚至单行),我们可以使用`git gui`。Git GUI 通常随 Git 安装包一起提供(Windows 上为 MsysGit),可以通过命令行启动:`git gui`。如果你的发行版没有提供 Git GUI,你可能可以通过包管理器安装它,名为`git-gui`。
# 正在准备中
我们将使用与上一个示例相同的仓库,并将其重置为相同的状态,这样我们就可以使用 Git GUI 执行相同的添加操作:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git checkout interactive
$ git reset HEAD^
# 如何操作...
在 `Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks` 仓库中加载 Git GUI。这里,你可以看到左上方是未暂存的更改(文件),下方是已暂存的更改(文件)。主窗口将显示当前标记文件中的未暂存更改。你可以右键点击某个块,看到一个上下文菜单,其中包含暂存等选项。Git GUI 显示的第一个块比我们之前用 `git add -p` 看到的要大。选择“显示更少上下文”以拆分该块,如下图所示:

现在,我们像之前一样获得一个较小的块,如下图所示:

对于第一个块,我们选择添加“为提交暂存块”,接下来的块会移到屏幕顶部,如下图所示:

在这里,我们可以选择要添加的行,而不是执行另一次拆分,并暂存这些行:为提交暂存行。我们可以添加除包含调试行的块外的其他所有块。现在,我们准备好创建一个提交了,可以通过 Git GUI 完成。我们只需要在屏幕底部的字段中写入提交信息,然后点击提交,如下图所示:

# 忽略文件
对于每个仓库,通常有一些类型的文件你不希望被跟踪。这些文件可能是配置文件、构建输出文件,或者是编辑器在编辑文件时生成的备份文件。为了避免这些文件出现在 `git status` 输出中的未跟踪文件部分,可以将它们添加到名为 `.gitignore` 的文件中。这个文件中与工作目录中文件匹配的条目将不会被 `git status` 考虑。
# 准备就绪
克隆 `Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks` 仓库并检出 `ignore` 分支:
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks.git
$ cd Git-Version-Control-Cookbook-Second-Edition_tips_and_tricks
$ git checkout ignore
# 如何操作...
1. 首先,我们将创建一些文件和目录:
$ echo "Testing" > test.txt
$ echo "Testing" > test.txt.bak
$ mkdir bin
$ touch bin/foobar
$ touch bin/frotz
1. 让我们查看 `git status` 的输出:
$ git status
On branch ignore
Your branch is up-to-date with 'origin/ignore'.
Untracked files:
(use "git add
test.txt
nothing added to commit but untracked files present (use "git add" to track)
1. 只有 `test.txt` 文件出现在输出中。这是因为其余的文件都被 Git 忽略了。我们可以检查 `.gitignore` 的内容,看看是如何发生的:
$ cat .gitignore
*.config
*.bak
Java files
*.class
bin/
这意味着 `*.bak`、`*.class`、`*.config` 以及 `bin` 目录中的所有内容都被 Git 忽略。
1. 如果我们尝试在 Git 忽略的路径中添加文件,例如 `bin`,它会报错:
$ git add bin/frotz
The following paths are ignored by one of your .gitignore files:
bin/frotz
Use -f if you really want to add them.
但是,它也给了我们使用 `-f` 的选项,如果我们真的想要添加它,那就是 `-f`:
$ git add -f bin/frotz
$ git status
On branch ignore
Your branch is up-to-date with 'origin/ignore'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: bin/frotz
Untracked files:
(use "git add
test.txt
1. 如果我们忽略了已经被跟踪的 `foo` 文件,并对其进行了修改,它仍然会出现在状态中,因为已跟踪的文件不会被忽略:
$ echo "foo" >> .gitignore
$ echo "more testing" >> foo
$ git status
On branch ignore
Your branch is up-to-date with 'origin/ignore'.
Changes to be committed:
(use "git reset HEAD
new file: bin/frotz
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: .gitignore
modified: foo
Untracked files:
(use "git add
test.txt
1. 现在我们可以添加并提交 `foo`、`.gitignore` 以及当前暂存区的内容:
$ git add foo .gitignore
$ git commit -m 'Add bin/frotz with force, foo & .gitignore'
[ignore fc60b44] Add bin/frotz with force, foo & .gitignore
3 files changed, 2 insertions(+)
create mode 100644 bin/frotz
# 还有更多...
也可以在没有`.gitignore`文件的情况下忽略仓库中的文件。你可以将被忽略的文件放在一个全局忽略文件中,例如`~/.gitignore_global`,并全局配置 Git,使其也考虑该文件中的条目作为被忽略的文件:
$ git config --global core.excludesfile ~/.gitignore_global
你也可以在`.git/info/exclude`文件中为每个仓库配置忽略文件。如果使用这些选项之一,你将无法轻松共享被忽略的文件;因为它们存储在仓库外部,无法被添加到仓库中。共享`.gitignore`文件则要简单得多;你只需将它们添加并提交到 Git。但让我们看看其他选项是如何工作的:
$ echo "*.test" > .git/info/exclude
$ touch test.test
$ git status
On branch ignore
Your branch is ahead of 'origin/ignore' by 1 commit.
(use "git push" to publish your local commits)
Untracked files:
(use "git add
test.txt
nothing added to commit but untracked files present (use "git add" to track)
$ ls
bar bin foo
test.test test.txt test.txt.bak
我们可以看到`.test`文件没有出现在`status`输出中,并且被忽略的文件依然存在于工作目录中。
# 另见
通常会忽略许多文件,例如,为了避免意外添加文本编辑器的备份文件,`*.swp`、`*~.`和`*.bak`文件通常会被忽略。如果你在做 Java 项目,可能会将`*.class`、`*.jar`和`*.war`添加到`.gitignore`文件中;如果你在做 C 语言项目,可能会添加`*.o`、`*.elf`和`*.lib`。GitHub 有一个专门的仓库,用于收集不同编程语言和编辑器/IDE 的 Git 忽略文件。你可以在[`github.com/github/gitignore`](https://github.com/github/gitignore)找到它。
# 显示和清理被忽略的文件
忽略文件对于过滤`git status`输出中的噪声非常有用。但有时,需要检查哪些文件被忽略了。这个例子将展示如何做到这一点。
# 准备工作
我们将从上一个例子中的仓库继续操作。
# 如何执行...
要显示我们忽略的文件,可以使用`clean`命令。通常,`clean`命令会从工作目录中移除未跟踪的文件,但也可以在干运行模式`-n`下执行,它仅显示将会发生的情况:
$ git clean -Xnd
Would remove bin/foobar
Would remove test.test
Would remove test.txt.bak
上述命令中使用的选项指定了以下内容:
+ `-n`,`--dry-run`:仅列出将被移除的文件
+ `-X`:仅移除 Git 忽略的文件
+ `-d`:除了未跟踪的文件,还会移除未跟踪的目录
被忽略的文件也可以通过`ls-files`命令列出:
$ git ls-files -o -i --exclude-standard
bin/foobar
test.test
test.txt.bak
其中`-o`选项,`--others`,显示未跟踪的文件;`-i`选项,`--ignored`,仅显示被忽略的文件;`--exclude-standard`使用标准的排除文件`.git/info/exclude`和`.gitignore`(每个目录中)以及用户的全局排除文件。
# 还有更多...
如果需要移除被忽略的文件,当然可以使用`git clean`来执行此操作;与干运行选项不同,我们传递强制选项`-f`:
$ git clean -Xfd
Removing bin/foobar
Removing test.test
Removing test.txt.bak
要移除所有未跟踪的文件,而不仅仅是被忽略的文件,可以使用`git clean -xfd`。小写的`x`表示我们不使用忽略规则,而是直接移除所有没有被 Git 跟踪的文件。
# 第十二章:Git 提供商、集成和客户端
在本章中,我们将介绍以下内容:
+ 在 GitHub 上设置组织
+ 在 GitHub 上创建一个仓库
+ 为问题和拉取请求添加模板
+ 创建 GitHub API 密钥
+ 使用 GitHub 在 Jenkins 上进行身份验证
+ 触发 Jenkins 构建
+ 使用 Jenkins 文件
# 简介
你可以托管自己的 Git 安装并维护一个中央服务器来为你的组织提供服务。如果你是一个小公司或开源项目,维护这样的基础设施可能是一种负担。但今天,已经有许多 Git 提供商可以减轻这种负担。
GitHub 是最知名的 Git 提供商,拥有 4000 万用户。许多知名的开源项目都托管在 GitHub 上。一旦你创建了 GitHub 账户,就可以浏览当前托管的 8500 万个 Git 仓库。
在现代软件开发中,**持续集成**(**CI**)是很流行的。其理念是开发者的更改应该尽快地合并到代码库中。Git 的**拉取请求**(**PRs**)就是一种实现方式。当然,GitHub 提供了创建 PR 的界面,并让协作者进行代码审查。CI 策略的一部分也是自动运行所有测试。像 Jenkins 这样的软件可以配置为每次提交时进行构建和测试。
# 在 GitHub 上设置组织
无论你是商业产品还是开源项目,都可能有公司或一群人在背后支持。GitHub 通过允许用户创建组织来支持这种结构。
一个组织可以关联多个仓库,并且有成员。使用组织的好处在于,成员可以随时加入或离开(开发人员可能换工作,离开),但仓库将与组织关联,因此无需转移仓库的所有权。
作为用户,你可以是多个组织的成员。通常,你是雇主组织的成员,同时也是多个开源项目背后组织的成员。
# 准备工作
你需要一个 GitHub 的用户账户。对于本教程,我们将使用 GitHub 用户`johndoepackt`。任何用户都可以创建一个组织。
如果你还没有 GitHub 账户,现在是时候创建一个了。一旦创建并登录你的账户,你就可以开始使用了。
# 如何操作...
1. 创建组织是设置中的一个功能。因此,你需要在 GitHub 账户中找到设置,如下图所示:

1. 设置中的一个菜单项是组织。你可以通过两种方式创建一个组织。你可以将你的用户账户转为组织,或者创建一个独立的组织。我们将创建一个组织,而不是将用户账户转为组织,如下图所示:

1. 创建组织后,您可以邀请用户成为成员。也可以添加外部或外部协作者。
# 它是如何工作的...
GitHub 上的组织是在 Git 之上的一层。可以将它们视为提供与关联仓库的访问控制的方式。
当 GitHub 用户浏览与您的组织相关的仓库时,他们将受到您设置的权限限制。这意味着您可以控制允许他人查看的内容。
如果您是 GitHub 的付费用户,您的组织可以拥有私有仓库。私有仓库仅限组织成员访问。公司可以拥有开源项目或示例作为公共仓库。但是,通过使用私有仓库,可以保护一些公司机密。
# 还有更多...
每个组织都有一些设置可以调整。为了提高组织的安全性,您可以要求成员启用双因素认证。
您还可以访问组织的审计日志。在许多情况下,您需要确保谁做了什么。审计日志还可以揭示是否有人访问了您的组织并试图篡改它。
# 另请参见
GitHub 组织是 GitHub 上的一个文档化特性。相关文档可以在 [`help.github.com/categories/setting-up-and-managing-organizations-and-teams/`](https://help.github.com/categories/setting-up-and-managing-organizations-and-teams/) 找到。
# 在 GitHub 上创建仓库
使用 Git 的核心是仓库。GitHub 提供了一个创建仓库的 UI 界面,使新的 Git 用户更容易入门。出于显而易见的原因,GitHub 不直接向您提供对其服务器的访问权限。
在上一个教程中,我们在 GitHub 上创建了一个组织。在本教程中,我们将在该组织中创建一个仓库。作为个人用户(而非组织),您也可以创建仓库。
# 准备中
首先登录 GitHub。在创建仓库之前,您需要做出两个决策。首先,仓库的名称应该是什么?其次,仓库应该是公开的还是私有的?
私有仓库仅对组织成员可见。但要创建私有仓库,您必须是付费客户。
# 如何操作...
1. 由于我们要为组织创建仓库,因此您需要从普通用户切换到您的组织。这是一个名为“切换仪表板上下文”的下拉菜单。切换上下文后,您的屏幕将如下所示:

1. 现在,你准备好创建代码库了。你需要设置名称、描述(可选),并决定代码库是公开的还是私有的。此外,GitHub 可以为你创建 `.gitignore`、`LICENSE` 和一个简单的 `README.md` 文件。通常,你会知道主要的编程语言,并可以生成一个基于最佳实践的 `.gitignore` 文件。在下图中,你可以看到所有填写的字段:

1. 一旦创建了代码库,你可以按照以下步骤将其克隆到你的计算机上:
$ git clone https://github.com/JohnDoePacktOrg/nomen-nescio.git
$ cd nomen-nescio
$ ls -a
. .. .git .gitignore LICENSE README.md
# 它是如何工作的…
你在本教程中经过的步骤是 *在 GitHub 上创建代码库*。本质上,这只是创建一个目录并运行 `git init`。
添加额外的文件(`.gitignore`、`LICENSE` 和 `README.md`)也非常简单。设置一个好的 `.gitignore` 需要时间,但你可以从生成的 `.gitignore` 文件中得到一个很好的起点。
GitHub 还为你的代码库设置了访问控制。只有组织成员才具有写权限;也就是说,他们有提交代码的权利。在代码库的设置中,你可以在菜单项“Collaborators & teams”下定义更精确的访问控制。一个团队是由一起工作的 GitHub 用户组成的。在你的公司中,可能有 iOS、Android 和 DevOps 团队。
# 还有更多…
在 GitHub 上使用代码库时,代码审查是拉取请求的一个重要组成部分。让我们更新 `README.md` 并在 GitHub 上进行代码审查:
$ git checkout -b update-readme
Switched to a new branch 'update-readme' $ echo "\nSoon a better name will be decided." >> README.md $ git add README.md $ git commit -m "Updating README.md"
[update-readme 6829c33] Updating README.md 1 file changed, 1 insertion(+) $ git push origin update-readme Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 330 bytes | 330.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/JohnDoePacktOrg/nomen-nescio.git
- [new branch] update-readme -> update-readme
现在,你可以去 GitHub,找到你的代码库并创建一个拉取请求。你可以邀请你的合作者在合并之前审查你的更改,如下图所示:

当我们创建代码库时,我们有选择将其创建为私有代码库的选项。私有代码库仅对付费用户开放。从 Git 的角度来看,公共代码库和私有代码库没有区别。主要的区别在于谁可以查看代码库。正如你想象的那样,公共代码库可以被任何人查看。你甚至不需要登录 GitHub 就可以查看公共代码库。这些代码库非常适合开源项目——如果你仔细阅读 GitHub 的条款,你会发现公共代码库是为了开源而设计的。
私有代码库只能被你授予访问权限的用户查看。通常情况下,你的组织中的每个成员都可以查看私有代码库。换句话说,私有代码库非常适合内部项目或专有软件。如今,许多公司将公共代码库和私有代码库结合使用,他们的软件开发人员可以在这些代码库之间流畅地切换。
# 为问题和拉取请求添加模板
在第七章,*通过 Git Hooks、别名和脚本提升你的日常工作*,我们展示了如何为提交添加模板。提交模板帮助开发者在提交信息中包含相关的内容。在 GitHub 上,用户会创建问题和拉取请求。问题或拉取请求的创建者负责写出有意义的描述。
本教程将向你解释如何为问题和拉取请求添加模板。目的是帮助人们记得提供足够的上下文,帮助你快速理解问题或拉取请求的内容。
# 准备工作
我们将继续使用我们在之前的教程中创建的`nomen-rescio`仓库。GitHub 在各个地方都使用**Markdown**作为标记语言。Markdown 对开发者友好,因为它是纯文本,并且具有一些特殊的转换功能,用于排版粗体、项目符号列表等内容。深入学习 Markdown 可能本身就足够写成一本书。
# 如何操作...
1. 首先,你需要找到你仓库的设置。在设置中有一个大大的“设置模板”按钮,这就是你要找的按钮。你可以选择使用其中一个现成的模板,但我们将创建一个自定义模板,如下图所示:

1. 你通过点击“提交更改”来保存模板。当用户创建问题时,你的模板将被显示。用户可以选择删除你写的所有文本,但大多数用户会在删除前阅读它;请参考以下截图:

# 工作原理...
你的模板会保存在仓库本身。实际上,你会在目录 `.github` 中找到它们。如果你愿意,你可以在你喜欢的编辑器中编辑模板,并像其他文件一样提交更改。
目录 `ISSUE_TEMPLATE` 包含问题的模板文件。类似地,如果你在 `PULL_REQUEST_TEMPLATE` 目录下创建一个文件,你将得到一个拉取请求的模板。通过多个模板,用户将被要求选择适合的模板。
# 创建一个 GitHub API 密钥
到目前为止,我们在 GitHub 上所做的工作都是手动的。程序员喜欢自动化流程,执行 GitHub 工作也不例外。在下一个教程中,我们将向你展示如何自动化这些任务。
# 准备工作
为了自动化 GitHub 任务,你需要能够访问 GitHub。你可以选择使用 API 密钥或个人访问令牌,而不是通过用户名和密码登录。这样的令牌不应该与他人共享,并且你需要始终保密。
所以,本教程从生成访问令牌开始,并展示一个简单的 Python 脚本。这个 Python 脚本会查找你所有的仓库,并为每个仓库查找所有的拉取请求。
# 如何操作...
1. 首先,我们需要生成个人访问令牌。你需要在菜单系统中逐步深入:设置,开发者设置,最后是个人访问令牌。我们会给我们的令牌命名为`basic-query`,因为我们仅打算执行这个操作。你可以指定令牌的访问权限。我们的令牌只需要访问仓库操作,如以下截图所示:

1. 一旦你生成了令牌,它将被显示出来。在 GitHub 上你只会看到一次令牌,因此重要的是将其复制到你的电脑。在以下截图中,你可以看到生成的令牌页面(除了我们添加了一个矩形框,因为我们需要保密):

1. 如前所述,我们将使用 Python。你需要安装一个小型库 PyGitHub。使用 Pip 安装非常简单:
$ pip install pygithub
1. 现在,我们准备运行一个 Python 脚本来获取仓库和拉取请求。这个脚本仅仅是遍历仓库和拉取请求:
from github import Github
import datetime
g = Github("YOUR_PERSONAL_ACCESS_TOKEN")
for repo in g.get_user().get_repos():
print(repo.name)
for pr in repo.get_pulls():
print(" " + pr.created_at.isoformat() + " : " + pr.title)
# 它是如何工作的……
令牌可以让你访问 GitHub,但仅限于创建令牌时指定的部分。后台,PyGitHub 的方法是通过 HTTP 请求调用 GitHub 的 API 实现的。
例如,调用`get_repos`就是对`/user/repos`进行 HTTP GET 请求。HTTP 请求将以 JSON 格式返回结果。PyGitHub 解析 JSON 结果并填充 Python 对象,使得结果对 Python 开发者来说更加自然。
Python 并不是唯一的编程语言。你几乎可以为任何已知的语言找到库。
当然,你可以超越简单的脚本,开发一个完整的 GitHub 客户端。我们将把这个作为一个练习留给你。
# 另见
PyGitHub 的完整 API 文档可以在 [`pygithub.readthedocs.io/en/latest/reference.html`](http://pygithub.readthedocs.io/en/latest/reference.html) 查找。
# 使用 GitHub 在 Jenkins 中进行身份验证
Jenkins 是最受欢迎的持续集成软件,它允许用户持续构建、测试和发布任何类型的软件。它在各个方面都非常灵活和可配置,包括用户能够登录和授权的方式。GitHub 能够作为 OAuth 提供者,这非常方便,因为它非常符合将参与项目的用户与 CI 系统中的相应区域进行映射的需求。
# 准备工作
为了演示过程,我们需要一个 Jenkins 实例。每个公司都会有不同的配置,因此,为了使事情更加可预测,我们将使用 Jenkins 的本地版本。
Jenkins 是一个 Java 应用,但对于我们的示例,最简单的方式是使用 Docker 来获取一个临时的 Jenkins 实例。只要在你的机器上安装并运行 Docker,简单执行以下命令:
$ docker run --rm -p 8080:8080 jenkinsci/blueocean
这个 Jenkins 实例在停止 Docker 容器后不会留下任何痕迹。
日志将开始显示在控制台上,并包含首次登录的密码。请查找以下内容:
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
YOUR_PASSWORD_HERE
This may also be found at: /Users/emanuelez/.jenkins/secrets/initialAdminPassword
此时,你可以将浏览器指向`http://localhost:8080/`,并将会提示你输入密码,如下图所示:

此时,你将被要求安装插件。对于本示例,我们只需安装建议的插件,如下图所示:

接下来,你将被要求创建一个管理员用户,但由于这是一个临时镜像,你可以直接点击“继续作为管理员”继续操作:

接下来,你将被要求设置实例配置。只需保持默认值并点击“保存并完成”。
此时,Jenkins 已经准备好使用了。只需点击“开始使用 Jenkins”,如下面的截图所示:

你现在将看到欢迎来到 Jenkins!首页,如下所示:

# 如何操作...
1. 为了使用 GitHub 验证 Jenkins,你需要安装 GitHub 身份验证插件。为此,请点击左侧面板中的“管理 Jenkins”,然后在新页面中点击“管理插件”,如下图所示:

1. 现在,你可以切换到“可用”标签,并在搜索框中输入 `github auth`,如下图所示:

1. 你可以通过勾选左侧的框并点击“无需重启安装”来安装插件。
1. 插件安装完成后,你可以通过点击左上角的 Jenkins 徽标返回到 Jenkins 首页,并再次点击“管理 Jenkins”。
1. 这次我们将点击“配置全局安全性”,如下图所示:

1. 现在,前往 GitHub,并按照下图所示,访问 [`github.com/settings/applications/new`](https://github.com/settings/applications/new) 注册一个新应用:

在此处,你需要填写一个任意的应用名称和授权回调 URL,如下所示。注册应用后,你将能够看到你的客户端 ID 和客户端密钥。它们将在 Jenkins 中用于填写全局安全设置中的相关字段。
1. 提交表单后,你将能够使用 GitHub 凭证登录 Jenkins。
# 它是如何工作的...
身份验证需要解决的问题是,谁是试图访问服务的人?有许多方式可以做到这一点:登录和密码、令牌等等。OAuth 是回答这个问题的另一种方式。OAuth 代表开放身份验证,它是一个开放的访问授权标准。它允许用户访问网站(如我们的 Jenkins 实例),而无需提供密码,因此也无需信任这些网站。
GitHub 有作为 OAuth 提供者的能力,这意味着其他网站可以配置为接受 GitHub 提供的凭据,从而允许用户访问他们的服务。
这意味着每当用户尝试访问配置为接受 GitHub OAuth 的 Jenkins 实例时,他将被重定向到 GitHub 本身进行身份验证,随后 GitHub 会将用户重定向回 Jenkins 实例,并附带身份令牌。
# 还有更多内容...
身份验证只是问题的一半。识别用户当然很重要,但我们如何利用这些信息同样至关重要。此时,授权发挥作用,它旨在回答这个问题:假设试图访问服务的人是 X,那么他被允许做什么、不能做什么?
我们配置的 Jenkins 实例是,任何能够登录的人都可以做任何事情。这可能不是我们期望的行为,这也引出了 Jenkins 的 Global Security 页面中的另一个部分。
你会看到一个名为授权的部分,其中提供了许多选项。其中一个是 **GitHub Committer 授权策略**,它决定用户是否被允许查看某个特定的 Jenkins 作业——但前提是他被允许访问相应的 GitHub 仓库。
# 另请参见
GitHub OAuth 插件的文档可以在 [`wiki.jenkins.io/display/JENKINS/GitHub+OAuth+Plugin`](https://wiki.jenkins.io/display/JENKINS/GitHub+OAuth+Plugin) 查阅。
# 触发 Jenkins 构建
当你创建一个 Jenkins 作业时,Jenkins 如何知道何时构建特定的分支或拉取请求?Jenkins 提供了许多方法来实现这一点,从基于计时器的持续构建到轮询 Git 仓库查看是否有变化。尽管这两种选项都不是非常高效,但幸运的是 GitHub 提供了更好的解决方案。
GitHub 有 Webhooks 的概念,这意味着它可以配置为在发生重要事件时联系一个服务器,例如我们的 Jenkins 实例。
达到目标的方式有很多,但在这个配方中,我们将专注于一种方法,这种方法在企业环境中尤其有用,特别是当使用 GitHub 组织,包含多个仓库时。每个仓库管理一个或多个作业会迅速变成一项重复的工作,而这时 **GitHub Branch Source** 插件便派上了用场。
# 准备工作
我们将需要一个 Jenkins 实例,因此需要与之前配方相同的准备步骤。
此外,如果 Jenkins 实例无法从互联网访问,那么需要一个反向代理,以便 GitHub 能够向 Jenkins 实例发送通知。
# 如何做...
步骤如下:
1. GitHub Branch Source 插件在默认安装中预先安装,但如果你的环境中尚未安装该插件,你可以像在前面的配方中一样轻松安装它。前往 Jenkins 首页,进入“管理 Jenkins”,点击“管理插件”,然后选择“可用”标签,搜索 GitHub Branch Source 并安装插件。
1. 现在,返回 Jenkins 首页,点击“创建新作业”:

1. 选择一个作业名称,并在点击“确定”之前选择 GitHub 组织。以下配置页面可能看起来有点令人望而却步,所以让我们分解它:

1. 唯一需要特别注意的部分是项目。首先需要设置的是 GitHub 凭证。这将允许 Jenkins 自动为我们设置 Webhook。在 Jenkins 中,凭证是以安全为前提进行处理并正确加密的,因此不必担心在这里保存它们。
1. 接下来,我们需要指定所有者,它只是我们想要控制的 GitHub 组织的名称。以下部分描述了这些行为,默认情况下它们完全正常。它们将允许构建:
+ 分支
+ 来自本仓库的 PR
+ 来自 Fork 的 PR,但来自受信任的用户
就这样!现在 Jenkins 将能够自动构建任何分支或 PR,只要代码中包含 Jenkinsfile。Jenkinsfile 的使用将在下一个配方中介绍,请继续关注!
# 它是如何工作的...
GitHub Branch Source 插件将允许 Jenkins 定期扫描指定的 GitHub 组织,并检查每个仓库中的所有分支和 PR,如果其中包含一个名为 `Jenkinsfile` 的文件,它们将会自动构建,并遵循 Jenkinsfile 中的指令。
这是一个非常强大的模式,它不仅可以将项目的代码纳入版本控制,还可以将如何构建、测试甚至发布项目的指令也纳入同一个代码库中。
# 还有更多...
该作业配置页面允许更改许多不同的行为。例如,如果我们不想构建所有的分支,而只构建那些名称与正则表达式匹配的分支怎么办?解决方案只需要几个点击!有很多可能性,Jenkins 的开发者不断增加新的功能,所以定期查看可用的选项是值得的。
# 另见
GitHub Branch Source 插件的文档可以在 [`go.cloudbees.com/docs/cloudbees-documentation/cje-user-guide/index.html#github-branch-source`](https://go.cloudbees.com/docs/cloudbees-documentation/cje-user-guide/index.html#github-branch-source) 查阅。
# 使用 Jenkinsfile
Jenkinsfile 是 Jenkins 世界中的相对较新功能,它有两种不同的类型和语法:
+ 声明式
+ Groovy DSL
Groovy DSL 非常灵活且强大,但它也容易导致一些反模式,因此在这个食谱中,我们将专注于声明式风格的 Jenkinsfile。
# 准备就绪
你将需要一个包含可以构建和测试的代码库的 GitHub 仓库。鉴于可用的编程语言和构建系统种类繁多,我们将选择一个任意的 Java 项目,并使用流行的 Maven 构建系统。
# 如何实现...
只需要添加一个名为 `Jenkinsfile` 的文件,并包含以下内容:
pipeline {
agent any
tools {
maven 'Maven 3.3.9'
jdk 'jdk8'
}
stages {
stage ('Initialize') {
steps {
sh '''
echo "PATH = ${PATH}"
echo "M2_HOME = ${M2_HOME}"
'''
}
}
stage ('Build') {
steps {
sh 'mvn -Dmaven.test.failure.ignore=true install'
}
post {
success {
junit 'target/surefire-reports/**/*.xml'
}
}
}
}
}
# 它是如何工作的...
Jenkinsfile 描述了如何构建和测试软件以及运行这些操作的环境。
让我们来看一下不同的部分:
+ `agent any` 指定此构建可以在任何可用的执行器上运行。
+ `tools {}` 部分指定了运行构建所需的程序。在这种情况下,需要 Maven 和 **Java 开发工具包** (**JDK**),并且也指定了版本。
+ `stages {}` 和 `stage() {}` 允许将运行过程分为明确的阶段,从而使构建结果的分析更加清晰。
+ 每个阶段需要包含一个 `steps {}` 部分,该部分将精确描述要执行的操作。在这种情况下,在 *初始化* 阶段,我们仅仅运行一个 shell 脚本,回显一些环境变量。
+ 构建阶段实际上会运行 Maven,如果构建成功,它将分析包含在 XML 文件中的单元测试结果。
# 还有更多...
Jenkinsfile 是一个庞大的话题,足以填满一本书,因此提供的示例仅仅触及了可能性的表面。举几个例子,你将能够:
+ 在不同的机器上并行运行步骤以节省构建时间
+ 保存并部署构建工件
+ 运行完整的发布版本
+ 等待一些用户输入
+ 以及更多更多
# 参见其他
声明式 Jenkinsfile 的语法可以在 [`jenkins.io/doc/book/pipeline/syntax/`](https://jenkins.io/doc/book/pipeline/syntax/) 中找到。
请注意,插件可以贡献各种步骤和工具,因此请务必查看你打算使用的插件的文档!