簡體   English   中英

你可以從另一個分支中存在的分支中刪除Git提交,就像提交的“交集”一樣嗎?

[英]Can you remove Git commits from a branch that exist in another branch like an “intersection” of commits?

我有時會發現自己處於簡單的情況,我正在進行一些更改並創建一個分支。 當我在我的變化中移動時,我開始發現需要清理的一些事情或者我想要開始進入的一些其他部分相關的事情。 所以,我想保持特定的分支,所以我快速分離另一個分支,開始處理可能與前一個分支沒有相同依賴關系的另一組更改。 最后,我最終得到了兩個分支,我試圖隔離這些變化,然而,這個第二個分支起源於第一個分支,其中術語來自“主”。

我可以(並且已經)通過將'master'合並到每個分支中來單獨更新每個分支,並且希望將第二個分支准備好合並到'master'中,因為它具有比創建的第一個分支更少的依賴性。 但是,此分支還包含自第一個分支分離后所做的更改。

所以我想知道,有沒有辦法告訴Git類似:“刪除這個其他分支中存在的所有提交”這樣我就離開了我的第二個分支而沒有在第一個分支中完成所有更改,允許我將第二個分支合並為“master”,讓我回到我創建的第一個分支上工作。

我可能只是在Git中找不到合適的術語,看看它是如何做到的。 但是,也許它不能。 看起來它應該是非常可行的,看看Git如何很好地向我展示分支1和2之間的適當差異,即使在我從'master'單獨更新兩個分支之后也是如此。

並且從分支中“刪除”是沒有必要的..即使這個想法正在創建另一個分支但是仍然以某種方式排除在第一個分支中也在第二個分支中完成的更改就足夠了。

是的,你可以這樣做。 在某些情況下,它甚至可笑,因為它只是由git rebase自動完成。 在某些情況下,這是非常艱難的。 我們來看看這些案例。

首先, 繪制提交圖是至關重要的,因為它幾乎總是在Git中。 為了實現這一目標,我們首先回顧一下Git的基礎知識。 (這是一個好主意,因為很多Git教程都跳過了基礎知識,因為基礎知識很無聊和令人困惑。:-))首先,讓我們看一下提交什么,為你做什么。

什么是承諾對你有用

在Git中, 提交是一個完全具體的事情。 我們可以看一下任何實際的提交 - 其中大多數都非常小 - 不是git show ,它們很喜歡它們,但是使用git cat-file -p ,它顯示了直接的原始內容(好吧, tree對象需要較小的實際Git對象的調整,有時“大部分是原始的”):

$ git cat-file -p 3bc53220cb2dcf709f7a027a3f526befd021d858
tree 5654dad720d5b0a8177537390575cd6171c5fc50
parent 3e5c63943d35be1804d302c0393affc4916c3dc3
author Junio C Hamano <gitster@pobox.com> 1488233064 -0800
committer Junio C Hamano <gitster@pobox.com> 1488233064 -0800

First batch after 2.12

Signed-off-by: Junio C Hamano <gitster@pobox.com>

那就是整個提交。 它的名稱 - 從現在到永遠標識提交的一個名稱 - 是3bc5322... (一些人類永遠不想處理的丑陋哈希ID,如果他們可以避免它)。 它存儲了幾個更大的丑陋哈希ID。 一個用於 ,一些數字 - 通常只有一個 - 用於父母 它有一個作者(姓名,電子郵件地址和時間戳)和提交者,他們通常是相同的; 它有一條日志消息,無論你想寫什么。

附連到提交是源樹快照。 這是整個事情 ,而不是一系列變化。 (在下面,Git 確實通過壓縮變得聰明,但樹的哈希ID獲取文件的哈希ID,這些文件是完整的文件,而不是一些奇怪的壓縮事物。)讓Git提取那棵樹,然后你得到所有的文件。

因為每個提交都存儲父哈希ID,所以我們可以從最近的提交開始並向后工作。 那是你的Git:倒退。 我們從最近提交的哈希ID開始,我們在分支名稱中為我們保存了Git。 我們說這個分支名稱指向提交:

<--C   <--master

