[英]Git rebase when previous commit changed
我經常發現自己正在處理不同git分支上的兩個不同的工作票,但是一個依賴於另一個,如下所示:
* later-branch
|
* earlier-branch
|
* some prior commit
|
(每個都是一個提交,因為我們使用的是gerrit,但是這個問題也可能適用於每個提交的多個提交。)早期的分支可能正在進行審核,因此我可能必須返回並在某個時候修改它使用git commit --amend
。 必須發生的事情,這將分叉歷史:
* earlier-branch
|
| * later-branch
| |
| * previous version of earlier-branch
| /
* some prior commit
|
在這一點上,我要變基的later-branch
上的新版本之上earlier-branch
。 但是,如果我只是做一個git checkout later-branch
隨后是git rebase earlier-branch
,它總是得到沖突,因為(我覺得)它必須先申請的previous version of earlier-branch
提交到最新版本的earlier-branch
。
我最終做的是git checkout earlier-branch -b new-later-branch-name
然后是git cherry-pick later-branch
和git br -D later-branch
。 這是一種痛苦。 誰能建議一個更好的方法來處理這個?
我看到兩種簡單的方法來做到這一點。
第一個,最好的選擇是在交互模式下使用git rebase
。 要做到這一點,你會這樣做
git checkout later-branch
git rebase -i earlier-branch
在彈出的屏幕中,您將選擇drop
previous version of earlier-branch
:
drop efb1c19 previous version of earlier-branch
pick a25ba16 later-branch
# Rebase 65f3afc..a25ba16 onto 65f3afc (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
...
這將在earlier-branch
later-branch
的頂部重新earlier-branch
,提供以下樹:
* later-branch
|
* earlier-branch
|
| * previous version of earlier-branch
| /
* some prior commit
|
另一種選擇是簡單地做一個git cherry-pick
。 如果你這樣做:
git checkout earlier-branch
git cherry-pick later-branch
你會得到以下樹:
* earlier-branch -> cherry-picked commit 1
|
* earlier-branch -> amended commit 0, now commit 2
|
| * later-branch -> commit 1
| |
| * previous version of earlier-branch -> commit 0
| /
* some prior commit
|
因此,實際上,這將產生您想要的結果,但它將提前earlier-branch
。 如果分支名稱對您很重要,您可以相應地重命名和重置它們。
除了交互式rebase(如houtanb的回答 ),還有兩種方法可以更自然地執行此操作:
git rebase --onto
,或 要使用后者,可以在later-branch
上運行git rebase --fork-point earlier-branch
later-branch
。
(你可以改為將early earlier-branch
設置為later-branch
earlier-branch
的上游 later-branch
可能只是暫時的,在git rebase
期間 - 然后在later-branch
時運行git rebase
。原因是--fork-point
是使用自動上游模式時的默認值 ,但在使用git rebase
的顯式<upstream>
參數時必須顯式請求 。)
不幸的是,最后一個看起來特別神奇,特別是那些剛接觸Git的人。 幸運的是,你的圖表中有理解它的種子 - 以及git rebase --onto
。
讓我們把你上面繪制的東西轉向側面,然后再轉動一點。 這給了我一些繪制分支名稱的空間。 我會用圓o
節點或大寫字母和數字替換每次提交的*
s。 我將向后一個分支添加第三個提交C
,以便進行說明。
:
.
\
o
\
A1 <-- earlier-branch
\
B--C <-- later-branch
現在,無論出於何種原因,您都被迫將提交A1
復制到新提交A2
,並將分支標簽earlier-branch
移動到指向新副本:
讓我們把你上面繪制的東西轉向側面,然后再轉動一點。 這給了我一些繪制分支名稱的空間。 我將使用圓形o
節點或大寫字母替換每次提交的*
s。
:
.
\
o--A2 <-- earlier-branch
\
A1
\
B--C <-- later-branch
如果只有Git會記住提交A1
存在,因為earlier-branch
用於包含提交A1
,我們可以告訴Git:“當復制later-branch
,刪除現在仍在其上的任何提交,但過去僅僅是因為earlier-branch
“。
但Git 確實記得這個,至少30天,默認情況下。 Git具有reflogs -logs,用於存儲在每個引用中 (包括常規分支和Git的所謂遠程跟蹤分支 )。 如果我們將reflog信息添加到繪圖中,它看起來像這樣:
:
.
\
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
實際上,如果由於某種原因你必須將A2
復制到A3
,那么該圖只會增加另一個reflog條目,重新編號現有的:
:
. A3 <-- earlier-branch
\ /
o--A2 <-- [earlier-branch@{1}]
\
A1 <-- [earlier-branch@{2}]
\
B--C <-- later-branch
fork-point代碼的作用是掃描reflog以獲取其他引用,例如early earlier-branch
,並找到這些提交(在這種情況下, A1
-it實際上找到A1
和A2
,在后一種情況下,但是然后winnows它下到兩個分支上的A1
;另見Git rebase - 在fork-point模式下提交select 。 然后它為你運行git rebase --onto
,就像你手動運行一樣:
git rebase --onto earlier-branch hash-of-A1
這讓我們了解了--onto
參數的工作原理。
--onto
通常,您可以使用一個參數運行git rebase
,如git rebase branch-name
,甚至根本沒有參數。 根本沒有參數, git rebase
使用當前分支的上游設置。 使用branch-name
參數, git rebase
調用該參數<upstream>
。 (作為一個奇怪的副作用,這也是 - 因為Git版本2.0無論如何 - 自動啟用或禁用--fork-point
選項,要求你使用顯式--no-fork-point
或--fork-point
如果你想另一種模式。)
在任何情況下,如果你沒有指定一個用於兩個目的,Git會自動使用<upstream>
- 選擇。 一種是限制將被復制的提交集:Git將考慮通過運行復制列出的提交集:
git rev-list <upstream>..HEAD
要以更友好的方式查看它們,請使用git log
或我首選的方法git log --oneline --decorate --graph
,而不是git rev-list
here:
git log --oneline --decorate --graph earlier-branch..HEAD
理想情況下,我們會在這里看到提交B
和C
,首先列出C
(Git必須使用--reverse
以確保它首先復制B
)。 但是,如果您將A1
復制到A2
和/或復制到A3
,並移動了分支earlier-branch
,我們將看到所有A1
, B
和C
(Git不包括A2
或A3
以earlier-branch
點為准 - 但它們無論如何都不在列表中。然后使用排除的A2
或A3
排除A1
之前的提交,這就是為什么我們看不到這些。)
此<upstream>
分支名稱(或提交哈希)的另一個目的是選擇副本的位置 。 當我們復制一個或多個提交時,每個復制的提交必須在一些現有提交之后進行。 <upstream>
參數提供將作為我們復制的第一個提交的父級的提交的ID。
因此,運行git rebase earlier-branch
會使Git列表按順序提交A1
, B
和C
然后使用“分離的HEAD”模式 - 將A1
復制到earlier-branch
:
:
. A1' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
然后將B
復制到A1'
:
:
. A1'--B' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
調整基線然后復制C
到C'
和移動分支標簽, later-branch
,到哪里HEAD
卷起,再附上你的頭在這個過程中:
:
. A1'--B'--C' <-- later-branch (HEAD)
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- [later-branch@{1}]
--onto
參數讓你告訴Git 副本去哪里 。
--onto
與git rebase
當您添加--onto
,您告訴Git rebase將副本放在何處。 這釋放了<upstream>
參數,現在它只指定不要復制的內容! 所以現在你可以自由地告訴Git:“通過寫入來復制提交A1
之后的所有內容”:
git rebase --onto earlier-branch <hash-of-A1>
Git做了它常用的事情,列出要復制的提交( B
和C
),從later-branch
分離你的HEAD,一次復制一個提交,副本跟在earlier-branch
的尖端之后,最后移動名字later-branch
重新連接你的HEAD。
這正是我們想要的,都是半自動完成的:我們告訴Git 不要復制A1
本身,所以它只復制B
和C
當我們指定上游時,就像在git rebase earlier-branch
,Git 禁用 fork-point模式。 如果我們明確啟用它,Git將通過earlier-branch
reflog。 只要提交A1
的reflog條目尚未到期,Git就會發現A1
曾經在earlier-branch
並且將使用--onto
為我們從to-copy列表中丟棄它。
請注意,這里有一點危險。 如果我們真的想要 A1
會怎么樣呢,例如,如果我們支持earlier-branch
到A1
只是因為我們意識到A1
不屬於另一個分支? Git仍然認為我們將它復制到其他一些提交中,並且現在不想復制它,並且會拋棄列表。 幸運的是,你總是可以撤消一個rebase:rebase根本不丟棄任何東西,它只是復制 。 然后它會更新一個分支,它將以前的值保存在分支的reflog中。 但是通過reflogs釣魚,試圖找到一組特定的提交,在一個完全相同的提交迷宮中,並不是很有趣 - 所以在運行rebase之前考慮一下是否明智--fork-point
無論有沒有--fork-point
。
在一些(罕見)情況下,Git的你不必做任何事情(無分叉點模式,無需手動--onto
分離,沒有--interactive
)。 具體來說,如果補丁本身根本沒有改變,但只有提交消息中的措辭發生了變化,Git將檢測已經復制的提交並跳過它。 這是因為git rebase
實際上使用git rev-list
的對稱差異模式和--cherry-pick --right-only --no-merges
選項。 也就是說,而不是:
git rev-list <upstream>..HEAD
Git實際上運行:
git rev-list --cherry-pick --right-only --no-merges <upstream>...HEAD
(注意三個點)。 不過,我沒時間在這里詳細介紹。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.