1. 程式人生 > >Git版本控制詳解

Git版本控制詳解

git版本控制軟體

GIT 學習手冊簡介

本站為 Git 學習參考手冊。目的是為學習與記憶 Git 使用中最重要、最普遍的命令提供快速翻閱。 這些命令以你可能需要的操作型別劃分,並且將提供日常使用中需要的一些常用的命令以及引數。

本手冊將從入門到精通指導大家。 首先,我們要從如何以 Git 的思維方式管理原始碼開始。

如何以 GIT 的方式思考(這節可以不用看懂,接著看下面的內容,看完就全懂了。

懂得 Git,第一件重要的事情就是要知道它與 Subversion、Perforce 或者任何你用過的版本控制工具都有著很大的差別。 通常,忘掉你預想的版本控制方式,改以 Git 的方式思考,能夠幫助你更好地學習 Git。

讓我們從頭開始。假設你正在設計一個新的原始碼管理系統。在你使用某個工具之前,是如何完成基本的原始碼版本控制工作的呢? 十有八九,你只是在專案到達某些階段的時候,對專案做一份拷貝。

 $ cp -R project project.bak

這樣,你就可以在事情變得一團糟的時候很方便的返回到之前的狀態,或者通過對比當前的專案與之前的拷貝,看看自己在之後的工作中,都做了哪些修改。

如果你有點偏執,你可能會經常作上面說的事情,或許還會給專案拷貝加個日期:

 $ cp -R project project.2010-06-01.bak

如此,你就有了一堆專案在各個階段的快照,來作比較、檢視。使用這種模式,你還可以有效地與人分享專案變更。 如果你會在專案到達一定階段的時候給它打個包,丟到自己的網站上,那其他的開發者們,就能很方便地下載它,做點改動,並給你補丁回饋。

 $ wget http://example.com/project.2010-06-01.zip
 $ unzip project.2010-06-01.zip
 $ cp -R project.2010-06-01 project-my-copy
 $ cd project-my-copy
 $ (做了某些修改)
 $ diff project-my-copy project.2010-06-01 > change.patch
 $ (通過E-mail傳送修改補丁)

以此方式,原先的開發者就能將其他人的改動應用到他的專案中去,其他開發者也能瞭解你做的變更。其實這便是許多開源專案採用過多年的協作方式。

這辦法其實很好使,所以假設我們現在想要寫個工具,讓這個辦法更快、更簡單。 我們與其實現一個工具以記錄每個檔案的版本,可能不如去實現個工具以使建立、儲存專案的快照更加方便,不用每次都去人肉作整個專案的拷貝。

這就是 Git 的精要所在。你通過 git commit告訴 Git 你想儲存一份專案快照, Git 就會為你的專案中的各個檔案的當前狀態存一份記錄。之後,絕大部分的 Git 命令都圍繞這些記錄展開。 比如檢視它們的區別(diff),提取它們的內容,等等。

snapshots git
如果你將 Git 看作一個排序、對比以及合併專案更新的工具,那就容易理解狀況和正確做事了。

目錄

基本的快照

分支與合併

檢查與比較

一、獲取與建立專案

你得先有一個 Git 倉庫,才能用它進行操作。倉庫是 Git 存放你要儲存的快照的資料的地方。

擁有一個 Git 倉庫的途徑有兩種。在已有的目錄中,初始化一個新的,其一。 比如一個新的專案,或者一個已存在的專案,但該專案尚未有版本控制。如果你想要複製一份別人的專案, 或者與別人合作某個專案,也可以從一個公開的 Git 倉庫克隆,其二。本章將對兩者都做介紹。

Git使用前配置

如果設定了,在輸入命令示介面可以很方便的使用複製和貼上(用左鍵選取要複製的,點右鍵直接就可以複製,貼上時只需點一下右鍵。)設定方法:Git Bash快捷圖示(桌面圖示)右鍵屬性-選項,把快速編輯模式勾上就可以,如下圖:

git快速編輯模式配置

設定Git本地專案開發庫預設路徑

如果設定了,就不用每次開啟Git再cd開啟目錄了。方法:右鍵Git Bash快捷圖示(桌面圖示)屬性,找到快捷方式-起始位置,把你的專案地址放在這裡就可以了。如下圖:

git預設專案開發庫路徑設定

配置本地使用者和郵箱

使用者名稱郵箱作用 : 我們需要設定一個使用者名稱 和 郵箱, 這是用來上傳本地倉庫到GitHub中, 在GitHub中顯示程式碼上傳者;
使用命令 :

git config --global user.name "HanShuliang" //設定使用者名稱 
git config --global user.email "[email protected]" //設定郵箱

git客戶端版本控制軟體

到此Git客戶端已安裝及GitHub配置完成,現在可以從GitHub傳輸程式碼了。

git init 將一個目錄初始化為 Git 倉庫

在目錄中執行 git init,就可以建立一個 Git 倉庫了。比如,我們恰好有個目錄,裡頭有些許檔案,如下:

$ cd konnichiwa
$ ls
README   hello.rb

在這個專案裡頭,我們會用各種程式語言寫 “Hello World” 例項。 到目前為止,我們只有 Ruby 的,不過,這才剛上路嘛。為了開始用 Git 對這個專案作版本控制,我們執行一下 git init

$ git init
Initialized empty Git repository in /opt/konnichiwa/.git/
# 在 /opt/konnichiwa/.git 目錄初始化空 Git 倉庫完畢。

現在你可以看到在你的專案目錄中有個 .git 的子目錄。 這就是你的 Git 倉庫了,所有有關你的此專案的快照資料都存放在這裡。

$ ls -a
.        ..       .git     README   hello.rb

恭喜,現在你就有了一個 Git 倉庫的架子,可以開始快照你的專案了。

簡而言之,用 git init 來在目錄中建立新的 Git 倉庫。 你可以在任何時候、任何目錄中這麼做,完全是本地化的。

git clone 複製一個 Git 倉庫,以上下其手

如果你需要與他人合作一個專案,或者想要複製一個專案,看看程式碼,你就可以克隆那個專案。 執行 git clone [url],[url] 為你想要複製的專案,就可以了。

$ git clone git://github.com/schacon/simplegit.git
Initialized empty Git repository in /private/tmp/simplegit/.git/
remote: Counting objects: 100, done.
remote: Compressing objects: 100% (86/86), done.
remote: Total 100 (delta 35), reused 0 (delta 0)
Receiving objects: 100% (100/100), 9.51 KiB, done.
Resolving deltas: 100% (35/35), done.
$ cd simplegit/
$ ls
README   Rakefile lib

上述操作將複製該專案的全部記錄,讓你本地擁有這些。並且該操作將拷貝該專案的主分支, 使你能夠檢視程式碼,或編輯、修改。進到該目錄中,你會看到 .git 子目錄。 所有的專案資料都存在那裡。

$ ls -a
.        ..       .git     README   Rakefile lib
$ cd .git
$ ls
HEAD        description info        packed-refs
branches    hooks       logs        refs
config      index       objects

預設情況下,Git 會按照你提供的 URL 所指示的專案的名稱建立你的本地專案目錄。 通常就是該 URL 最後一個 / 之後的任何東西。如果你想要一個不一樣的名字, 你可以在該命令後加上它,就在那個 URL 後面。

簡而言之,使用 git clone 拷貝一個 Git 倉庫到本地,讓自己能夠檢視該專案,或者進行修改。

二、基本快照

Git 的工作就是建立和儲存你的專案的快照及與之後的快照進行對比。本章將對有關建立與提交你的專案的快照的命令作介紹。

這裡有個重要的概念,Git 有一個叫做“索引”的東東,有點像是你的快照的快取區。這就使你能夠從更改的檔案中創建出一系列組織良好的快照,而不是一次提交所有的更改。

簡而言之,使用 git add 新增需要追蹤的新檔案和待提交的更改, 然後使用 git status 和 git diff 檢視有何改動, 最後用 git commit 將你的快照記錄。這就是你要用的基本流程,絕大部分時候都是這樣的。

git add 新增檔案到快取

在 Git 中,在提交你修改的檔案之前,你需要把它們新增到快取。如果該檔案是新建立的,你可以執行 git add 將該檔案新增到快取,但是,即使該檔案已經被追蹤了 —— 也就是說,曾經提交過了 —— 你仍然需要執行 git add 將新更改的檔案新增到快取去。讓我們看幾個例子:

回到我們的 Hello World 示例,初始化該專案之後,我們就要用 git add 將我們的檔案新增進去了。 我們可以用 git status 看看我們的專案的當前狀態。

$ git status -s
?? README
?? hello.rb

我們有倆尚未被追蹤的檔案,得新增一下。

$ git add README hello.rb

現在我們再執行 git status,就可以看到這倆檔案已經加上去了。

$ git status -s
A  README
A  hello.rb

新專案中,新增所有檔案很普遍,可以在當前工作目錄執行命令:git add .。 因為 Git 會遞迴地將你執行命令時所在的目錄中的所有檔案新增上去,所以如果你將當前的工作目錄作為引數, 它就會追蹤那兒的所有檔案了。如此,git add . 就和 git add README hello.rb 有一樣的效果。 此外,效果一致的還有 git add *,不過那只是因為我們這還木有子目錄,不需要遞迴地新增新檔案。

好了,現在我們改個檔案,再跑一下 git status,有點古怪。

$ vim README
$ git status -s
AM README
A  hello.rb

“AM” 狀態的意思是,這個檔案在我們將它新增到快取之後又有改動。這意味著如果我們現在提交快照, 我們記錄的將是上次跑 git add 的時候的檔案版本,而不是現在在磁碟中的這個。 Git 並不認為磁碟中的檔案與你想快照的檔案必須是一致的 —— (如果你需要它們一致,)得用 git add 命令告訴它。

一言以蔽之, 當你要將你的修改包含在即將提交的快照裡的時候,執行 git add。 任何你沒有新增的改動都不會被包含在內 —— 這意味著你可以比絕大多數其他原始碼版本控制系統更精確地歸置你的快照。

請檢視《Pro Git》中 git add 的 “-p” 引數,以瞭解更多關於提交檔案的靈活性的例子。

git status 檢視你的檔案在工作目錄與快取的狀態

正如你在 git add 小節中所看到的,你可以執行 git status 命令檢視你的程式碼在快取與當前工作目錄的狀態。我演示該命令的時候加了 -s 引數,以獲得簡短的結果輸出。 若沒有這個標記,命令 git status 將告訴你更多的提示與上下文欣喜。 以下便是同樣狀態下,有跟沒有 -s 引數的輸出對比。簡短的輸出如下:

$ git status -s
AM README
A  hello.rb

而同樣的狀態,詳細的輸出看起來是這樣的:

$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
# new file:   README
# new file:   hello.rb
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   README
#

你很容易發現簡短的輸出看起來很緊湊。而詳細輸出則很有幫助,提示你可以用何種命令完成你接下來可能要做的事情。

Git 還會告訴你在你上次提交之後,有哪些檔案被刪除、修改或者存入快取了。

$ git status -s
M  README
 D hello.rb

你可以看到,在簡短輸出中,有兩欄。第一欄是快取的,第二欄則是工作目錄的。 所以假設你臨時提交了 README 檔案,然後又改了些,並且沒有執行 git add,你會看到這個:

$ git status -s
MM README
 D hello.rb

一言以蔽之,執行 git status 以檢視在你上次提交之後有啥被修改或者臨時提交了, 從而決定自己是否需要提交一次快照,同時也能知道有什麼改變被記錄進去了。

git diff 顯示已寫入快取與已修改但尚未寫入快取的改動的區別

git diff 有兩個主要的應用場景。我們將在此介紹其一, 在 檢閱與對照 一章中,我們將介紹其二。 我們這裡介紹的方式是用此命令描述已臨時提交的或者已修改但尚未提交的改動。

git diff #尚未快取的改動

如果沒有其他引數,git diff 會以規範化的 diff 格式(一個補丁)顯示自從你上次提交快照之後尚未快取的所有更改。

$ vim hello.rb
$ git status -s
 M hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index d62ac43..8d15d50 100644
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 class HelloWorld

   def self.hello
-    puts "hello world"
+    puts "hola mundo"
   end

 end

所以,git status顯示你上次提交更新至後所更改或者寫入快取的改動, 而 git diff 一行一行地顯示這些改動具體是啥。 通常執行完 git status 之後接著跑一下 git diff 是個好習慣。

git diff –cached #檢視已快取的改動

git diff --cached 命令會告訴你有哪些內容已經寫入快取了。 也就是說,此命令顯示的是接下來要寫入快照的內容。所以,如果你將上述示例中的 hello.rb 寫入快取,因為 git diff顯示的是尚未快取的改動,所以在此執行它不會顯示任何資訊。

$ git status -s
 M hello.rb
$ git add hello.rb 
$ git status -s
M  hello.rb
$ git diff
$ 

如果你想看看已快取的改動,你需要執行的是 git diff --cached

$ git status -s
M  hello.rb
$ git diff
$ 
$ git diff --cached
diff --git a/hello.rb b/hello.rb
index d62ac43..8d15d50 100644
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 class HelloWorld

   def self.hello
-    puts "hello world"
+    puts "hola mundo"
   end

 end

git diff HEAD 檢視已快取的與未快取的所有改動

如果你想一併檢視已快取的與未快取的改動,可以執行 git diff HEAD —— 也就是說你要看到的是工作目錄與上一次提交的更新的區別,無視快取。 假設我們又改了些 ruby.rb 的內容,那快取的與未快取的改動我們就都有了。 以上三個 diff 命令的結果如下:

$ vim hello.rb 
$ git diff
diff --git a/hello.rb b/hello.rb
index 4f40006..2ae9ba4 100644
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 class HelloWorld

+  # says hello
   def self.hello
     puts "hola mundo"
   end

 end
$ git diff --cached
diff --git a/hello.rb b/hello.rb
index 2aabb6e..4f40006 100644
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 class HelloWorld

   def self.hello
-    puts "hello world"
+    puts "hola mundo"
   end

 end
$ git diff HEAD
diff --git a/hello.rb b/hello.rb
index 2aabb6e..2ae9ba4 100644
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 class HelloWorld

+  # says hello
   def self.hello
-    puts "hello world"
+    puts "hola mundo"
   end

 end

git diff –stat 顯示摘要而非整個 diff

如果我們不想要看整個 diff 輸出,但是又想比 git status 詳細點, 就可以用 --stat 選項。該選項使它顯示摘要而非全文。上文示例在使用 --stat 選項時,輸出如下:

$ git status -s
MM hello.rb
$ git diff --stat
 hello.rb |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
$ git diff --cached --stat
 hello.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git diff HEAD --stat
 hello.rb |    3 ++-
 1 files changed, 2 insertions(+), 1 deletions(-)

你還可以在上述命令後面制定一個目錄,從而只檢視特定檔案或子目錄的 diff 輸出。

簡而言之, 執行 git diff 來檢視執行 git status 的結果的詳細資訊 —— 一行一行地顯示這些檔案是如何被修改或寫入快取的。

git commit 記錄快取內容的快照

現在你使用 git add 命令將想要快照的內容寫入了快取, 執行 git commit 就將它實際儲存快照了。 Git 為你的每一個提交都記錄你的名字與電子郵箱地址,所以第一步是告訴 Git 這些都是啥。

$ git config --global user.name 'Your Name'
$ git config --global user.email [email protected]

讓我們寫入快取,並提交對 hello.rb 的所有改動。在首個例子中,我們使用 -m 選項以在命令列中提供提交註釋。

$ git add hello.rb 
$ git status -s
M  hello.rb
$ git commit -m 'my hola mundo changes'
[master 68aa034] my hola mundo changes
 1 files changed, 2 insertions(+), 1 deletions(-)

現在我們已經記錄了快照。如果我們再執行 git status,會看到我們有一個“乾淨的工作目錄”。 這意味著我們在最近一次提交之後,沒有做任何改動 —— 在我們的專案中沒有未快照的工作。

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

如果你漏掉了 -m 選項,Git 會嘗試為你開啟一個編輯器以填寫提交資訊。 如果 Git 在你對它的配置中找不到相關資訊,預設會開啟 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)
#
# modified:   hello.rb
#
~
~
".git/COMMIT_EDITMSG" 9L, 257C

