簡體   English   中英

如何真正應用使用 Git 差異創建的補丁?

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

我一直在這個網站上閱讀很多相關/類似的問題,但沒有一個會起作用,而且我似乎沒有看到同樣的錯誤,所以我決定就此提出一個新問題。

我正在嘗試更多地了解 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 diff evl/v5.4 master >../patchfile

請記住, git diff比較兩個提交,或者更准確地說,比較兩個提交中的快照。 我喜歡將兩個提交稱為LR ,分別代表“左”和“右”,盡管這里沒有共同商定的命名約定。

對於L (左側)提交,您選擇evl/v5.4選擇的提交。 對於R (右側)提交,您選擇了master選擇的提交。 到目前為止,這沒有問題。

現在,請記住git diff中的 output 是一系列指令。 如果應用這些指令,將更改提交L中出現的文件集,以生成提交R中出現的文件集。 換句話說,這個git diff的 output 給出了將evl/v5.4更改為master的指令。 通常,這將包括以下形式的指令:在出現在此上下文中的path/to/file.ext的第 45 行之后添加以下三行some/file出現在以下上下文

上下文L中的內容,指令(如果應用時)產生R中的內容。

 git checkout master

這將獲得提交R 你沒有提交L L更改為R的說明在這里毫無意義。

您可以反向應用補丁。 畢竟,將L轉換為R的指令可以“向后執行”,將R轉換為L 好吧,也就是說,只要沒有任何指令只是刪除文件 F ,因為這需要創建一個新文件F 如果指令說刪除內容為...的文件 F ,我們可以使用它來創建新文件F

關於這個主題的一個變體......

如何... 從某些分支中提取提交並將 [它們] 應用到其他分支

提交一個快照,而不是一組更改。 但它不僅僅是一個快照:它是一個快照加上一些關於快照的信息。 元數據有關數據的額外信息(即數據的快照)包括提交人的姓名和 email 地址。 它包括一些日期和時間戳。 它包括一條日志消息,這幾乎是任意的,取決於提交的人。 但重要的是,對於 Git,它還包括一些早期提交的原始hash ID

Git 通過其 hash ID 找到每個提交。 hash ID 本質上是提交的“真實姓名”。 提交的 hash ID 永遠不會改變,提交本身的內容也永遠不會改變。 (Git 通過將每個內部對象存儲在鍵值數據庫中來確保這兩者,其中鍵是 hash ID,而 hash ID 是存儲在該鍵下的內容的加密校驗和。)

分支名稱僅包含某個提交鏈中最后一次提交的 hash ID。 鏈條可以非常簡單和線性,而且很多都是。 如果我們使用大寫字母來代表 hash ID,我們會得到如下圖:

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

最后一次提交是最右邊的一次,即提交H 此提交包含數據(每個文件的完整快照)和元數據:創建者、時間和原因,以及先前提交G的 hash ID

我們選擇一個我們想用來查找H的分支名稱,並讓 Git 以該名稱存儲提交H的實際 hash ID:

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

我已經停止將提交之間的向后箭頭繪制箭頭,但它們確實是每次提交中出現的一種箭頭。 只是,隨着提交內容永遠凍結, H將永遠指向G ,並且由於我們知道提交 hash ID 看起來是隨機的,因此G無法知道其未來父級H的 hash ID 將是什么,所以連接必須向后 go。

那么,給定名稱master ,我們有 Git 通過其 hash ID(存儲在名稱master中)找到提交H 給定提交H ,我們可以讓 Git 找到G的 hash ID:這是H中元數據的一部分。 給定G的 hash ID,我們可以讓 Git 找到提交G 因此,一旦我們找到了最后一個提交,我們就可以返回一跳,到倒數第二個提交。

當然,該提交也嵌入了一個 hash ID。 G ,我們可以跳回到F 只要箭頭繼續前進,我們就可以保持這種狀態,一直到第一次提交。 (作為有史以來的第一次提交,它沒有后向箭頭,這就是我們 / Git 知道停止返回的方式。)

