如何真正应用使用 Git diff 创建的补丁?

How do I really apply a patch created with Git diff?

我已经在这个网站上阅读了很多 related/similar 个问题,但其中 none 个问题是可行的,而且我似乎没有遇到同样的错误,所以我决定在此打开一个新问题。

我正在尝试学习更多 git,具体来说,如何应用补丁并从一些分支中提取提交并将其应用到其他分支。我最初想做一个虚拟测试,它包括从一个分支中挑选一些提交(直到过去的某个时间点)并将这些提交重新应用到过去的同一时间点,让我回到最初的时间点。

但是,我收到了大量类似“错误:补丁不适用”的错误消息。

我不明白为什么它不起作用。我尝试添加诸如 --whitespace=fix 等选项(在本网站的其他问题中建议),但无济于事。我还尝试使用 -3,希望我可以手动合并文件,但这只是将错误消息更改为“错误:补丁失败:文件名”,几乎所有文件都再次出现。


要重现此错误,我使用以下 git 存储库:https://git.evlproject.org/linux-evl.git

具体来说,有commit的分支是evl/v5.4,没有commit的分支是master。我当时试过:

git diff evl/v5.4 master > ../patchfile
git checkout master
git apply ../patchile

如果应用了这样的补丁,那就有点意外了:

git diff evl/v5.4 master > ../patchfile

记住 git diff 比较两个提交,或者更准确地说,比较两个提交中的快照。我喜欢将这两个提交称为 LR,用于“左”和“右”,尽管没有通用的 agreed-upon 命名这里的约定。

对于 L (left-side) 提交,您选择 evl/v5.4 选择的提交。对于 R (right-side) 提交,您选择了 master 选择的提交。到目前为止没问题。

现在,请记住 git diff 的输出是一系列指令。如果应用这些指令,将更改出现在提交 L 中的文件集,以生成出现在提交 R 中的文件集。换句话说,此 git diff 的输出给出了将 evl/v5.4 更改为 master 的指令。通常,这将包括形式为 path/to/file.ext 的第 45 行之后添加以下三行的说明,这些说明出现在此上下文中 删除一行some/file 的以下几行出现在以下上下文中 .

上下文L中的内容,说明(如果应用) 生成 R.

中的内容
git checkout master

这将获得提交 R。您没有提交 L。将 L 更改为 R 的说明在这里毫无意义。

你可以reverse-apply补丁。毕竟,将 L 变为 R 的指令可以“向后跟随”,可以说是将 R 变成 L。嗯,也就是说,只要none的指令只是删除文件F,因为那需要创建一个新文件F。如果指令说删除内容为...的文件F,我们可以用它来创建新文件F.

关于这个主题的变体...

how to ... extract commits from some branches and apply [them] to other branches

一次提交一个快照,而不是一组更改。但这不仅仅是只是一个快照:它是一个快照加上一些关于快照的信息。此 元数据 或额外信息 关于 数据(快照即数据)包括提交人的姓名和电子邮件地址。它包括一些 date-and-time-stamps。它包括一条日志消息,这几乎是任意的,取决于提交的人。但对于 Git 来说重要的是,它 包括原始的 散列 ID 一些 更早的 ] 提交。

Git 通过哈希 ID 查找每个提交。哈希 ID 本质上是提交的“真实名称”。提交的哈希 ID 永远不会改变,提交本身的内容也永远不会改变。 (Git 通过将其每个内部对象存储在 key-value database 中来确保这两者,其中密钥是哈希 ID,而哈希 ID 是存储在该密钥下的内容的加密校验和.)

A branch name 简单地保存一些提交链中 last 提交的哈希 ID。链条可以非常简单和线性,而且很多都是。如果我们用大写字母来代表哈希ID,我们会得到这样一张图:

... <-F <-G <-H

其中 last 提交是 right-most 提交,即提交 H。此提交包含数据(每个文件的完整快照)和元数据:创建者、时间和原因,以及 早期提交的哈希 ID G.

我们选择一个我们想用来 find H 的分支名称,并让 Git 存储提交的实际哈希 ID H 在那个名字中:

...--F--G--H   <-- master

我已经停止绘制 backwards-pointing 箭头 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。只要箭头继续前进,我们就可以保持这种状态,一直回到有史以来的第一次提交。 (作为第有史以来第一次提交,它没有 backwards-pointing 箭头,这就是我们/Git 知道停止返回的方式。)

这意味着存储库中的提交存储库中的历史记录。历史不过是承诺。提交全部连接,向后。存储库只是提交的集合,名称——分支名称,或任何其他名称——只是给我们一种进入提交的方式。

要向此存储库添加 提交,我们检查现有提交 H:

...--G--H   <-- master (HEAD)

这使得 master 成为当前的 分支 并提交 H 当前的 commit,所有这些我们可以使用特殊名称 HEAD 查找,该名称现在附加到名称 master.