在此,你在檔案頭部新增實際的提交資訊。以“#”開頭的行都會被無視 ——Git 將 git status 的輸出結果放在那兒以提示你都改了、快取了啥。

通常,撰寫良好的提交資訊是很重要的。以開放原始碼專案為例,多多少少以以下格式寫你的提示訊息是個不成文的規定:

簡短的關於改動的總結(25個字或者更少)

如果有必要,更詳細的解釋文字。約 36 字時換行。在某些情況下,
第一行會被作為電子郵件的開頭,而剩餘的則會作為郵件內容。
將小結從內容隔開的空行是至關重要的(除非你沒有內容);
如果這兩個待在一起,有些 git 工具會犯迷糊。

空行之後是更多的段落。

 - 列表也可以

 - 通常使用連字元(-)或者星號(*)來標記列表,前面有個空格,
   在列表項之間有空行,不過這些約定也會有些變化。

# 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)
#
# modified:   hello.rb
#
~
~
~
".git/COMMIT_EDITMSG" 25L, 884C written

提交註解是很重要的。因為 Git 很大一部分能耐就是它在組織本地提交和與他人分享的彈性, 它很給力地能夠讓你為邏輯獨立的改變寫三到四條提交註解,以便你的工作被同仁審閱。因為提交與推送改動是有區別的, 請務必花時間將各個邏輯獨立的改動放到另外一個提交,並附上一份良好的提交註解, 以使與你合作的人能夠方便地瞭解你所做的,以及你為何要這麼做。