名稱master指向提交C (我使用一個字母名稱而不是大丑陋的哈希ID,這限制了我26次提交但更方便。)但是,提交C有另一個哈希ID,所以C指向另一個提交。 那是C的父母, B B當然也指向另一個提交,但是假設我們的存儲庫總共只有三個提交,所以B指回AA是第一個提交。

由於A 第一個,它不能有父母。 所以它沒有:它沒有進一步指出。 我們稱A提交。 每個存儲庫至少有一個(通常只有一個)根提交。 1這就是行動必須停止的地方:我們(或Git)不能再回頭了。

無論如何,一旦提出,提交是永久性的,不變的。 2這是因為它們的哈希ID是通過計算提交中所有的加密哈希來完成的(所有這些都是你用git cat-file -p看到的)。 如果您更改了任何內容,則會獲得一個新的不同的哈希ID。 每個哈希ID始終是唯一的。 3

所以,讓我們把它畫出來,但不要打擾內部箭頭; 讓我們保留一個用於分支名稱本身。

A--B--C  <-- master

然后,每個提交都會為您保存快照。 當你將它們與它們的向后箭頭組合在一起時,就會得到提交圖


1,除了,一個完全空的存儲庫,顯然沒有提交。 這就是你在第一時間獲得root提交的方式,通過在沒有父級的情況下進行提交。

2然而,一旦你對它們沒有用處,它們就可以被垃圾收集 Git通常無形地做到這一點; 我們會很快看到它是如何產生的。

3不要關注窗簾后面的網站! 但是,嚴重的是,最近SHA-1哈希的破壞對Git來說不是一個直接的問題 ,但它確實有助於推動Git切換到SHA-256。


添加新提交

現在我們看到圖表看起來如何通過三次提交,讓我們為master添加一個新的提交,看看它是如何工作的。 首先,我們將像往常一樣git checkout master 這填寫了索引工作樹 然后我們會工作, git add東西,然后git commit

(提醒: 工作樹就是你工作的地方。當Git保存文件時,它會將它們列在不可發音的哈希ID名稱下,並將它們存儲為壓縮文件,從而將它們保存在僅對Git本身有用的形式中。要使用這些文件,你需要它們的正常形式,那就是工作樹。同時索引是你和Git構建下一個提交的地方。你在工作樹中處理文件,然后你運行git add來復制它們從工作樹到索引。你可以隨時git add :只是再次從工作樹更新索引。索引開始匹配當前提交,然后你修改它直到你准備好做一個新的提交。)

當你運行git commit ,Git會收集你的日志消息,然后:

  1. 將索引寫為新tree :這是您保存的快照,基於您在工作樹索引中替換的內容。 新樹獲得自己的哈希ID。
  2. 使用此新樹ID, 當前提交的ID作為parent ,作為作者和提交者(現在作為時間戳)和日志消息寫入新的commit對象。

第2步為Git提供了新提交的新哈希ID; 我們稱之為D 由於新提交中包含C的哈希ID,因此D指向C

A--B--C     <-- master (HEAD)
       \
        D

不過,Git做的最后一件事就是將D的ID寫入當前的分支名稱 如果當前分支是master分支,則master指向D

A--B--C
       \
        D   <-- master (HEAD)

如果我們首先 git checkout -b一些分支 - 盡管 - 在我們進行新提交之前,那就是 - 然后查看我們的新啟動設置:

A--B--C     <-- branch (HEAD), master

兩個名稱, branchmaster指向C ,但是HEAD說我們在分支branch ,而不是在master 因此,當我們讓D和Git更新當前分支時,我們得到:

A--B--C     <-- master
       \
        D   <-- branch (HEAD)

這就是分支增長的方式。 分支名稱只指向分支的提示 ; 它是構成圖形的提交本身。

在這一點上值得停下來並考慮提交ABC 他們當然是master ,當然。 但他們也在 branch 在Git中,提交可能同時在許多分支上。 我們經常需要做的是限制我們告訴它時我們讓Git走了多遠:“從這個分支機構開始並向后工作,讓我獲得所有提交。”

現在為令人興奮的部分!

好吧,也許令人興奮。 :-)你已經用一堆新的提交做了幾個分支,所以讓我們畫出:

...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