然后,我们对一些实际上不在 Git 中的文件进行一些更改。 (Git 中的文件无法更改。)我们已 Git 将这些文件复制到新的提交中,添加一些元数据——包括姓名和电子邮件地址,以及“现在”作为例如,作者和提交者的时间戳——然后将其全部散列并获得一个新的、唯一的散列 ID。 (时间戳有助于确保此提交获得一个全新的哈希 ID,即使 其他所有内容 都相同,但通常新提交中的数据与新提交中的数据不同之前的提交 ... 而且,父哈希 ID 将不匹配。但时间也不匹配。)我们新提交的 parent 将是 commit H。 Git 现在可以写出所有数据和元数据,从而进行新的提交。我们将它的大丑 random-looking 哈希 ID 称为 I,并将其绘制进来,指向 H:

...--F--G--H
            \
             I

偷偷摸摸的把戏来了:Git 简单地将 I 的哈希 ID 写入 name master,特殊的已附加名称 HEAD。所以我们毕竟不需要在自己的线上绘制 I

...--F--G--H--I   <-- master

现有提交中的任何内容均未更改。 New commit Ilast 一个,它指向 H分支名称 已更改,或者更确切地说,存储在 分支名称的哈希 ID 已更改。该名称指向最后一次提交——实际上是根据定义。如果我们强制 Git 将名称指向提交 H,提交 I 就会从视图中消失:它仍然存在,但我们无法再找到它,除非我们保存它某处的哈希 ID。

现在,无论发生什么,我们都有这些图形事物之一,分支名称指向每个链中的最后一次提交。所以如果我们有,说:

          I--J   <-- branch1
         /
...--G--H   <-- master
         \
          K--L   <-- branch2

那么 branch2 上的 last 提交是 L,[=68= 上的 last 提交] 是 J 最后 master 上的提交是 H。提交 H 实际上是在 所有三个 分支上,因为在 Git 中,“在一个分支上”的概念只是意味着我们可以从最后开始—— Git 的方式,向后 - 并向后工作以达到给定的提交。从 L,我们可以跳到 K,然后跳到 H,所以提交 Hbranch2。或者,使用名称 master,我们从 H 开始,因此提交 Hmaster.

同时,如果我们采用任何 parent/child 对——比如 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 应用这些说明。我们可以通过找到提交 KL 以及 运行ning:

的两个哈希 ID 来完成此操作
git diff <hash-of-K> <hash-of-L>

获取这些说明。然后我们可以尝试在我们现在签出的文件上使用这些指令。 它们可能无法正常工作,因为可能有些文件不见了,或者我们应该更改第 42 行的某些文件不再有该行。但我们可以尝试应用这些更改。

要在 Git 中自动执行此操作,我们不必使用 git diffgit patch。相反,我们可以使用 git cherry-pick。这实际上相当奇妙,因为 cherry-pick 使用 Git 的内部 合并机制 合并 变化。但是,现在,您可以将 cherry-pick 视为 比较父项和子项,找出差异,并将差异应用于我们现在的任何提交.

因为Git有图表,提交K连接(向后)提交J,我们只需要告诉Git到cherry-pick提交的哈希 ID K:

git cherry-pick <hash-of-K>

有一些更简单、更短的指定特定提交的方法,不需要输入整个哈希 ID。当然,没有人会首先尝试输入整个哈希 ID:我们使用 cut-and-paste 来复制哈希 ID。打错东西太容易了(不过,幸运的是,哈希 ID 足够稀疏,这只会导致 Git 说 whaddaya talkin' bout?!)。但我不会在这里深入探讨;现在这些就够了。


[编辑,2021 年 1 月 2 日] 克隆问题中的存储库后,我可以 运行 以下操作。请注意,当前分支是 master 并且 work-tree 最初没有未跟踪的文件。 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 末尾的提交 中,因此在 diff 中作为新文件。因此,当 Git 应用差异时,它 创建 这些文件。

如果您应用差异 而没有 --index,Git 将差异应用于您的 work-tree(仅)。然后您必须使用 git add 添加更新的文件。但是由于新创建的文件列在 .gitignore 中,如果您单独添加它们,它们会 被忽略 master 中不存在整个 tools/perf/lib/include/perf/ 目录,因此 currently-checked-out 提交的索引中没有此类文件。那些文件 evl/v5.4 的提交中,所以如果你 运行 git checkout evl/v5.4,它们会在 Git' s 索引: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 和行 * 的提交都不会添加提交中的任何文件。然而,该提交将包含它包含的文件,并且检查它将使您提交这些文件。只是将这些文件提取到 otherwise-empty 存储库,然后使用 git add,不会进行存储同一树的提交。您将获得的提交是 path-dependent.

我认为这样的 .gitignore 文件至少是可疑的,而且一般来说是错误的,尽管有些人认为它很好(因为你可以使用 git add -f 来覆盖忽略,或者暂时移动 .gitignore 将文件移开,或其他)。这个特定的 linux-evl 提交就是这样的一个提交,它一开始就把我们俩都绊倒了。