git commit -a 自動將在提交前將已記錄、修改的檔案放入快取區

如果你覺得 git add 提交快取的流程太過繁瑣,Git 也允許你用 -a 選項跳過這一步。 基本上這句話的意思就是,為任何已有記錄的檔案執行 git add —— 也就是說,任何在你最近的提交中已經存在,並且之後被修改的檔案。 這讓你能夠用更 Subversion 方式的流程,修改些檔案,然後想要快照所有所做的改動的時候執行 git commit -a。 不過你仍然需要執行 git add 來新增新檔案,就像 Subversion 一樣。

$ vim hello.rb
$ git status -s
 M  hello.rb
$ git commit -m 'changes to hello file'
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   hello.rb
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -am 'changes to hello file'
[master 78b2670] changes to hello file
 1 files changed, 2 insertions(+), 1 deletions(-)

注意,如果你不快取改動,直接執行 git commit,Git 會直接給出 git status 命令的輸出,提醒你啥也沒快取。我已將該訊息中的重要部分高亮,它說沒有新增需要提交的快取。 如果你使用 -a,它會快取並提交每個改動(不含新檔案)。

現在你就完成了整個快照的流程 ——改些檔案,然後用 git add 將要提交的改動提交到快取, 用 git status 和 git diff 看看你都改了啥,最後 git commit 永久地儲存快照。