這里, masterG處結束,即提交G是主人的提示。 feature1L處結束,而feature2Q處結束。 提交EFG所有三個分支機構。 提交PQ 僅適用feature2 提交IJK同時出現在feature1feature2 提交L僅適用於feature1

請記住,這些字母代表了大丑陋的哈希ID,其中實際的哈希ID對提交中的所有內容進行編碼:保存的樹 parent ID。 例如, L 需要 K的哈希ID。 這種事情很重要,因為我們打算復制一些提交。

你所描述的想要做的是以某種方式移植提交PQ以便它們位於master之上。 如果有辦法復制提交怎么辦? 事實證明,它有:它被稱為git cherry-pick

采摘櫻桃

請記住,我們之前已經注意到提交一個快照。 這不是一組變化。 但是現在我們希望提交一組更改,因為提交P 很像它的父提交K ,但是做了一些更改。 畢竟,你通過K簽出來制作P ,然后編輯文件和git add新版本git add到索引中,然后git commit ting。

幸運的是,把一個快照變成變更,通過比較它 (一個簡單的4git diff對其父提交)。 git diff的輸出是一組最小的5條指令:“從這個文件中刪除這一行,將這些其他行添加到該文件中,等等” 將這些指令應用於K的樹將其轉換為P的樹。

但是,如果我們將這些指令應用於其他樹,會發生什么? 事實證明,這通常“正常”。 我們可以git checkout提交G branch master的提示,但是讓我們使用不同的分支名稱:

...--E--F--G                <-- master, temp (HEAD)
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

然后將diff應用於工作樹。 我們假設這很順利,自動git add結果git add到索引,並在從commit P 復制日志消息時進行git commit 我們將新的提交P'稱為“像P一樣,但使用不同的哈希ID”(因為它有不同的樹和不同的父):

             P'             <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

現在讓我們用Q重復一遍。 我們運行git diff PQQ轉換為更改,將這些更改應用於P' ,並將結果提交為新的Q'

             P'-Q'          <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

這只是兩個git cherry-pick步驟,當然還有創建臨時分支。 但是看看現在發生了什么,如果我們刪除舊名稱feature2並將temp更改為feature2

             P'-Q'          <-- feature2 (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

現在看起來我們做出feature2git checkout -b feature2 master ,然后寫P'Q'從頭開始! 這就是你想要的。


4簡單,即在字符串到字符串編輯問題上任意數量的碩士和/或博士論文之后。

在某種意義上, 5 “最小”,並通過不同的差異算法進行一些調整。 最小化編輯距離對於壓縮很重要,但實際上不是正確性 但是,當我們將編輯指令應用於其他樹時,最小化和精確指令真正開始變得重要。


Git的rebase是自動櫻桃選擇加上分支標簽移動

我們可以使用以下方法一次完成以上所有操作:

git checkout feature2
git rebase --onto master feature1

我們在這里做的是使用feature1作為告訴Git 停止復制的方法。 在放棄原始提交之前,請回顧原始圖表。 如果我們告訴Git從feature1開始feature1工作,那將標識提交LKJIGF等。 這些是我們明確表示復制的提交:分支feature1上的feature1

同時, 我們要復制的提交是那些feature2QPKJ ,等等。 但是一旦我們擊中任何被禁止的,我們就會停止,所以我們復制PQ提交。

我們告訴git rebase復制到的地方是 - 或者是“剛剛” - master的提示,即復制提交以便他們來到G之后。

Git rebase為我們做了一切,這非常容易。 但可能有一個障礙 - 或者可能是幾個。

解決問題

讓我們說我們像以前一樣開始:

...--E--F--G                <-- master (HEAD)
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

我們重訂feature2master ,跳過大部分的feature1 ,但事實證明,我們需要什么,我們改變了承諾J了。

我們不需要I ,或K ,或L ,只需J (加上當然PQ )。

我們不能只使用 git rebase來做到這一點。 我們可能需要一個明確的git cherry-pick來復制J 但這是Git,所以有很多方法可以做到這一點。

首先,讓我們看一下顯式櫻桃挑選方法。 我們將繼續創建一個新的分支和挑選J

git checkout -b temp
git cherry-pick <hash-ID-of-J>

現在我們有:

             J'             <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

現在我們可以像以前一樣移植PQ 我們只需更改--onto指令:

git checkout feature2
git rebase --onto temp feature1

結果是:

               P'-Q'        <-- feature2 (HEAD)
              /
             J'             <-- temp
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

我們根本不需要temp ,所以我們可以git branch -d temp並理順我們的繪圖:

             J'-P'-Q'       <-- feature2 (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

另一種獲得相同結果的方法

假設,我們讓git rebase復制IJKPQ ,而不是只復制PQ 這實際上可能更容易:

git checkout feature2
git rebase master

這一次,我們不需要--ontomaster告訴Git的這兩個承諾離開了哪里放置副本。 我們遺漏了提交G和更早,並且我們在G之后復制。 結果是:

             I'-J'-K'-P'-Q'  <-- feature2
            /
...--E--F--G                 <-- master
            \
             I--J--K--L      <-- feature1
                    \
                     P--Q    [abandoned]

現在我們復制了太多提交,但現在我們運行:

git rebase -i master

這給了我們每個提交I'J'K'P'Q'的一堆“選擇”行。 我們刪除I'K' Git現在再次復制,給出:

             J''-P''-Q''    <-- feature2
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

這就是我們想要的東西(原始副本仍在那里,像最初的原始PQ一樣被拋棄,但他們在那里的時間很短,無論如何都在乎?:-))。 當然,我們可以讓第一個git rebase使用-i並刪除pick線,並且只需一步就可以獲得J'-P'-Q'副本。

