![](/img/trans.png)
[英]How to "git log --follow <path>" in JGit? (To retrieve the full history including renames)
[英]Git log (--follow) not working to show history beyond renames
我嘗試通過 gitlog 在我的 git 中顯示文件的完整歷史記錄。 問題是該文件的父文件夾在歷史記錄中被重命名,我喜歡查看完整的歷史記錄。
git-log 文檔說參數--follow
和-M
show 在重命名之后生成 git log。
我嘗試了 gitlog 參數的不同組合,例如
git log -M --oneline --all -- --follow newpath/my-file.php
git log -M --oneline --all -- newpath/my-file.php
甚至
git rev-list --all -- newpath/my-file.php --objects --in-commit-order | git log --no-walk --oneline --stdin
但是無論我嘗試什么,歷史總是在文件的父文件夾被重命名的提交處結束。
我已經可以確認:
只有文件夾在重命名提交中被重命名,文件內容 100% 不變,所以 git 應該簡單地發現舊路徑上的文件和新路徑上的文件是相同的並且應該重命名。
重命名提交的git shot name-status
顯示R100 oldpath/my-file.php newpath/my-file.php
(確認文件內容 100% 相同)
歷史的“舊一半”和“新一半”似乎是正確的,都包括 rename-commt
當我運行git log -M --oneline --all -- --follow newpath/my-file.php
時,最舊的提交是0979744 renamed: oldpath/ -> newpath/
當我運行git log -M --oneline --all -- --follow oldpath/my-file.php
時,最新提交是0979744 renamed: oldpath/ -> newpath/
所以一切看起來像我的 git 成功理解了新路徑中的文件和舊路徑中的文件被重命名。 誰能告訴我為什么即使我使用-M
和--follow
選項,重命名提交的歷史仍然會中斷?
如注釋中所述,-- --follow
選項必須在獨立的--
之前,這表示選項列表的結尾。
即使它下面的重命名現在似乎也有效,當我添加
--grep="rename" --invert-grep
以刪除“重命名”提交時,我得到 0 個結果
這是有道理的(但這是一個錯誤), 1因為--follow
的工作方式。 這里的問題是 Git 根本沒有任何類型的文件歷史記錄。 Git 所擁有的只是存儲庫中的一組提交。 提交是歷史:
每個提交都通過其大而丑陋的哈希 ID 進行編號,該 ID 對於該特定提交是唯一的。 沒有其他提交(在任何Git 存儲庫2中)具有該哈希 ID。
每個提交都有每個文件的完整快照。
每個提交還存儲先前提交的哈希 ID,或者,對於合並提交,兩個或多個先前提交。
所以這些數字將提交串在一起,向后:
... <-F <-G <-H
這里的大寫字母代表實際的提交哈希 ID,Git 通過它找到提交。 每個提交都有一個“后向箭頭”——前一次提交的存儲哈希 ID——因此,如果我們能記住鏈中最后一次提交的哈希 ID,我們可以讓 Git 向后工作。鏈。
分支名稱只是告訴 Git 哪個提交是該分支中的最后一個提交:
I--J <-- feature1
/
...--F--G--H
\
K--L <-- feature2
在這里,提交J
是功能分支的最后一次提交,而提交L
是另一個上的最后一次提交。 請注意,通過H
向上提交在兩個分支上(很可能也在主分支或主分支上)。
git log
命令簡單地處理提交,一次一個,從您選擇的任何“最后一次提交”開始。 默認的“最后一次提交”是您現在簽出的任何分支的尖端。 這個過程向后工作:Git 從最后一次提交開始並向后工作,一次提交一個。
git diff
的-M
選項是--find-renames
,可以在git diff
中啟用重命名檢測。 git log 的--follow
選項對git log
git log
相同的操作,但也使用單個文件的名稱來查找。 (為git log
提供-M
選項使其在每個差異中使用重命名檢測器,但由於它不是在尋找一個特定的文件,這只會影響-p
或--name-status
輸出樣式。使用--follow
, git log
正在尋找那個特定的文件,我們稍后會看到。)
重命名檢測器以這種方式工作:
你給 Git 兩次提交,之前和之后或舊和新,或者說, F
和G
(你可以把新的提交放在左邊,舊的放在右邊,但是git log
本身總是把舊的放在左邊,新的放在右邊。)
你讓 Git 比較這兩個提交中的快照。
這些提交中的一些文件是 100% 相同的:它們具有相同的名稱和相同的內容。 Git 的內部存儲系統已經對這些文件進行了重復數據刪除,這使得git diff
或git log
很容易確定這些文件是相同的,因此它可以在適當的時候跳過它們。
其他文件名稱相同但內容不同。 Git 假設,默認情況下,如果兩個文件具有相同的名稱——例如path/to/file.ext
:注意嵌入的斜杠只是文件名的一部分——它們代表“相同的文件”,即使內容發生了變化。 所以該文件被修改,從舊的/左側提交到新的/右側提交。 如果您要求--name-status
,您將獲得M
, modified ,作為該文件名的狀態。
有時,左側提交有一個名為fileL
的文件,而右側提交根本沒有該文件。 顯然,在從舊(左)到新(右)的變化中,該文件被刪除。 使用--name-status
你會得到D
的狀態。
有時,右側提交有一個名為fileR
的文件,而左側提交則沒有。 顯然,該文件是新添加的,並且使用--name-status
您將獲得A
的狀態。
但是如果左邊的fileL
和右邊的fileR
應該被認為是“同一個文件”呢? 也就是說,如果我們將 fileL重命名為fileL
會fileR
? 這就是 Git 的重命名檢測器的用武之地。給定這樣的刪除/添加對,可能fileR
的內容與fileL
的內容足夠接近或完全相同。 如果:
然后——也只有那時——Git 會聲明fileL
被重命名為fileR
。 --name-status
輸出將包括R
、相似性索引值和兩個文件名,而不是在左側和右側提交中匹配的單個文件名。
現在您知道了重命名檢測器的工作原理——並且必須打開它——你可以看到--follow
是如何工作的。 請記住,使用git log
,您可以給它一個文件名,並告訴它不要顯示不修改該特定文件的提交。 3結果是您只看到確實修改該文件的提交: git log
訪問的所有提交集合的一個子集。 因此,假設您運行git log --follow -- newpath/my-file.php
:
git log
遍歷歷史,一次提交一次,像往常一樣向后。
在每次提交時,它都會將此提交(較新,在右側)與其父提交(較舊,在左側)進行比較。 如果沒有--follow
它仍然會這樣做,但只需查看您命名的文件是否已更改( M
狀態,來自git diff --name-status
)或添加或刪除( A
, D
)。 4但使用--follow
,它還會查找R
狀態。
如果文件被更改——具有M
或A
或D
狀態—— git log
會打印出這個提交,但如果沒有,它只會抑制打印輸出。 使用--follow
,我們添加R
狀態,如果發生這種情況,添加兩個文件名。 如果狀態是R
,那么git log
之前一直在尋找newpath/my-file.php
。 但現在它知道,從父提交開始,該文件被稱為oldpath/my-file.php
。 (再次注意,這里沒有文件夾。文件名是整個字符串,包括所有斜杠。)
因此,使用--follow
——打開重命名檢測器—— git log
可以獲得重命名狀態,因此可以看到文件被重命名。 它還在尋找一個特定的文件名,在本例中為newpath/my-file.php
。 如果它檢測到重命名, git log
不僅會打印提交,還會更改它正在尋找的一個名稱。 現在,它不是newpath/my-file.php
,而是從父提交向后,它正在尋找oldpath/my-file.php
。
1 --follow
代碼本身......不是很好; 整個實現需要重新設計,這可能會比我想的更簡單的hack更好地解決這個問題。
2從技術上講,其他一些 Git 存儲庫可能有一個不同的提交來重用該哈希 ID,只要您從不將這兩個提交相互介紹。 但在實踐中,你不會找到一個。
3 --follow
選項只能跟隨一個文件名。 如果沒有--follow
,您可以為git log
提供多個名稱,或者一個“目錄”的名稱,即使 Git 根本不存儲目錄。 沒有--follow
的git log
代碼在通用路徑規范上運行。 使用--follow
,它只處理一個文件名。 這是 Git 在這里使用的算法所施加的限制。
4它也可以有T
,類型改變,我認為這很重要。 完整的狀態字母集是ABCDMRTUX
但X
表示 Git 中的一個錯誤, U
只能在未完成的合並期間出現, B
只能在帶有-B
選項的git diff
中出現, C
和R
只能在--find-copies
中出現--find-copies
和--find-renames
( -C
和-M
)選項已啟用。 請注意, git diff
可能會根據您的diff.renames
設置自動啟用--find-renames
renames ,但git log
不會。
--follow
中的錯誤這個從git log
的輸出顯示中刪除一些提交的過程稱為歷史簡化。 文檔中有很長的部分描述了這一點,它以這個相當奇怪的聲明開頭:
有時您只對歷史的一部分感興趣,例如修改特定 <path> 的提交。 但是History Simplification有兩個部分,一個是選擇提交,另一個是如何去做,因為有多種策略可以簡化歷史。
這個奇怪的措辭——“一部分是選擇提交,另一部分是如何去做”——試圖說明的是,啟用歷史簡化后, git git log
有時甚至不會執行一些提交。 特別是,考慮一個合並提交,其中兩個提交字符串組合在一起:
C--...--K
/ \
...--A--B M--N--O <-- branch
\ /
D--...--L
要顯示所有提交, git log
將必須遍歷提交O
,然后是N
,然后是M
,然后是K
和L
(按某種順序),然后是K
之前的所有提交和L
之前的所有提交,然后返回C
和D
,然后在提交B
處重新加入單個線程,並從那里繼續向后。
但是,如果我們不打算顯示所有提交,也許——只是也許——在提交M
處,我們可以返回只提交K
或只提交L
並完全忽略合並的另一“邊”。 這將節省大量工作和時間,並避免向您展示無關緊要的內容。 這通常是一件非常好的事情。
然而,對於--follow
來說,這通常是一件非常糟糕的事情。 這是--follow
的問題之一:有時 Git 在進行這種簡化時會走“錯誤的腿”。 添加--full-history
可以避免這種情況,但我們立即遇到了另一個問題。 --follow
選項只有一個文件名。 如果我們在提交的兩個分支中的一個中進行了重命名,但在另一個中沒有,並且git log
首先沿着重命名分支向下,那么當它沿着另一分支向下時它可能會查找錯誤的名稱。
如果文件在兩條腿中重命名,以便將其從M
重命名為K
並從M
重命名為L
,或者如果 Git 碰巧首先沿着正確的腿而你不關心另一條腿,一切正常。 但這是需要注意的。 (這不是用--grep
打擊你的問題,否則它會在沒有--grep
的情況下發生。)
我認為您看到的錯誤是--grep
正在“過早”啟動,事實上。 --grep
選項通過從git log
的輸出中消除任何在其提交消息中具有( --invert-grep
)或缺少( --grep
without --invert-grep
)某些特定文本的提交來起作用。 然后,假設重命名提交(導致git log --follow
知道使用名稱oldpath/my-file.php
的提交)被您的--grep
選項跳過。 Git 不會看到R
狀態,也不會知道將名稱從newpath/my-file.php
更改為oldpath/my-file.php
。 因此git log --follow
將繼續尋找新路徑,並且您將只獲得那些既符合 grep 標准又使用新名稱修改文件的提交。
這個錯誤可以通過讓git log --follow
運行差異引擎來修復,即使它會因為其他原因跳過提交。 但更一般地說--follow
需要完全重寫:它有一堆奇怪的特殊情況代碼通過差異引擎線程化,只是為了使這個案例工作。 它需要處理多個路徑名和/或路徑規范,並使用--reverse
和其他選項。 它需要一種將新舊名稱堆疊到提交路徑上的方法,以便使用--full-history
,沿着合並的兩條腿,它知道要尋找哪條路徑。 請注意,這還有其他含義:如果在合並的兩條腿上都有不同的重命名怎么辦? 如果有人在合並中手動修復了重命名/重命名沖突,我們該如何處理?
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.