[英]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
比較兩個提交,或者更准確地說,比較兩個提交中的快照。 我喜歡將兩個提交稱為L和R ,分別代表“左”和“右”,盡管這里沒有共同商定的命名約定。
對於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
上的最后一次提交是L
, branch1
上的最后一次提交是J
, master
上的最后一次提交是H
。 提交H
實際上是在所有三個分支上,因為在 Git 中,“在一個分支上”的概念只是意味着我們可以從最后開始——就像 Git 所做的那樣,向后——並向后工作以達到給定的提交。 從L
,我們可以跳到K
,然后到H
,所以提交H
在branch2
上。 或者,使用名稱master
,我們從H
開始,因此提交H
在master
上。
同時,如果我們采用任何父/子對——比如說, 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 應用這些指令。 我們可以通過查找提交K
和L
的兩個 hash ID 並運行:
git diff <hash-of-K> <hash-of-L>
獲取這些說明。 然后我們可以嘗試在我們現在簽出的文件上使用這些說明。 它們可能無法全部工作,因為可能某些文件已經消失,或者我們應該更改第 42 行的某些文件不再具有該行。 但我們可以嘗試應用這些更改。
要在 Git 中自動執行此操作,我們不必使用git diff
和git 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。 將--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 將差異應用於您的工作樹(僅)。 然后您必須使用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.