1. 程式人生 > >Git系統從0到1的完整學習歷程(第二節 掌握git基礎)

Git系統從0到1的完整學習歷程(第二節 掌握git基礎)

有兩種取得 Git 專案倉庫的方法。

第一種是在現存的目錄下,通過匯入所有檔案來建立新的 Git 倉庫。

第二種是從已有的 Git 倉庫克隆出一個新的映象倉庫來,這種公司專案中用的比較多。

在工作目錄中初始化新倉庫

要對現有的某個專案開始用 Git 管理,只需到此專案所在的目錄,執行:

$ git init

 

圖中程式碼解釋:

cd myWebSite———進入新建的資料夾 
echo 'My website is alive!' > index.html——新建的目錄下 新建了一個index.html檔案,並初始化了內容 
git init—————將所在目錄(myWebSite)轉化為git版本庫(此時的版本庫是空的,目錄下的檔案預設不會被放入版本庫中,視為臨時檔案)

從現有倉庫克隆

如果想對某個開源專案出一份力,可以先把該專案的 Git 倉庫複製一份出來,這就需要用到 git clone 命令。

如果你熟悉其他的 VCS 比如 Subversion,你可能已經注意到這裡使用的是 clone 而不是 checkout。這是個非常重要的差別,Git 收取的是專案歷史的所有資料(每一個檔案的每一個版本),伺服器上有的資料克隆之後本地也都有了。

實際上,即便伺服器的磁碟發生故障,用任何一個克隆出來的客戶端都可以重建伺服器上的倉庫,回到當初克隆時的狀態(雖然可能會丟失某些伺服器端的掛鉤設定,但所有版本的資料仍舊還在)。

克隆倉庫的命令格式為 git clone [url]。比如,要克隆 Ruby 語言的 Git 程式碼倉庫 Grit,可以用下面的命令:

$ git clone git://github.com/schacon/grit.git

這會在當前目錄下建立一個名為grit的目錄,其中包含一個 .git 的目錄,用於儲存下載下來的所有版本記錄,然後從中取出最新版本的檔案拷貝。如果進入這個新建的 grit 目錄,你會看到專案中的所有檔案已經在裡邊了,準備好後續的開發和使用。

如果希望在克隆的時候,自己定義要新建的專案目錄名稱,可以在上面的命令末尾指定新的名字

$ git clone git://github.com/schacon/grit.git mygrit

唯一的差別就是,現在新建的目錄成了 mygrit,其他的都和上邊的一樣。

Git 支援許多資料傳輸協議。

之前的例子使用的是 git:// 協議,不過你也可以用 http(s):// 或者 [email protected]:/path.git 表示的 SSH 傳輸協議。

記錄每次更新到倉庫

現在我們手上已經有了一個真實專案的 Git 倉庫,並從這個倉庫中取出了所有檔案的工作拷貝。接下來,對這些檔案作些修改,在完成了一個階段的目標之後,提交本次更新到倉庫。

請記住,工作目錄下面的所有檔案都不外乎這兩種狀態:已跟蹤未跟蹤

已跟蹤的檔案是指本來就被納入版本控制管理的檔案,在上次快照中有它們的記錄,工作一段時間後,它們的狀態可能是未更新,已修改或者已放入暫存區。而所有其他檔案都屬於未跟蹤檔案。它們既沒有上次更新時的快照,也不在當前的暫存區域。

初次克隆某個倉庫時,工作目錄中的所有檔案都屬於已跟蹤檔案,且狀態為未修改。

在編輯過某些檔案之後,Git 將這些檔案標為已修改。我們逐步把這些修改過的檔案放到暫存區域,直到最後一次性提交所有這些暫存起來的檔案,如此重複。所以使用 Git 時的檔案狀態變化週期如圖 2-1 所示。

 

檔案的狀態變化週期

檢查當前檔案狀態

要確定哪些檔案當前處於什麼狀態,可以用 git status 命令。如果在克隆倉庫之後立即執行此命令,會看到類似這樣的輸出:

$ git status
    # On branch master
    nothing to commit (working directory clean)

這說明你現在的工作目錄相當乾淨。

換句話說,所有已跟蹤檔案在上次提交後都未被更改過。

 

此外,上面的資訊還表明,當前目錄下沒有出現任何處於未跟蹤的新檔案,否則 Git 會在這裡列出來。最後,該命令還顯示了當前所在的分支是 master,這是預設的分支名稱,實際是可以修改的,現在先不用考慮。

現在讓我們 git status 會看到該檔案出現在未跟蹤檔案列表中:

在狀態報告中可以看到新建的檔案出現在“Untracked files”下面。未跟蹤的檔案意味著Git在之前的快照(提交)中沒有這些檔案;Git 不會自動將之納入跟蹤範圍,除非你明明白白地告訴它“我需要跟蹤該檔案”,因而不用擔心把臨時檔案什麼的也歸入版本管理。不過現在的例子中,我們確實想要跟蹤管理 index.html這個檔案。

跟蹤新檔案

使用命令 git add 開始跟蹤一個新檔案。所以,要跟蹤 README 檔案,執行:

$ git add README

此時再執行 git status 命令,會看到 README 檔案已被跟蹤,並處於暫存狀態:

$ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    #

只要在 “Changes to be committed” 這行下面的,就說明是已暫存狀態。如果此時提交,那麼該檔案此時此刻的版本將被留存在歷史記錄中。