簡而言之,執行 git commit 記錄快取區的快照。如果需要的話,這個快照可以用來做比較、共享以及恢復。

git reset HEAD 取消快取已快取的內容

git reset 可能是人類寫的最費解的命令了。 我用 Git 有些年頭了,甚至還寫了本書,但有的時候還是會搞不清它會做什麼。 所以,我只說三個明確的,通常有用的呼叫。請你跟我一樣儘管用它 —— 因為它可以很有用。

在此例中,我們可以用它來將不小心快取的東東取消快取。假設你修改了兩個檔案,想要將它們記錄到兩個不同的提交中去。 你應該快取並提交一個,再快取並提交另外一個。如果你不小心兩個都快取了,那要如何才能取消快取呢? 你可以用 git reset HEAD -- file。 技術上說,在這裡你不需要使用 -- —— 它用來告訴 Git 這時你已經不再列選項,剩下的是檔案路徑了。 不過養成使用它分隔選項與路徑的習慣很重要,即使在你可能並不需要的時候。

好,讓我們看看取消快取是什麼樣子的。這裡我們有兩個最近提交之後又有所改動的檔案。我們將兩個都快取,並取消快取其中一個。

$ git status -s
 M README
 M hello.rb
$ git add .
$ git status -s
M  README
M  hello.rb
$ git reset HEAD -- hello.rb 
Unstaged changes after reset:
M hello.rb
$ git status -s
M  README
 M hello.rb

現在你執行 git commit 將只記錄 README 檔案的改動,並不含現在並不在快取中的 hello.rb

如果你好奇,它實際的操作是將該檔案在“索引”中的校驗和重置為最近一次提交中的值。 git add 會計算一個檔案的校驗和,將它新增到“索引”中, 而 git reset HEAD 將它改寫回原先的,從而取消快取操作。

如果你想直接執行 git unstage,你可以在 Git 中配置個別名。 執行 git config --global alias.unstage "reset HEAD" 即可。 一旦執行完它,你就可以直接用 git unstage [file]作為代替了。

如果你忘了取消快取的命令,Git 的常規 git status 輸出的提示會很有幫助。 例如,在你有已快取的檔案時,如果你不帶 -s 執行 git status,它將告訴你怎樣取消快取:

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

簡而言之,執行 git reset HEAD 以取消之前 git add 新增,但不希望包含在下一提交快照中的快取。

git rm 將檔案從快取區移除

git rm 會將條目從快取區中移除。這與 git reset HEAD 將條目取消快取是有區別的。 “取消快取”的意思就是將快取區恢復為我們做出修改之前的樣子。 在另一方面,git rm 則將該檔案徹底從快取區踢出,因此它不再下一個提交快照之內,進而有效地刪除它。

預設情況下,git rm file 會將檔案從快取區和你的硬碟中(工作目錄)刪除。 如果要在工作目錄中留著該檔案,可以使用 git rm --cached

git mv git rm –cached orig; mv orig new; git add new

不像絕大多數其他版本控制系統,Git 並不記錄記錄檔案重新命名。它反而只記錄快照,並對比快照以找到有啥檔案可能被重新命名了。 如果一個檔案從更新中刪除了,而在下次快照中新新增的另一個檔案的內容與它很相似,Git 就知道這極有可能是個重新命名。 因此,雖然有 git mv 命令,但它有點多餘 —— 它做得所有事情就是 git rm --cached, 重新命名磁碟上的檔案,然後再執行 git add 把新檔案新增到快取區。 你並不需要用它,不過如果覺得這樣容易些,儘管用吧。

我自己並不使用此命令的普通形式 —— 刪除檔案。通常直接從硬碟刪除檔案,然後執行 git commit -a 會簡單些。 它會自動將刪除的檔案從索引中移除。

簡而言之, 執行 git rm 來刪除 Git 追蹤的檔案。它還會刪除你的工作目錄中的相應檔案。

三、分支與合併

分支是我最喜歡的 Git 特性之一。如果你用過其他版本控制系統,把你所知的分支給忘記,倒可能更有幫助些 —— 事實上,以我們使用分支的方式,把 Git 的分支看作 上下文 反而更合適。 當你檢出分支時,你可以在兩三個不同的分支之間來回切換。

簡而言之,你可以執行 git branch (branchname) 來建立分支, 使用 git checkout (branchname) 命令切換到該分支,在該分支的上下文環境中, 提交快照等,之後可以很容易地來回切換。當你切換分支的時候,Git 會用該分支的最後提交的快照替換你的工作目錄的內容, 所以多個分支不需要多個目錄。使用 git merge 來合併分支。你可以多次合併到統一分支, 也可以選擇在合併之後直接刪除被併入的分支。