消除冗余提交

就目前而言,這很好,但現在有JJ' 實際上這沒有什么不妥 - 你可以把這種情況留在原地,甚至像這樣合並,沒有真正的傷害。 但是你可能想讓 J'成為master J'一部分然后分享它。

同樣,有不止一種方法可以做到這一點。 我想說明一種特殊的方式,因為git rebase有一些神奇之處。

假設我們已經完成了feature2所以現在我們已經完成了。 我們將完全丟棄被遺棄的提交,就像Git最終會進行垃圾收集一樣(請注意:在此之前,默認情況下至少會有30天的時間,給你一個月的時間來改變主意):

             J'-P'-Q'     <-- feature2
            /
...--E--F--G              <-- master
            \
             I--J--K--L   <-- feature1

您現在可以快進 master以包含J'

git checkout master
git merge --ff-only <hash-id-of-J'>

這會移動標簽 ,而不會更改提交圖 但是,為了便於以ASCII文本繪制,我將J'向下移動一行:

                P'-Q'     <-- feature2
               /
...--E--F--G--J'          <-- master
            \
             I--J--K--L   <-- feature1

(我們也可以通過明確地到達這里git cherry-pick荷蘭國際集團J進入master原,然后重訂feature2 ,沒有任何花哨的腳法。)所以, 現在我們想復制feature1的承諾,加入后他們J'移除 J

我們可以使用另一個git rebase -i執行此操作,它允許我們顯式刪除原始提交J 但我們沒有必要 嗯, 大部分時間我們都沒有。 相反,我們只運行:

git checkout feature1
git rebase master

這告訴Git它應該將IJKL視為副本的候選者,並將副本放在J' (其中master現在指向)。 但是 - 這里的魔法git rebase 密切關注 master上所有6個不在feature1 上的提交(這些提交稱為上游提交,至少在幾個文檔中)。 在這種情況下,那就是J'本身。 對於每個這樣的提交,Git將提交與其父級(la git cherry-pick )區分開來,並將結果轉換為補丁ID 它對每個候選提交都是一樣的 如果候選者( J )中的一個具有與上游提交之一相同的補丁ID,則Git從列表中刪除候選者!

因此,只要JJ'都具有相同的補丁ID,Git就會自動丟棄J ,因此最終結果為:

                P'-Q'     <-- feature2
               /
...--E--F--G--J'          <-- master
               \
                I'-K'-L'  <-- feature1

這正是我們想要的。