你可能會想起之前我們使用 git init 後就運行了 git add 命令,開始跟蹤當前目錄下的檔案。

在 git add 後面可以指明要跟蹤的檔案或目錄路徑。如果是目錄的話,就說明要遞迴跟蹤該目錄下的所有檔案。

(譯註:其實 git add 的潛臺詞就是把目標檔案快照放入暫存區域,也就是 add file into staged area,同時未曾跟蹤過的檔案標記為需要跟蹤。這樣就好理解後續 add 操作的實際意義了。

暫存已修改檔案

現在我們修改下之前已跟蹤過的檔案 benchmarks.rb,然後再次執行 status 命令,會看到這樣的狀態報告:

$ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    #
    # Changes not staged for commit:
    # (use "git add <file>..." to update what will be committed)
    #
    # modified: benchmarks.rb
    #

檔案 benchmarks.rb 出現在 “Changes not staged for commit” 這行下面,說明已跟蹤檔案的內容發生了變化,但還沒有放到暫存區。要暫存這次更新,需要執行 git add 命令(這是個多功能命令,根據目標檔案的狀態不同,此命令的效果也不同:可以用它開始跟蹤新檔案,或者把已跟蹤的檔案放到暫存區,還能用於合併時把有衝突的檔案標記為已解決狀態等)。

現在讓我們執行 git add 將 benchmarks.rb 放到暫存區,然後再看看 git status 的輸出:

$ git add benchmarks.rb
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    # modified: benchmarks.rb
    #

現在兩個檔案都已暫存,下次提交時就會一併記錄到倉庫。

假設此時,你想要在 benchmarks.rb 裡再加條註釋,重新編輯存檔後,準備好提交。

不過且慢,再執行 git status 看看:

$ vim benchmarks.rb
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    # modified: benchmarks.rb
    #
    # Changes not staged for commit:
    # (use "git add <file>..." to update what will be committed)
    #
    # modified: benchmarks.rb
    #

怎麼回事? benchmarks.rb 檔案出現了兩次!一次算未暫存,一次算已暫存,這怎麼可能呢?好吧,實際上 Git 只不過暫存了你執行 git add 命令時的版本,如果現在提交,那麼提交的是添加註釋前的版本,而非當前工作目錄中的版本。

所以,運行了 git add 之後又作了修訂的檔案,需要重新執行 git add 把最新版本重新暫存起來

$ git add benchmarks.rb
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    # modified: benchmarks.rb
    #

忽略某些檔案

一般我們總會有些檔案無需納入 Git 的管理,也不希望它們總出現在未跟蹤檔案列表。通常都是些自動生成的檔案,比如日誌檔案,或者編譯過程中建立的臨時檔案等。我們可以建立一個名為 .gitignore 的檔案,列出要忽略的檔案模式。來看一個實際的例子:

$ cat .gitignore
    *.[oa]
    *~

第一行告訴 Git 忽略所有以 .o 或 .a 結尾的檔案。一般這類物件檔案和存檔檔案都是編譯過程中出現的,我們用不著跟蹤它們的版本。

第二行告訴 Git 忽略所有以波浪符(~)結尾的檔案,許多文字編輯軟體(比如 Emacs)都用這樣的檔名儲存副本。

此外,你可能還需要忽略 logtmp 或者 pid 目錄,以及自動生成的文件等等。

要養成一開始就設定好 .gitignore 檔案的習慣,以免將來誤提交這類無用的檔案。

檔案 .gitignore 的格式規範如下:

  • 所有空行或者以註釋符號  開頭的行都會被 Git 忽略。
  • 可以使用標準的 glob 模式匹配。
  • 匹配模式最後跟反斜槓(/)說明要忽略的是目錄。
  • 要忽略指定模式以外的檔案或目錄,可以在模式前加上驚歎號(!)取反。

所謂的 glob 模式是指 shell 所使用的簡化了的正則表示式

星號(*)匹配零個或多個任意字元;

[abc] 匹配任何一個列在方括號中的字元(這個例子要麼匹配一個 a,要麼匹配一個 b,要麼匹配一個 c);

問號(?)只匹配一個任意字元;

如果在方括號中使用短劃線分隔兩個字元,表示所有在這兩個字元範圍內的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的數字)。

我們再看一個 .gitignore 檔案的例子:

# 此為註釋 – 將被 Git 忽略
    # 忽略所有 .a 結尾的檔案
    *.a
    # 但 lib.a 除外
    !lib.a
    # 僅僅忽略專案根目錄下的 TODO 檔案,不包括 subdir/TODO
    /TODO
    # 忽略 build/ 目錄下的所有檔案
    build/
    # 會忽略 doc/notes.txt 但不包括 doc/server/arch.txt
    doc/*.txt

檢視已暫存和未暫存的更新

實際上 git status 的顯示比較簡單,僅僅是列出了修改過的檔案,如果要檢視具體修改了什麼地方,可以用 git diff 命令。稍後我們會詳細介紹 git diff,不過現在,它已經能回答我們的兩個問題了:

當前做的哪些更新還沒有暫存?

有哪些更新已經暫存起來準備好了下次提交?

 git diff 會使用檔案補丁的格式顯示具體新增和刪除的行。

假如再次修改 README 檔案後暫存,然後編輯 benchmarks.rb 檔案後先別暫存,執行 status 命令將會看到:

$ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    #
    # Changes not staged for commit:
    # (use "git add <file>..." to update what will be committed)
    #
    # modified: benchmarks.rb
    #

要檢視尚未暫存的檔案更新了哪些部分,不加引數直接輸入 git diff

$ git diff
    diff --git a/benchmarks.rb b/benchmarks.rb
    index 3cb747f..da65585 100644
    --- a/benchmarks.rb
    +++ b/benchmarks.rb
    @@ -36,6 +36,10 @@ def main
    @commit.parents[0].parents[0].parents[0]
    end

    + run_code(x, 'commits 1') do
    + git.commits.size
    + end
    +
    run_code(x, 'commits 2') do
    log = git.commits('master', 15)
    log.size

此命令比較的是工作目錄中當前檔案和暫存區域快照之間的差異,也就是修改之後還沒有暫存起來的變化內容。

若要看已經暫存起來的檔案和上次提交時的快照之間的差異,可以用 git diff --cached 命令。(Git 1.6.1 及更高版本還允許使用 git diff --staged,效果是相同的,但更好記些。)來看看實際的效果:

$ git diff --cached
    diff --git a/README b/README
    new file mode 100644
    index 0000000..03902a1
    --- /dev/null
    +++ b/README2
    @@ -0,0 +1,5 @@
    +grit
    + by Tom Preston-Werner, Chris Wanstrath
    + http://github.com/mojombo/grit
    +
    +Grit is a Ruby library for extracting information from a Git repository

請注意,單單 git diff 不過是顯示還沒有暫存起來的改動,而不是這次工作和上次提交之間的差異。所以有時候你一下子暫存了所有更新過的檔案後,執行 git diff 後卻什麼也沒有,就是這個原因。

像之前說的,暫存 benchmarks.rb 後再編輯,執行 git status 會看到暫存前後的兩個版本:

$ git add benchmarks.rb
    $ echo '# test line' >> benchmarks.rb
    $ git status
    # On branch master
    #
    # Changes to be committed:
    #
    # modified: benchmarks.rb
    #
    # Changes not staged for commit:
    #
    # modified: benchmarks.rb
    #

現在執行 git diff 看暫存前後的變化:

$ git diff
    diff --git a/benchmarks.rb b/benchmarks.rb
    index e445e28..86b2f7c 100644
    --- a/benchmarks.rb
    +++ b/benchmarks.rb
    @@ -127,3 +127,4 @@ end
    main()

    ##pp Grit::GitRuby.cache_client.stats
    +# test line

然後用 git diff --cached 檢視已經暫存起來的變化:

$ git diff --cached
    diff --git a/benchmarks.rb b/benchmarks.rb
    index 3cb747f..e445e28 100644
    --- a/benchmarks.rb
    +++ b/benchmarks.rb
    @@ -36,6 +36,10 @@ def main
    @commit.parents[0].parents[0].parents[0]
    end

    + run_code(x, 'commits 1') do
    + git.commits.size
    + end
    +
    run_code(x, 'commits 2') do
    log = git.commits('master', 15)
    log.size

提交更新

現在的暫存區域已經準備妥當可以提交了。在此之前,請一定要確認還有什麼修改過的或新建的檔案還沒有 git add 過,否則提交的時候不會記錄這些還沒暫存起來的變化。

所以,每次準備提交前,先用 git status 看下,是不是都已暫存起來了,然後再執行提交命令 git commit

$ git commit

這種方式會啟動文字編輯器以便輸入本次提交的說明。(預設會啟用 shell 的環境變數 $EDITOR 所指定的軟體,一般都是 vim 或 emacs。當然也可以按照第一章介紹的方式,使用 git config --global core.editor 命令設定你喜歡的編輯軟體。)

編輯器會顯示類似下面的文字資訊(本例選用 Vim 的屏顯方式展示):

# Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # new file: README
    # modified: benchmarks.rb
    ~
    ~
    ~
    ".git/COMMIT_EDITMSG" 10L, 283C

可以看到,預設的提交訊息包含最後一次執行 git status 的輸出,放在註釋行裡,另外開頭還有一空行,供你輸入提交說明。你完全可以去掉這些註釋行,不過留著也沒關係,多少能幫你回想起這次更新的內容有哪些。(如果覺得這還不夠,可以用 -v 選項將修改差異的每一行都包含到註釋中來。)退出編輯器時,Git 會丟掉註釋行,將說明內容和本次更新提交到倉庫。

另外也可以用 -m 引數後跟提交說明的方式,在一行命令中提交更新:

$ git commit -m "Story 182: Fix benchmarks for speed"
    [master]: created 463dc4f: "Fix benchmarks for speed"
    2 files changed, 3 insertions(+), 0 deletions(-)
    create mode 100644 README

好,現在你已經建立了第一個提交!

可以看到,提交後它會告訴你,當前是在哪個分支(master)提交的,本次提交的完整 SHA-1 校驗和是什麼(463dc4f),以及在本次提交中,有多少檔案修訂過,多少行添改和刪改過。

記住,提交時記錄的是放在暫存區域的快照,任何還未暫存的仍然保持已修改狀態,可以在下次提交時納入版本管理。每一次執行提交操作,都是對你專案作一次快照,以後可以回到這個狀態,或者進行比較。

跳過使用暫存區域

儘管使用暫存區域的方式可以精心準備要提交的細節,但有時候這麼做略顯繁瑣。Git 提供了一個跳過使用暫存區域的方式,只要在提交的時候,給 git commit 加上 -a 選項,Git 就會自動把所有已經跟蹤過的檔案暫存起來一併提交,從而跳過 git add 步驟:

$ git status
    # On branch master
    #
    # Changes not staged for commit:
    #
    # modified: benchmarks.rb
    #
    $ git commit -a -m 'added new benchmarks'
    [master 83e38c7] added new benchmarks
    1 files changed, 5 insertions(+), 0 deletions(-)

看到了嗎?提交之前不再需要 git add 檔案 benchmarks.rb 了。

移除檔案

要從 Git 中移除某個檔案,就必須要從已跟蹤檔案清單中移除(確切地說,是從暫存區域移除),然後提交。可以用 git rm 命令完成此項工作,並連帶從工作目錄中刪除指定的檔案,這樣以後就不會出現在未跟蹤檔案清單中了。

如果只是簡單地從工作目錄中手工刪除檔案,執行 git status 時就會在 “Changes not staged for commit” 部分(也就是未暫存清單)看到:

$ rm grit.gemspec
    $ git status
    # On branch master
    #
    # Changes not staged for commit:
    # (use "git add/rm <file>..." to update what will be committed)
    #
    # deleted: grit.gemspec
    #

然後再執行 git rm 記錄此次移除檔案的操作:

$ git rm grit.gemspec
    rm 'grit.gemspec'
    $ git status
    # On branch master
    #
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # deleted: grit.gemspec
    #

最後提交的時候,該檔案就不再納入版本管理了。如果刪除之前修改過並且已經放到暫存區域的話,則必須要用強制刪除選項 -f(譯註:即 force 的首字母),以防誤刪除檔案後丟失修改的內容。

另外一種情況是,我們想把檔案從 Git 倉庫中刪除(亦即從暫存區域移除),但仍然希望保留在當前工作目錄中。換句話說,僅是從跟蹤清單中刪除。比如一些大型日誌檔案或者一堆 .a 編譯檔案,不小心納入倉庫後,要移除跟蹤但不刪除檔案,以便稍後在 .gitignore 檔案中補上,用 --cached 選項即可:

$ git rm --cached readme.txt

後面可以列出檔案或者目錄的名字,也可以使用 glob 模式。比方說:

$ git rm log/\*.log

注意到星號 * 之前的反斜槓 \,因為 Git 有它自己的檔案模式擴充套件匹配方式,所以我們不用 shell 來幫忙展開(譯註:實際上不加反斜槓也可以執行,只不過按照 shell 擴充套件的話,僅僅刪除指定目錄下的檔案而不會遞迴匹配。上面的例子本來就指定了目錄,所以效果等同,但下面的例子就會用遞迴方式匹配,所以必須加反斜槓。)。此命令刪除所有 log/ 目錄下副檔名為 .log 的檔案。類似的比如:

$ git rm \*~

會遞迴刪除當前目錄及其子目錄中所有 ~ 結尾的檔案。

移動檔案

不像其他的 VCS 系統,Git 並不跟蹤檔案移動操作。如果在 Git 中重新命名了某個檔案,倉庫中儲存的元資料並不會體現出這是一次改名操作。不過 Git 非常聰明,它會推斷出究竟發生了什麼,至於具體是如何做到的,我們稍後再談。

既然如此,當你看到 Git 的 mv 命令時一定會困惑不已。要在 Git 中對檔案改名,可以這麼做:

$ git mv file_from file_to

它會恰如預期般正常工作。實際上,即便此時檢視狀態資訊,也會明白無誤地看到關於重新命名操作的說明:

$ git mv README.txt README
    $ git status
    # On branch master
    # Your branch is ahead of 'origin/master' by 1 commit.
    #
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # renamed: README.txt -> README
    #

其實,執行 git mv 就相當於運行了下面三條命令:

$ mv README.txt README
    $ git rm README.txt
    $ git add README

如此分開操作,Git 也會意識到這是一次改名,所以不管何種方式都一樣。

當然,直接用 git mv 輕便得多,不過有時候用其他工具批處理改名的話,要記得在提交前刪除老的檔名,

再新增新的檔名。

2.3 檢視提交歷史

在提交了若干更新之後,又或者克隆了某個專案,想回顧下提交歷史,可以使用 git log 命令檢視。

接下來的例子會用我專門用於演示的 simplegit 專案,執行下面的命令獲取該專案原始碼:

git clone git://github.com/schacon/simplegit-progit.git

然後在此專案中執行 git log,應該會看到下面的輸出:

$ git log
    commit ca82a6dff817ec66f44342007202690a93763949
    Author: Scott Chacon <[email protected]>
    Date: Mon Mar 17 21:52:11 2008 -0700

    changed the version number

    commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
    Author: Scott Chacon <[email protected]>
    Date: Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

    commit a11bef06a3f659402fe7563abf99ad00de2209e6
    Author: Scott Chacon <[email protected]>
    Date: Sat Mar 15 10:31:28 2008 -0700

    first commit

預設不用任何引數的話,git log 會按提交時間列出所有的更新,最近的更新排在最上面。看到了嗎,每次更新都有一個 SHA-1 校驗和、作者的名字和電子郵件地址、提交時間,最後縮排一個段落顯示提交說明。

git log 有許多選項可以幫助你搜尋感興趣的提交,接下來我們介紹些最常用的。

我們常用 -p 選項展開顯示每次提交的內容差異,用 -2 則僅顯示最近的兩次更新

$ git log -p -2
    commit ca82a6dff817ec66f44342007202690a93763949
    Author: Scott Chacon <[email protected]>
    Date: Mon Mar 17 21:52:11 2008 -0700

    changed the version number

    diff --git a/Rakefile b/Rakefile
    index a874b73..8f94139 100644
    --- a/Rakefile
    +++ b/Rakefile
    @@ -5,7 +5,7 @@ require 'rake/gempackagetask'
    spec = Gem::Specification.new do |s|
    - s.version = "0.1.0"
    + s.version = "0.1.1"
    s.author = "Scott Chacon"

    commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
    Author: Scott Chacon <[email protected]>
    Date: Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

    diff --git a/lib/simplegit.rb b/lib/simplegit.rb
    index a0a60ae..47c6340 100644
    --- a/lib/simplegit.rb
    +++ b/lib/simplegit.rb
    @@ -18,8 +18,3 @@ class SimpleGit
    end

    end
    -
    -if $0 == __FILE__
    - git = SimpleGit.new
    - puts git.show
    -end
    \ No newline at end of file

在做程式碼審查,或者要快速瀏覽其他協作者提交的更新都作了哪些改動時,就可以用這個選項。此外,還有許多摘要選項可以用,比如 --stat,僅顯示簡要的增改行數統計:

$ git log --stat
    commit ca82a6dff817ec66f44342007202690a93763949
    Author: Scott Chacon <[email protected]>
    Date: Mon Mar 17 21:52:11 2008 -0700

    changed the version number

    Rakefile | 2 +-
    1 files changed, 1 insertions(+), 1 deletions(-)

    commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
    Author: Scott Chacon <[email protected]>
    Date: Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

    lib/simplegit.rb | 5 -----
    1 files changed, 0 insertions(+), 5 deletions(-)

    commit a11bef06a3f659402fe7563abf99ad00de2209e6
    Author: Scott Chacon <[email protected]>
    Date: Sat Mar 15 10:31:28 2008 -0700

    first commit

    README | 6 ++++++
    Rakefile | 23 +++++++++++++++++++++++
    lib/simplegit.rb | 25 +++++++++++++++++++++++++
    3 files changed, 54 insertions(+), 0 deletions(-)

每個提交都列出了修改過的檔案,以及其中新增和移除的行數,並在最後列出所有增減行數小計。

還有個常用的 --pretty 選項,可以指定使用完全不同於預設格式的方式展示提交歷史。

比如用 oneline 將每個提交放在一行顯示,這在提交數很大時非常有用。另外還有 shortfull 和 fuller可以用,展示的資訊或多或少有些不同,請自己動手實踐一下看看效果如何。

$ git log --pretty=oneline
    ca82a6dff817ec66f44342007202690a93763949 changed the version number
    085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code
    a11bef06a3f659402fe7563abf99ad00de2209e6 first commit

但最有意思的是 format,可以定製要顯示的記錄格式,這樣的輸出便於後期程式設計提取分析,像這樣:

$ git log --pretty=format:"%h - %an, %ar : %s"
    ca82a6d - Scott Chacon, 11 months ago : changed the version number
    085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code
    a11bef0 - Scott Chacon, 11 months ago : first commit

表 2-1 列出了常用的格式佔位符寫法及其代表的意義。

選項 說明
    %H 提交物件(commit)的完整雜湊字串
    %h 提交物件的簡短雜湊字串
    %T 樹物件(tree)的完整雜湊字串
    %t 樹物件的簡短雜湊字串
    %P 父物件(parent)的完整雜湊字串
    %p 父物件的簡短雜湊字串
    %an 作者(author)的名字
    %ae 作者的電子郵件地址
    %ad 作者修訂日期(可以用 -date= 選項定製格式)
    %ar 作者修訂日期,按多久以前的方式顯示
    %cn 提交者(committer)的名字
    %ce 提交者的電子郵件地址
    %cd 提交日期
    %cr 提交日期,按多久以前的方式顯示
    %s 提交說明

你一定奇怪作者(author)提交者(committer)之間究竟有何差別,其實作者指的是實際作出修改的人,提交者指的是最後將此工作成果提交到倉庫的人。所以,當你為某個專案釋出補丁,然後某個核心成員將你的補丁併入專案時,你就是作者,而那個核心成員就是提交者。我們會在第五章再詳細介紹兩者之間的細微差別。

用 oneline 或 format 時結合 --graph 選項,可以看到開頭多出一些 ASCII 字串表示的簡單圖形,形象地展示了每個提交所在的分支及其分化衍合情況。在我們之前提到的 Grit 專案倉庫中可以看到:

$ git log --pretty=format:"%h %s" --graph
    * 2d3acf9 ignore errors from SIGCHLD on trap
    * 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
    |\
    | * 420eac9 Added a method for getting the current branch.
    * | 30e367c timeout code and tests
    * | 5a09431 add timeout protection to grit
    * | e1193f8 support for heads with slashes in them
    |/
    * d6016bc require time for xmlschema
    * 11d191e Merge branch 'defunkt' into local

以上只是簡單介紹了一些 git log 命令支援的選項。表 2-2 還列出了一些其他常用的選項及其釋義。

選項 說明
    -p 按補丁格式顯示每個更新之間的差異。
    --stat 顯示每次更新的檔案修改統計資訊。
    --shortstat 只顯示 --stat 中最後的行數修改新增移除統計。
    --name-only 僅在提交資訊後顯示已修改的檔案清單。
    --name-status 顯示新增、修改、刪除的檔案清單。
    --abbrev-commit 僅顯示 SHA-1 的前幾個字元,而非所有的 40 個字元。
    --relative-date 使用較短的相對時間顯示(比如,“2 weeks ago”)。
    --graph 顯示 ASCII 圖形表示的分支合併歷史。
    --pretty 使用其他格式顯示歷史提交資訊。可用的選項包括 oneline,short,full,fuller 和 format(後跟指定格式)。

限制輸出長度

除了定製輸出格式的選項之外,git log 還有許多非常實用的限制輸出長度的選項,也就是隻輸出部分提交資訊。之前我們已經看到過 -2 了,它只顯示最近的兩條提交,實際上,這是 -<n> 選項的寫法,其中的 n 可以是任何自然數,表示僅顯示最近的若干條提交。不過實踐中我們是不太用這個選項的,Git 在輸出所有提交時會自動呼叫分頁程式(less),要看更早的更新只需翻到下頁即可。

另外還有按照時間作限制的選項,比如 --since 和 --until。下面的命令列出所有最近兩週內的提交:

$ git log --since=2.weeks

你可以給出各種時間格式,比如說具體的某一天(“2008-01-15”),或者是多久以前(“2 years 1 day 3 minutes ago”)。

還可以給出若干搜尋條件,列出符合的提交。用 --author 選項顯示指定作者的提交,用 --grep 選項搜尋提交說明中的關鍵字。(請注意,如果要得到同時滿足這兩個選項搜尋條件的提交,就必須用 --all-match 選項。否則,滿足任意一個條件的提交都會被匹配出來)

另一個真正實用的git log選項是路徑(path),如果只關心某些檔案或者目錄的歷史提交,可以在 git log 選項的最後指定它們的路徑。因為是放在最後位置上的選項,所以用兩個短劃線(--)隔開之前的選項和後面限定的路徑名。

表 2-3 還列出了其他常用的類似選項。

選項 說明
    -(n) 僅顯示最近的 n 條提交
    --since, --after 僅顯示指定時間之後的提交。
    --until, --before 僅顯示指定時間之前的提交。
    --author 僅顯示指定作者相關的提交。
    --committer 僅顯示指定提交者相關的提交。

來看一個實際的例子,如果要檢視 Git 倉庫中,2008 年 10 月期間,Junio Hamano 提交的但未合併的測試指令碼(位於專案的 t/ 目錄下的檔案),可以用下面的查詢命令:

$ git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \
    --before="2008-11-01" --no-merges -- t/
    5610e3b - Fix testcase failure when extended attribute
    acd3b9e - Enhance hold_lock_file_for_{update,append}()
    f563754 - demonstrate breakage of detached checkout wi
    d1a43f2 - reset --hard/read-tree --reset -u: remove un
    51a94af - Fix "checkout --track -b newbranch" on detac
    b0ad11e - pull: allow "git pull origin $something:$cur

Git 專案有 20,000 多條提交,但我們給出搜尋選項後,僅列出了其中滿足條件的 6 條。

使用圖形化工具查閱提交歷史

有時候圖形化工具更容易展示歷史提交的變化,隨 Git 一同釋出的 gitk 就是這樣一種工具。它是用 Tcl/Tk 寫成的,基本上相當於 git log 命令的視覺化版本,凡是 git log 可以用的選項也都能用在 gitk 上。在專案工作目錄中輸入 gitk 命令後,就會啟動圖 2-2 所示的介面。

 gitk 的圖形介面

上半個視窗顯示的是歷次提交的分支祖先圖譜,下半個視窗顯示當前點選的提交對應的具體差異。

2.4 撤消操作

任何時候,你都有可能需要撤消剛才所做的某些操作。接下來,我們會介紹一些基本的撤消操作相關的命令。請注意,有些撤銷操作是不可逆的,所以請務必謹慎小心,一旦失誤,就有可能丟失部分工作成果。

修改最後一次提交

有時候我們提交完了才發現漏掉了幾個檔案沒有加,或者提交資訊寫錯了。想要撤消剛才的提交操作,可以使用 --amend 選項重新提交:

$ git commit --amend

此命令將使用當前的暫存區域快照提交。如果剛才提交完沒有作任何改動,直接執行此命令的話,相當於有機會重新編輯提交說明,但將要提交的檔案快照和之前的一樣。

啟動文字編輯器後,會看到上次提交時的說明,編輯它確認沒問題後儲存退出,就會使用新的提交說明覆蓋剛才失誤的提交。

如果剛才提交時忘了暫存某些修改,可以先補上暫存操作,然後再執行 --amend 提交:

$ git commit -m 'initial commit'
    $ git add forgotten_file
    $ git commit --amend

上面的三條命令最終只是產生一個提交,第二個提交命令修正了第一個的提交內容。

取消已經暫存的檔案

接下來的兩個小節將演示如何取消暫存區域中的檔案,以及如何取消工作目錄中已修改的檔案。不用擔心,檢視檔案狀態的時候就提示了該如何撤消,所以不需要死記硬背。

來看下面的例子,有兩個修改過的檔案,我們想要分開提交,但不小心用 git add . 全加到了暫存區域。該如何撤消暫存其中的一個檔案呢?其實,git status 的命令輸出已經告訴了我們該怎麼做:

$ git add .
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # modified: README.txt
    # modified: benchmarks.rb
    #

就在 “Changes to be committed” 下面,括號中有提示,可以使用 git reset HEAD <file>... 的方式取消暫存。好吧,我們來試試取消暫存 benchmarks.rb 檔案:

$ git reset HEAD benchmarks.rb
    benchmarks.rb: locally modified
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # modified: README.txt
    #
    # Changes not staged for commit:
    # (use "git add <file>..." to update what will be committed)
    # (use "git checkout -- <file>..." to discard changes in working directory)
    #
    # modified: benchmarks.rb
    #

這條命令看起來有些古怪,先別管,能用就行。現在 benchmarks.rb 檔案又回到了之前已修改未暫存的狀態。

取消對檔案的修改

如果覺得剛才對 benchmarks.rb 的修改完全沒有必要,該如何取消修改,回到之前的狀態(也就是修改之前的版本)呢?git status 同樣提示了具體的撤消方法,接著上面的例子,現在未暫存區域看起來像這樣:

# Changes not staged for commit:
    # (use "git add <file>..." to update what will be committed)
    # (use "git checkout -- <file>..." to discard changes in working directory)
    #
    # modified: benchmarks.rb
    #

在第二個括號中,我們看到了拋棄檔案修改的命令(至少在 Git 1.6.1 以及更高版本中會這樣提示,如果你還在用老版本,我們強烈建議你升級,以獲取最佳的使用者體驗),讓我們試試看:

$ git checkout -- benchmarks.rb
    $ git status
    # On branch master
    # Changes to be committed:
    # (use "git reset HEAD <file>..." to unstage)
    #
    # modified: README.txt
    #

可以看到,該檔案已經恢復到修改前的版本。你可能已經意識到了,這條命令有些危險,所有對檔案的修改都沒有了,因為我們剛剛把之前版本的檔案複製過來重寫了此檔案。

所以在用這條命令前,請務必確定真的不再需要保留剛才的修改。

如果只是想回退版本,同時保留剛才的修改以便將來繼續工作,可以用下章介紹的 stashing 和分支來處理,應該會更好些。

記住,任何已經提交到 Git 的都可以被恢復。即便在已經刪除的分支中的提交,或者用 --amend 重新改寫的提交,都可以被恢復。所以,你可能失去的資料,僅限於沒有提交過的,對 Git 來說它們就像從未存在過一樣。

遠端倉庫的使用

要參與任何一個 Git 專案的協作,必須要了解該如何管理遠端倉庫。遠端倉庫是指託管在網路上的專案倉庫,可能會有好多個,其中有些你只能讀,另外有些可以寫。同他人協作開發某個專案時,需要管理這些遠端倉庫,以便推送或拉取資料,分享各自的工作進展。管理遠端倉庫的工作,包括新增遠端庫,移除廢棄的遠端庫,管理各式遠端庫分支,定義是否跟蹤這些分支,等等。本節我們將詳細討論遠端庫的管理和使用。

檢視當前的遠端庫

要檢視當前配置有哪些遠端倉庫,可以用 git remote 命令,它會列出每個遠端庫的簡短名字。在克隆完某個專案後,至少可以看到一個名為 origin 的遠端庫,Git 預設使用這個名字來標識你所克隆的原始倉庫:

$ git clone git://github.com/schacon/ticgit.git
    Initialized empty Git repository in /private/tmp/ticgit/.git/
    remote: Counting objects: 595, done.
    remote: Compressing objects: 100% (269/269), done.
    remote: Total 595 (delta 255), reused 589 (delta 253)
    Receiving objects: 100% (595/595), 73.31 KiB | 1 KiB/s, done.
    Resolving deltas: 100% (255/255), done.
    $ cd ticgit
    $ git remote
    origin

也可以加上 -v 選項(譯註:此為 --verbose 的簡寫,取首字母),顯示對應的克隆地址:

$ git remote -v
    origin git://github.com/schacon/ticgit.git

如果有多個遠端倉庫,此命令將全部列出。比如在我的 Grit 專案中,可以看到:

$ cd grit
    $ git remote -v
    bakkdoor git://github.com/bakkdoor/grit.git
    cho45 git://github.com/cho45/grit.git
    defunkt git://github.com/defunkt/grit.git
    koke git://github.com/koke/grit.git
    origin [email protected]:mojombo/grit.git

這樣一來,我就可以非常輕鬆地從這些使用者的倉庫中,拉取他們的提交到本地。請注意,上面列出的地址只有 origin 用的是 SSH URL 連結,所以也只有這個倉庫我能推送資料上去(我們會在第四章解釋原因)。

新增遠端倉庫

要新增一個新的遠端倉庫,可以指定一個簡單的名字,以便將來引用,執行 git remote add [shortname] [url]

$ git remote
    origin
    $ git remote add pb git://github.com/paulboone/ticgit.git
    $ git remote -v
    origin git://github.com/schacon/ticgit.git
    pb git://github.com/paulboone/ticgit.git

現在可以用字串 pb 指代對應的倉庫地址了。比如說,要抓取所有 Paul 有的,但本地倉庫沒有的資訊,可以執行 git fetch pb

$ git fetch pb
    remote: Counting objects: 58, done.
    remote: Compressing objects: 100% (41/41), done.
    remote: Total 44 (delta 24), reused 1 (delta 0)
    Unpacking objects: 100% (44/44), done.
    From git://github.com/paulboone/ticgit
    * [new branch] master -> pb/master
    * [new branch] ticgit -> pb/ticgit

現在,Paul 的主幹分支(master)已經完全可以在本地訪問了,對應的名字是 pb/master,你可以將它合併到自己的某個分支,或者切換到這個分支,看看有些什麼有趣的更新。

從遠端倉庫抓取資料

正如之前所看到的,可以用下面的命令從遠端倉庫抓取資料到本地:

$ git fetch [remote-name]

此命令會到遠端倉庫中拉取所有你本地倉庫中還沒有的資料。執行完成後,你就可以在本地訪問該遠端倉庫中的所有分支,將其中某個分支合併到本地,或者只是取出某個分支,一探究竟。(我們會在第三章詳細討論關於分支的概念和操作。)

如果是克隆了一個倉庫,此命令會自動將遠端倉庫歸於 origin 名下。所以,git fetch origin 會抓取從你上次克隆以來別人上傳到此遠端倉庫中的所有更新(或是上次 fetch 以來別人提交的更新)。有一點很重要,需要記住,fetch 命令只是將遠端的資料拉到本地倉庫,並不自動合併到當前工作分支,只有當你確實準備好了,才能手工合併。

如果設定了某個分支用於跟蹤某個遠端倉庫的分支(參見下節及第三章的內容),可以使用 git pull 命令自動抓取資料下來,然後將遠端分支自動合併到本地倉庫中當前分支。在日常工作中我們經常這麼用,既快且好。實際上,預設情況下 git clone 命令本質上就是自動建立了本地的 master 分支用於跟蹤遠端倉庫中的 master 分支(假設遠端倉庫確實有 master 分支)。所以一般我們執行 git pull,目的都是要從原始克隆的遠端倉庫中抓取資料後,合併到工作目錄中的當前分支。

推送資料到遠端倉庫

專案進行到一個階段,要同別人分享目前的成果,可以將本地倉庫中的資料推送到遠端倉庫。實現這個任務的命令很簡單: git push [remote-name] [branch-name]。如果要把本地的 master 分支推送到 origin 伺服器上(再次說明下,克隆操作會自動使用預設的 master 和 origin 名字),可以執行下面的命令:

$ git push origin master

只有在所克隆的伺服器上有寫許可權,或者同一時刻沒有其他人在推資料,這條命令才會如期完成任務。如果在你推資料前,已經有其他人推送了若干更新,那你的推送操作就會被駁回。你必須先把他們的更新抓取到本地,合併到自己的專案中,然後才可以再次推送。有關推送資料到遠端倉庫的詳細內容見第三章。

檢視遠端倉庫資訊

我們可以通過命令 git remote show [remote-name] 檢視某個遠端倉庫的詳細資訊,比如要看所克隆的 origin 倉庫,可以執行:

$ git remote show origin
    * remote origin
    URL: git://github.com/schacon/ticgit.git
    Remote branch merged with 'git pull' while on branch master
    master
    Tracked remote branches
    master
    ticgit

除了對應的克隆地址外,它還給出了許多額外的資訊。它友善地告訴你如果是在 master 分支,就可以用 git pull 命令抓取資料合併到本地。

另外還列出了所有處於跟蹤狀態中的遠端分支。

上面的例子非常簡單,而隨著使用 Git 的深入,git remote show 給出的資訊可能會像這樣:

$ git remote show origin
    * remote origin
    URL: [email protected]:defunkt/github.git
    Remote branch merged with 'git pull' while on branch issues
    issues
    Remote branch merged with 'git pull' while on branch master
    master
    New remote branches (next fetch will store in remotes/origin)
    caching
    Stale tracking branches (use 'git remote prune')
    libwalker
    walker2
    Tracked remote branches
    acl
    apiv2
    dashboard2
    issues
    master
    postgres
    Local branch pushed with 'git push'
    master:master

它告訴我們,執行 git push 時預設推送的分支是什麼(譯註:最後兩行)。它還顯示了有哪些遠端分支還沒有同步到本地(譯註:第六行的 caching 分支),哪些已同步到本地的遠端分支在遠端伺服器上已被刪除(譯註:Stale tracking branches 下面的兩個分支),以及執行 git pull 時將自動合併哪些分支(譯註:前四行中列出的 issues 和 master 分支)。

遠端倉庫的刪除和重新命名

在新版 Git 中可以用 git remote rename 命令修改某個遠端倉庫在本地的簡稱,比如想把 pb 改成 paul,可以這麼執行:

$ git remote rename pb paul
    $ git remote
    origin
    paul

注意,對遠端倉庫的重新命名,也會使對應的分支名稱發生變化,原來的 pb/master 分支現在成了 paul/master

碰到遠端倉庫伺服器遷移,或者原來的克隆映象不再使用,又或者某個參與者不再貢獻程式碼,那麼需要移除對應的遠端倉庫,可以執行 git remote rm命令:

$ git remote rm paul
    $ git remote
    origin