git branch 列出、建立與管理工作上下文    git checkout 切換到新的分支上下文

git branch 命令是 Git 中的通用分支管理工具,可以通過它完成多項任務。 我們先說你會用到的最多的命令 —— 列出分支、建立分支和刪除分支。 我們還會介紹用來切換分支的 git checkout 命令。

git branch 列出可用的分支

沒有引數時,git branch 會列出你在本地的分支。你所在的分支的行首會有個星號作標記。 如果你開啟了彩色模式,當前分支會用綠色顯示。

$ git branch
* master

此例的意思就是,我們有一個叫做“master”的分支,並且該分支是當前分支。 當你執行 git init 的時候,預設情況下 Git 就會為你建立“master”分支。 但是這名字一點特殊意味都沒有 —— 事實上你並不非得要一個叫做“master”的分支。 不過由於它是預設分支名的緣故,絕大部分專案都有這個分支。

git branch (branchname) 建立新分支

我們動手建立一個分支,並切換過去。執行 git branch (branchname) 即可。

$ git branch testing
$ git branch
* master
  testing

現在我們可以看到,有了一個新分支。當你以此方式在上次提交更新之後建立了新分支,如果後來又有更新提交, 然後又切換到了“testing”分支,Git 將還原你的工作目錄到你建立分支時候的樣子 —— 你可以把它看作一個記錄你當前進度的書籤。讓我們實際運用看看 —— 我們用 git checkout (branch) 切換到我們要修改的分支。

$ ls
README   hello.rb
$ echo 'test content' > test.txt
$ echo 'more content' > more.txt
$ git add *.txt
$ git commit -m 'added two files'
[master 8bd6d8b] added two files
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 more.txt
 create mode 100644 test.txt
$ ls
README   hello.rb more.txt test.txt
$ git checkout testing
Switched to branch 'testing'
$ ls
README   hello.rb

當我們切換到“測試”分支的時候,我們新增的新檔案被移除了。切換回“master”分支的時候,它們有重新出現了。

$ ls
README   hello.rb
$ git checkout master
Switched to branch 'master'
$ ls
README   hello.rb more.txt test.txt

git checkout -b (branchname) 建立新分支,並立即切換到它

通常情況下,你會更希望立即切換到新分支,從而在該分支中操作,然後當此分支的開發日趨穩定時, 將它合併到穩定版本的分支(例如“master”)中去。 執行 git branch newbranch; git checkout newbranch 也很簡單, 不過 Git 還為你提供了快捷方式:git checkout -b newbranch

$ git branch
* master
$ ls
README   hello.rb more.txt test.txt
$ git checkout -b removals
Switched to a new branch 'removals'
$ git rm more.txt 
rm 'more.txt'
$ git rm test.txt 
rm 'test.txt'
$ ls
README   hello.rb
$ git commit -am 'removed useless files'
[removals 8f7c949] removed useless files
 2 files changed, 0 insertions(+), 2 deletions(-)
 delete mode 100644 more.txt
 delete mode 100644 test.txt
$ git checkout master
Switched to branch 'master'
$ ls
README   hello.rb more.txt test.txt

如你所見,我們建立了一個分支,在該分支的上下文中移除了一些檔案,然後切換回我們的主分支,那些檔案又回來了。 使用分支將工作切分開來,從而讓我們能夠在不同上下文中做事,並來回切換。

建立新分支,在其中完成一部分工作,完成之後將它合併到主分支並刪除。你會覺得這很方便,因為這麼做很快很容易。 如此,當你覺得這部分工作並不靠譜,捨棄它很容易。並且,如果你必須回到穩定分支做些事情, 也可以很方便地這個獨立分支的工作先丟在一邊,完成要事之後再切換回來。

git branch -d (branchname) 刪除分支

假設我們要刪除一個分支(比如上例中的“testing”分支,該分支沒啥特殊的內容了), 可以執行 git branch -d (branch) 把它刪掉。

$ git branch
* master
  testing
$ git branch -d testing
Deleted branch testing (was 78b2670).
$ git branch
* master

簡而言之 使用 git branch 列出現有的分支、建立新分支以及刪除不必要或者已合併的分支。

git merge 將分支合併到你的當前分支

一旦某分支有了獨立內容,你終究會希望將它合併回到你的主分支。 你可以使用 git merge 命令將任何分支合併到當前分支中去。 我們那上例中的“removals”分支為例。假設我們建立了一個分支,移除了一些檔案,並將它提交到該分支, 其實該分支是與我們的主分支(也就是“master”)獨立開來的。 要想將這些移除操作包含在主分支中,你可以將“removals”分支合併回去。

$ git branch
* master
  removals
$ ls
README   hello.rb more.txt test.txt
$ git merge removals
Updating 8bd6d8b..8f7c949
Fast-forward
 more.txt |    1 -
 test.txt |    1 -
 2 files changed, 0 insertions(+), 2 deletions(-)
 delete mode 100644 more.txt
 delete mode 100644 test.txt
$ ls
README   hello.rb

更多複雜合併

當然,合併並不僅僅是簡單的檔案新增、移除的操作,Git 也會合並修改 —— 事實上,它很會合並修改。 舉例,我們看看在某分支中編輯某個檔案,然後在另一個分支中把它的名字改掉再做些修改, 最後將這倆分支合併起來。你覺得會變成一坨 shi?我們試試看。

$ git branch
* master
$ cat hello.rb 
class HelloWorld
  def self.hello
    puts "Hello World"
  end
end

HelloWorld.hello

首先,我們建立一個叫做“change_class”的分支,切換過去,從而將重新命名類等操作獨立出來。我們將類名從 “HelloWorld” 改為 “HiWorld”。

