參考:大約 9 年前的以下問題:
Pull request without forking?
背景:
我傾向于使用 GitHub/Git,但遇到了一些問題。我已經努力搜索,但沒有找到任何解決這個特定問題的東西 - 我發現的最接近的是上面提到的問題。
問題:
我“分叉”了一個打算做一些作業的存盤庫,對我自己的分叉進行更改,然后創建一個拉回原始專案的拉取請求,作為對其做出貢獻的一種方式。
我終于想通了,并且能夠成功地創建一個包含我提議的更改的拉取請求。
請注意,我還想做其他事情來為這個專案做出貢獻,在我創建拉取請求后,我繼續作業并對我的本地副本進行了額外的提交,包括匯入一些技術檔案等。
顯然,無論出于何種未知原因,在我提出拉取請求后,拉取請求“擁有”我對原始倉庫的分叉,此后我所做的任何事情都成為該拉取請求的一部分——它是否相關并不重要,我是否將其推送到專案的分支,是否將其添加到 PR 或其他任何內容。它看起來就像魔法一樣,只有在我洗掉/恢復我自己的存盤庫分支中的更改時才能被洗掉。
這是否意味著與該專案有關的所有作業都必須完全停止,直到該 PR 被接受和/或拒絕?如果是這樣的話,其他人,尤其是在單一代碼庫上作業的公司,如何設法完成作業?
當然,我確信這是可能的,人們一直都在這樣做。
我所做的研究沒有披露任何似乎解決這個特定問題的東西,但是對不同問題的其他答案似乎暗示這樣一個事實,即一旦你分叉一個 repo 并創建一個拉取請求,拉取請求似乎“擁有“您本地回購的那個實體 - 減輕這種情況的唯一方法是:
- 分叉回購。
- 創建 repo 的整個分支并開始作業。
- 提交到該分支并創建拉取請求,然后放棄該分支。
要完成額外的作業,無論專案在哪里,您都必須:
- 創建一個全新的分支。
- 做任何你想做的作業,這些作業應該與原始作業分開。
- 提交到新分支,創建拉取請求,然后放棄該分支。
對于您想要做的任何額外作業,“沖洗并重復”,最終擁有比圣誕樹更多分支的叉子。
這引發了幾個問題:
- 這是真的?我理解正確嗎?
- 為什么?這似乎是不必要的復雜和令人費解的,尤其是對于單個貢獻者。
最后一個也是最重要的問題:
3. 如何清理我的本地副本?顯然我應該克隆存盤庫,然后創建一個分支來作業,然后創建拉取請求。(即有沒有辦法把我更新的“main”變成一個分支,然后重新創建原始的main,這樣我就可以創建額外的分支來做額外的作業?)
我不愿僅僅“破解”現有的 repo 試圖弄清楚事情,因為我不想污染原始的拉取請求或搞砸上游專案。
謝謝!
uj5u.com熱心網友回復:
注意:這很長,但你真的需要知道這些事情。我已經用完了空間(字符數限制為 30k),所以我將把它分成兩個單獨的答案。第 2 部分在這里。
雖然“拉取請求”不是 Git 的一部分(它們特定于 Git Hub ?1),但即使沒有專門提到 GitHub,我們也可以對它們說一些話。然后我們可以稍后插入 GitHub 特定的專案。所以讓我們從這個開始:
Git 是關于提交的。雖然 Git 提交包含檔案,但 Git 并不是關于檔案,而是關于提交。而且,雖然我們使用分支名稱來查找提交,但 Git 也不是關于分支名稱:它實際上只是關于提交。
這意味著您需要了解有關提交的所有資訊:一個是什么以及每個提交以及連續的一串提交可以為您做什么。
因此,我們將從一個提交的快速概述開始,然后連續查看它們的字串。
1 Bitbucket 也有“拉取請求”,但它們略有不同,GitLab 有“合并請求”,它們又是相同但不同的。所有這些都建立在 Git 本身的相同基礎支持之上。
提交
每個 Git 提交都有編號。但是,這些數字不是簡單的連續計數數字:我們沒有提交 #1 后跟 #2 和 #3 等等。取而代之的是,每個提交都會獲得一個唯一的哈希ID——在所有存盤庫中都是唯一的,即使它們與你的存盤庫完全不相關2——這似乎是隨機的,但實際上并非如此。3 哈希 ID 又大又丑,而且人類無法使用:計算機可以處理它們,但我們虛弱的大腦會變得混亂。?? 所以,下面,我將使用假哈希 ID,我只使用一個大寫字母來代替真正的哈希 ID。請注意,要使這些哈希 ID 起作用,提交的每個部分都必須是完全只讀的. 也就是說,一旦您做出新的提交,該提交將永遠凍結在時間上。那個特定的哈希 ID,無論它得到什么哈希 ID,都用于那個提交,并且沒有其他提交——過去、現在或將來——可以使用那個哈希 ID。
在任何情況下,每個 Git 提交都會存盤兩件事:
提交存盤每個檔案的完整快照(無論如何,Git 在您或任何人制作它時就知道)。為了防止存盤庫變得非常臃腫,這些檔案被 (a) 壓縮和 (b)重復資料洗掉。因此,它們以只有 Git 可以讀取的格式存盤,沒有任何東西,甚至是 Git 本身,都無法覆寫。正如我們將看到的,這解決了一些問題,但產生了一個大問題。
提交還存盤一些元資料,或有關提交本身的資訊。這包括,例如,提交人的姓名和電子郵件地址(來自他們的
user.name和user.email設定,他們可以隨時更改,因此如果沒有驗證它是不可靠的,但它仍然有用)。它包括一條日志訊息:當您為自己的提交提供一個日志訊息時,您應該寫下您為什么提交的解釋。 你所做的——比如將一個實體從 7 更改為 14——Git 可以自己顯示,但你為什么將 7 更改為 14?是從幾周到兩周,還是因為 7 個小矮人都被克隆了?
在提交的元資料中,Git 出于自己的目的添加了先前提交的原始哈希 ID 串列。這個串列通常只有一個元素長:對于合并提交(我們不會在這里介紹),它有兩個元素長,并且任何非空存盤庫中的至少一個提交是第一個提交,其中沒有任何以前的提交,所以這個串列是空的。
2這就是為什么哈希 ID 必須如此龐大和丑陋的原因。嚴格來說,它們不必在兩個永遠不會相遇的存盤庫中是唯一的,但是 Git 不知道兩個存盤庫將來是否或何時會相遇,以及兩個不同的提交是否具有相同的哈希 ID那個時候,壞事就發生了。我稱這種行為為分身,一種邪惡的雙胞胎,預示著災難。真正的災難是——或者至少應該是——只是這兩個 Git 存盤庫的會議失敗了。在一些非常舊的 Git 版本中,由于錯誤,實際上確實發生了更糟糕的事情。無論如何,它根本不應該發生,散列的大小有助于避免這種情況。
3當前哈希是提交中所有資料的 SHA-1 校驗和,其中包括有關導致提交的提交的資料,因此它是導致該點的整個歷史記錄的校驗和。 SHA-1 不再是加密安全的。盡管這本身并沒有破壞 Git,但 Git正在轉向 SHA-256。
提交鏈
鑒于上述情況,我們可以在一個很小的三提交存盤庫中繪制三個提交,如下所示:
A <-B <-C
CommitC是我們的第三次也是迄今為止最新的一次提交。它有一些看起來很隨機的哈希 ID,以及所有檔案的快照。in 中的 一兩個檔案C可能與較早 commit 中的所有檔案不同,B其余檔案與 in 相同,B因此實際上與較早 commit 共享B。所以他們不占用任何實際空間。修改后的檔案確實占用了一些空間,但它們被壓縮了——有時非常壓縮——并且可能幾乎不占用任何空間。提交元資料有一點空間(順便說一句,它也被壓縮了),但總的來說,這個每個檔案的完整快照可能不會占用太多空間。
同時, commitC包含較早 commit 的原始哈希 ID B。我們說那C 指向 B。這意味著如果 Git 可以找到 C——我們稍后會看到它是如何做到的——Git 也可以使用哈希 ID進行 C查找B。然后,Git 可以從兩個提交中提取兩個快照中的所有檔案,并進行比較。比較檔案的結果是一個差異:將檔案更改為檔案的說明B(C反之亦然,如果您以其他順序完成差異)。
Git 和 GitHub 等網站通常會將提交顯示為差異,因為這通常比顯示原始快照更有用。但是,如果您愿意,您可以輕松地獲取快照:對于 Git 而言,有時這比獲取差異更容易。(由于重復資料洗掉技巧,git diff可以快速跳過相同的檔案,但它仍然必須查看兩個提交,而不僅僅是一個。所以它有點混合,哪個更容易。)
CommitB作為一個提交,既有快照又有元資料,并向后指向更早的提交A。但是提交A是第一次提交,所以它的元資料沒有列出任何更早的提交。這意味著根據定義,其快照中的所有檔案都是新的。(它們將針對任何其他提交中的任何檔案進行壓縮和重復資料洗掉,但那時,這是第一次提交,因此它們僅對自身進行壓縮和重復資料洗掉。這最后意味著,如果第一次提交包含一個大檔案的 100 個相同副本,在 commit 中實際上只有一個A副本。)
分支名稱和其他名稱
Git 需要一種快速的方法來找到某個鏈中的最后一次提交。Git 可以強迫我們——使用 Git 的人——寫下最后一次提交的哈希 ID,在這種情況下C。我們可以將其保存在紙上、白板或其他東西上。但這很愚蠢:我們有一臺電腦。為什么不讓計算機將這些哈希 ID 保存在檔案或其他東西中?事實上,為什么不讓Git為我們保存最新的哈希 ID ?
這正是分支名稱的含義:保存最新提交的哈希 ID 的地方。Git 只需要最新的,因為最新的指向第二個最新的,它又指向更早的一個,依此類推。這會盡可能地持續下去,只有在沒有更早的提交時才結束,這就是Git 的作業方式:它從我們告訴它的提交開始——通常是分支名稱——然后向后作業。
讓我們畫一個簡單的以散列 ID 結尾的提交鏈H(對于散列),并讓分支名稱main指向(包含的散列 ID)H:
...--G--H <-- main
現在讓我們添加一個新名稱,例如feature1. 這個名字必須指向一些現有的提交。我們可以選擇G, 或H, 或一些較早的提交,但選擇似乎很自然,H因為它是我們最新的:
...--G--H <-- feature, main
請注意,Git 有很多種名稱——不僅僅是分支名稱——而且它們都做這種事情,即指向一個提交。所以我們可以創建一個指向 commit的標簽H,例如:
...--G--H <-- feature, main, tag: v1.0
不過,大多數情況下,我們只會使用分支名稱,這就是我現在要在這里展示的全部內容。
在分支上作業
Git 有它自己的特殊功能可以讓我們作業。正如我們之前提到的,提交快照的內容一直被凍結,并且只能由 Git 本身讀取。所以我們實際上不能處理/處理提交中包含的這些檔案。我們必須讓 Git在某處提取檔案。那個“某處”是我們的作業樹或作業樹。
Git 還有一個很重要的東西,Git 給它起了三個名字:索引、暫存區,有時還有快取。我們不會在這里討論,除了要注意,當你運行時git commit,Git 實際上是從 Git 的 index / the-staging-area 中的檔案而不是作業樹中的檔案中進行新的提交 。所有要提交的檔案都必須在暫存區:這些是 Git 知道的檔案。提取提交會將提交的檔案復制到暫存區域以及作業樹,以便它們從那里開始。
無論如何,一旦檔案在您的作業樹中,它們就只是您計算機上的普通檔案。它們不再在Git 中了。它們來自Git (來自提交),您可以稍后在新的提交中將它們放回Git 中,但是當您作業時,您會處理和處理不在 Git 中的檔案。只有提交的檔案在 Git 中。
您使用作業樹檔案完成作業并git add照常運行。(這會將您列出的檔案的作業樹版本復制回索引中,以便它們準備好提交。在git addGit 進行初始壓縮和重復資料洗掉的階段。在 Git 的索引中看到的檔案是換句話說,預先去重。這意味著索引的副本大多不占用空間,除了您更改和添加的任何檔案。您可以添加未更改的檔案:這只是對 Git 的輕微浪費時間會發現是復制的,只保留原版。浪費了廉價的計算機時間,而不是寶貴的人力時間,所以隨意浪費吧!你的時間也是,隨意跳過它。)
無論如何,既然您的新提交已準備好,您可以運行git commit. 這:
- 收集任何必要的元資料,例如您的姓名和電子郵件地址以及當前日期和時間;
- 獲取當前提交的哈希 ID — 您之前簽出以填充作業樹(和 Git 的索引)的哈希 ID;
- 凍結索引的快照;和
- 將所有這些寫成一個新的提交,它會獲得一個新的、唯一的哈希 ID。
如果你有:
...--G--H <-- feature, main
就在剛才,你當前的提交是H,所以你的新提交——我們稱之為——I指向H:
I
/
...--G--H
但是,Git 確實需要知道您使用哪個分支名稱來查找 H. 因此,這兩個名稱之一具有HEAD“附加到它”的特殊名稱。假設這個名字曾經是,現在仍然是 feature。那么我們的繪圖現在看起來是這樣的:
I <-- feature (HEAD)
/
...--G--H <-- main
也就是GitHEAD以前找名字feature,先找hash ID H,再往里面寫入新的hash I ID feature。
這樣做的效果是,當前分支名稱,無論它是什么,現在都指向您剛剛進行的新提交。(請注意,快照I使用了索引/暫存區域,您更新它以匹配您的作業樹,因此所有三個現在都匹配,就像您開始使用“干凈”結帳或 時一樣git switch。)如果您制作另一個新的使用通常的修改檔案添加和提交程序提交,你會得到:
I--J <-- feature (HEAD)
/
...--G--H <-- main
如果你現在git switch main或git checkout main,Git 所做的是:
- 洗掉所有
J提交檔案并用提交檔案替換它們H;和 - 將特殊名稱附加
HEAD到main.
你現在有:
I--J <-- feature
/
...--G--H <-- main (HEAD)
on branch main正如將要說的那樣,您是git status,并且您的作業樹和暫存區域是“干凈的”(與H提交匹配),您的更新檔案安全地永久保存 - 或者只要提交本身持續存在 - 在 commitJ中,您可以使用名字feature。
如果您愿意,您現在可以創建一個新分支,例如feature2, 并切換到它(使用git branchandgit switch或結合使用git switch -c一次完成所有操作):
I--J <-- feature
/
...--G--H <-- feature2 (HEAD), main
當您在這個新分支上進行新提交時,分支名稱會自動更新以指向最新提交:
I--J <-- feature
/
...--G--H <-- main
\
K--L <-- feature2 (HEAD)
H請注意,在 Git 的術語中,通過并包含提交是在所有三個分支上。提交I-J當前僅onfeature并且提交K-L僅on。feature2CommitH是 上的最新提交main,盡管它不是最新的提交(此時是您的存盤庫中的L提交)。J此外,提交和:之間沒有直接關系L:它們只是表親,實際上。他們是共同祖父母的孩子的孩子H。
合并
要了解會發生什么,我們現在需要查看通常更難合并的情況。Git 有一個簡單案例的快捷方式,但由于各種原因(有些好,有些不太好),尤其是 GitHub 從不使用此快捷方式。無論如何,一旦您了解了更一般的情況,就更容易看到簡單的情況。
在 Git 中,使用git merge是關于組合作業。讓我們畫兩個特征分支,不畫名字 main(它可能還存在,只是擋住了我想畫的樣子)。我們先切換到分支feature:
I--J <-- feature (HEAD)
/
...--G--H
\
K--L <-- feature2
我們當前的提交是 now J,我們現在會J在作業樹中找到 's 檔案。我們現在運行git merge feature2, 和git merge:
- 定位提交
J(簡單:只需閱讀HEAD然后feature); - 定位提交
L(也很簡單:feature2包含正確的哈希 ID); - 定位最佳公共起點提交。
最后一部分可能很難,盡管在這里很容易看出這是 commit : andH的祖父。如果 Git 現在將快照中的快照與中的快照進行比較,Git 將生成一個包含您所做的所有作業的配方:JLHJfeature
git diff --find-renames <hash-of-H> <hash-of-J> # what "we" did
通過從to運行第二個差異,Git 將生成一個包含在 上完成的所有作業的配方:HLfeature2
git diff --find-renames <hash-of-H> <hash-of-J> # what "they" did
在這一點上,誰做了哪些作業并不重要:唯一重要的是“我們”更改了哪些檔案,“他們”更改了哪些檔案,以及我們對每個檔案做了哪些更改。兩人git diff想通了。
如果 Git 可以自己組合這兩組更改,那么它可以將組合的更改應用到來自H. 無論您喜歡如何看待它,這要么保留我們的更改并添加他們的更改,要么將兩個更改相加,或者其他。Git 假設最終結果是存盤在新提交中的正確快照。
If Git can't combine these changes on its own, Git will stop in the middle of the merge with a merge conflict. The programmer must now come up with the correct result. We'll skip right over this part. ?? We'll just assume that Git came up with the right result all on its own. In that case git merge goes on to run git commit for you.
Normally, the resulting commit M would have commit J as its parent. Our new merge commit does in fact have J as a parent—the first parent—but also has commit L, the commit we named on the git merge command line, as its second parent, like this:
I--J
/ \
...--G--H M <-- feature (HEAD)
\ /
K--L <-- feature2
The name feature, to which HEAD is attached, moves as usual to point to new commit M. But since M points backwards to both J and L, commits K-L are now also "on" branch feature. This means all commits up through M are on feature, while feature2 still ends at L and does not contain commits I-J.
如果需要,我們現在可以洗掉該名稱feature2:它只對L直接查找有用,如果我們不覺得需要L直接查找,我們可以隨時通過查看 的第二個父項來找到它M。如果我們現在想添加更多提交feature2,我們應該保留名稱并執行此操作:
I--J
/ \
...--G--H M <-- feature
\ /
K--L--N--O <-- feature2 (HEAD)
如果我們愿意,我們現在可以再次feature2合并:feature
I--J
/ \
...--G--H M-----P <-- feature (HEAD)
\ / /
K--L--N--O <-- feature2
制作一種鴨頭??圖片,盡管我們也可以在沒有頂行的腫塊的情況下重新繪制它:
...--G--H--I--J--M-----P <-- feature (HEAD)
\ / /
K----L--N--O <-- feature2
(不知道這個是什么樣子的)。
快進
Git 的特殊快捷方式git merge適用于以下情況:
...--D--E <-- main (HEAD)
\
F--G <-- bugfix
如果我們運行git merge bugfix,Git 將找到提交E和G,然后找到和 的合并基礎:兩個分支上的最佳提交。但這就是 commit本身,即當前的 commit。EGE
Git可以繼續E與自己進行比較,以發現沒有任何變化。然后它可以比較找到他們的變化E。G然后它將應用這些更改并 E提出一個新的 commit H,并給它兩個父母:
...--D--E------H <-- main (HEAD)
\ /
F--G <-- bugfix
提交H將是一個合并提交,有兩個父母,就像“真正的合并”案例一樣。但是很明顯E,對自己進行差異化是愚蠢的,添加他們的更改只會讓我們得到一個提交H,其快照與他們的提交中的快照完全匹配G。因此,對于這種情況,除非我們告訴它,否則 Git 根本不會打擾合并。
相反,Git 會做它所謂的快進合并。這意味著 Git 只是直接檢查提交G,同時向前拖動當前分支名稱:
...--D--E
\
F--G <-- bugfix, main (HEAD)
現在根本沒有理由在圖表中畫出扭結:
...--D--E--F--G <-- bugfix, main (HEAD)
并且洗掉名稱bugfix顯然足夠安全,盡管main以后可能會進一步推進。
為了抑制快進而不是合并的事情,我們會運行git merge --no-ff. GitHub 總是有效地執行此操作,因此您不會在GitHub 上看到快進合并;但很高興了解它們。
何時洗掉姓名
何時以及是否洗掉其他分支名稱取決于用戶。請注意,洗掉名稱不會洗掉提交:它只會使找到它們變得更加困難。但還有一件事要知道。假設我們有:
...--G--H <-- main
\
I--J <-- bugfix (HEAD)
在哪里提交I并且J根本不起作用。你將運行:
git switch main
git branch -d --force bugfix
放棄您修復錯誤的嘗試。這給你留下了:
...--G--H <-- main
\
I--J ???
Commits I-J still exist, but unless you wrote down J's hash ID, you may never be able to find commit J again.
Git will—eventually—detect that commit J is unreachable (that there's no way for you to find it) and will delete it for real. The same goes for commit I once J is gone. You get a grace period, normally at least 30 days, during which Git won't do this, and various Git commands to help find accidentally-lost commits. But if you don't bother finding them and adding a name back, the "reflog entries" by which Git keeps track of "lost" commits like this eventually expire, and then—when Git gets around to doing its maintenance and janitorial work—the "lost" commits will really go away from this repository. So, while commits are read-only, they are only "mostly permanent". They remain in your repository as long as you can find them (and then a little bit longer).
Clones, remotes, and multiple repositories
Git is not just a Version Control System (VCS); it's a Distributed VCS (DVCS). The way Git does this distribution is to allow for—or rather, strongly encourage—many copies of a repository to exist. As such, a Git repository is:
- a collection of commits and other Git objects, some or all of which may be in other repositories too; and
- a collection of names, such as branch and tag names, that help you (and Git) find the commits and other internal objects.
These are stored as two simple key-value databases. The keys in the names database are branch names like refs/heads/main, tag names like refs/tags/v1.2, and many other kinds of names. Each name lives in a namespace under refs/. Each name stores exactly one hash ID.
The keys in the objects database are hash IDs. Each object in this database has some Git internal object type (commit, tree, blob, or annotated tag). The commit objects, along with supporting tree and blob objects, wind up storing your files; and you will mostly just work with the commits and don't normally have to care much at all about these details.
Since commit hash IDs are globally unique, the object database keys in your clone of some repository are the same as the keys in every other clone of that same repository. When you clone a repository, you get all, or almost all, of their commits and supporting objects. But the names database in your clone is entirely separate from theirs.
What this means is that a clone of a repository starts out with no branch names at all. You run:
git clone <url>
or:
git clone -b <branch> <url>
and your Git software creates a new, totally-empty Git repository to start. Your Git software, using your Git repository (I like to shorten this to "your Git") calls up their Git software and points it to their Git repository ("their Git"). Their Git lists out all their branch and tag and other names and the hash IDs that go with them, and your Git then asks for the objects it would like to copy (normally, all of them). For each commit you're going to get, their Git is obligated to offer all of that commit's parents, and the parents' parents, and so on. So you end up copying every commit into your Git.
Now that you have all the commits (and supporting objects), your Git takes each of their branch names and renames them. This renaming process makes use of the concept of a "remote".
A remote, in Git, is just a short name that stores at least a URL (you can have it store various extra features later). The URL is the one you type into git clone, and the name of the first "remote" is always origin.4 So origin from now on means the URL I cloned from, unless and until you change something.
Git uses this name—the origin string—to make up new names for their branch names. Their main becomes your origin/main; their debug becomes your origin/debug; if they have a feature/tall, you get an origin/feature/tall; and so on. These names are not actually branch names; I like to call them remote-tracking names.5 Their function is to remember, for your Git repository, what their branch names are, and what commit each of those names selected, the last time your Git got an update from their Git.
完成此重命名后,您的 Git 會為它們擁有的每個分支名稱創建遠程跟蹤名稱。您擁有他們所有的提交,并且可以找到所有這些提交,因為您的遠程跟蹤名稱與他們的分支名稱擁有相同的哈希 ID,它們用于查找他們的提交。
git clone現在,在您完成并將控制權交還給您以便您可以開始作業之前不久,您的 Git:
- 根據您提供的引數,在您的存盤庫中創建一個新的分支名稱
-b:如果您說-b bugfix,您的 Git 會找到origin/bugfix與他們對應的您的分支bugfix并創建您自己的bugfix,指向相同的提交。 - 簽出(切換到)這個新分支。
所以現在你的克隆里面有一個分支,匹配它們的一個分支。如果您不使用-b,您的 Git 會詢問他們的 Git 他們推薦的名稱。通常的標準推薦是他們的主要分支(現在通常是main;過去是master)。
一旦你有一個克隆,你可以添加更多的遙控器,使用git remote add. 這需要遙控器的名稱和URL;它設定了遙控器,但尚未運行git fetch。現在是時候談談獲取和推送了;看另一個答案。
4您可以選擇其他名稱,但這樣做幾乎沒有任何意義。用作origin“主遙控器”的名稱。您可以隨時重命名遙控器,因此即使您不打算保留起始 URL,也可以將git clone默認設定為origin此處。
5 Git 稱它們為遠程跟蹤分支名稱,將可憐的超載詞branch從血腥、畸形的野獸變成幾乎無法辨認的污點。說真的,把分支這個詞放在這里,它沒有任何幫助。
uj5u.com熱心網友回復:
第 2 部分——見第 1 部分
git fetch
要運行git fetch,您選擇一個遙控器并將其呼叫為. 如果你省略了遠程名稱,Git 會從某個地方選擇一個遠程,或者嘗試使用默認名稱,這取決于很多配置項。如果您只有一個名為 的標準遙控器,則無需其他引數即可運行:無論如何您都沒有別的意思。git fetch remoteoriginorigingit fetch
fetch的作用是:
- 呼叫任何 Git 軟體回答存盤的 URL;
- 讓他們列出所有的名稱(分支、標簽等)和相應的哈希 ID;和
- 從他們那里獲得他們擁有的任何你沒有的提交。
請注意,這與我們對 的操作相同git clone,除了“獲取他們所有的提交”,現在是“獲取他們擁有的我們沒有的提交”。由于提交具有全域唯一的 ID,我們可以很容易地判斷我們有(比如說)提交a123456,因為我們有一些具有 ID 的物件a123456,而我們缺少——因此需要——b789abc因為我們沒有這樣的 ID。在獲得了他們的新提交后,我們的 Git 現在更新了我們相應的遠程跟蹤名稱。
換句話說,除了我們的git fetchGit 存盤庫已經存在之外,我們所做的事情幾乎相同,我們可能會獲得更少的資料,并且我們沒有最終的“創建分支并檢查它”步驟。由于我們可以擁有多個remote,我們可以運行:git clone
git fetch origin
并更新我們所有的origin/*名字,然后運行:
git fetch upstream
并更新我們所有的upstream/*名字,如果我們曾經git remote add添加第二個名為upstream.
要一次更新所有遙控器,我們可以使用git fetch --allor git remote update; 兩者基本上做同樣的事情。請注意,--alltogit fetch表示所有遙控器,而不是所有分支:我們已經獲得了所有分支。(我提到這一點是因為人們一直認為--all意味著所有分支,但它從來沒有。)
如果我們愿意,我們可以限制我們git fetch這樣:
git fetch origin main
這讓我們的 Git 像往常一樣呼叫他們的 Git 并列出所有內容,但是這一次,我們的 Git 只麻煩詢問他們在他們的main. 當一切都完成后,我們的 Git 會更新我們的origin/main(我們知道origin'main現在在哪里,所以我們對應的遠程跟蹤名稱,即 ,origin/main可以更新)。如果他們有新的提交dev,我們不會得到它們,我們不會更新我們的origin/dev; 我們的 Git 被告知只使用main.
在一些(罕見的)設定中,這種事情可以節省大量的資料傳輸。因此,Git 提供了一種稱為單分支克隆的東西,默認情況下git fetch會這樣做。這是人們嘗試使用的地方(但它不起作用):要從單分支克隆中獲取其他分支,您必須添加它們 - 請參閱檔案-或使用顯式refspec。不過,出于篇幅原因,我們不會在這里正確介紹 refspecs。--allgit remote
由于您將有兩個遙控器,一個用于您的 GitHub fork,一個用于您 fork 的 GitHub 存盤庫,您需要運行git fetch兩次,或者不時使用git remote updateor git fetch --all。除此之外——upstream/*如果你像大多數人一樣呼叫第二個遠程upstream,你的存盤庫仍然和任何其他存盤庫一樣。
git push
該git push命令與 非常相似git fetch,但有幾個關鍵區別:
首先,當然
git push是指發送東西。您用于git fetch從其他Git(與其他存盤庫一起作業的其他軟體)獲取新的提交(和其他內部物件) 。您用于發送新的提交,通常是您制作的提交 - 但它們可能是您剛剛獲得的提交實體——對其他 Git。git pushupstream其次,一旦你發送了這些提交,你通常會要求另一個 Git設定它的一個分支名稱。在推送方面,沒有像遠程跟蹤名稱這樣的東西。
最后一部分意味著您必須有權寫入存盤庫。Git 本身根本沒有真正的訪問控制,但大多數 Web 托管站點,包括 GitHub,都添加了它們。特別是 GitHub 在這里添加了許多精美的控制元件。您和/或其他任何人是否使用它們取決于您和他們。
要做一個git push,你通常運行一個簡單的:
git push <remote> <name>
這表示您希望您的 Git 查看名為 的分支上的提交name,找到另一個 Git 的新提交origin,將它們發送到該 Git,然后禮貌地詢問他們,如果他們愿意,請他們的名字name指向你name指向的同一個提交。
換句話說,您要求他們創建或更新與您的分支同名的分支。一般來說,當且僅當這只是簡單地添加到他們的分支(并且您當然有權限)時,他們才會接受這一點。也就是說,當我們有:
...--G--H <-- main (HEAD), origin/main
因為我們main匹配的原點main,我們添加了一個或兩個新的提交:
I--J <-- main (HEAD)
/
...--G--H <-- origin/main
然后我們運行git push origin main,我們的 Git 呼叫他們的 Git,向他們發送提交I-J,并要求他們將他們main的指向設定為J.
如果他們main仍然指向H——或者不知何故,G因為有人讓他們放棄 H——他們會很樂意接受我們添加到他們的main. 由于我們的 Git 看到了他們的接受,我們最終得到:
...--G--H--I--J <-- main, origin/main
明知故犯。origin_ main_J
K但是假設其他人出現并向他們 添加了一些承諾main:
...--G--H--K <-- main [over on origin]
我們的請求現在將要求他們放棄他們的 commit K,這將使他們留下:
...--G--H--I--J <-- main
\
K ???
他們會說no,并且您將收到的錯誤訊息不是快進的(還記得那些來自合并的訊息嗎?這是相同的想法)。
您可以使用--forceor--force-with-lease嘗試讓他們接受更改,從而丟失他們的新提交,但通常這是錯誤的做法。但是,對于您對 GitHub 的使用,有時這是在您的 fork 上做的正確的事情!我們稍后再談。
還有一種方法可以使用 洗掉名稱git push。其實有好幾種,但最清楚的大概就是:會舍棄過來。這對您的筆記本電腦存盤庫沒有影響。git push --delete remote branchgit push --delete origin foobranchfoobranchorigin
GitHub“分叉”
我們現在有足夠的背景來定義 GitHub 的FORK按鈕是如何作業的。您選擇一些不屬于您自己的現有存盤庫并單擊它,GitHub 將在 GitHub 上創建一個您自己的新存盤庫。這個 GitHub “fork”是一種克隆,但增加了幾個特性和一個變化。
現在您知道不git clone復制任何分支,因此更改很明顯。當您使用 GitHub 的 fork 按鈕時,它會復制所有分支。您的新克隆具有與原始克隆相同的一組提交和分支,而常規克隆到您的筆記本電腦,它獲取所有提交但沒有任何分支,然后創建一個新分支,該分支完全不小心故意匹配origin's 分支之一。fork 按鈕使您的 fork 中的所有分支名稱與其他存盤庫的所有分支完全匹配。
增加的功能包括提出拉取請求的想法,我們稍后再討論。在 GitHub 方面——你看不到,但對 GitHub 本身非常重要——增加的功能包括不使用任何空間來保存提交:你的 fork 只是重新使用原始提交。任何提交都不能改變,所以這很好;唯一可能發生的問題是如果要洗掉提交,因此 GitHub 只是安排永遠不會洗掉提交。1
但是,一旦你創建了分支,這些分支名稱,在你的分支中的 GitHub 上,不會再更新,除非你這樣做。您可以從 GitHub 的 Web 界面執行一些操作(例如,您可以洗掉分支名稱),或者您可以git push照常在筆記本電腦上使用。
因此,一旦您確實有一個 fork,您將希望將該 fork 克隆到您的筆記本電腦,然后在筆記本電腦上添加第二個 URL,該 URL 指向您 fork 的存盤庫。命名第二個 URL 的標準 GitHub 方法是使用遠程名稱upstream。我個人不喜歡這個名字,因為上游這個詞在 Git, 2中已經有幾個含義,但是要使用它運行,如果你已經 forkssh://github.com/them/repo.git到ssh://github.com/you/repo.git,你會運行:
git clone ssh://github.com/you/repo.git
cd repo
git remote add upstream ssh://github.com/them/repo.git
git fetch upstream
你現在有origin/*和upstream/*名字。我們現在來看看一個方便的技巧。
1這意味著如果有人不小心在 GitHub 上輸入了密碼,它可能會永遠存在,即使他們很快強行將其隱藏。GitHub 支持可以“真正地”清除提交,但一般來說,始終考慮任何意外暴露的秘密,即使是一瞬間也將永遠受到損害。
2所以,至少比分支這個詞好。??
小技巧:更新你的 fork
運行后git fetch upstream,git remote update您的upstream/*名稱都已更新,您可能希望自己的 fork 將所有更新都放在相同的分支名稱下。這意味著對于每個upstream/whatever,您要運行:
git push origin upstream/whatever:whatever
這種git push使用refspec,我們將“源”名稱放在左側,然后是冒號,然后在右側放置“目標”名稱。Git 將從給定的源(我們的本地upstream/whatever遠程跟蹤名稱)中挑選提交,但是當它們到達目的地(origin)時,要求目的地設定它們的目的地端名稱(他們的whatever)。
您可以使用回圈來執行此操作,但有更短的方法。請注意,您可能需要保護*您的 shell 中的字符,具體取決于您的特定命令列解釋器:
git push origin "refs/remotes/upstream/*:refs/heads/*"
我假設您需要雙引號才能獲得正確的保護。如果您不能使用雙引號,請使用所需的任何參考機制(可能根本不需要)。
在這里,我們已經完整地拼出了名稱:遠程跟蹤名稱存在于refs/remotes/名稱空間中,而分支名稱存在于refs/heads/. git pushGit 匹配兩顆星,并在此處對每個分支進行常規(非強制) 。
您可以創建一個執行此操作的 Git 別名git push,以避免鍵入長命令并避免參考 refspec(簡單的 Git 別名不通過 shell):
[alias]
up2hub = push origin refs/remotes/upstream/*:refs/heads/*
請注意,這嵌入了名稱upstream和origin,但現在您可以運行:
git up2hub
成功后git fetch upstream,更新您的 GitHub 分支。
拉取請求
現在我們終于找到了問題的核心:拉取請求是如何作業的。 當您使用CREATE PULL REQUESTGitHub 上的按鈕時,您會選擇兩件事,盡管 GitHub 會默認為您選擇其中一項:
- 叉子中的分支名稱;和
- 另一個GitHub 存盤庫3中的“基礎分支” ,您希望針對它進行此 PR。
GitHub 現在將運行“測驗合并”,在那里他們嘗試對git merge分支上的一組提交進行常規的提交,這些提交由 Pull Request 匯入到他們的存盤庫,到他們分支的當前提示提交。也就是說,GitHub 會獲取您在 fork 中的每一個提交,而它們在其存盤庫中根本沒有,并將這些提交復制到它們的存盤庫。4
他們現在可以在拉取請求的全名下找到您的提交,在 GitHub 上是. 測驗合并如果有效,將創建一個新的提交并使其可以通過. 如果它因合并沖突而失敗,PR 仍然會生成,只是沒有名稱。refs/pull/number/headrefs/pull/number/mergerefs/pull/number/merge
3您可以將拉取請求發送到共享訪問存盤庫,您和其他人都在該存盤庫中推送到單個存盤庫,而不是各自推送到各自的分支。在這種情況下,您會選擇這個存盤庫本身作為“其他”Git 存盤庫。但這只是一種特殊情況,其中“其他”存盤庫是“這個”。
4這一切都是虛擬發生的,以節省磁盤空間:他們的存盤庫有一個指向您的鏈接,因此沒有實際的復制,就像您在分叉他們的存盤庫時沒有真正復制他們的提交一樣。同樣,Git 使用了這樣一個事實,即每個提交都有一個唯一的哈希 ID:您的提交以及您的哈希 ID,保證與所有提交都有不同的哈希 ID。因此,當他們的 Git 軟體嘗試查找提交fee1cab或任何哈希 ID 時,但找不到它,他們只需查看您連接的分支,就可以了。你的 fork 參考了他們的 repo,而他們的 repo 又參考了你的 fork,在一種亂倫的回圈中。
那么,這意味著什么?
好吧,讓我們看一個經典的例子。您 fork 一些存盤庫,并將其克隆到您的筆記本電腦并創建一個新分支:
...--G--H <-- main, my-feature-1, origin/main
my-feature-1你在你的分支上做了幾個新的提交:
I--J <-- my-feature-1 (HEAD)
/
...--G--H <-- main, origin/main
您將這些提交發送到您的 GitHub 分支:
I--J <-- my-feature-1 [on your fork]
/
...--G--H <-- main [on your fork]
然后,您單擊按鈕進行 PR,在他們的分叉中,他們現在擁有:
I--J <-- refs/pull/123/head
/ \
/ M <-- refs/pull/123/merge
/ /
...--G--H---K--L <-- main
CommitM是 GitHub 的“測驗合并”,它奏效了;commitsK-L是他們在你忙于使用 fork 和筆記本電腦時所做的新提交。
如果你現在繼續做:
I--J <-- my-feature-1, my-feature-2 (HEAD)
/
...--G--H <-- main, origin/main
在您的筆記本電腦上,然后再進行兩次提交,您將獲得:
N--O <-- my-feature-2 (HEAD)
/
I--J <-- my-feature-1
/
...--G--H <-- main, origin/main
您可以將git push這些添加到您的 GitHub 分支中,以便它具有:
N--O <-- my-feature-2 [on your fork]
/
I--J <-- my-feature-1 [on your fork]
/
...--G--H <-- main [on your fork]
如果您現在使用此my-feature-2 名稱在 GitHub 上創建 PR ,則該 PR 包含尚未提交的提交I-J-N-O,main因為他們尚未決定如何處理 PR#123:
N--O <-- refs/pull/124/head
/
I--J <-- refs/pull/123/head
/ \
/ M <-- refs/pull/123/merge
/ /
...--G--H---K--L <-- main
(加上,也許,一個測驗合并,如果他們能夠合并O)L。
如果這不是你想要的,你應該放在 GitHub 上的是:
I--J <-- my-feature-1 [on your fork]
/
...--G--H <-- main [on your fork]
\
N--O <-- my-feature-2 [on your fork]
您my-feature-2現在只包含兩個不在其 中的提交main,因此 PR#124 使他們的存盤庫看起來像這樣:
I--J <-- refs/pull/123/head
/ \
/ M <-- refs/pull/123/merge
/ /
...--G--H---K--L <-- main
\ \
\ P <-- refs/pull/123/merge
\ /
N--O <-- refs/pull/124/head
uj5u.com熱心網友回復:
第 3 部分:你的下一個絆腳石:變基
當他們(無論他們是誰)開始審查您的 PR 時,他們可能會:
- 照原樣接受;
- 要求您修復其中的某些內容;
- 接受他們所做的改變;要么
- 完全拒絕它。
最后一個在這里不需要更多討論,但其他三個確實需要。
如果他們“按原樣”接受您的提交,他們可以選擇MERGE在 GitHub 上使用三個不同的“接受此 PR”按鈕:
- MERGE. 這個很簡單。
- REBASE AND MERGE. 這個不太直接。
- SQUASH AND MERGE. 這需要了解 Git 的“壁球合并”,這根本不是合并。
如果他們自己做出任何改變,情況很像REBASE AND MERGE,我們將看到。如果他們希望您進行更改,您將需要git rebase在筆記本電腦上使用,之后您將需要使用git push --force或git push --force-with-lease更新您的 GitHub 存盤庫。5
5從技術上講,您可以洗掉并重新創建分支,而不是強制推送。不過,我認為這會扼殺現有的 PR(我還沒有嘗試過)。無論如何,強制推送選項是人們在實踐中使用的。
Rebase 是關于復制提交
我們使用git rebase將現有的提交(其中有我們喜歡的和不喜歡的)復制到我們使用而不是原始的新的和(據說無論如何)改進的提交。
在我們看之前,讓我們看一下復制一個提交的命令。正如我們所知,任何提交都無法更改,但我們總是可以提取提交,或將其轉換為差異,或其他任何方式。我們可以使用此屬性來獲取一些現有的提交,將其轉換為更改,然后再次或在其他地方應用這些更改。Git將此操作稱為cherry-picking,并使用該命令git cherry-pick來執行此操作。
通常git cherry-pick,出于某種原因,我們可能會使用它作為一種快速獲取提交的一次性副本的方法。例如,也許有人有一個好主意,或者一個錯誤修復,我們現在需要在我們的分支上,我們將弄清楚如何處理這將在以后造成的混亂。在這里,我們在我們的分支上:
...--J--K <-- feature (HEAD)
與此同時,在他們的分支上,他們修復了一些困擾我們的討厭的小錯誤:
...--P--C--R--S <-- theirs
(在這里,我們將在P、“父”提交、 “子”之后稱為 fix-commit,而不是我通常使用C的字母)。Q我們跑:
git cherry-pick <hash-of-C>
告訴 Git:去弄清楚誰在 commit 中做了什么,它C的子節點P,通過比較P并C查看發生了什么變化。然后在我的 commit 上進行相同的更改K,并從中進行新的提交。 生成的圖表如下所示:
...--J--K--C' <-- feature (HEAD)
Git 會將他們列為 commit 的作者,并重C'用他們的提交資訊;我們將被列為. C'(大多數時候,作者和提交者是同一個人,但不一定;這樣的副本通常讓他們保持作者身份,并且也會保留他們的提交日期和時間戳。)
Git 實際實作這種“復制他們的更改”的方式是,Git 必須弄清楚要觸摸哪些檔案以及這些行去了哪里。為了做到這一點,Git 做一個 diff from PtoC看看他們做了什么,然后第二個 diff from PtoK看看我們做了什么。回想一下是如何git merge作業的:這是合并的核心:合并兩個差異并將合并的差異應用于合并基礎。Git 只是強制“基礎”為 commit P,不管其他一切。我們的 commit 是 commit K,他們的 commit是 commit C,僅此而已——除了 Git 提交合并時,它會將其作為普通的單父提交。CommitC'指回K僅,不至P或C。
如果一切順利(如果沒有合并沖突),最終結果是我們從 commit獲得了更改C,但在 commit 處應用了這里K。因此,新提交C'是它的副本C:
- 有一些與以下相同的元資料
C:特別是日志訊息和作者身份;和 - 具有相同的diff,除了如果我們必須解決合并沖突而添加或洗掉的任何內容。
有了git rebase,我們可以:
- 獲取一系列現有提交并簡單地移動它們,或者
- 使用互動式rebase,做各種額外的擺弄。
互動式 rebase 本身就是一篇很大的博文或文章,我們不會在這里詳細介紹。我們將只看一個簡單的 rebase-to-move 作業。我們從例如:
I--J <-- feature (HEAD), origin/feature
/
...--G--H--K--L <-- upstream/main
比方說,在這一點上,我們喜歡一切,I-J 除了他們不追隨K-L。讓我們再添加一個問題:存在合并沖突,例如,因為我們接觸的J其中一條線與我們(或他們)接觸的其中一條線相鄰L。這種合并沖突,或者被 Git 視為沖突的事情,確實很容易修復,但 Git 不會這樣做,所以我們必須自己去做。
此時,我們只需運行:
git rebase upstream/main
請注意,我們不必在這里使用分支名稱,我們甚至不必更新origin:我們可以基于我們擁有的任何提交,使用該提交的任何名稱。名稱upstream/main找到了 commit L,這就是我們想要復制I和J追求的提交,所以這就是我們在這里給的名稱git rebase。
Git 將在內部保存提交的原始哈希 IDI和J,它們是要復制的提交。(Git 是如何知道這一點的——以及我們如何將被復制的內容更改到哪里——在其他地方得到了回答,但請注意,我們當前的分支名稱指向J并且提交I-J是唯一可以從當前分支訪問的。)然后,Git將切換到提交L——我們命名的那個——在 Git 所謂的分離 HEAD模式下。這里根本沒有當前分支:HEAD直接指向當前提交。所以現在我們有了這個:
I--J <-- feature, origin/feature
/
...--G--H--K--L <-- upstream/main, HEAD
Git 現在做一個櫻桃挑選來復制I. 這有效,Git 進行了新的提交I':
I--J <-- feature, origin/feature
/
...--G--H--K--L <-- upstream/main
\
I' <-- HEAD
Git 現在嘗試第二次挑選來復制J. 這一個因合并沖突而失敗。我們通過在編輯器中打開沖突檔案,將正確的合并結果放入檔案中,然后將檔案寫回我們的作業樹,然后git add在該檔案上運行來解決沖突。然后我們使用:
git rebase --continue
使 Git 恢復提交和復制等等。Git 為J'(副本,我們的解決方案,作為git add-ed)進行提交:
I--J <-- feature, origin/feature
/
...--G--H--K--L <-- upstream/main
\
I'-J' <-- HEAD
如果有更多的提交要復制,Git 會嘗試挑選下一個。盡管如此,沒有什么可以復制的,git rebase它的最終操作也是如此:
- rebase 將名稱拉到
feature這里,無論在哪里HEAD;和 - rebase 重新附加
HEAD
所以我們現在有:
I--J origin/feature
/
...--G--H--K--L <-- upstream/main
\
I'-J' <-- feature (HEAD)
原始I-J提交仍然存在。如果您記下他們的哈希 ID(或仍然在螢屏上顯示它們),您仍然可以看到它們。使用名稱origin/feature,您仍然可以看到它們。但是,如果您使用該名稱 feature來查找提交,您將找到新副本而不是原始副本。
更新拉取請求
為了更新我們現有的拉取請求(可能是 PR#125,更新feature到我們從其分叉的存盤庫中的一個分支名稱),我們只需告訴GitHub接受這兩個新提交。一個平原:
git push origin feature
不會作業,因為 GitHub 上的 Git 會反對:嘿,如果我更新我的feature,我將失去這兩個非常有價值的I-J提交!不是快進! 被拒絕! 我們必須強制它更新,使用新的替代品I'-J'。6 所以我們運行:
git push --force-with-lease origin feature
或更短的--force變體。(這個--with-lease增加了一些錯誤檢查,是個好主意,但至少對我來說仍然感覺很新奇并且打字很笨拙。這是使用 Git 15 年以上的缺點之一。)我們告訴 GitHub我們是認真的,他們接受了新的承諾。
由于有一個公開的 PR 參考name feature,此時 GitHub 將再次嘗試合并。他們最后一次嘗試此合并時,與 commit 存在合并沖突L。這一次,我們添加了他們的 commit L,所以沒有合并沖突,并且 PR 可能會被原樣接受。
如果我們需要進行額外的更改,我們可以使用花哨的互動式 rebase,或者做額外的提交然后 squash,或者任何我們喜歡的。
6如果 Git 知道這些是進化的替代品,那就太好了。
擠壓
Git 提供了它所拼寫的東西git merge --squash。這不是合并,就像快進不是合并一樣:沒有最終的合并提交。但它是一個合并,就像git merge做一對差異和組合作業一樣:有一個組合作業部分。
鑒于:
...--G--H--I--J <-- br1 (HEAD)
\
K--L <-- br2
如果我們運行git merge --squash br2,Git 將:
H像往常一樣找到合并基地;* 像往常一樣做兩個差異;- 合并差異,如果有合并沖突就停止,讓我們修復混亂;要么
- 如果沒有沖突,無論如何都要停止。
我相信這個“??無論如何都停止”只是原始實作的一個意外——git merge有一個--no-commit標志讓它停止,它應該與 分開--squash,但--squash總是打開--no-commit。但是,無論如何,我們現在必須通過運行來完成操作git commit,它會像往常一樣提交 Git 索引和作業樹中的內容,并且不會進行合并提交。M我們沒有與兩個父級合并,而是像往常一樣與一個父級進行簡單的普通提交S——“壁球合并”:
...--G--H--I--J--S <-- br1 (HEAD)
\
K--L <-- br2
新提交中的快照與我們將擁有S的快照相同,如果我們進行了常規合并,但沒有鏈接回,現在唯一有用的名稱是洗掉它。7 這兩個提交實際上已合并或壓縮為單個提交。提交具有與之前相同的效果,因此現在無用并且應該被遺忘。SLbr1K-LSSK-LK-L
7可以這樣做,但這超出了此答案的范圍。
squash-merge 與 GitHub PR 的關系
如果上游有人拿走了你的 PR 并squash-merge它,你給他們的東西——也許是多次提交——現在被一個單一的 squash 提交替換:
I--J <-- refs/pull/125/head
/
...--G--H--K--L--S <-- theirbranch
在這里,他們的提交S代表了你在I-J. 您所有的實際作業,以及至少您的一些提交日志訊息,都被它們的壁球所取代(它們可能會或可能不會保留您的一些提交日志訊息)。您應該獲得提交S(進入您的upstream/theirbranch)并使用它,放棄您的I-J原件。
rebase-and-merge 對你的 PR 有什么影響
如果上游有人拿走你的 PR 并使用REBASE AND MERGE按鈕,GitHub 的 Git 軟體會將你的每個提交復制到 new-and-improved?提交。即使沒有真正的需要,他們也會這樣做。例如,您可能已經仔細地將您的基礎重新設定為I-J他們的基礎L,以便您擁有:
I'-J' <-- refs/pull/125/head
/
...--G--H--K--L <-- theirbranch
在你做 PR#125 的時候。但是他們點擊了“錯誤”的8按鈕,因為他們喜歡線性圖,所以現在在他們的存盤庫中他們有:
I'-J' <-- refs/pull/125/head
/
...--G--H--K--L--I"--J" <-- theirbranch
其中I"是 的副本I',J"是 的副本J'。這些副本保留了您的原始日志訊息,并以您為作者,但它們具有新的和不同的哈希 ID。
你需要放棄你原來的提交,轉而支持這些“新的和改進的”提交。不過,有一件好事——另一個方便的技巧——git rebase讓你更容易。
8我只稱其為“錯誤”,因為它不必要地重復提交。對于您的 PR 掛起 commit 的情況H,確實必須重新設定提交,以使圖表呈線性。但是,效果是您是作者,他們是提交者,并且這些提交具有新的和不同的哈希 ID。
方便的技巧:rebase 知道副本
當有人完成這種“變基和合并”時,你幾乎總是可以用git rebase 自己來替換你原來的提交——即使你自己對它們進行了多次變基——用他們新的變基副本。原因是當 Git 列出要復制的提交時,它會檢查這些提交是否已經在您要復制到的位置。也就是說,假設你的筆記本電腦上有這個:
K--L <-- feature2 (requires feature)
/
I--J <-- feature
/
...--G--H <-- upstream/theirbranch
您現在從 中進行 PR feature,他們按原樣執行,但使用“錯誤”按鈕,以便您的git fetch upstream結果如下:
K--L <-- feature2 (requires feature)
/
I--J <-- feature
/
...--G--H--I'-J' <-- upstream/theirbranch
您現在必須在 commitK'-L'上進行提交J'。
您可以顯式執行此操作 ( git switch feature2; git rebase --onto upstream/theirbranch feature),但是:
git switch feature2
git rebase upstream/theirbranch
將完成這項作業。原因是 GitI-J-K-L首先列出了要復制的四個提交,然后查看提交I'-J'并確定這些是IandJ的副本。rebase 代碼使用它來完全洗掉提交I-J,從而導致:
I--J <-- feature
/
...--G--H--I'-J' <-- upstream/theirbranch
\
K'-L' <-- feature2 (HEAD)
事實上,如果您還運行git switch featurethen git rebase upstream/theirbranch,您的 Git 將簡單地從復制程序中洗掉提交I-J,留下以下內容:
...--G--H--I'-J' <-- upstream/theirbranch, feature (HEAD)
\
K'-L' <-- feature2
如果有人必須手動修復至少一個提交,這(完全)不起作用。在過去,在 GitHub 獲得一些額外工具之前,這永遠不可能直接在GitHub 上發生。現在(他們擁有這些工具)至少在理論上是可能的。
uj5u.com熱心網友回復:
當您執行拉取請求時,您建議將您的一個分支合并到原始存盤庫的一個分支中。每次更新分支時,合并都會更新。當您進行修復或審查后更新時,這非常有用。
針對您的案例的幾種解決方案,簡單地關閉您的拉取請求,為每個要提交的主題創建一個分支(每個分支基于分叉存盤庫的主干)。
第二種解決方案:創建一個分支來讓你額外的作業回到主分支(或主分支)強制已經提交的分支到原始提交并推送它
git checkout -b my_second_feature
git checkout main
git reset --hard <commit_sha>
git push -f
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/428337.html
上一篇:描述命名GITHUB的問題