6全部,即合並除外。 Rebase實際上無法復制合並 - 新合並具有與原始不同的父集合,並且櫻桃選擇首先“解除”合並 - 因此默認情況下它完全跳過它們。 合並不會分配補丁ID,也不會從集合中刪除,因為它們從未集合中。 修改包含合並的圖形片段通常是個壞主意。

Git確實有一種嘗試這樣做的模式。 此模式重新執行合並(因為它必須:我將詳細信息作為練習)。 但這里有一堆危險,所以通常最好不要這樣做。 我曾經說過,也許git rebase應該默認為“保存”合並,但示數出是否合並,要求無論是“是的,繼續前進,並嘗試重新創建合並”標志,或“變平遠和刪除合並“標志繼續。

但事實並非如此,因此您需要繪制圖表並確保您的rebase有意義。


當rebase出錯時:合並沖突

任何時候你git rebase一些提交,你冒着合並沖突的風險。 如果您從長鏈中提取一部分提交,則尤其如此:

        o--...--B--1--2--3--4--...--o   <-- topic
       /
...o--*--o--o--o--T                     <-- develop

如果我們想要“移動”(復制,然后刪除)將1-4提交到develop ,那么這四個提交中的某些部分或部分部分很可能在某種程度上依賴於之前的其他頂級提交。他們( B和更早)。 當發生這種情況時,我們傾向於發生合並沖突,有時甚至是很多 Git最終將提交1的副本視為三向合並操作,將從B1的更改與從BT的更改合並。 “從” BT的變化可能看起來相當復雜,並且在上下文中可能看起來不合理,因為我們必須在B之前“向后”通過提交到*然后“轉發”到T

由你決定如何,甚至是否明智,這樣做。

當rebase出錯時:其他人仍在使用原件

因為rebase基本上是一個復制操作,所以你必須考慮誰可能仍然擁有原始提交。 由於提交可以在許多分支上,因此可能是擁有原件。 例如,當我們同時擁有JJ' ,這就是我們所看到的。

有時 - 甚至有點經常 - 這可能不是什么大問題。 有時它是。 如果所有額外副本在您自己的存儲庫中 ,您可以自行解決所有這些問題。 但是如果您發布了某些提交(推送或讓其他人從您那里獲取)會發生什么? 特別是,如果某個其他存儲庫具有原始哈希ID的原始提交,該怎么辦? 如果您已經發布了原始提交,您必須告訴其他擁有它們的人:“嘿,我放棄了原件,我在其他地方有新的副本。” 你必須讓他們做同樣的事情,否則忍受額外的提交副本。

額外提交有時是無害的。 對於合並來說尤其如此,因為git merge很難只獲取任何給定更改的一個副本 (盡管Git不能總是自己得到這個,因為每個更改 - 每個git diff輸出 - 取決於上下文和其他更改,並且最小編輯距離算法有時會出錯,選擇錯誤的“最小變化”。 但是,即使它們沒有打破 ,它們也會使提交歷史變得混亂。 是否以及何時可能成為問題很難預測。

摘要

為了您的目標, git rebase是一個強大的工具。 在使用它時需要一點小心,最重要的是要記住它復制提交,然后放棄 - 或者試圖放棄 - 原件。 這可能在幾個方面出錯,但最糟糕的情況往往發生在其他人已經擁有原始提交的副本時,這通常意味着“當你發布(推送)它們時”。

繪圖可以提供幫助。 每個人都應養成繪制圖形的習慣,和/或使用git log --graph (從“狗”中獲取幫助: git log --all --decorate --oneline --graphA ll D ecorate O neline G raph)和/或像gitk這樣的圖形瀏覽器(雖然我個人討厭GUI一般:-))。 不幸的是,“真實”的圖表很快變得非常混亂。 Git的內置log --graph - log --graph在分離大鼠巢圖log --graph做得不好。 有很多特殊工具可以解決這個問題,有些工具是內置於Git的,但它確實有助於大量練習閱讀圖形。

如果您的歷史記錄如下:

...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

在簡單的情況下刪除feature1然后執行:

git checkout feature2
git rebase --onto master feature1

這是@torek的答案的縮寫,這是一個@torek的,但很難找到問題的實際答案。 閱讀@torek的答案,了解更多細節以及在非簡單情況下該怎么做。

暫無
暫無

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

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