$ git checkout -b change_class
M hello.rb
Switched to a new branch 'change_class'
$ vim hello.rb 
$ head -1 hello.rb 
class HiWorld
$ git commit -am 'changed the class name'
[change_class 3467b0a] changed the class name
 1 files changed, 2 insertions(+), 4 deletions(-)

然後,將重新命名類操作提交到 “change_class” 分支中。 現在,假如切換回 “master” 分支我們可以看到類名恢復到了我們切換到 “change_class” 分支之前的樣子。 現在,再做些修改(即程式碼中的輸出),同時將檔名從 hello.rb 改為 ruby.rb

$ git checkout master
Switched to branch 'master'
$ git mv hello.rb ruby.rb
$ vim ruby.rb 
$ git diff
diff --git a/ruby.rb b/ruby.rb
index 2aabb6e..bf64b17 100644
--- a/ruby.rb
+++ b/ruby.rb
@@ -1,7 +1,7 @@
 class HelloWorld

   def self.hello
-    puts "Hello World"
+    puts "Hello World from Ruby"
   end

 end
$ git commit -am 'added from ruby'
[master b7ae93b] added from ruby
 1 files changed, 1 insertions(+), 1 deletions(-)
 rename hello.rb => ruby.rb (65%)

現在這些改變已經記錄到我的 “master” 分支了。請注意,這裡類名還是 “HelloWorld”,而不是 “HiWorld”。 然後我想將類名的改變合併過來,我把 “change_class” 分支合併過來就行了。 但是,我已經將檔名都改掉了,Git 知道該怎麼辦麼?

$ git branch
  change_class
* master
$ git merge change_class
Renaming hello.rb => ruby.rb
Auto-merging ruby.rb
Merge made by recursive.
 ruby.rb |    6 ++----
 1 files changed, 2 insertions(+), 4 deletions(-)
$ cat ruby.rb
class HiWorld
  def self.hello
    puts "Hello World from Ruby"
  end
end

HiWorld.hello

不錯,它就是發現了。請注意,在這部操作,我沒有遇到合併衝突,並且檔案已經重新命名、類名也換掉了。挺酷。

合併衝突

那麼,Git 合併很有魔力,我們再也不用處理合並衝突了,對嗎?不太確切。 不同分支中修改了相同區塊的程式碼,電腦自己猜不透神馬的情況下,衝突就擺在我們面前了。 我們看看兩個分支中改了同一行程式碼的例子。

$ git branch
* master
$ git checkout -b fix_readme
Switched to a new branch 'fix_readme'
$ vim README 
$ git commit -am 'fixed readme title'
[fix_readme 3ac015d] fixed readme title
 1 files changed, 1 insertions(+), 1 deletions(-)

我們在某分支中修改了 README 檔案中的一行,並提交了。我們再在 “master” 分支中對同個檔案的同一行內容作不同的修改。

$ git checkout master
Switched to branch 'master'
$ vim README 
$ git commit -am 'fixed readme title differently'
[master 3cbb6aa] fixed readme title differently
 1 files changed, 1 insertions(+), 1 deletions(-)

有意思的來了 —— 我們將前一個分支合併到 “master” 分支,一個合併衝突就出現了。

$ git merge fix_readme
Auto-merging README
CONFLICT (content): Merge conflict in README
Automatic merge failed; fix conflicts and then commit the result.
$ cat README 
<<<<<<< HEAD
Many Hello World Examples
=======
Hello World Lang Examples
>>>>>>> fix_readme

This project has examples of hello world in
nearly every programming language.

你可以看到,Git 在產生合併衝突的地方插入了標準的與 Subversion 很像的合併衝突標記。 輪到我們去解決這些衝突了。在這裡我們就手動把它解決。如果你要 Git 開啟一個圖形化的合併工具, 可以看看 git 合併工具 (比如 kdiff3、emerge、p4merge 等)。

$ vim README   here I'm fixing the conflict
$ git diff
diff --cc README
index 9103e27,69cad1a..0000000
--- a/README
+++ b/README
@@@ -1,4 -1,4 +1,4 @@@
- Many Hello World Examples
 -Hello World Lang Examples
++Many Hello World Lang Examples

  This project has examples of hello world in

在 Git 中,處理合並衝突的時候有個很酷的提示。 如果你執行 git diff,就像我演示的這樣,它會告訴你衝突的兩方,和你是如何解決的。 現在是時候把它標記為已解決了。在 Git 中,我們可以用 git add —— 要告訴 Git 檔案衝突已經解決,你必須把它寫入快取區。

$ git status -s
UU README
$ git add README 
$ git status -s
M  README
$ git commit 
[master 8d585ea] Merge branch 'fix_readme'

現在我們成功解決了合併中的衝突,並提交了結果

簡而言之 使用 git merge 將另一個分支併入當前的分支中去。 Git 會自動以最佳方式將兩個不同快照中獨特的工作合併到一個新快照中去。

git log 顯示一個分支中提交的更改記錄

到目前為止,我們已經提交快照到專案中,在不同的各自分離的上下文中切換, 但假如我們忘了自己是如何到目前這一步的那該怎麼辦?或者假如我們想知道此分支與彼分支到底有啥區別? Git 提供了一個告訴你使你達成當前快照的所有提交訊息的工具,叫做 git log

要理解日誌(log)命令,你需要了解當執行 git commit 以儲存一個快照的時候,都有啥資訊被儲存了。 除了檔案詳單、提交訊息和提交者的資訊,Git 還儲存了你的此次提交所基於的快照。 也就是,假如你克隆了一個專案,你是在什麼快照的基礎上做的修改而得到新儲存的快照的? 這有益於為專案程序提供上下文,使 Git 能夠弄明白誰做了什麼改動。 如果 Git 有你的快照所基於的快照的話,它就能自動判斷你都改變了什麼。而新提交所基於的提交,被稱作新提交的“父親”。

某分支的按時間排序的“父親”列表,當你在該分支時,可以執行 git log 以檢視。 例如,如果我們在本章中操作的 Hello World 專案中執行 git log,我們可以看到已提交的訊息。