這意味着存儲庫中的提交是存儲庫的歷史記錄。 歷史不過是承諾。 提交全部向后連接。 存儲庫只是提交的集合,而名稱(分支名稱或任何其他名稱)只是為我們提供了進入提交的方法。

要向此存儲庫添加提交,我們檢查現有提交H

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

這使得master成為當前分支並 commit H成為當前提交,所有這些我們都可以通過使用特殊名稱HEAD找到,現在附加到名稱master

然后,我們對一些實際上不在Git 中的文件進行一些更改。 (Git 中的文件無法更改。)我們將 Git 復制到新的提交中,添加一些元數據——包括名稱和 Z0C83F57C786A0B4A39EFAB22,以及“現在作為作者和提交時間戳”地址,實例—和 hash 這一切都完成並獲得一個新的、唯一的 hash ID。 (時間戳的東西有助於確保這個提交獲得一個全新的 hash ID,即使其他一切都相同,盡管通常新提交中的數據與前一次提交中的數據不同......而且,此外,父級 hash ID 不匹配。但時間也不匹配。)我們新提交的級將是提交H Git 現在可以寫出所有數據和元數據,從而進行新的提交。 我們將其稱為大而丑陋的隨機外觀 hash ID I ,然后將其繪制,指向H

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

現在出現了鬼把戲: Git 只需將I的 hash ID 寫入名稱master ,並附加特殊名稱HEAD 所以我們畢竟不需要在自己的線上畫I

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

任何現有提交中的任何內容都沒有改變。 提交I最后一個,它指向H 分支名稱已更改,或者更確切地說,存儲分支名稱中的 hash ID 已更改。 該名稱指向最后一次提交——實際上,根據定義。 如果我們強制 Git 將名稱指向提交H ,提交I就會從視圖中消失:它仍然存在,但我們再也找不到它,除非我們將其 hash ID 保存在某處。

現在,無論發生什么其他事情,我們都有這些圖形事物之一,分支名稱指向每個鏈中的最后一個提交。 因此,如果我們有,請說:

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

那么branch2上的最后一次提交是Lbranch1上的最后一次提交是Jmaster上的最后一次提交是H 提交H實際上是在所有三個分支上,因為在 Git 中,“在一個分支上”的概念只是意味着我們可以從最后開始——就像 Git 所做的那樣,向后——並向后工作以達到給定的提交。 L ,我們可以跳到K ,然后到H ,所以提交Hbranch2上。 或者,使用名稱master ,我們從H開始,因此提交Hmaster上。

同時,如果我們采用任何父/子對——比如說, KL ,它出現在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的兩個 hash 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 挑選 hash 提交K的 ID

git cherry-pick <hash-of-K>

有一些更簡單、更短的指定特定提交的方法,不需要輸入整個 hash ID。 當然,沒有理智的人會首先嘗試輸入整個 hash ID:我們使用剪切和粘貼來復制 hash ID。 打錯字太容易了(不過,幸運的是,hash ID 足夠稀疏,以至於這只會導致 Git 說whaddaya talkin' 'bout?! )。 但我不會 go 到這里; 這已經足夠了。


[編輯,2021 年 1 月 2 日] 克隆問題中的存儲庫后,我可以運行以下命令。 請注意,當前分支是master並且工作樹最初沒有未跟蹤的文件。 git clean -dfx產生 output。 --indexgit 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 將差異應用於您的工作樹(僅)。 然后您必須使用git add添加更新的文件。 但是由於新創建的文件列在.gitignore中,如果您單獨添加它們,它們將被忽略 master中不存在整個tools/perf/lib/include/perf/目錄,因此當前簽出提交的索引中沒有此類文件。 這些文件位於evl/v5.4尖端的提交中,因此如果您運行git checkout evl/v5.4 ,它們最終會出現在 Git 的索引中: git checkout出會將所有文件從所選提交復制到索引,即使這些文件名義上被忽略 但是我們的git apply方法不會將那些(新)文件復制到索引中,除非我們使用--index ,然后在隨后的git add *obeys 新創建的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提交就是這樣一個提交,一開始我們倆都被它絆倒了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM