我首先要說我對 git 很陌生,并且主要將它與 Atom 的內置插件(以及 Git-plus)一起使用。
當我部署到生產環境時,我需要多個版本的代碼,每個版本都在自己的目錄中。我最初的想法是只在一個名為 的目錄中管理一個版本的代碼(在Dev分支中)dev,當一個版本準備好時:標記它,然后合并到Release分支。在這里,我將運行一個腳本來更改名稱空間、目錄名稱(dev--> v1)等。然后將其合并到Main中。
DEV RELEASE (pre-script) RELEASE (post-script) MAIN (PRODUCTION)
======================================================================================
app/ app/ app/ app/
|--src/ |--src/ |--src/ |--src/
|----globals/ |----globals/ |----globals/ |----globals/
|----api/ |----api/ |----api/ |----api/
|------dev/ |------dev/ |------v1/ |------v1/
|--------file1-dev |--------file1-dev |--------file1-v1 |--------file1-v1
|--------file2-dev |--------file2-dev |--------file2-v1 |--------file2-v1
然后我回到Dev并開始在 v2 上作業(比如我修改file1andfile2并添加一個新的file3)完成它,標記它,合并到Release中。
這是此 Dev-Release 合并的問題!
Git 太聰明了,將我的 v2 更改添加到Release的v1檔案夾,而不是像我錯誤地期望的那樣添加Dev的dev檔案夾(我通常會在該檔案夾上運行腳本將其轉換為v2. 所以這就是它的樣子:
DEV RELEASE (pre-script) RELEASE (what I need)
===================================================================
app/ app/ app/
|--src/ |--src/ |--src/
|----globals/ |----globals/ |----globals/
|----api/ |----api/ |----api/
|------dev/ |------v1/ |------v1/
|--------file1-dev |--------file1-v1(mod) |--------file1-v1
|--------file2-dev |--------file2-v1(mod) |--------file2-v1
|--------file3-dev |--------file3-dev |------dev/
|--------file1-dev
|--------file2-dev
|--------file3-dev
我期待發布分支保持v1檔案夾不變并引入“新”dev檔案夾,但事實并非如此。Git 認識到這一點dev并且v1是相同的,因此它修改檔案v1而不是在dev.
我能想到的唯一替代方法是手動管理“生產就緒”目錄,并在存盤庫之外執行此操作。在這種情況下,我將不再需要Release和Main分支,所以一旦我在Dev中完成并標記了版本,我只需將我的 repo 復制到一個臨時目錄中(我在其中運行腳本來打開dev-> v1),然后將其移動v1到單獨的生產目錄中。這似乎有點愚蠢且容易出錯,但可行。
有任何想法、評論、解決方案甚至反饋嗎?我基本上認為可以有一個包含所有代碼版本的專用分支。
謝謝,
uj5u.com熱心網友回復:
TL;博士
您可能可以使用-X find-renames=高閾值(例如,100),或者-X no-renames完全禁用重命名檢測。
長
就個人而言,我認為你最好的選擇可能是在 Git 之外發生的部署程序,或者至少在 Git 之外完成它的作業,然后從結果中進行提交。然而,很容易直接從 Git 獲得你想要的東西(盡管這有其他后果)。這里的訣竅是,對于 Git,每次提交實際上都是所有檔案的快照,但是,好吧:
對于git merge您運行的命令——“作為動詞合并”——Git 定位三個快照,運行兩個git diff操作,然后組合兩個差異,包括檢測重命名。然后將組合的差異應用于三個快照之一。這里的目標是結合作業。這提出了一個顯而易見的問題,除了很多人似乎錯過了它:
如果提交只是檔案的快照,那么“作業”甚至意味著什么?
這里的訣竅是提交不僅僅是檔案的快照。相反,提交是快照加上元資料。提交中的元資料包括相關提交的作者和提交者的姓名和電子郵件地址,以及他們愿意提供的任何日志訊息,以幫助代碼的未來讀者弄清楚他們為什么做出該特定提交。但是,對于 Git 的內部操作來說至關重要的是,每個提交還包括一個原始提交哈希 ID 串列。
(讓我們在這里暫停一下,注意每個提交都有一個唯一的哈希 ID。當我說唯一時,我的意思是唯一的:在任何 Git 存盤庫中,任何其他地方都不允許使用相同的哈希 ID。你所做的每個提交獲得一個新的哈希 ID,以前從未使用過,永遠不會再次使用。理論上,這是行不通的,但實際上它確實有效;哈希 ID 空間足夠大,不太可能發生沖突,盡管有可能使它們發生沖突目的:查看新發現的 SHA-1 沖突如何影響 Git? 這里還有很多可以探索的,但出于實際目的,每個哈希 ID 都是唯一的,這意味著僅哈希 ID 就足以準確地知道哪個提交是哪個。)
所以,給定一些任意的提交,我們只需將其哈希稱為H哈希,我們可以讓 Git 從哈希 ID 中查找提交。這包括先前或父提交哈希 ID 的串列。大多數提交在這里只有一個父級;ifH是這樣一個提交,它包含一些哈希 ID,為了方便起見,我們只需呼叫G. 然后我們說 commitH 指向它的父 commit G,我們可以得出:
<-G <-H
當然G也是一個提交,因此指向它的parent F,而它又是一個指向它的parent 的提交:
... <-F <-G <-H
歸納說,這樣做的結果是一個向后看的提交鏈:我們從最后開始,最新的開始,我們可以找到每個較早的提交,一直到第一個提交,其先前的串列-提交哈希 ID 為空,這樣我們就可以停止倒退。
這為我們提供了“提交中完成的作業”的簡單定義,例如 commit H:我們查找 的快照,H而且,使用 中的元資料, H我們查找其父項的快照G。然后我們使用git diff(比較或檔案差異引擎)來比較兩個快照。從這個差異中溢位的東西是commit 所做的更改H。
分枝
當我們得到分支合并序列時,這變得特別有趣。讓我們從分支部分開始:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
在這里,我們有一個簡單的樹狀資料結構,由提交形成,其中作業在提交之后發生分歧H。我們I-J在分支上進行了兩次提交br1(因此名稱br1指向最后一次此類提交),并且我們K-L在分支上進行了兩次提交br2(因此名稱br2指向最后一次此類提交。)
(在這里我們可以暫停一下,注意在內部,在 Git 中,一個分支名稱只指向一個提交。我們說這個提交是該分支上的最后一個提交:它是分支的尖端提交。 查看提交涉及在該提示提交中提取保存的快照,然后該分支名稱是當前分支,該提交是當前提交。然后,如果我們創建一個新提交,則新提交將獲得一個新的唯一哈希 ID 和指向當前提交作為其父提交,然后 Git 簡單地將新的哈希 ID 填充到分支名稱中以便名稱現在指向新的提交。這就是我們得到不同情況的原因:我們有兩個名稱都指向H. 我們推進一個,制作Iand J,然后回到。)其他名稱再次獲取H,然后推進其他名稱制作K-L
合并
現在我們在存盤庫中有這個分支結構,我們可以看到git merge它的“組合更改”將如何作業。我們檢查其中一個br1or br2(或用于git switch使其成為最新并檢查其提示提交,這意味著相同的事情,但在 Git 2.23 中是一個新命令,因為git checkout它過于復雜并且做了太多事情)。假設我們切換到br1這里,并通過將特殊名稱附加HEAD到分支名稱來對其進行注釋(這實際上是 Git 所做的):
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
然后我們運行git merge br2. Git 定位三個提交:
- commit
J(--ours) 因為這是我們當前的提交; - commit
L(--theirs) 因為它是我們在命令列命名的提交,使用分支名稱,翻譯為該分支的提示提交;和 - 提交
H,因為它是兩個分支上的最佳提交(或技術上,最低的共同祖先1)。
因此,Git 現在git diff使用快照 inH作為合并基礎提交,針對 中的快照運行J,以查看“我們更改了什么”。然后 Gitgit diff使用相同的合并基礎快照運行快照K以查看“他們改變了什么”。現在可以通過簡單而愚蠢的文本組合演算法來組合生成的逐行差異。在差異不重疊或鄰接的地方,Git 只接受兩組更改。在差異確實重疊的地方,當且僅當它們完全重疊時,Git 才會復制一份更改。在差異重疊且不匹配或只是鄰接的地方,2 Git 將宣告合并沖突,中途停止合并,讓你收拾殘局。不過,出于篇幅原因,我們將跳過所有這些細節。
然后,在組合了兩組差異后,Git 將組合的差異應用于合并基礎提交中的快照——即 commit H。這“保留我們的更改并添加他們的”,或者等效地,“保留他們的更改并添加我們的”:結果是對稱的。3 但是,合并提交不是很對稱。Git 現在創建了一個“合并提交”型別的新提交,我們可以這樣繪制:
I--J
/ \?
...--G--H M <-- br1 (HEAD)
\ /2
K--L <-- br2
新提交M在當前分支上進行,就像任何新提交一樣,所以名稱br1現在指向M. 新提交M有一個快照,就像任何提交一樣:這是合并作為動詞、組合作業程序生成的快照。但是這個合并提交與其他提交有一個完全不同的地方:它有一個包含兩個父級的串列,而不僅僅是一個。它向后指向 commit J,前一個分支提示提交,作為它的第一個父級(因此是 tiny 1),并向后指向 commitL作為它的第二個父級(因此是 tiny 2)。4
大多數時候,我們并不關心 first-vs-non-first 合并父級,但我們可以在合并提交時git log --first-parent只跟蹤第一個父級,這樣我們就可以查看“開發主線”而無需查看每個功能的每次提交:我們只看到引入該功能的合并提交。如果您喜歡這種能夠使用的想法--first-parent,您可能希望避免使用git pull,至少如果沒有使用--rebase:請參閱如何防止在我的“主”分支中合并狐步舞? 但那是另一回事。讓我們繼續。
1由于我們在這里使用 LCA 的完整有向無環圖版本,因此可以有多個 LCA。請參閱我關于遞回合并的較長答案之一,以了解 Git 如何處理此問題。
2一些合并演算法允許無怨無悔地鄰接更改;Git 沒有,也沒有選擇旋鈕來啟用它們。這只是 Git 在某些時候做出的選擇:它對任何事情都不是真正的基礎。
3在發生沖突的情況下,可以使用使結果不對稱的標志;我們只關心這里簡單的無沖突案例。
4 Git 支持它稱為octopus 合并的東西,其中合并提交具有三個或更多父級。我們也不會在這里介紹這些,但它們并沒有真正做任何特別的事情,它們只是讓有向無環圖更難思考。
合并提供了新的合并基礎
如果我們看一下主線和功能分支在簡化世界中的作業方式,我們可以看到每個功能如何提供為一種連接到主線的“合并氣泡”:
o--o--o <-- feature2
/ \
...--o--B---------M---------N <-- main
\ /
o--o--o <-- feature1
M當我們 make的時候,合并基礎M是 commit B;合并基礎N,一旦我們做出N,就是提交M。不過,這些比現實世界的合并要簡單得多,可能會像這樣變得混亂:
...--o--B----o----M--P--o--S--o <-- main
\ / \ \
o--o--L--N---R--o--T <-- feature
在這里,我們為一個特性創建了一個分支,從 commit 分支出來B,這是我們早期合并M回主線時的合并基礎:
...--o--B----o----M <-- main
\ /
o--o--L <-- feature
但是我們還沒有完成這個功能,所以我們繼續對N功能進行新的提交,然后有一個重要的修復直接進入main提交P(我跳過了這封信O,因為它與小寫字母太相似,o更無聊在這里提交):
...--o--B----o----M--P <-- main
\ /
o--o--L--N <-- feature
我們決定將修復合并P到featuretoo 中,導致合并提交R(跳過Q,因為它也與Oor太相似o):
...--o--B----o----M--P <-- main
\ / \
o--o--L--N---R <-- feature
但是當我們制作時R,合并基礎是什么?合并基礎是最好的共享提交,即兩個分支上的最佳提交。這不再是 commit B,因為如果我們從開始N并向后作業,我們會看到 commitN和 then L,如果我們從開始P并向后作業,我們會同時遇到 commit和M然后兩者。所以合并基礎現在是 commit !oLL
后來,在進行了幾次提交之后,我們有:
...--o--B----o----M--P--o--S <-- main
\ / \
o--o--L--N---R--o <-- feature
從提示提交回傳feature,我們通過R并到達P;我們也P開始向后作業,當我們進行新的提交時,我們的合并基礎S也是如此,然后將另一個提交添加到:PTmain
...--o--B----o----M--P--o--S--o <-- main
\ / \ \
o--o--L--N---R--o--T <-- feature
此時 commitS現在是 和 之間的合并main基礎feature。因此,您可以看到作為 的主要控制點的合并基礎如何隨著時間的推移git merge而移動,因為我們進行了較早的合并提交。
本質上,我們有兩個規則:
- 存盤庫中的提交是存盤庫中的歷史記錄;
- 存盤庫中的合并提交為將來的合并提供了基礎。
這意味著當我們進行合并提交時,它的快照和父級對于未來的合并至關重要。它將幫助確定未來的合并基礎,合并提交本身或它的一些派生,將提供進入合并程序的快照之一。
重命名檢測
當git merge運行它運行的兩個git diff命令時,它git diff決定了某個檔案是否被重命名。Git 中的重命名被檢測而不是記錄:每個提交都包含所有檔案的完整快照,但資料或元資料中沒有任何內容表明在 commit 中I,檔案與commit中的檔案foo.py“相同檔案” ,或者不是。bar.pyH
其他版本控制系統提供某種檔案識別符號,例如“inode 編號”或“檔案密鑰”或其他東西,以便我們知道已bar.py重命名為foo.py. Git 不這樣做。相反,如果將提交H作為一個整體來看bar.py,L有一個名為同一個檔案,只是重命名了!bar.pyfoo.py
這實際上在很多情況下都非常有效。Git 注意到:“嘿,檔案 X 和 Y 存在于左側而右側消失了,但出現了新檔案 F1、F2、F3、F4,也許 X 是其中之一,而 Y 是其中之一”。Git 為所有此類檔案配對計算相似性指數(通過一些調整和巧妙的優化,否則這至少是O(mn) 且具有大常數,但即使這樣,這種計算也相當昂貴)。如果相似度索引超過某個最小閾值,Git 會將具有最佳(最高)索引值的對配對,并將它們稱為“相同的檔案,但已重命名”。
您始終可以git diff在合并之前手動運行:使用git merge-base自己查找合并基礎提交,然后使用您喜歡的任何重命名閾值運行您自己的git diff命令。-M您可能還希望添加--name-status以抑制實際差異并僅獲取檔案串列,包括重命名的檔案。(您有時可能也需要--diff-filter;請參閱檔案。)當重命名檔案串列與您想要的匹配時,您已經找到了合適的閾值。
如果您只想完全禁用重命名檢測,也git diff可以提供--no-renames。(另外,--find-renames=是-M這里的同義詞。)
給定適當的選項——-X我喜歡稱這些擴展策略選項來幫助記住標志字母5——git merge將傳遞其中一個--no-renames或傳遞給兩個命令。這將確定哪個重命名命令“看到”。什么時候合并作業,如果它看到一側有重命名,而另一側沒有,則進行重命名;如果它看到兩個相同的重命名,它也會進行重命名;如果它看到兩個不同的重命名,它宣告一個合并沖突。這些高級或樹級沖突有時無法通過擴展策略選項解決。--find-renames=numbergit diffgit mergegit mergegit merge-X
簡單地完全禁用重命名檢測可能適用于您的特定用例。請注意,盡管 Git不會記錄此處使用的合并選項,并且如果您曾經運行git rebase -raka git rebase --rebase-merges,Git 將重新執行任何合并,但由于它沒有保存您使用的選項,因此會將這些合并作為默認合并執行,使用默認重命名檢測。
5 Git 檔案將-s選項稱為策略選項,將-X選項稱為策略選項選項。所以我們有-s, strategy 和-X, strategy-option,這只是令人困惑。但是,如果我們將這些視為-s= 策略,-X= eXtended-(strategy-)-option,那么這一切都是有道理的。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/488206.html
