學慣用工具來駕馭 Git 歷史
在你的日常工作中,不可能每天都從頭開始去開發一個新的應用程序。而真實的情況是,在日常工作中,我們大多數時候所面對的都是遺留下來的一個代碼庫,去修改一些特性的內容或者現存的一些代碼行,這是我們在日常工作中很重要的一部分。而這也就是分散式版本控制系統 git
的價值所在。現在,我們來深入了解怎麼去使用 git
的歷史以及如何很輕鬆地去瀏覽它的歷史。
Git 歷史
首先和最重要的事是,什麼是 git
歷史?正如其名字一樣,它是一個 git
倉庫的提交歷史。它包含一堆提交信息,其中有它們的作者的名字、該提交的哈希值以及提交日期。查看一個 git
倉庫歷史的方法很簡單,就是一個 git log
命令。
旁註:為便於本文的演示,我們使用 Ruby on Rails 的倉庫的
master
分支。之所以選擇它的理由是因為,Rails 有良好的git
歷史,漂亮的提交信息、引用以及對每個變更的解釋。如果考慮到代碼庫的大小、維護者的年齡和數量,Rails 肯定是我見過的最好的倉庫。當然了,我並不是說其它的git
倉庫做的不好,它只是我見過的比較好的一個倉庫。
那麼,回到 Rails 倉庫。如果你在 Ralis 倉庫上運行 git log
。你將看到如下所示的輸出:
commit 66ebbc4952f6cfb37d719f63036441ef98149418
Author: Arthur Neves <foo@bar.com>
Date: Fri Jun 3 17:17:38 2016 -0400
Dont re-define class SQLite3Adapter on test
We were declaring in a few tests, which depending of the order load will cause an error, as the super class could change.
see https://github.com/rails/rails/commit/ac1c4e141b20c1067af2c2703db6e1b463b985da#commitcomment-17731383
commit 755f6bf3d3d568bc0af2c636be2f6df16c651eb1
Merge: 4e85538 f7b850e
Author: Eileen M. Uchitelle <foo@bar.com>
Date: Fri Jun 3 10:21:49 2016 -0400
Merge pull request #25263 from abhishekjain16/doc_accessor_thread
[skip ci] Fix grammar
commit f7b850ec9f6036802339e965c8ce74494f731b4a
Author: Abhishek Jain <foo@bar.com>
Date: Fri Jun 3 16:49:21 2016 +0530
[skip ci] Fix grammar
commit 4e85538dddf47877cacc65cea6c050e349af0405
Merge: 082a515 cf2158c
Author: Vijay Dev <foo@bar.com>
Date: Fri Jun 3 14:00:47 2016 +0000
Merge branch 'master' of github.com:rails/docrails
Conflicts:
guides/source/action_cable_overview.md
commit 082a5158251c6578714132e5c4f71bd39f462d71
Merge: 4bd11d4 3bd30d9
Author: Yves Senn <foo@bar.com>
Date: Fri Jun 3 11:30:19 2016 +0200
Merge pull request #25243 from sukesan1984/add_i18n_validation_test
Add i18n_validation_test
commit 4bd11d46de892676830bca51d3040f29200abbfa
Merge: 99d8d45 e98caf8
Author: Arthur Nogueira Neves <foo@bar.com>
Date: Thu Jun 2 22:55:52 2016 -0400
Merge pull request #25258 from alexcameron89/master
[skip ci] Make header bullets consistent in engines.md
commit e98caf81fef54746126d31076c6d346c48ae8e1b
Author: Alex Kitchens <foo@bar.com>
Date: Thu Jun 2 21:26:53 2016 -0500
[skip ci] Make header bullets consistent in engines.md
正如你所見,git log
展示了提交的哈希、作者及其 email 以及該提交創建的日期。當然,git
輸出的可定製性很強大,它允許你去定製 git log
命令的輸出格式。比如說,我們只想看提交信息的第一行,我們可以運行 git log --oneline
,它將輸出一個更緊湊的日誌:
66ebbc4 Dont re-define class SQLite3Adapter on test
755f6bf Merge pull request #25263 from abhishekjain16/doc_accessor_thread
f7b850e [skip ci] Fix grammar4e85538 Merge branch 'master' of github.com:rails/docrails
082a515 Merge pull request #25243 from sukesan1984/add_i18n_validation_test
4bd11d4 Merge pull request #25258 from alexcameron89/master
e98caf8 [skip ci] Make header bullets consistent in engines.md
99d8d45 Merge pull request #25254 from kamipo/fix_debug_helper_test
818397c Merge pull request #25240 from matthewd/reloadable-channels
2c5a8ba Don't blank pad day of the month when formatting dates
14ff8e7 Fix debug helper test
如果你想看 git log
的全部選項,我建議你去查閱 git log
的 man 頁面,你可以在一個終端中輸入 man git-log
或者 git help log
來獲得。
小提示:如果你覺得
git log
看起來太恐怖或者過於複雜,或者你覺得看它太無聊了,我建議你去尋找一些git
的 GUI 或命令行工具。在之前,我使用過 GitX ,我覺得它很不錯,但是,由於我看命令行更「親切」一些,在我嘗試了 tig 之後,就再也沒有去用過它。
尋找尼莫
現在,我們已經知道了關於 git log
命令的一些很基礎的知識之後,我們來看一下,在我們的日常工作中如何使用它更加高效地瀏覽歷史。
假如,我們懷疑在 String#classify
方法中有一個預期之外的行為,我們希望能夠找出原因,並且定位出實現它的代碼行。
為達到上述目的,你可以使用的第一個命令是 git grep
,通過它可以找到這個方法定義在什麼地方。簡單來說,這個命令輸出了匹配特定模式的那些行。現在,我們來找出定義它的方法,它非常簡單 —— 我們對 def classify
運行 grep,然後看到的輸出如下:
➜ git grep 'def classify'
activesupport/lib/active_support/core_ext/string/inflections.rb: def classifyactivesupport/lib/active_support/inflector/methods.rb: def classify(table_name)tools/profile: def classify
現在,雖然我們已經看到這個方法是在哪裡創建的,但是,並不能夠確定它是哪一行。如果,我們在 git grep
命令上增加 -n
標誌,git
將提供匹配的行號:
➜ git grep -n 'def classify'
activesupport/lib/active_support/core_ext/string/inflections.rb:205: def classifyactivesupport/lib/active_support/inflector/methods.rb:186: def classify(table_name)tools/profile:112: def classify
更好看了,是吧?考慮到上下文,我們可以很輕鬆地找到,這個方法在 activesupport/lib/active_support/core_ext/string/inflections.rb
的第 205 行的 classify
方法,它看起來像這樣,是不是很容易?
# Creates a class name from a plural table name like Rails does for table names to models.
# Note that this returns a string and not a class. (To convert to an actual class
# follow +classify+ with +constantize+.)
#
# 'ham_and_eggs'.classify # => "HamAndEgg"
# 'posts'.classify # => "Post"
def classify
ActiveSupport::Inflector.classify(self)
end
儘管我們找到的這個方法是在 String
上的一個常見的調用,它調用了 ActiveSupport::Inflector
上的另一個同名的方法。根據之前的 git grep
的結果,我們可以很輕鬆地發現結果的第二行, activesupport/lib/active_support/inflector/methods.rb
在 186 行上。我們正在尋找的方法是這樣的:
# Creates a class name from a plural table name like Rails does for table
# names to models. Note that this returns a string and not a Class (To
# convert to an actual class follow +classify+ with constantize).
#
# classify('ham_and_eggs') # => "HamAndEgg"
# classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
# classify('calculus') # => "Calculus"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*./, ''.freeze)))
end
酷!考慮到 Rails 倉庫的大小,我們藉助 git grep
找到它,用時都沒有超越 30 秒。
那麼,最後的變更是什麼?
現在,我們已經找到了所要找的方法,現在,我們需要搞清楚這個文件所經歷的變更。由於我們已經知道了正確的文件名和行數,我們可以使用 git blame
。這個命令展示了一個文件中每一行的最後修訂者和修訂的內容。我們來看一下這個文件最後的修訂都做了什麼:
git blame activesupport/lib/active_support/inflector/methods.rb
雖然我們得到了這個文件每一行的最後的變更,但是,我們更感興趣的是對特定方法(176 到 189 行)的最後變更。讓我們在 git blame
命令上增加一個選項,讓它只顯示那些行的變化。此外,我們將在命令上增加一個 -s
(忽略)選項,去跳過那一行變更時的作者名字和修訂(提交)的時間戳:
git blame -L 176,189 -s activesupport/lib/active_support/inflector/methods.rb
9fe8e19a 176) #Creates a class name from a plural table name like Rails does for table
5ea3f284 177) # names to models. Note that this returns a string and not a Class (To
9fe8e19a 178) # convert to an actual class follow +classify+ with #constantize).
51cd6bb8 179) #
6d077205 180) # classify('ham_and_eggs') # => "HamAndEgg"
9fe8e19a 181) # classify('posts') # => "Post"
51cd6bb8 182) #
51cd6bb8 183) # Singular names are not handled correctly:
5ea3f284 184) #
66d6e7be 185) # classify('calculus') # => "Calculus"
51cd6bb8 186) def classify(table_name)
51cd6bb8 187) # strip out any leading schema name
5bb1d4d2 188) camelize(singularize(table_name.to_s.sub(/.*./, ''.freeze)))
51cd6bb8 189) end
現在,git blame
命令的輸出展示了指定行的全部內容以及它們各自的修訂。讓我們來看一下指定的修訂,換句話說就是,每個變更都修訂了什麼,我們可以使用 git show
命令。當指定一個修訂哈希(像 66d6e7be
)作為一個參數時,它將展示這個修訂的全部內容。包括作者名字、時間戳以及完整的修訂內容。我們來看一下 188 行最後的修訂都做了什麼?
git show 5bb1d4d2
你親自做實驗了嗎?如果沒有做,我直接告訴你結果,這個令人驚嘆的 提交 是由 Schneems 完成的,他通過使用 frozen 字元串做了一個非常有趣的性能優化,這在我們當前的場景中是非常有意義的。但是,由於我們在這個假設的調試會話中,這樣做並不能告訴我們當前問題所在。因此,我們怎麼樣才能夠通過研究來發現,我們選定的方法經過了哪些變更?
搜索日誌
現在,我們回到 git
日誌,現在的問題是,怎麼能夠看到 classify
方法經歷了哪些修訂?
git log
命令非常強大,因此它提供了非常多的列表選項。我們嘗試使用 -p
選項去看一下保存了這個文件的 git
日誌內容,這個選項的意思是在 git
日誌中顯示這個文件的完整補丁:
git log -p activesupport/lib/active_support/inflector/methods.rb
這將給我們展示一個很長的修訂列表,顯示了對這個文件的每個修訂。但是,正如下面所顯示的,我們感興趣的是對指定行的修訂。對命令做一個小的修改,只顯示我們希望的內容:
git log -L 176,189:activesupport/lib/active_support/inflector/methods.rb
git log
命令接受 -L
選項,它用一個行的範圍和文件名做為參數。它的格式可能有點奇怪,格式解釋如下:
git log -L <start-line>,<end-line>:<path-to-file>
當我們運行這個命令之後,我們可以看到對這些行的一個修訂列表,它將帶我們找到創建這個方法的第一個修訂:
commit 51xd6bb829c418c5fbf75de1dfbb177233b1b154
Author: Foo Bar <foo@bar.com>
Date: Tue Jun 7 19:05:09 2011 -0700
Refactor
diff--git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -58,0 +135,14 @@
+ # Create a class name from a plural table name like Rails does for table names to models.
+ # Note that this returns a string and not a Class. (To convert to an actual class
+ # follow +classify+ with +constantize+.)
+ #
+ # Examples:
+ # "egg_and_hams".classify # => "EggAndHam"
+ # "posts".classify # => "Post"
+ #
+ # Singular names are not handled correctly:
+ # "business".classify # => "Busines"
+ def classify(table_name)
+ # strip out any leading schema name
+ camelize(singularize(table_name.to_s.sub(/.*./, '')))
+ end
現在,我們再來看一下 —— 它是在 2011 年提交的。git
可以讓我們重回到這個時間。這是一個很好的例子,它充分說明了足夠的提交信息對於重新了解當時的上下文環境是多麼的重要,因為從這個提交信息中,我們並不能獲得足夠的信息來重新理解當時的創建這個方法的上下文環境,但是,話說回來,你不應該對此感到惱怒,因為,你看到的這些項目,它們的作者都是無償提供他們的工作時間和精力來做開源工作的。(向開源項目貢獻者致敬!)
回到我們的正題,我們並不能確認 classify
方法最初實現是怎麼回事,考慮到這個第一次的提交只是一個重構。現在,如果你認為,「或許、有可能、這個方法不在 176 行到 189 行的範圍之內,那麼就你應該在這個文件中擴大搜索範圍」,這樣想是對的。我們看到在它的修訂提交的信息中提到了「重構」這個詞,它意味著這個方法可能在那個文件中是真實存在的,而且是在重構之後它才存在於那個行的範圍內。
但是,我們如何去確認這一點呢?不管你信不信,git
可以再次幫助你。git log
命令有一個 -S
選項,它可以傳遞一個特定的字元串作為參數,然後去查找代碼變更(添加或者刪除)。也就是說,如果我們執行 git log -S classify
這樣的命令,我們可以看到所有包含 classify
字元串的變更行的提交。
如果你在 Ralis 倉庫上運行上述命令,首先你會發現這個命令運行有點慢。但是,你應該會發現 git
實際上解析了在那個倉庫中的所有修訂來匹配這個字元串,其實它的運行速度是非常快的。在你的指尖下 git
再次展示了它的強大之處。因此,如果去找關於 classify
方法的第一個修訂,我們可以運行如下的命令:
git log -S 'def classify'
它將返回所有這個方法的引用和修改的地方。如果你一直往下看,你將看到日誌中它的最後的提交:
commit db045dbbf60b53dbe013ef25554fd013baf88134
Author: David Heinemeier Hansson <foo@bar.com>
Date: Wed Nov 24 01:04:44 2004 +0000
Initial
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
很酷!是吧?它初次被提交到 Rails,是由 DHH 在一個 svn
倉庫上做的!這意味著 classify
大概在一開始就被提交到了 Rails 倉庫。現在,我們去看一下這個提交的所有變更信息,我們運行如下的命令:
git show db045dbbf60b53dbe013ef25554fd013baf88134
非常好!我們終於找到它的根源了。現在,我們使用 git log -S 'def classify'
的輸出,結合 git log -L
命令來跟蹤這個方法都發生了哪些變更。
下次見
當然,我們並沒有真的去修改任何 bug,因為我們只是去嘗試使用一些 git
命令,來演示如何查看 classify
方法的演變歷史。但是不管怎樣,git
是一個非常強大的工具,我們必須學好它、用好它。我希望這篇文章可以幫助你掌握更多的關於如何使用 git
的知識。
你喜歡這些內容嗎?
作者簡介:
後端工程師,對 Ruby、Go、微服務、構建彈性架構來解決大規模部署帶來的挑戰很感興趣。我在阿姆斯特丹的 Rails Girls 擔任顧問,維護著一個小而精的列表,並且經常為開源做貢獻。
那個列表是我寫的關於軟體開發、編程語言以及任何我感興趣的東西。
via: https://ieftimov.com/learn-your-tools-navigating-git-history
作者:Ilija Eftimov 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive