【问题标题】:How do I really apply a patch created with Git diff?如何真正应用使用 Git diff 创建的补丁?
【发布时间】:2020-12-17 09:39:52
【问题描述】:

我在这个网站上阅读了很多相关/类似的问题,但没有一个会起作用,而且我似乎没有看到同样的错误,所以我决定就此提出一个新问题.

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

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

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


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

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

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

【问题讨论】:

    标签: git patch


    【解决方案1】:

    如果这样的补丁确实适用,那会有点令人惊讶:

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

    请记住,git diff 比较两个提交,或者更准确地说,比较两个提交中的快照。我喜欢将这两个提交称为 LR,分别代表“左”和“右”,尽管这里没有共同商定的命名约定。

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

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

    contextL 中的内容,指令(如果应用时)产生 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 提交是 Lbranch1 上的 last 提交是 J,而 last em> 在master 上的提交是H。提交H 实际上是在所有三个 分支上,因为在Git 中,“在一个分支上”的概念只是意味着我们可以从最后开始——就像Git 那样,倒退——然后工作向后到达给定的提交。从L,我们可以跳转到K,然后跳转到H,所以提交Hbranch2。或者,使用名称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 应用这些说明。我们可以通过找到提交KL 的两个哈希ID 并运行:

    git diff <hash-of-K> <hash-of-L>
    

    获取这些说明。然后我们可以尝试在我们现在签出的文件上使用这些说明。 它们可能无法全部工作,因为可能某些文件已经消失,或者我们应该更改第 42 行的某些文件不再具有该行。但我们可以尝试应用这些更改。

    要在 Git 中自动执行此操作,我们不必使用 git diffgit 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 提交就是这样一个提交,一开始我们俩都被它绊倒了。

    【讨论】:

    • 感谢您提供的大而翔实的答案。那么,这似乎只是在 git diff 中排序两个分支的问题。我交换了它,创建了一个新的补丁文件,它工作得更好,但仍然不完美。在您的分支示例中,我认为我做了相当于从 H 到 L 的差异,回到 H,然后应用这些更改。然而,这并没有把我带到L。我无法理解。是不是因为中间有commit?
    • 对 - 你已经交换了左右两边(所以要么使用 git apply -R 来反转应用程序,或者以其他方式运行差异或使用 -R 选项)。但是,一旦您以另一种方式进行差异,它应该适用。为了找出答案,我克隆了同一个项目并将对其进行测试。克隆需要很长时间!出于某种原因,我被限制在 200 KiB/s 左右。
    • 克隆终于完成了。使用相同的命令集(在创建本地分支evl/v5.4 之后)但git apply -R,我收到了一组 5 个已宣布的空白错误,还有一个关于 321 个的警告,并且成功了 - 尽管我留下了一大堆未跟踪的文件,显然来自git apply 无法处理的所有重命名。在制作补丁时,禁用重命名检测也是一个好主意(或使用 Git 的管道差异命令之一,无论用户配置或 Git 年份如何,它都没有重命名检测)。
    • 这里添加/删除的文件似乎仍然存在问题,因此应用于master 的补丁不会在evl/v5.4 中创建那些“新”文件,如果masterevl/v5.4(按此顺序)进行比较。我不太清楚发生了什么,因为补丁肯定可以添加和删除文件。
    • 感谢您花时间克隆并尝试重现错误。我注意到同样的事情:在 evl/v5.4 中创建的新文件在我修补 master 时不会出现。也许 master 不是 evl/v5.4 的直接祖先,但实际上是并行发展的东西,尽管与祖先有细微的差异。我将尝试使用 merge-base 来查找和祖先,然后从中创建一个新补丁。
    猜你喜欢
    • 2020-08-13
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-26
    • 2010-12-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多