$ git log
commit 8d585ea6faf99facd39b55d6f6a3b3f481ad0d3d
Merge: 3cbb6aa 3ac015d
Author: Scott Chacon <[email protected]>
Date:   Fri Jun 4 12:59:47 2010 +0200

    Merge branch 'fix_readme'

    Conflicts:
        README

commit 3cbb6aae5c0cbd711c098e113ae436801371c95e
Author: Scott Chacon <[email protected]>
Date:   Fri Jun 4 12:58:53 2010 +0200

    fixed readme title differently

commit 3ac015da8ade34d4c7ebeffa2053fcac33fb495b
Author: Scott Chacon <[email protected]>
Date:   Fri Jun 4 12:58:36 2010 +0200

    fixed readme title

commit 558151a95567ba4181bab5746bc8f34bd87143d6
Merge: b7ae93b 3467b0a
Author: Scott Chacon <[email protected]>
Date:   Fri Jun 4 12:37:05 2010 +0200

    Merge branch 'change_class'
...

我們可以用 --oneline 選項來檢視歷史記錄的緊湊簡潔的版本。

$ git log --oneline
8d585ea Merge branch 'fix_readme'
3cbb6aa fixed readme title differently
3ac015d fixed readme title
558151a Merge branch 'change_class'
b7ae93b added from ruby
3467b0a changed the class name
17f4acf first commit

這告訴我們的是,此專案的開發歷史。如果提交訊息描述性很好,這就能為我們提供關於有啥改動被應用、或者影響了當前快照的狀態、以及這快照裡頭都有啥。

我們還可以用它的十分有幫助的 --graph 選項,檢視歷史中什麼時候出現了分支、合併。以下為相同的命令,開啟了拓撲圖選項:

$ git log --oneline --graph
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit

現在我們可以更清楚明瞭地看到何時工作分叉、又何時歸併。 這對檢視發生了什麼、應用了什麼改變很有幫助,並且極大地幫助你管理你的分支。 讓我們建立一個分支,在裡頭做些事情,然後切回到主分支,也做點事情,然後看看 log 命令是如何幫助我們理清這倆分支上都發生了啥的。

首先我們建立一個分支,來新增 Erlang 程式語言的 Hello World 示例 —— 我們想要在一個分支裡頭做這個,以避免讓可能還不能工作的程式碼弄亂我們的穩定分支。 這樣就可以切來切去,片葉不沾身。

$ git checkout -b erlang
Switched to a new branch 'erlang'
$ vim erlang_hw.erl
$ git add erlang_hw.erl 
$ git commit -m 'added erlang'
[erlang ab5ab4c] added erlang
 1 files changed, 5 insertions(+), 0 deletions(-)
 create mode 100644 erlang_hw.erl

由於我們玩函數語言程式設計很開心,以至於沉迷其中,又在“erlang”分支中添加了一個 Haskell 的示例程式。

$ vim haskell.hs
$ git add haskell.hs 
$ git commit -m 'added haskell'
[erlang 1834130] added haskell
 1 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 haskell.hs

最後,我們決定還是把 Ruby 程式的類名改回原先的樣子。與其建立另一個分支,我們可以返回主分支,改變它,然後直接提交。

$ git checkout master
Switched to branch 'master'
$ ls
README  ruby.rb
$ vim ruby.rb 
$ git commit -am 'reverted to old class name'
[master 594f90b] reverted to old class name
 1 files changed, 2 insertions(+), 2 deletions(-)

現在假設我們有段時間不做這個專案了,我們做別的去了。 當我們回來的時候,我們想知道“erlang”分支都是啥,而主分支的進度又是怎樣。 僅僅看分支的名字,我們是無從知道自己還在裡面有 Haskell 的改動的,但是用 git log 我們就可以。 如果你在命令列中提供一個分支名字,它就會顯示該分支歷史中“可及”的提交,即從該分支創立起可追溯的影響了最終的快照的提交。

$ git log --oneline erlang
1834130 added haskell
ab5ab4c added erlang
8d585ea Merge branch 'fix_readme'
3cbb6aa fixed readme title differently
3ac015d fixed readme title
558151a Merge branch 'change_class'
b7ae93b added from ruby
3467b0a changed the class name
17f4acf first commit

如此,我們很容易就看到分支裡頭還包括了 Haskell 程式碼(高亮顯示了)。 更酷的是,我們很容易地告訴 Git,我們只對某個分支中可及的提交感興趣。換句話說,某分支中與其他分支相比唯一的提交。

在此例中,如果我們想要合併“erlang”分支,我們需要看當合並的時候,都有啥提交會作用到我們的快照上去。 我們告訴 Git 的方式是,在不想要看到的分支前放一個 ^。 例如,如果我們想要看“erlang”分支中但不在主分支中的提交,我們可以用 erlang ^master,或者反之。

$ git log --oneline erlang ^master
1834130 added haskell
ab5ab4c added erlang
$ git log --oneline master ^erlang
594f90b reverted to old class name

這為我們提供了一個良好的、簡易的分支管理工具。它使我們能夠非常容易地檢視對某個分支唯一的提交,從而知道我們缺少什麼,以及當我們要合併時,會有什麼被合併進去。

簡而言之 使用 git log 列出促成當前分支目前的快照的提交歷史記錄。這使你能夠看到專案是如何到達現在的狀況的。

git tag 給歷史記錄中的某個重要的一點打上標籤

如果你達到一個重要的階段,並希望永遠記住那個特別的提交快照,你可以使用 git tag 給它打上標籤。 該 tag 命令基本上會給該特殊提交打上永久的書籤,從而使你在將來能夠用它與其他提交比較。 通常,你會在切取一個釋出版本或者交付一些東西的時候打個標籤。

