[英]Git: How to squash all commits between two commits into a single commit
我有一個分支機構,過去幾個月我一直在幾台計算機上親自工作。 結果是一個很長的歷史鏈,我想在將它合並到主分支之前進行清理。 最終目標是擺脫我在處理服務器代碼時經常做的所有wip提交。
以下是gitk歷史可視化的屏幕截圖:
在這個底部的方式是我從主人分支的點。 自從我開始這個分支以來,Master已經改變了一點,但是這些變化是不相交的,所以合並應該是小菜一碟。 我通常的工作流程是重新加入master,然后壓縮wip提交。
我試着執行一個簡單的
git rebase -i master
我編輯了對sqush的提交。
它似乎開始很好,但后來失敗了,並希望我解決沖突。 然而,似乎沒有好辦法通過觀察差異來解決它。 每個部分都使用范圍中未定義的變量,因此我不確定如何解決它們。
我也嘗試使用git rebase -i -s recursive -X theirs master
the git rebase -i -s recursive -X theirs master
,這不會導致沖突,但它改變了HEAD從修改后的分支狀態(我想以最終結果的方式編輯歷史記錄)在HEAD中不會改變)。
我相信這些沖突來自於你可以看到鑽石圖案的鏈條部分。 (例如,在重新分類的分類器......和Merge分支iccv之間)。
為了更好地表達我的問題,讓A
=“Merge branch iccv”, B
=“reworked classifiers”指的是圖像中的示例。 中間的提交將是X
和Y
...
|
|
A
/ \
| X
Y |
\ /
B
|
|
...
我想重寫歷史記錄,以便A
的狀態完全如此,並有效地破壞中間表示X
和Y
,因此生成的歷史記錄看起來像這樣
...
|
|
A
|
|
B
|
|
...
有沒有辦法將A
, X
和Y
的已解決狀態Y
到像這樣的歷史鏈中間的單個提交中?
如果A
和B
是提交的SHAID,那么我可以運行一個簡單的命令(或者可能是腳本)來實現我想要的結果嗎?
如果A
是HEAD,我相信我能做到
git reset B
git commit -am "recreating the A state"
創建一個新頭,但如果A
位於這樣的歷史鏈中間,我該怎么做呢。 我想保留它之后的所有節點的歷史記錄。
首先使當前工作樹清理,然后運行以下命令:
#initial state
git branch backup thesis4
git checkout -b tmp thesis4
git reset A --hard
git reset B --soft
git commit
git cherry-pick A..thesis4
git checkout thesis4
git reset tmp --hard
git branch -D tmp
S
是X,Y,A
的壁球。 M'
相當於M
和N'
對於N
如果要恢復初始狀態,請運行
git checkout thesis4
git reset backup --hard
這可以做到,但是通過常規機制,它可以從一點痛苦到很多痛苦。
根本問題在於,只要您想要更改內容,就必須將提交復制到新的(略有不同的)提交。 原因是任何提交都無法改變 。 1原因是提交的哈希ID 是提交,在非常真實的意義上:Git的哈希ID是Git如何找到底層對象。 更改對象中的任何位,它將獲得一個新的,不同的哈希ID。 2因此,當你想要去的時候:
X
/ \
...--B A--C--D--E <-- branch
\ /
Y
到看起來像這樣的東西:
...--B--A--C--D--E <-- branch
B
之后的東西不能是A
,它必須是一個不同的提交,只是聞起來像A
我們可以將此提交A'
稱為分開:
...--B--A'-...
但是如果我們將A
復制到一個新的,更新鮮的(但相同的樹) A'
,它的歷史中不再有中間的東西 - 也就是說, A'
直接連接到B
我們還必須復制第一個提交后的 A'
。 一旦我們這樣做,我們必須在那之后復制提交,依此類推。 結果是:
...--B--A'-C'-D'-E' <-- branch
1心理學家喜歡說改變很難 ,但對於Git來說,這幾乎是不可能的! :-)
2 Hash沖突在技術上是可行的 ,但如果它們發生,則意味着您的存儲庫停止添加新內容。 也就是說,如果你設法提出一個類似舊的提交但是有你想要的更改並且具有相同的哈希ID的新提交,Git會禁止你添加它!
git rebase -i
注意:如果可能,請使用此方法; 它更容易理解和正確。
復制這樣提交的標准命令是git rebase
。 但是,對於像A
這樣A
合並提交,rebase交易非常糟糕。 事實上,它通常會完全拋棄它們,而是傾向於線性化所有東西:
...--B--X--Y'-C'-D'-E' <-- branch
例如。
現在,如果合並提交A
進展順利,即X
任何內容都不依賴於Y
,反之亦然,那么簡單的git rebase -i <hash-of-B>
就足夠了。 你可以改變除了提交X
和Y
的第一個pick
的所有提交 - 實際上可能是許多提交 - squash
並且所有事情都進展順利並且你完成了:Git完全支持單個X
和Y'
組合XY'
提交,它具有您的合並提交A
具有的相同樹。 結果是:
...--B--XY'-C'-D'-E' <-- branch
如果我們稱之為XY'
A'
,然后忘記自己原來的散列的ID刪除所有刻度線,我們可以得到你想要什么。
git replace
但是,如果合並很困難,那么你想要的是保留合並中的樹 ,同時刪除所有的X
和Y
提交。 這里git replace
是(或者)正確的解決方案 。 Git的替換有點復雜,但你可以指示Git創建一個新的提交A'
,它“像A
一樣A
但是B
是它的單父哈希ID”。 Git現在將具有此提交圖結構:
X
/ \
...--B A--C--D--E <-- branch
|\ /
| Y
\
A' <-- refs/replace/<complicated-thing>
這個特殊的refs/replace
名稱告訴Git,當它執行諸如git log
和其他使用提交ID的命令之類的事情時,Git應該將其隱喻的眼睛從提交A
轉移而不是提交A'
。 由於A'
是否則的副本 A
, git checkout <hash of A>
使得GIT中看看A'
,並檢查了相同的樹; 當它看起來放在A'
而不是A
時, git log
顯示相同的日志消息。
請注意,此時存儲庫中都存在A
和A'
。 它們是並排的,Git只是向你顯示A'
而不是A
除非你使用特殊的--no-replace-objects
標志。 一旦Git向你顯示(並使用) A'
代替A
,它就會跟隨從A'
到B
的向后鏈接,跳過X
和Y
所有。
X
和Y
一旦您對替換感到滿意,您可能希望將其永久化。 您可以使用git filter-branch
執行此操作,它只是復制提交。 它從一些起點開始復制並在歷史上向前移動,與Git的正常向后相反“從今天開始,在歷史中向后工作”的方式。
當filter-branch正在制作它的副本時 - 以及它的復制內容列表 - 它通常會像Git的其余部分一樣完成另一個避開眼睛的事情。 因此,如果我們有上面顯示的歷史記錄,並且我們告訴filter-branch在branch
上結束並在提交B
之后啟動,它將收集現有的提交列表,如下所示:
E, D, C, A'
然后顛倒順序。 (事實上,如果我們願意,我們可以停在A'
,正如我們所看到的那樣。)
接下來,filter-branch將A'
復制到新的提交。 這種新的承諾將B
作為其母公司,相同的日志消息A'
同一棵樹,同樣的作者和日期,郵票等,總之,它簡直就等同於A'
。 因此它將獲得與A'
相同的哈希ID,並且實際上是提交A'
。
接下來, filter-branch
將C
復制到新的提交。 這個新提交將使用A'
作為其父級,與C
相同的日志消息以及相同的樹等等。 這與原始C
略有不同,原始C
的父級是A
,而不是A'
。 所以這個新的提交獲得了一個不同的哈希ID:它變成了提交C'
。
接下來, filter-branch
將復制D
這將成為D'
,就像C
的副本是C'
。
最后, filter-branch
將E
復制到E'
並使branch
指向E'
,給我們這樣:
X
/ \
...--B A--C--D--E <-- refs/original/refs/heads/branch
|\ /
| Y
\
A' <-- refs/replace/<complicated-thing>
\
C'-D'-E' <-- branch
我們現在可以刪除refs/replace/
name以及refs/heads/branch
的備份副本,filter-branch用來保存原始E
當我們這樣做時,名稱就會消失,我們可以重新繪制圖表:
...--B--A'-C'-D'-E' <-- branch
這正是我們想要(並獲得)使用git rebase -i
,但無需再次進行合並。
要告訴git filter-branch
要停止的位置 ,請使用^<hash-id>
或^<name>
。 否則git filter-branch
將不會停止列出提交,直到它用完提交:它將跟隨提交B
到其父級,並且跟隨父級的父級,依此類推,直到歷史記錄為止。 這些提交的副本將與原始文件一點一點地相同,這意味着它們實際上將是原件,相同的哈希ID和所有; 但他們需要很長時間才能完成。
由於我們可以停在<hash-id-of-B>
甚至<hash-id-of-A'>
,我們可以使用^refs/replace/<hash>
來識別提交A
或者我們可以使用^<hash-id>
,這可能實際上更容易。
此外,我們可以編寫^<hash> branch
或<hash>..branch
。 兩者意味着相同的事情(詳情請參閱gitrevisions文檔 )。 所以:
git filter-branch -- <hash>..branchname
足以進行過濾以將更換固定到位。
如果一切順利,請刪除git filter-branch
文檔末尾附近顯示的refs/original/
reference,並刪除替換引用,然后就完成了。
作為git replace
,您還可以使用git cherry-pick
來復制提交。 有關詳細信息,請參閱ElpieKay的答案 。 這基本上與以前的想法相同,但使用“復制提交”工具而不是“rebase復制提交然后隱藏原件”工具。 它有一個棘手的步驟,使用git reset --soft
來設置索引以匹配提交A
以進行提交A'
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.