學習用工具來駕馭 Git 歷史

Ilija Eftimov發表於2018-02-27

學習用工具來駕馭 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

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章