比如說,我們想為我們的 Hello World 專案釋出一個“1.0”版本。 我們可以用 git tag -a v1.0 命令給最新一次提交打上(HEAD)“v1.0”的標籤。 -a 選項意為“建立一個帶註解的標籤”,從而使你為標籤添加註解。絕大部分時候都會這麼做的。 不用 -a 選項也可以執行的,但它不會記錄這標籤是啥時候打的,誰打的,也不會讓你添加個標籤的註解。 我推薦一直建立帶註解的標籤。

$ git tag -a v1.0 

當你執行 git tag -a 命令時,Git 會開啟你的編輯器,讓你寫一句標籤註解,就像你給提交寫註解一樣。

現在,注意當我們執行 git log --decorate 時,我們可以看到我們的標籤了:

$ git log --oneline --decorate --graph
* 594f90b (HEAD, tag: v1.0, master) reverted to old class name
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d (fix_readme) fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit

如果我們有新提交,該標籤依然會待在該提交的邊上,所以我們已經給那個特定快照永久打上標籤,並且能夠將它與未來的快照做比較。

不過我們並不需要給當前提交打標籤。如果我們忘了給某個提交打標籤,又將它釋出了,我們可以給它追加標籤。 在相同的命令末尾加上提交的 SHA,執行,就可以了。 例如,假設我們釋出了提交 558151a(幾個提交之前的事情了),但是那時候忘了給它打標籤。 我們現在也可以:

$ git tag -a v0.9 558151a
$ git log --oneline --decorate --graph
* 594f90b (HEAD, tag: v1.0, master) reverted to old class name
*   8d585ea Merge branch 'fix_readme'
|\
| * 3ac015d (fix_readme) fixed readme title
* | 3cbb6aa fixed readme title differently
|/
*   558151a (tag: v0.9) Merge branch 'change_class'
|\
| * 3467b0a changed the class name
* | b7ae93b added from ruby
|/
* 17f4acf first commit

四、分享與更新專案

Git 並不像 Subversion 那樣有個中心伺服器。 目前為止所有的命令都是本地執行的,更新的知識本地的資料庫。 要通過 Git 與其他開發者合作,你需要將資料放到一臺其他開發者能夠連線的伺服器上。 Git 實現此流程的方式是將你的資料與另一個倉庫同步。在伺服器與客戶端之間並沒有實質的區別 —— Git 倉庫就是 Git 倉庫,你可以很容易地在兩者之間同步。

一旦你有了個 Git 倉庫,不管它是在你自己的伺服器上,或者是由 GitHub 之類的地方提供, 你都可以告訴 Git 推送你擁有的遠端倉庫還沒有的資料,或者叫 Git 從別的倉庫把差別取過來。

聯網的時候你可以隨時做這個,它並不需要對應一個 commit 或者別的什麼。 一般你會本地提交幾次,然後從你的專案克隆自的線上的共享倉庫提取資料以保持最新,將新完成的合併到你完成的工作中去,然後推送你的改動會伺服器。

簡而言之 使用 git fetch 更新你的專案,使用 git push 分享你的改動。 你可以用 git remote 管理你的遠端倉庫。

git remote 羅列、新增和刪除遠端倉庫別名

不像中心化的版本控制系統(客戶端與服務端很不一樣),Git 倉庫基本上都是一致的,並且並可以同步他們。 這使得擁有多個遠端倉庫變得容易 —— 你可以擁有一些只讀的倉庫,另外的一些也可寫的倉庫。

當你需要與遠端倉庫同步的時候,不需要使用它詳細的連結。Git 儲存了你感興趣的遠端倉庫的連結的別名或者暱稱。 你可以使用 git remote 命令管理這個遠端倉庫列表。

git remote 列出遠端別名

如果沒有任何引數,Git 會列出它儲存的遠端倉庫別名了事。預設情況下,如果你的專案是克隆的(與本地建立一個新的相反), Git 會自動將你的專案克隆自的倉庫新增到列表中,並取名“origin”。 如果你執行時加上 -v 引數,你還可以看到每個別名的實際連結地址。

$ git remote
origin
$ git remote -v
origin	[email protected]:github/git-reference.git (fetch)
origin	[email protected]:github/git-reference.git (push)

在此你看到了該連結兩次,是因為 Git 允許你為每個遠端倉庫新增不同的推送與獲取的連結,以備你讀寫時希望使用不同的協議。

git remote add 為你的專案新增一個新的遠端倉庫

如果你希望分享一個本地建立的倉庫,或者你想要獲取別人的倉庫中的貢獻 —— 如果你想要以任何方式與一個新倉庫溝通,最簡單的方式通常就是把它新增為一個遠端倉庫。 執行 git remote add [alias] [url] 就可以。 此命令將 [url] 以 [alias] 的別名新增為本地的遠端倉庫。

例如,假設我們想要與整個世界分享我們的 Hello World 程式。 我們可以在一臺伺服器上建立一個新倉庫(我以 GitHub 為例子)。 它應該會給你一個連結,在這裡就是“[email protected]:schacon/hw.git”。 要把它新增到我們的專案以便我們推送以及獲取更新,我們可以這樣:

$ git remote
$ git remote add github [email protected]:schacon/hw.git
$ git remote -v
github	[email protected]:schacon/hw.git (fetch)
github	[email protected]:schacon/hw.git (push)

像分支的命名一樣,遠端倉庫的別名是強制的 —— 就像“master”,沒有特別意義,但它廣為使用, 因為 git init 預設用它;“origin”經常被用作遠端倉庫別名,就因為 git clone 預設用它作為克隆自的連結的別名。此例中,我決定給我的遠端倉庫取名“github”,但我叫它隨便什麼都可以。

git remote rm 刪除現存的某個別名

Git addeth and Git taketh away. 如果你需要刪除一個遠端 —— 不再需要它了、專案已經沒了,等等 —— 你可以使用 git remote rm [alias] 把它刪掉。

$ git remote -v
github	[email protected]:schacon/hw.git (fetch)
github	[email protected]:schacon/hw.git (push)
$ git remote add origin git://github.com/pjhyett/hw.git
$ git remote -v
github	[email