如果这样的补丁确实适用,那会有点令人惊讶:
git diff evl/v5.4 master > ../patchfile
请记住,git diff 比较两个提交,或者更准确地说,比较两个提交中的快照。我喜欢将这两个提交称为 L 和 R,分别代表“左”和“右”,尽管这里没有共同商定的命名约定。
对于 L(左侧)提交,您选择 evl/v5.4 选择的提交。对于 R(右侧)提交,您选择了 master 选择的提交。到目前为止没问题。
现在,请记住 git diff 的输出是一系列指令。如果应用这些指令,将更改出现在提交 L 中的文件集,以生成出现在提交 R 中的文件集。换句话说,这个git diff 的输出给出了将evl/v5.4 更改为master 的指令。这通常包括以下形式的指令在path/to/file.ext 的第45 行之后添加以下三行 或删除@987654331 的以下行中的一行@,出现在以下上下文中。
context 是 L 中的内容,指令(如果应用时)产生 R 中的内容>.
git checkout master
这将获得提交R。您没有提交 L 。将 L 更改为 R 的说明在这里毫无意义。
您可以反向应用补丁。毕竟,将 L 变成 R 的指令可以“向后执行”,就像将 R 变成 L 。好吧,也就是说,只要没有任何指令是简单的删除文件F,因为这需要创建一个新文件F。如果指令说delete file F其内容为...,我们可以使用它来创建新文件F。
关于这个主题的一个变体......
如何...从某些分支中提取提交并将 [它们] 应用到其他分支
提交是一个快照,而不是一组更改。但这不是只是一个快照:它是一个快照加上一些信息关于快照。此元数据,或关于数据的额外信息(即数据的快照)包括提交人的姓名和电子邮件地址。它包括一些日期和时间戳。它包括一条日志消息,这几乎是任意的,取决于提交的人。但对于 Git 来说重要的是,它还包含了一些较早提交的原始hash IDs。
Git 通过其哈希 ID 查找每个提交。哈希 ID 本质上是提交的“真实名称”。提交的哈希 ID 永远不会改变,提交本身的内容也永远不会改变。 (Git 通过将每个内部对象存储在 key-value database 中的方式来确保这两者,其中密钥是哈希 ID,哈希 ID 是对该密钥下存储的内容的加密校验和。)
branch name 只是保存某个提交链中last 提交的哈希 ID。链条可以非常简单和线性,而且很多都是。如果我们使用大写字母来代替哈希 ID,我们会得到这样一张图:
... <-F <-G <-H
last 提交是最右边的提交,即提交 H。该提交包含数据(每个文件的完整快照)和元数据:谁提交、何时提交、为什么提交,以及之前提交的哈希 ID G。
我们选择一个我们想用来查找 H 的分支名称,并让 Git 将提交 H 的实际哈希 ID 存储在该名称中:
...--F--G--H <-- master
我已经停止绘制向后指向的箭头between 提交as 箭头,但它们确实是每次提交中出现的一种箭头。只是,随着提交内容一直被冻结,H 将永远指向G,并且由于我们知道提交哈希 ID 看起来是随机的,G 无法知道它未来的父级 H的哈希 ID 将是,因此连接必须向后。
给定名称 master,然后,我们让 Git 通过其哈希 ID(存储在名称 master 中)找到提交 H。给定提交H,我们可以让Git 找到G 的哈希ID:这是H 中元数据的一部分。给定G 的哈希ID,我们可以让Git 找到提交G。因此,一旦我们找到 last 提交,我们就可以返回一跳,到 second-to-last 提交。
当然,该提交也嵌入了一个哈希 ID。从G,我们可以跳回F。只要箭头继续前进,我们就可以保持这种状态,一直到第一次提交。 (作为第一次提交,它没有向后的箭头,这是我们/Git 知道停止返回的方式。)
这意味着存储库中的提交是存储库中的历史记录。历史不过是承诺。提交全部向后连接。存储库只是提交的集合,names(分支名称或任何其他名称)只是为我们提供了进入提交的方法。
要向此存储库添加 新 提交,我们检查现有提交 H:
...--G--H <-- master (HEAD)
这使得master成为当前的分支,并提交H成为当前的提交,我们可以通过使用特殊名称HEAD找到所有这些,这现在附加到名称master。
然后,我们对一些实际上不在 中 Git 的文件进行一些更改。 (Git 中的文件无法更改。)我们让 Git 将这些文件复制到一个新的提交中,添加一些元数据——包括姓名和电子邮件地址,以及“现在”作为作者和提交者的时间戳,用于实例——然后对这一切进行哈希处理,得到一个新的、唯一的哈希 ID。 (时间戳有助于确保此提交获得全新的哈希 ID,即使 其他所有内容 相同,但通常新提交中的数据与前一次提交中的数据不同...而且,父哈希 ID 不匹配。但时间也不匹配。)我们新提交的 父 将是提交 H。 Git 现在可以写出所有数据和元数据,从而进行新的提交。我们将它的大而丑陋的随机散列 ID 称为 I,并将其绘制,指向 H:
...--F--G--H
\
I
现在来一个诡计多端的技巧:Git 只是将I 的哈希 ID 写入 name master,并附加了特殊名称 HEAD。所以我们毕竟不需要在自己的一行上画I:
...--F--G--H--I <-- master
任何现有提交中的任何内容都没有更改。 New 提交I 是最后一个,它指向H。 分支名称已更改,或者更确切地说,存储在中的哈希 ID 已更改。该名称指向最后一次提交——实际上,根据定义。如果我们强制 Git 将名称指向回提交 H,提交 I 就会从视图中消失:它仍然存在,但我们再也找不到它了,除非我们将它的哈希 ID 保存在某个地方。
现在,无论发生什么其他事情,我们都有这些图形事物之一,分支名称指向每个链中的最后一个提交。所以如果我们有,说:
I--J <-- branch1
/
...--G--H <-- master
\
K--L <-- branch2
那么 branch2 上的 last 提交是 L,branch1 上的 last 提交是 J,而 last em> 在master 上的提交是H。提交H 实际上是在所有三个 分支上,因为在Git 中,“在一个分支上”的概念只是意味着我们可以从最后开始——就像Git 那样,倒退——然后工作向后到达给定的提交。从L,我们可以跳转到K,然后跳转到H,所以提交H是branch2。或者,使用名称master,我们从H 开始,因此提交H 位于master。
同时,如果我们获取任何父/子对(例如,K-L,就像它出现在 branch2 上一样),我们可以让 Git 比较这些快照。对于所有相同的文件,Git 什么也没说。将K 更改为L 的说明该文件 是什么都不做。对于每个不同的文件,Git 会显示一些指令;这些告诉我们如何更改出现在K 中的文件,使其与出现在L 中的文件相同。
如果我们愿意,可以git checkout branch1:
I--J <-- branch1 (HEAD)
/
...--G--H <-- master
\
K--L <-- branch2
现在,作为我们可以处理的常规文件,我们拥有J 中的每个文件。 Git基本上将所有文件从提交J复制到一个工作区。
如果将K 更改为L 的说明适用,我们可以让Git 应用这些说明。我们可以通过找到提交K 和L 的两个哈希ID 并运行:
git diff <hash-of-K> <hash-of-L>
获取这些说明。然后我们可以尝试在我们现在签出的文件上使用这些说明。 它们可能无法全部工作,因为可能某些文件已经消失,或者我们应该更改第 42 行的某些文件不再具有该行。但我们可以尝试应用这些更改。
要在 Git 中自动执行此操作,我们不必使用 git diff 和 git patch。相反,我们可以使用git cherry-pick。这实际上相当漂亮,因为cherry-pick 使用 Git 的内部 merge 机制 来 combine 更改。但是,就目前而言,您可以将cherry-pick 视为比较父母和孩子,找出差异,并将差异应用到我们现在的任何提交中。
因为 Git 有图,并且提交 K 连接(向后)提交 J,我们只需要告诉 Git 挑选提交 K 的哈希 ID:
git cherry-pick <hash-of-K>
有一些更简单、更短的指定特定提交的方法,不需要输入整个哈希 ID。当然,没有人会首先尝试输入完整的哈希 ID:我们使用剪切和粘贴来复制哈希 ID。打错字太容易了(不过,幸运的是,哈希 ID 足够稀疏,这只会导致 Git 说 whaddaya talkin' 'bout?!)。但我不会在这里讨论。现在已经足够了。
[编辑,2021 年 1 月 2 日] 克隆问题中的存储库后,我可以运行以下命令。请注意,当前分支是master,并且工作树最初没有未跟踪的文件。 git clean -dfx 不产生任何输出。使用--index 和下面的git apply 很重要;稍后我会解释原因。
$ git diff --no-renames master evl/v5.4 > ../patchfile
$ git apply --index < ../patchfile
<stdin>:18659: space before tab in indent.
int data;
<stdin>:18660: space before tab in indent.
/* Other data fields */
<stdin>:29742: space before tab in indent.
apq8016
<stdin>:29743: space before tab in indent.
apq8074
<stdin>:29744: space before tab in indent.
apq8084
warning: squelched 352 whitespace errors
warning: 357 lines add whitespace errors.
$ git status | head
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .clang-format
modified: .gitattributes
modified: .gitignore
modified: .mailmap
modified: COPYING
$ git checkout -b tmp && git commit -q -m apply
Switched to a new branch 'tmp'
$ git diff evl/v5.4 tmp
$
如您所见,这个差异(我交换了顺序)与--index 一起应用(使用-3 或--3way 将与他们设置--index 选项一样工作)就足够了。
需要--index 的原因(无论是明确的还是暗示的)是补丁本身创建了在.gitignore 文件中列出的文件。具体来说,tools/perf/lib/include/perf/* 文件全部被忽略。然而,这些文件在evl/v5.4 的尖端在提交中,因此在差异中作为新文件。因此,当 Git 应用差异时,它会创建这些文件。
如果您应用差异没有 --index,Git 将差异应用到您的工作树(仅)。然后您必须使用git add 添加更新的文件。但由于新创建的文件列在.gitignore 中,如果您单独添加它们,它们将被忽略。整个tools/perf/lib/include/perf/ 目录在master 中不存在,因此当前签出提交的索引中没有此类文件。这些文件是在evl/v5.4 的提交中,所以如果你运行git checkout evl/v5.4,它们会在Git 的索引中结束:a git checkout 复制所选文件中的所有文件提交到索引,即使这些文件名义上被忽略。但是我们的git apply 方法不会将那些(新)文件复制到索引中,除非我们使用--index,然后是随后的git add *服从新创建的tools/perf/.gitignore 文件:
$ cat -n tools/perf/.gitignore
1 PERF-CFLAGS
2 PERF-GUI-VARS
3 PERF-VERSION-FILE
4 FEATURE-DUMP
5 perf
6 perf-read-vdso32
7 perf-read-vdsox32
8 perf-help
9 perf-record
10 perf-report
11 perf-stat
12 perf-top
13 perf*.1
14 perf*.xml
15 perf*.html
16 common-cmds.h
17 perf.data
18 perf.data.old
19 output.svg
20 perf-archive
21 perf-with-kcore
22 tags
23 TAGS
24 cscope*
25 config.mak
26 config.mak.autogen
27 *-bison.*
28 *-flex.*
29 *.pyc
30 *.pyo
31 .config-detected
32 util/intel-pt-decoder/inat-tables.c
33 arch/*/include/generated/
34 trace/beauty/generated/
35 pmu-events/pmu-events.c
36 pmu-events/jevents
37 feature/
38 fixdep
39 libtraceevent-dynamic-list
第 5 行告诉 Git 忽略 tools/perf/lib/perf 中的所有文件。所以git add . 会忽略它们,并且新的提交与evl/v5.4 的提示提交不匹配。
我们可以换一种说法:您可以创建一个提交,其文件不会被提交接受。例如,任何顶级目录包含.gitignore 和* 行的提交都不会添加提交中的任何文件。然而,该提交将包含它包含的文件,并且检查它将使您获得这些文件的提交。只是将这些文件提取到一个原本为空的存储库中,然后使用git add,不会进行存储相同树的提交。您将获得的提交取决于路径。
我认为这样的.gitignore 文件至少是可疑的,并且通常是错误的,尽管有些人认为它很好(因为你可以使用git add -f 来覆盖忽略,或者暂时将.gitignore 文件移出方式,或其他)。这个特殊的 linux-evl 提交就是这样一个提交,一开始我们俩都被它绊倒了。