[英]Git rebase while maintaining the latest version of a file in one branch
我在我的本地分支中有一個文件,我希望能夠對origin/main
進行變基,同時確保在變基之后我本地分支中的這個文件將與現在完全相同。
有沒有辦法做一個變基並保證? 如果在變基期間我不必回答任何問題或解決此文件的任何沖突,那就更好了。
使用臨時標記來標記具有所需文件副本的提交。 然后,使用git rebase -i
和 insert x
命令在每次pick
后運行一個簡短的腳本。 您可以准確地選擇要放入此腳本中的內容,但這(未經測試)可能是您想要的:
#! /bin/sh
git checkout temp-tag -- path
git diff-index --quiet HEAD || git commit --amend --no-edit
一旦這一切都完成了,刪除臨時標簽(和腳本;這並不難寫,而且它有標簽和路徑硬編碼)。
要理解這個答案,首先要記住這個事實:在 Git 中,文件實際上並不在分支中。 文件確實在commits中。
提交包含在分支中——或者換句話說,通過使用分支名稱找到,然后通過 Git 在每個提交中存儲的鏈接從提交工作到提交,向后。 因此,您可以 go 從分支名稱提交,然后提交文件。 但是“提交”這一步很關鍵,因為每次提交都有每個文件的完整快照。
接下來我們看看git rebase
是干什么的,是怎么做的。 請記住 Git 是關於提交的,每個提交都有一個唯一的 hash ID。 任何現有提交的任何部分都不能更改。 因此,由於 rebase 從字面上看不能更改任何現有提交,因此它必須通過將舊的(和糟糕的,或者至少在某種程度上不充分的)提交復制到新的和改進的提交來工作。 這些新的和改進的提交在某些方面與舊提交相同,但在某些方面有所不同。
根據其唯一的 hash ID 找到的每個提交都包含兩個部分:
有提交的主要數據:與此提交一起使用的源代碼快照。 這些不是變化。 如果稍后檢出一個特定的提交,快照中的每個文件都與它應該出現的完全一樣。
除了數據之外,每個提交都有一些元數據,或者關於提交本身的信息:提交人(名稱和 email 地址)、時間(日期和時間戳)等等。
元數據將“誰進行了此提交”分為兩部分:作者是最初提交者的姓名 email 和時間戳,提交者是姓名 email 以及進行此變體的人的時間戳提交。 所以當我們像這樣復制一個舊的提交時,我們保留了原作者,但設置了一個新的提交者。 如果您正在復制自己的提交,這意味着名稱和電子郵件並沒有真正改變——舊的有你兩個,新的有你兩個——但提交者時間戳確實改變了。
不過,最重要的是,每個提交都會記錄其先前或父提交的 hash ID。 變基的要點通常是像這樣進行一串提交:
I--J--K <-- feature /...--G--H--L <-- mainline
並制作提交I
、 J
和K
的新版本和改進版本,以便新提交來自L
而不是H
:
I--J--K <-- feature /...--G--H--L <-- mainline \ I'-J'-K' <-- new-and-improved-feature
其中提交I'
是提交I
的“副本”(某種程度上), J'
是J
的副本,而K'
是K
的副本。
無需過多擔心復制過程的機制——盡管我會在這里提到它使用git cherry-pick
讓我們做最后一個觀察,即我們(和 Git)查找提交的方式是使用分支名稱找到鏈中的最后一次提交。 當提交H
是mainline
的最后一次提交時,我們發現它是因為我們有:
...--G--H <-- mainline
名稱mainline
持有提交H
的 hash ID。 因此git checkout mainline
將提取提交H
供我們使用或處理/處理。 但是后來我們,或者某人,做了一個新的提交,添加到mainline
,我們稱之為提交L
,所以我們有:
...--G--H--L <-- mainline
名稱mainline
現在擁有提交L
的 hash ID。 git checkout mainline
命令將提取提交L
供我們使用。 為了找到提交H
,我們必須讓 Git 打開提交L
並讀取它的元數據。 此元數據包含早期提交H
的原始 hash ID。
這對我們來說意味着一旦我們完成了這個:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- new-and-improved-feature
我們可以從提交K
中刪除名稱feature
並將其粘貼到提交K'
上,如下所示:
I--J--K ???
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- feature
現在,當我們嘗試查看分支feature
上有哪些提交時,我們將從 Git 開始,使用名稱feature
來定位提交K'
。 提交K'
指向早期的提交J'
,后者指向I',
后者指向L
。 一旦我們移動分支名稱,我們的 rebase 將完成,並丟棄我們在構建I'-J'-K'
序列時可能使用的任何時髦的特殊名稱。
(練習:提交IJK
會發生什么?這重要嗎?我們怎么知道它們是否仍在存儲庫中?)
git rebase
是如何工作的我在上面相當簡短地提到, git rebase
使用git cherry-pick
來復制每個提交。 反過來,cherry-pick 命令的工作方式是……好吧,從技術上講,它是一個成熟的三向合並,但首先通過查看我們僅比較兩個提交時發生的情況,可以更容易地看到它。
讓我們從這張“之前”的照片開始:
I--J--K <-- feature
/
...--G--H--L <-- mainline
我們需要讓 Git檢出提交L
,這是我們想要新提交 go 的地方。如果我們以正常方式執行此操作,我們將創建一個新的分支名稱,例如tmp
,使用:
git checkout -b tmp <hash-of-L>
(或與 Git 2.23 或更高版本中的git switch
命令相同)。 Git 實際上為此使用了所謂的分離 HEAD模式,特殊名稱HEAD
直接指向提交:
git checkout <hash-of-L>
要么:
git switch --detach <hash-of-L>
產生這個:
I--J--K <-- feature
/
...--G--H--L <-- HEAD, mainline
現在 Git 運行git cherry-pick hash-of-I
。 Git 在整個設置過程中保存了提交I
、 J
和K
的 hash ID。 如果您在此處使用git rebase --interactive
,您將看到列出這些 hash ID 的pick
命令。 1 pick
表示 cherry-pick 命令。
cherry-pick 本身最終會將提交H
中保存的快照與提交I
中保存的快照進行比較。 這兩個快照之間的區別實際上是一組也可以應用於快照的指令。 將該指令集應用於H
中的快照會生成I
中的快照。 但是,如果我們將這些指令應用於L
中的快照呢?
如果我們這樣做 - 並假設它有效並且沒有合並沖突2 - 並根據結果進行新的提交,我們將獲得提交I'
。 我們將 Git 原樣保存原始作者信息和原始提交消息,並生成一組新的提交者信息並使用我們通過應用 diff 獲得的快照。 結果是:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I' <-- HEAD
Git 現在繼續執行git cherry-pick hash-of-J
,通過比較I
-vs- J
並將其應用於I'
來復制提交J
:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J' <-- HEAD
最后——因為只有三個提交——我們最后一次選擇提交K
,它比較J
-vs- K
(如果你對 cherry-pick 的合並方面感興趣的話,還有J
-vs- J'
)來構建提交K'
,這給我們留下了這個:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- HEAD
剩下的唯一任務是將名稱feature
移動到指向當前提交K'
以獲取:
I--J--K ???
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- feature (HEAD)
這樣就完成了變基過程。
1您要編輯的git rebase
的說明表具有縮寫的 hash ID。 我一直不太清楚為什么:Git 必須將它們擴展回來才能在內部使用它們。 也許 Git 的人只是認為當有 7 或 12 個看起來隨機的字符而不是 40 個時,他們看起來不那么令人生畏。對於git describe
output,這可能是 go 在某人的 email 或其他東西中,當然只是 - 但在這里,他們的說明一個臨時頁面,如果你編輯它們,你可以在你的編輯器中使用“移動行”指令。
2合並沖突(如果有的話)也來自比較H
中的快照與L
中的快照。 至少,第一次挑選就是這種情況。 隨后的兩個 cherry-picks 使用提交I
和J
作為合並基礎, --ours
提交是在上一步中構建的提交。 這就是一切變得有點棘手的地方。
我相信您想要的是,在每次挑選之后,您希望新(復制)中的某些特定文件與某些特定的早期提交中的某些特定文件完全匹配。
假設現有提交K
具有所需的文件版本。 我們要做的是——為了避免依賴 Git 不移動名稱feature
,並讓你選擇任何提交——是創建一個臨時的輕量級標簽來標識這個提交:
git tag temp-tag <hash-of-K-or-whatever>
注意:如果沒有一個固定版本的文件應該 go 到每個復制的提交中,您將需要一個不同的策略來定位checkout
的源提交,但 rest 可以繼續工作。
接下來,我們將使用git rebase -i
。 這會將精選集變成可編輯的說明書。 使用我們的編輯器,在每個pick
命令之后,我們使用exec
或x
命令添加一行:
pick <hash>
x /tmp/script
(假設我們的小腳本已放入/tmp/script
並可執行)。
Git 將執行 cherry-pick 命令,一直執行到完成,這涉及進行新的提交(在我們的示例中為I'
、 J'
或K'
)。 然后它會因為這個x
行而運行腳本。 劇本:
從特定提交中提取特定文件:使用temp-tag
,我們從所需提交中獲取所需文件,並將其放入 Git 的索引和工作樹中。 (索引副本是最重要的,但最好也更新工作樹,如果沒有別的,為了理智起見。)
測試結果是否值得替換提示提交 ( git commit --amend
)。 這是我們的git diff-index --quiet HEAD
。 如果索引仍然與當前提交匹配,則無需更改。 否則,我們將運行git commit --amend
,它將當前提交推開並創建一個新提交。 使用--no-edit
,我們告訴git commit
簡單地重新使用現有的提交消息。
注意:在這種情況下,即使沒有任何變化, git commit --amend --no-edit
其實也是安全的,只是白費力氣。 對於這個腳本和任務,這可能並不真正相關,但不執行大量不必要的工作似乎很好。
因此,這將確保在變基期間每個替換提交本身都被替換,並使用“更正”的替換將單個文件換出到我們想要的文件。 這樣,當 Git 開始將分支名稱從舊分支中拉出來並將其放在替換提交的末尾時,每個替換提交都是實際需要的新的和改進的提交。
除了清理(刪除輕量級temp-tag
標簽和刪除腳本)之外,不需要做任何其他事情。
一種解決方法是將文件復制粘貼到暫存器,使用-Xours
運行變基,然后從暫存器粘貼最終結果。
我真的不喜歡這種解決方案(如果我們討論的是多個處於沖突狀態的文件,它也不會一概而論)但它似乎是最快的前進方式。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.