前言:
這里簡單介紹一下Git的歷史。
同生活中的許多偉大事件一樣,Git 誕生於一個極富紛爭大舉創新的年代。Linux 內核開源項目有着為數眾廣的參與者。絕大多數的 Linux 內核維護工作都花在了提交補丁和保存歸檔的繁瑣事務上(1991-2002年間)。到 2002 年,整個項目組開始啟用分布式版本控制系統 BitKeeper 來管理和維護代碼。
到了 2005 年,開發 BitKeeper 的商業公司同 Linux 內核開源社區的合作關系結束,他們收回了免費使用 BitKeeper 的權力。這就迫使 Linux 開源社區(特別是 Linux 的締造者 Linus Torvalds )不得不吸取教訓,只有開發一套屬於自己的版本控制系統才不至於重蹈覆轍。他們對新的系統制訂了若干目標:
- 速度
- 簡單的設計
- 對非線性開發模式的強力支持(允許上千個並行開發的分支)
- 完全分布式
- 有能力高效管理類似 Linux 內核一樣的超大規模項目(速度和數據量)
自誕生於 2005 年以來,Git 日臻成熟完善,在高度易用的同時,仍然保留着初期設定的目標。它的速度飛快,極其適合管理大項目,它還有着令人難以置信的非線性分支管理系統(見第三章),可以應付各種復雜的項目開發需求。
一、Git起步
1、直接記錄快照,而非差異比較
Git 和其他版本控制系統的主要差別在於,Git 只關心文件數據的整體是否發生變化,而大多數其他系統則只關心文件內容的具體差異。這類系統(CVS,Subversion,Perforce,Bazaar 等等)每次記錄有哪些文件作了更新,以及都更新了哪些行的什么內容,具體如圖
Git 並不保存這些前后變化的差異數據。實際上,Git 更像是把變化的文件作快照后,記錄在一個微型的文件系統中。每次提交更新時,它會縱覽一遍所有文件的指紋信息並對文件作一快照,然后保存一個指向這次快照的索引。為提高性能,若文件沒有變化,Git 不會再次保存,而只對上次保存的快照作一鏈接。Git 的工作方式就像圖所示
2、運行Git前的配置
這里如果還有小伙伴們沒有安裝好Git,請自行去安裝一下先哦(https://git-scm.com/downloads)。
Git 提供了一個叫做 git config 的工具,專門用來配置或讀取相應的工作環境變量。而正是由這些環境變量,決定了 Git 在各個環節的具體工作方式和行為。這些變量可以存放在以下三個不同的地方:
/etc/gitconfig
文件:系統中對所有用戶都普遍適用的配置。若使用git config
時用--system
選項,讀寫的就是這個文件。~/.gitconfig
文件:用戶目錄下的配置文件只適用於該用戶。若使用git config
時用--global
選項,讀寫的就是這個文件。- 當前項目的 git 目錄中的配置文件(也就是工作目錄中的
.git/config
文件):這里的配置僅僅針對當前項目有效。每一個級別的配置都會覆蓋上層的相同配置,所以.git/config
里的配置會覆蓋/etc/gitconfig
中的同名變量。
a. 用戶信息配置
第一個要配置的是你個人的用戶名稱和電子郵件地址。這兩條配置很重要,每次 Git 提交時都會引用這兩條信息,說明是誰提交了更新,所以會隨更新內容一起被永久納入歷史記錄
git config --global user.name "qiangdada" git config --global user.email qiangdada@example.com
如果用了 --global
選項,那么更改的配置文件就是位於你用戶主目錄下的那個,以后你所有的項目都會默認使用這里配置的用戶信息。如果要在某個特定的項目中使用其他名字或者電郵,只要去掉 --global
選項重新配置即可,新的設定保存在當前項目的 .git/config
文件里。
b. 差異分析工具
Git 可以理解 kdiff3,tkdiff,meld,xxdiff,emerge,vimdiff,gvimdiff,ecmerge,和 opendiff 等合並工具的輸出信息,這里,如果說在解決合並沖突時使用的是vimdiff差異分析工具。改用命令如下
git config --global merge.tool vimdiff
c. 查看配置信息
git config --list #user.name=qiangdada #user.email=qiangdada@tencent.com #color.status=auto #color.branch=auto #color.interactive=auto #color.diff=auto #...
這里會看到重復的變量名,那就說明它們來自不同的配置文件(比如 /etc/gitconfig
和 ~/.gitconfig
),不過最終 Git 實際采用的是最后一個。也可以直接查閱某個環境變量的設定,只要把特定的名字跟在后面即可,像這樣
git config user.name #qiangdada
d. 獲取幫助
想了解 Git 的各式工具該怎么用,可以閱讀它們的使用幫助,方法有三
# 方法1 git help #方法二 git --help #方法三 man git
比如,要學習 config 命令可以怎么用,運行
git help config
二、Git基礎
1、取得項目的Git倉庫
有兩種取得 Git 項目倉庫的方法。第一種是在現存的目錄下,通過導入所有文件來創建新的 Git 倉庫。第二種是從已有的 Git 倉庫克隆出一個新的鏡像倉庫來
a. 從工作目錄中初始化新倉庫
要對現有的某個項目開始用 Git 管理,只需到此項目所在的目錄,執行
git init
初始化后,在當前目錄下會出現一個名為 .git 的目錄,所有 Git 需要的數據和資源都存放在這個目錄中。如果當前目錄下有幾個文件想要納入版本控制,需要先用 git add
命令告訴 Git 開始對這些文件進行跟蹤,然后提交
# step 1 git add *.c # step 2 git add README # step 3 git commit -m 'initial project version'
b. 從現有倉庫克隆
如果想對某個開源項目出一份力,可以先把該項目的 Git 倉庫復制一份出來,這就需要用到 git clone
命令。如果你熟悉其他的 VCS 比如 Subversion,你可能已經注意到這里使用的是 clone
而不是 checkout
。這是個非常重要的差別,Git 收取的是項目歷史的所有數據(每一個文件的每一個版本),服務器上有的數據克隆之后本地也都有了。實際上,即便服務器的磁盤發生故障,用任何一個克隆出來的客戶端都可以重建服務器上的倉庫,回到當初克隆時的狀態
git clone [url] # 如 git clone https://github.com/xuqiang521/data-visualization.git
2、記錄每次更新到倉庫
先上一張文件的狀態變化周期的圖示
a. 檢查當前文件狀態
git status
b. 跟蹤新文件
使用命令 git add
開始跟蹤一個新文件README。運行
# step 1 git add README # step 2 git status # 此時文件屬於暫存狀態 # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # new file: 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
文件 benchmarks.rb
出現在 “Changes not staged for commit” 這行下面,說明已跟蹤文件的內容發生了變化,但還沒有放到暫存區。要暫存這次更新,需要運行 git add
命令。現在讓我們運行 git add
將 benchmarks.rb 放到暫存區,然后再看看 git status
的輸出
# step 1 git add benchmarks.rb # step 2 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
看看
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
把最新版本重新暫存起來
# step 1 git add benchmarks.rb # step 2 git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: README # modified: benchmarks.rb
c. 忽略某些文件
一般我們總會有些文件無需納入 Git 的管理,也不希望它們總出現在未跟蹤文件列表。通常都是些自動生成的文件,比如日志文件,或者編譯過程中創建的臨時文件等。我們可以創建一個名為 .gitignore
的文件,列出要忽略的文件模式。來看一個實際的例子
cat .gitignore #告訴 Git 忽略所有以 .o 或 .a 結尾的文件。一般這類對象文件和存檔文件都是編譯過程中出現的,我們用不着跟蹤它們的版本 *.[oa] #告訴 Git 忽略所有以波浪符(~)結尾的文件,許多文本編輯軟件(比如 Emacs)都用這樣的文件名保存副本 *~ #此外,你可能還需要忽略 log,tmp 或者 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
d. 查看已暫存和未暫存的更新
實際上 git status
的顯示比較簡單,僅僅是列出了修改過的文件,假如再次修改 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
此命令比較的是工作目錄中當前文件和暫存區域快照之間的差異,也就是修改之后還沒有暫存起來的變化內容。
若要看已經暫存起來的文件和上次提交時的快照之間的差異,可以用 git diff --cached
命令。
git diff --cached
e. 提交更新
現在的暫存區域已經准備妥當可以提交了。在此之前,請一定要確認還有什么修改過的或新建的文件還沒有 git add
過,否則提交的時候不會記錄這些還沒暫存起來的變化。所以,每次准備提交前,先用 git status
看下,是不是都已暫存起來了,然后再運行提交命令 git commit
git commit
這種方式會啟動文本編輯器以便輸入本次提交的說明,編輯器會顯示類似下面的文本信息(本例選用 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
另外也可以用 -m 參數后跟提交說明的方式,在一行命令中提交更新
git commit -m "up"
如果我們需要跳過使用暫存區域,不用擔心。Git 提供了一個跳過使用暫存區域的方式,只要在提交的時候,給 git commit
加上 -a
選項,Git 就會自動把所有已經跟蹤過的文件暫存起來一並提交,從而跳過 git add
步驟
# step 1 git status # On branch master # # Changes not staged for commit: # # modified: benchmarks.rb # # step 2 git commit -a -m 'up' #[master 83e38c7] added new benchmarks #1 files changed, 5 insertions(+), 0 deletions(-)
f. 移除文件
要從 Git 中移除某個文件,就必須要從已跟蹤文件清單中移除(確切地說,是從暫存區域移除),然后提交。可以用 git rm
命令完成此項工作,並連帶從工作目錄中刪除指定的文件,這樣以后就不會出現在未跟蹤文件清單中了。
如果只是簡單地從工作目錄中手工刪除文件,運行 git status
時就會在 “Changes not staged for commit” 部分(也就是未暫存清單)看到
# step 1 rm benchmarks.rb # step 2 git status # On branch master # # Changes not staged for commit: # (use "git add/rm <file>..." to update what will be committed) # # deleted: benchmarks.rb
然后再運行 git rm
記錄此次移除文件的操作:
# step 1 git rm benchmarks.rb # step 2 git status # On branch master # # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # deleted: benchmarks.rb
這樣最后提交的時候,該文件就不再納入版本管理了。如果刪除之前修改過並且已經放到暫存區域的話,則必須要用強制刪除選項 -f
(force 的首字母),以防誤刪除文件后丟失修改的內容。
另外一種情況是,我們想把文件從 Git 倉庫中刪除(亦即從暫存區域移除),但仍然希望保留在當前工作目錄中。換句話說,僅是從跟蹤清單中刪除。比如一些大型日志文件或者一堆 .a
編譯文件,不小心納入倉庫后,要移除跟蹤但不刪除文件,以便稍后在 .gitignore
文件中補上,用 --cached
選項即可
git rm --cached readme.txt
3、查看提交歷史
在提交了若干更新之后,又或者克隆了某個項目,想回顧下提交歷史,可以使用 git log
命令查看。
git log
默認不用任何參數的話,git log
會按提交時間列出所有的更新,最近的更新排在最上面。
git log
命令支持的選項具體如下所示
#選項 說明 -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(后跟指定格式)。 -(n) 僅顯示最近的 n 條提交 --since, --after 僅顯示指定時間之后的提交。 --until, --before 僅顯示指定時間之前的提交。 --author 僅顯示指定作者相關的提交。 --committer 僅顯示指定提交者相關的提交。
4、撤銷操作
有時候我們提交完了才發現漏掉了幾個文件沒有加,或者提交信息寫錯了。想要撤消剛才的提交操作,可以使用 --amend
選項重新提交:
git commit --amend
如果剛才提交時忘了暫存某些修改,可以先補上暫存操作,然后再運行 --amend
提交:
# step 1 git commit -m 'initial commit' # step 2 git add forgotten_file # step 3 git commit --amend
上面的三條命令最終只是產生一個提交,第二個提交命令修正了第一個的提交內容。
這篇博客先寫到這里,后期我會再寫上一篇作為后續補上,后續中詳細介紹Git分支以及遠程倉庫的操作。希望可以幫助到大家,也希望大家可以支持一下我,大家的支持將是我寫作最大的動力(*^__^*) !
前言
上篇我就Git協議講了兩大點,Git的起步,Git的基礎操作,其中詳細的小伙伴可以先去閱讀我上一篇文章https://my.oschina.net/qiangdada/blog/800093(一入前端深似海,從此紅塵是路人系列第十彈之如何合理利用Git進行團隊協作(一))。接下來直接和大家分享有關Git分支以及如何操作遠程倉庫進行團隊協作。
一、遠程倉庫的使用
1、查看當前的遠程倉庫
要查看當前配置有哪些遠程倉庫,可以用 git remote
命令,它會列出每個遠程庫的簡短名字。在克隆完某個項目后,至少可以看到一個名為 origin 的遠程庫,Git 默認使用這個名字來標識你所克隆的原始倉庫
# step 1 $ git clone https://github.com/xuqiang521/data-visualization.git #Cloning into 'data-visualization'... #remote: Counting objects: 38, done. #remote: Compressing objects: 100% (29/29), done. #remote: Total 38 (delta 7), reused 38 (delta 7), pack-reused 0 #Unpacking objects: 100% (38/38), done. # step 2 $ cd data-visualization # step 3 $ git remote #origin
也可以加上 -v
選項(譯注:此為 --verbose
的簡寫,取首字母),顯示對應的克隆地址:
$ git remote -v
#origin https://github.com/xuqiang521/data-visualization.git (fetch) #origin https://github.com/xuqiang521/data-visualization.git (push)
如果有多個遠程倉庫,此命令也可以將其全部列出。這樣一來,我就可以非常輕松地從這些用戶的倉庫中,拉取他們的提交到本地。
2、添加遠程倉庫
要添加一個新的遠程倉庫,可以指定一個簡單的名字,以便將來引用,運行 git remote add [shortname] [url]
:
# step 1 $ git remote #origin # step 2 $ git remote add pb git://github.com/paulboone/ticgit.git # step 3 $ 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
,你可以將它合並到自己的某個分支,或者切換到這個分支,看看有些什么有趣的更新。
3、從遠程倉庫抓取數據
正如之前所看到的,可以用下面的命令從遠程倉庫抓取數據到本地:
$ git fetch [remote-name]
此命令會到遠程倉庫中拉取所有你本地倉庫中還沒有的數據。運行完成后,你就可以在本地訪問該遠程倉庫中的所有分支,將其中某個分支合並到本地,或者只是取出某個分支,一探究竟。
如果是克隆了一個倉庫,此命令會自動將遠程倉庫歸於 origin 名下。所以,git fetch origin
會抓取從你上次克隆以來別人上傳到此遠程倉庫中的所有更新(或是上次 fetch 以來別人提交的更新)。有一點很重要,需要記住,fetch 命令只是將遠端的數據拉到本地倉庫,並不自動合並到當前工作分支,只有當你確實准備好了,才能手工合並。
如果設置了某個分支用於跟蹤某個遠端倉庫的分支,可以使用 git pull
命令自動抓取數據下來,然后將遠端分支自動合並到本地倉庫中當前分支。在日常工作中我們經常這么用,既快且好。實際上,默認情況下 git clone
命令本質上就是自動創建了本地的 master 分支用於跟蹤遠程倉庫中的 master 分支(假設遠程倉庫確實有 master 分支)。所以一般我們運行 git pull
,目的都是要從原始克隆的遠端倉庫中抓取數據后,合並到工作目錄中的當前分支。
4、推送數據到遠程倉庫
項目進行到一個階段,要同別人分享目前的成果,可以將本地倉庫中的數據推送到遠程倉庫。實現這個任務的命令很簡單: git push [remote-name] [branch-name]
。如果要把本地的 master 分支推送到 origin
服務器上(再次說明下,克隆操作會自動使用默認的 master 和 origin 名字),可以運行下面的命令:
$ git push origin master
只有在所克隆的服務器上有寫權限,或者同一時刻沒有其他人在推數據,這條命令才會如期完成任務。如果在你推數據前,已經有其他人推送了若干更新,那你的推送操作就會被駁回。你必須先把他們的更新抓取到本地,合並到自己的項目中,然后才可以再次推送。
5、查看遠程倉庫信息
我們可以通過命令 git remote show [remote-name]
查看某個遠程倉庫的詳細信息,比如要看所克隆的 origin
倉庫,可以運行:
$ git remote show origin #* remote origin #Fetch URL: https://github.com/xuqiang521/data-visualization.git #Push URL: https://github.com/xuqiang521/data-visualization.git #HEAD branch: master #Remote branch: # master tracked #Local branch configured for 'git pull': # master merges with remote master #Local ref configured for 'git push': # master pushes to master (up to date)
除了對應的克隆地址外,它還給出了許多額外的信息。它友善地告訴你如果是在 master 分支,就可以用 git pull
命令抓取數據合並到本地。另外還列出了所有處於跟蹤狀態中的遠端分支。
上面的例子非常簡單,而隨着使用 Git 的深入,git remote show
給出的信息可能會像這樣:
$ git remote show origin #* remote origin #URL: git@github.com: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
分支)。
6、遠程倉庫的刪除和重命名
在新版 Git 中可以用 git remote rename
命令修改某個遠程倉庫在本地的簡稱,比如想把 pb
改成 paul
,可以這么運行:
# step 1 $ git remote rename pb paul # step 2 $ git remote #origin #paul
注意,對遠程倉庫的重命名,也會使對應的分支名稱發生變化,原來的 pb/master
分支現在成了 paul/master
。
碰到遠端倉庫服務器遷移,或者原來的克隆鏡像不再使用,又或者某個參與者不再貢獻代碼,那么需要移除對應的遠端倉庫,可以運行 git remote rm
命令:
# step 1 $ git remote rm paul # step 2 $ git remote #origin
二、Git分支
1、何為分支
為了理解 Git 分支的實現方式,我們需要回顧一下 Git 是如何儲存數據的。看過我上一篇博客的朋友應該知道,Git 保存的不是文件差異或者變化量,而只是一系列文件快照。
在 Git 中提交時,會保存一個提交(commit)對象,該對象包含一個指向暫存內容快照的指針,包含本次提交的作者等相關附屬信息,包含零個或多個指向該提交對象的父對象指針:首次提交是沒有直接祖先的,普通提交有一個祖先,由兩個或多個分支合並產生的提交則有多個祖先。
為直觀起見,我們假設在工作目錄中有三個文件,准備將它們暫存后提交。暫存操作會對每一個文件計算校驗和,然后把當前版本的文件快照保存到 Git 倉庫中(Git 使用 blob 類型的對象存儲這些快照),並將校驗和加入暫存區域:
# step 1 $ git add README test.rb LICENSE # step 2 $ git commit -m 'initial commit of my project'
當使用 git commit
新建一個提交對象前,Git 會先計算每一個子目錄的校驗和,然后在 Git 倉庫中將這些目錄保存為樹(tree)對象。之后 Git 創建的提交對象,除了包含相關提交信息以外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就可以在將來需要的時候,重現此次快照的內容了。
現在,Git 倉庫中有五個對象:三個表示文件快照內容的 blob 對象;一個記錄着目錄樹內容及其中各個文件對應 blob 對象索引的 tree 對象;以及一個包含指向 tree 對象(根目錄)的索引和其他提交信息元數據的 commit 對象。概念上來說,倉庫中的各個對象保存的數據和相互關系看起來如圖2-1所示:
圖 2-1. 單個提交對象在倉庫中的數據結構
作些修改后再次提交,那么這次的提交對象會包含一個指向上次提交對象的指針(譯注:即下圖中的 parent 對象)。兩次提交后,倉庫歷史會變成圖2-2的樣子:
圖 2-2. 多個提交對象之間的鏈接關系
現在來談分支。Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 作為分支的默認名字。在若干次提交后,你其實已經有了一個指向最后一次提交對象的 master 分支,它在每次提交的時候都會自動向前移動。
圖2-3.分支其實就是從某個提交對象往回看的歷史
那么,Git 又是如何創建一個新的分支的呢?答案很簡單,創建一個新的分支指針。比如新建一個 testing 分支,可以使用 git branch
命令:
$ git branch testing
這會在當前 commit 對象上新建一個分支指針(見圖2-4)。
圖 2-4. 多個分支指向提交數據的歷史
那么,Git 是如何知道你當前在哪個分支上工作的呢?其實答案也很簡單,它保存着一個名為 HEAD 的特別指針。請注意它和你熟知的許多其他版本控制系統(比如 Subversion 或 CVS)里的 HEAD 概念大不相同。在 Git 中,它是一個指向你正在工作中的本地分支的指針。運行 git branch
命令,僅僅是建立了一個新的分支,但不會自動切換到這個分支中去,所以在這個例子中,我們依然還在 master 分支里工作(參考圖 2-5)
圖 2-5. HEAD 指向當前所在的分支
要切換到其他分支,可以執行 git checkout
命令。我們現在轉換到新建的 testing 分支:
$ git checkout testing
這樣 HEAD 就指向了 testing 分支(見圖2-6)。
圖 3-6. HEAD 在你轉換分支時指向新的分支
這樣的實現方式會給我們帶來什么好處呢?好吧,現在不妨再提交一次:
# step 1 $ vim test.rb # step 2 $ git commit -a -m 'made a change'
圖 2-7 展示了提交后的結果
圖 2-7. 每次提交后 HEAD 隨着分支一起向前移動
非常有趣,現在 testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout
時所在的 commit 對象。現在我們回到 master 分支看看:
$ git checkout master
圖 2-8 顯示了結果。
圖 2-8. HEAD 在一次 checkout 之后移動到了另一個分支
這條命令做了兩件事。它把 HEAD 指針移回到 master 分支,並把工作目錄中的文件換成了 master 分支所指向的快照內容。也就是說,現在開始所做的改動,將始於本項目中一個較老的版本。它的主要作用是將 testing 分支里作出的修改暫時取消,這樣你就可以向另一個方向進行開發。
我們作些修改后再次提交:
# step 1 $ vim test.js # step 2 $ git commit -a -m 'made other changes'
現在我們的項目提交歷史產生了分叉(如圖 2-9 所示),因為剛才我們創建了一個分支,轉換到其中進行了一些工作,然后又回到原來的主分支進行了另外一些工作。這些改變分別孤立在不同的分支里:我們可以在不同分支里反復切換,並在時機成熟時把它們合並到一起。而所有這些工作,僅僅需要branch
和 checkout
這兩條命令就可以完成。
圖 2-9. 不同流向的分支歷史
2、分支的新建與合並
現在讓我們來看一個簡單的分支與合並的例子,實際工作中大體也會用到這樣的工作流程:
- 開發某個網站。
- 為實現某個新的需求,創建一個分支。
- 在這個分支上開展工作。
假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那么可以按照下面的方式處理:
- 返回到原先已經發布到生產服務器上的分支。
- 為這次緊急修補建立一個新分支,並在其中修復問題。
- 通過測試后,回到生產服務器所在的分支,將修補分支合並進來,然后再推送到生產服務器上。
- 切換到之前實現新需求的分支,繼續工作。
a.分支的新建與切換
首先,我們假設你正在項目中愉快地工作,並且已經提交了幾次更新(見圖 2-10)。
圖 2-10. 一個簡短的提交歷史
現在,你決定要修補問題追蹤系統上的 #53 問題。順帶說明下,Git 並不同任何特定的問題追蹤系統打交道。這里為了說明要解決的問題,才把新建的分支取名為 iss53。要新建並切換到該分支,運行 git checkout
並加上 -b
參數:
$ git checkout -b iss53 #Switched to a new branch "iss53"
這相當於執行下面這兩條命令:
# step 1 $ git branch iss53 # step 2 $ git checkout iss53
圖 2-11 示意該命令的執行結果。
圖 2-11. 創建了一個新分支的指針
接着你開始嘗試修復問題,在提交了若干次更新后,iss53
分支的指針也會隨着向前推進,因為它就是當前分支(換句話說,當前的 HEAD
指針正指向 iss53
,見圖 2-12):
# step 1 $ vim index.html # step 2 $ git commit -a -m 'added a new footer [issue 53]'
圖 2-12. iss53 分支隨工作進展向前推進
現在你就接到了那個網站問題的緊急電話,需要馬上修補。有了 Git ,我們就不需要同時發布這個補丁和 iss53
里作出的修改,也不需要在創建和發布該補丁到服務器之前花費大力氣來復原這些修改。唯一需要的僅僅是切換回 master
分支。
不過在此之前,留心你的暫存區或者工作目錄里,那些還沒有提交的修改,它會和你即將檢出的分支產生沖突從而阻止 Git 為你切換分支。切換分支的時候最好保持一個清潔的工作區域。稍后會介紹幾個繞過這種問題的辦法(分別叫做 stashing 和 commit amending)。目前已經提交了所有的修改,所以接下來可以正常轉換到 master
分支:
$ git checkout master #Switched to branch "master"
此時工作目錄中的內容和你在解決問題 #53 之前一模一樣,你可以集中精力進行緊急修補。這一點值得牢記:Git 會把工作目錄的內容恢復為檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時完全一樣。
接下來,你得進行緊急修補。我們創建一個緊急修補分支 hotfix
來開展工作,直到搞定(見圖 2-13):
# step 1 $ git checkout -b 'hotfix' #Switched to a new branch "hotfix" # step 2 $ vim index.html # step 3 $ git commit -a -m 'fixed the broken email address' #[hotfix]: created 3a0874c: "fixed the broken email address" #1 files changed, 0 insertions(+), 1 deletions(-)
圖 2-13. hotfix 分支是從 master 分支所在點分化出來的
有必要作些測試,確保修補是成功的,然后回到 master
分支並把它合並進來,然后發布到生產服務器。用 git merge
命令來進行合並:
# step 1 $ git checkout master # step 2 $ git merge hotfix #Updating f42c576..3a0874c #Fast forward #README | 1 - #1 files changed, 0 insertions(+), 1 deletions(-)
請注意,合並時出現了“Fast forward”的提示。由於當前 master
分支所在的提交對象是要並入的 hotfix
分支的直接上游,Git 只需把 master
分支指針直接右移。換句話說,如果順着一個分支走下去可以到達另一個分支的話,那么 Git 在合並兩者時,只會簡單地把指針右移,因為這種單線的歷史分支不存在任何需要解決的分歧,所以這種合並過程可以稱為快進(Fast forward)。
現在最新的修改已經在當前 master
分支所指向的提交對象中了,可以部署到生產服務器上去了(見圖 2-14)。
圖 2-14. 合並之后,master 分支和 hotfix 分支指向同一位置。
在那個超級重要的修補發布以后,你想要回到被打擾之前的工作。由於當前 hotfix
分支和 master
都指向相同的提交對象,所以 hotfix
已經完成了歷史使命,可以刪掉了。使用 git branch
的 -d
選項執行刪除操作:
$ git branch -d hotfix #Deleted branch hotfix (3a0874c).
現在回到之前未完成的 #53 問題修復分支上繼續工作(圖 2-15):
# step 1 $ git checkout iss53 #Switched to branch "iss53" # step 2 $ vim index.html # step 3 $ git commit -a -m 'finished the new footer [issue 53]' #[iss53]: created ad82d7a: "finished the new footer [issue 53]" #1 files changed, 1 insertions(+), 0 deletions(-)
圖 2-15. iss53 分支可以不受影響繼續推進。
不用擔心之前 hotfix
分支的修改內容尚未包含到 iss53
中來。如果確實需要納入此次修補,可以用 git merge master
把 master 分支合並到 iss53
;或者等 iss53
完成之后,再將 iss53
分支中的更新並入 master
。
b.分支的合並
在問題 #53 相關的工作完成之后,可以合並回 master
分支。實際操作同前面合並 hotfix
分支差不多,只需回到 master
分支,運行git merge
命令指定要合並進來的分支:
# step 1 $ git checkout master # step 2 $ git merge iss53 #Merge made by recursive. #README | 1 + #1 files changed, 1 insertions(+), 0 deletions(-)
請注意,這次合並操作的底層實現,並不同於之前 hotfix
的並入方式。因為這次你的開發歷史是從更早的地方開始分叉的。由於當前 master
分支所指向的提交對象(C4)並不是 iss53
分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合並計算。圖 2-16 用紅框標出了 Git 用於合並的三個提交對象:
圖 2-16. Git 為分支合並自動識別出最佳的同源合並點。
這次,Git 沒有簡單地把分支指針右移,而是對三方合並后的結果重新做一個新的快照,並自動創建一個指向它的提交對象(C6)(見圖 2-17)。這個提交對象比較特殊,它有兩個祖先(C4 和 C5)。
值得一提的是 Git 可以自己裁決哪個共同祖先才是最佳合並基礎;這和 CVS 或 Subversion(1.5 以后的版本)不同,它們需要開發者手工指定合並基礎。所以此特性讓 Git 的合並操作比其他系統都要簡單不少。
圖 2-17. Git 自動創建了一個包含了合並結果的提交對象。
既然之前的工作成果已經合並到 master
了,那么 iss53
也就沒用了。你可以就此刪除它,並在問題追蹤系統里關閉該問題。
$ git branch -d iss53
c.遇到沖突時的分支合並
有時候合並操作並不會如此順利。如果在不同的分支中都修改了同一個文件的同一部分,Git 就無法干凈地把兩者合到一起。如果你在解決問題 #53 的過程中修改了 hotfix
中修改的部分,將得到類似下面的結果:
$ git merge iss53 #Auto-merging index.html #CONFLICT (content): Merge conflict in index.html #Automatic merge failed; fix conflicts and then commit the result.
Git 作了合並,但沒有提交,它會停下來等你解決沖突。要看看哪些文件在合並時發生沖突,可以用 git status
查閱:
[master*]$ git status # index.html: needs merge # On branch master # 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) # # unmerged: index.html #
任何包含未解決沖突的文件都會以未合並(unmerged)的狀態列出。Git 會在有沖突的文件里加入標准的沖突解決標記,可以通過它們來手工定位並解決這些沖突。可以看到此文件包含類似下面這樣的部分:
<<<<<<< HEAD:index.html <div id="footer">contact : email.support@github.com</div> ======= <div id="footer"> please contact us at support@github.com </div> >>>>>>> iss53:index.html
可以看到 =======
隔開的上半部分,是 HEAD
(即 master
分支,在運行 merge
命令時所切換到的分支)中的內容,下半部分是在 iss53
分支中的內容。解決沖突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段內容替換為下面這樣來解決:
<div id="footer"> please contact us at email.support@github.com </div>
這個解決方案各采納了兩個分支中的一部分內容,而且我還刪除了 <<<<<<<
,=======
和 >>>>>>>
這些行。在解決了所有文件里的所有沖突后,運行 git add
將把它們標記為已解決狀態(實際上就是來一次快照保存到暫存區域)。因為一旦暫存,就表示沖突已經解決。
再運行一次 git status
來確認所有沖突都已解決:
$ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html #
如果覺得滿意了,並且確認所有沖突都已解決,也就是進入了暫存區,就可以用 git commit
來完成這次合並提交。提交的記錄差不多是這樣:
# Merge branch 'iss53' # Conflicts: # index.html # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. #
如果想給將來看這次合並的人一些方便,可以修改該信息,提供更多合並細節。比如你都作了哪些改動,以及這么做的原因。有時候裁決沖突的理由並不直接或明顯,有必要略加注解。
3、分支的管理
到目前為止,你已經學會了如何創建、合並和刪除分支。除此之外,我們還需要學習如何管理分支,在日后的常規工作中會經常用到下面介紹的管理命令。
git branch
命令不僅僅能創建和刪除分支,如果不加任何參數,它會給出當前所有分支的清單:
$ git branch # iss53 # * master # testing
注意看 master
分支前的 *
字符:它表示當前所在的分支。也就是說,如果現在提交更新,master
分支將隨着開發進度前移。若要查看各個分支最后一個提交對象的信息,運行 git branch -v
:
$ git branch -v #iss53 93b412c fix javascript issue #* master 7a98805 Merge branch 'iss53' #testing 782fd34 add qiangdada to the author list in the readmes
要從該清單中篩選出你已經(或尚未)與當前分支合並的分支,可以用 --merge
和 --no-merged
選項(Git 1.5.6 以上版本)。比如用 git branch --merge
查看哪些分支已被並入當前分支
$ git branch --merged # iss53 # * master
之前我們已經合並了 iss53
,所以在這里會看到它。一般來說,列表中沒有 *
的分支通常都可以用 git branch -d
來刪掉。原因很簡單,既然已經把它們所包含的工作整合到了其他分支,刪掉也不會損失什么。
另外可以用 git branch --no-merged
查看尚未合並的工作:
$ git branch --no-merged # testing
它會顯示還未合並進來的分支。由於這些分支中還包含着尚未合並進來的工作成果,所以簡單地用 git branch -d
刪除該分支會提示錯誤,因為那樣做會丟失數據:
$ git branch -d testing # error: The branch 'testing' is not an ancestor of your current HEAD. # If you are sure you want to delete it, run 'git branch -D testing'.
不過,如果你確實想要刪除該分支上的改動,可以用大寫的刪除選項 -D
強制執行,就像上面提示信息中給出的那樣。
4、利用分支進行開發的工作流程
a.長期分支
由於 Git 使用簡單的三方合並,所以就算在較長一段時間內,反復多次把某個分支合並到另一分支,也不是什么難事。也就是說,你可以同時擁有多個開放的分支,每個分支用於完成特定的任務,隨着開發的推進,你可以隨時把某個特性分支的成果並到其他分支中。
許多使用 Git 的開發者都喜歡用這種方式來開展工作,比如僅在 master
分支中保留完全穩定的代碼,即已經發布或即將發布的代碼。與此同時,他們還有一個名為 develop
或 next
的平行分支,專門用於后續的開發,或僅用於穩定性測試 — 當然並不是說一定要絕對穩定,不過一旦進入某種穩定狀態,便可以把它合並到 master
里。這樣,在確保這些已完成的特性分支(短期分支,比如之前的 iss53
分支)能夠通過所有測試,並且不會引入更多錯誤之后,就可以並到主干分支中,等待下一次的發布。
本質上我們剛才談論的,是隨着提交對象不斷右移的指針。穩定分支的指針總是在提交歷史中落后一大截,而前沿分支總是比較靠前(見圖 2-18)。
圖 2-18. 穩定分支總是比較老舊。
或者把它們想象成工作流水線,或許更好理解一些,經過測試的提交對象集合被遴選到更穩定的流水線(見圖 2-19)。
圖 2-19. 想象成流水線可能會容易點。
你可以用這招維護不同層次的穩定性。某些大項目還會有個 proposed
(建議)或 pu
(proposed updates,建議更新)分支,它包含着那些可能還沒有成熟到進入 next
或 master
的內容。這么做的目的是擁有不同層次的穩定性:當這些分支進入到更穩定的水平時,再把它們合並到更高層分支中去。再次說明下,使用多個長期分支的做法並非必需,不過一般來說,對於特大型項目或特復雜的項目,這么做確實更容易管理。
b.特性分支
在任何規模的項目中都可以使用特性(Topic)分支。一個特性分支是指一個短期的,用來實現單一特性或與其相關工作的分支。可能你在以前的版本控制系統里從未做過類似這樣的事情,因為通常創建與合並分支消耗太大。然而在 Git 中,一天之內建立、使用、合並再刪除多個分支是常見的事。
現在我們來看一個實際的例子。請看圖 2-20,由下往上,起先我們在 master
工作到 C1,然后開始一個新分支 iss91
嘗試修復 91 號缺陷,提交到 C6 的時候,又冒出一個解決該問題的新辦法,於是從之前 C4 的地方又分出一個分支 iss91v2
,干到 C8 的時候,又回到主干 master
中提交了 C9 和 C10,再回到 iss91v2
繼續工作,提交 C11,接着,又冒出個不太確定的想法,從 master
的最新提交 C10 處開了個新的分支 dumbidea
做些試驗。
圖 2-20. 擁有多個特性分支的提交歷史。
現在,假定兩件事情:我們最終決定使用第二個解決方案,即 iss91v2
中的辦法;另外,我們把 dumbidea
分支拿給同事們看了以后,發現它竟然是個天才之作。所以接下來,我們准備拋棄原來的 iss91
分支(實際上會丟棄 C5 和 C6),直接在主干中並入另外兩個分支。最終的提交歷史將變成圖 2-21 這樣:
圖 2-21. 合並了 dumbidea 和 iss91v2 后的分支歷史。
請務必牢記這些分支全部都是本地分支,這一點很重要。當你在使用分支及合並的時候,一切都是在你自己的 Git 倉庫中進行的 — 完全不涉及與服務器的交互。
5、遠程分支
遠程分支(remote branch)是對遠程倉庫中的分支的索引。它們是一些無法移動的本地分支;只有在 Git 進行網絡交互時才會更新。遠程分支就像是書簽,提醒着你上次連接遠程倉庫時上面各分支的位置。
我們用 (遠程倉庫名)/(分支名)
這樣的形式表示遠程分支。比如我們想看看上次同 origin
倉庫通訊時 master
分支的樣子,就應該查看origin/master
分支。如果你和同伴一起修復某個問題,但他們先推送了一個 iss53
分支到遠程倉庫,雖然你可能也有一個本地的 iss53
分支,但指向服務器上最新更新的卻應該是 origin/iss53
分支。
可能有點亂,我們不妨舉例說明。假設你們團隊有個地址為 git.ourcompany.com
的 Git 服務器。如果你從這里克隆,Git 會自動為你將此遠程倉庫命名為 origin
,並下載其中所有的數據,建立一個指向它的 master
分支的指針,在本地命名為 origin/master
,但你無法在本地更改其數據。接着,Git 建立一個屬於你自己的本地 master
分支,始於 origin
上 master
分支相同的位置,你可以就此開始工作(見圖 2-22):
圖 2-22. 一次 Git 克隆會建立你自己的本地分支 master 和遠程分支 origin/master,並且將它們都指向 origin
上的 master
分支。
如果你在本地 master
分支做了些改動,與此同時,其他人向 git.ourcompany.com
推送了他們的更新,那么服務器上的 master
分支就會向前推進,而於此同時,你在本地的提交歷史正朝向不同方向發展。不過只要你不和服務器通訊,你的 origin/master
指針仍然保持原位不會移動(見圖 2-23)。
圖 2-23. 在本地工作的同時有人向遠程倉庫推送內容會讓提交歷史開始分流。
可以運行 git fetch origin
來同步遠程服務器上的數據到本地。該命令首先找到 origin
是哪個服務器,從上面獲取你尚未擁有的數據,更新你本地的數據庫,然后把 origin/master
的指針移到它最新的位置上(見圖 2-24)
圖 3-24. git fetch 命令會更新 remote 索引。
為了演示擁有多個遠程分支(在不同的遠程服務器上)的項目是如何工作的,我們假設你還有另一個僅供你的敏捷開發小組使用的內部服務器 git.team1.ourcompany.com
。可以用我上面提到的 git remote add
命令把它加為當前項目的遠程分支之一。我們把它命名為 teamone
,以便代替完整的 Git URL 以方便使用(見圖 2-25)
圖 3-25. 把另一個服務器加為遠程倉庫
現在你可以用 git fetch teamone
來獲取小組服務器上你還沒有的數據了。由於當前該服務器上的內容是你 origin
服務器上的子集,Git 不會下載任何數據,而只是簡單地創建一個名為 teamone/master
的遠程分支,指向 teamone
服務器上 master
分支所在的提交對象31b8e
(見圖 2-26)。
圖 2-26. 你在本地有了一個指向 teamone 服務器上 master 分支的索引。
6、推送本地分支
要想和其他人分享某個本地分支,你需要把它推送到一個你擁有寫權限的遠程倉庫。你創建的本地分支不會因為你的寫入操作而被自動同步到你引入的遠程服務器上,你需要明確地執行推送分支的操作。換句話說,對於無意分享的分支,你盡管保留為私人分支好了,而只推送那些協同工作要用到的特性分支。
如果你有個叫 serverfix
的分支需要和他人一起開發,可以運行 git push (遠程倉庫名) (分支名)
:
$ git push origin serverfix
# Counting objects: 20, done. # Compressing objects: 100% (14/14), done. # Writing objects: 100% (15/15), 1.74 KiB, done. # Total 15 (delta 5), reused 0 (delta 0) # To git@github.com:schacon/simplegit.git # * [new branch] serverfix -> serverfix
這里Git 會自動把 serverfix
分支名擴展為 refs/heads/serverfix:refs/heads/serverfix
,意為“取出我在本地的 serverfix 分支,推送到遠程倉庫的 serverfix 分支中去”。也可以運行 git push origin serverfix:serverfix
來實現相同的效果,它的意思是“上傳我本地的 serverfix 分支到遠程倉庫中去,仍舊稱它為 serverfix 分支”。通過此語法,你可以把本地分支推送到某個命名不同的遠程分支:若想把遠程分支叫作 awesomebranch
,可以用 git push origin serverfix:awesomebranch
來推送數據。
接下來,當你的協作者再次從服務器上獲取數據時,他們將得到一個新的遠程分支 origin/serverfix
,並指向服務器上 serverfix
所指向的版本:
$ git fetch origin
# remote: Counting objects: 20, done. # remote: Compressing objects: 100% (14/14), done. # remote: Total 15 (delta 5), reused 0 (delta 0) # Unpacking objects: 100% (15/15), done. # From git@github.com:schacon/simplegit # * [new branch] serverfix -> origin/serverfix
值得注意的是,在 fetch
操作下載好新的遠程分支之后,你仍然無法在本地編輯該遠程倉庫中的分支。換句話說,在本例中,你不會有一個新的 serverfix
分支,有的只是一個你無法移動的 origin/serverfix
指針。
如果要把該遠程分支的內容合並到當前分支,可以運行 git merge origin/serverfix
。如果想要一份自己的 serverfix
來開發,可以在遠程分支的基礎上分化出一個新的分支來:
$ git checkout -b serverfix origin/serverfix # Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. # Switched to a new branch "serverfix"
這會切換到新建的 serverfix
本地分支,其內容同遠程分支 origin/serverfix
一致,這樣你就可以在里面繼續開發了。
7、跟蹤遠程分支
從遠程分支 checkout
出來的本地分支,稱為 跟蹤分支 (tracking branch)。跟蹤分支是一種和某個遠程分支有直接聯系的本地分支。在跟蹤分支里輸入 git push
,Git 會自行推斷應該向哪個服務器的哪個分支推送數據。同樣,在這些分支里運行 git pull
會獲取所有遠程索引,並把它們的數據都合並到本地分支中來。
在克隆倉庫時,Git 通常會自動創建一個名為 master
的分支來跟蹤 origin/master
。這正是 git push
和 git pull
一開始就能正常工作的原因。當然,你可以隨心所欲地設定為其它跟蹤分支,比如 origin
上除了 master
之外的其它分支。剛才我們已經看到了這樣的一個例子:git checkout -b [分支名] [遠程名]/[分支名]
。如果你有 1.6.2 以上版本的 Git,還可以用 --track
選項簡化:
$ git checkout --track origin/serverfix # Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. # Switched to a new branch "serverfix"
要為本地分支設定不同於遠程分支的名字,只需在第一個版本的命令里換個名字:
$ git checkout -b sf origin/serverfix # Branch sf set up to track remote branch refs/remotes/origin/serverfix. # Switched to a new branch "sf"
現在你的本地分支 sf
會自動將推送和抓取數據的位置定位到 origin/serverfix
了。
8、刪除遠程分支
如果不再需要某個遠程分支了,比如搞定了某個特性並把它合並進了遠程的 master
分支(或任何其他存放穩定代碼的分支),可以用這個非常無厘頭的語法來刪除它:git push [遠程名] :[分支名]
。如果想在服務器上刪除 serverfix
分支,運行下面的命令:
$ git push origin :serverfix # To git@github.com:schacon/simplegit.git # - [deleted] serverfix
咚!服務器上的分支沒了。你最好特別留心這一頁,因為你一定會用到那個命令,而且你很可能會忘掉它的語法。有種方便記憶這條命令的方法:記住我們不久前見過的 git push [遠程名] [本地分支]:[遠程分支]
語法,如果省略 [本地分支]
,那就等於是在說“在這里提取空白然后把它變成[遠程分支]
”。
9、分支的衍合
把一個分支中的修改整合到另一個分支的辦法有兩種:merge
和 rebase
。
a.基本的衍合操作
請回顧我上面講到的合並(見圖 2-27),你會看到開發進程分叉到兩個不同分支,又各自提交了更新。
圖 2-27. 最初分叉的提交歷史。
前面介紹過,最容易的整合分支的方法是 merge
命令,它會把兩個分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)進行三方合並,合並的結果是產生一個新的提交對象(C5)。如圖 2-28 所示:
圖 2-28. 通過合並一個分支來整合分叉了的歷史。
其實,還有另外一個選擇:你可以把在 C3 里產生的變化補丁在 C4 的基礎上重新打一遍。在 Git 里,這種操作叫做衍合(rebase)。有了 rebase
命令,就可以把在一個分支里提交的改變移到另一個分支里重放一遍。
在上面這個例子中,運行:
$ git checkout experiment # $ git rebase master # First, rewinding head to replay your work on top of it... # Applying: added staged command
它的原理是回到兩個分支最近的共同祖先,根據當前分支(也就是要進行衍合的分支 experiment
)后續的歷次提交對象(這里只有一個 C3),生成一系列文件補丁,然后以基底分支(也就是主干分支 master
)最后一個提交對象(C4)為新的出發點,逐個應用之前准備好的補丁文件,最后會生成一個新的合並提交對象(C3'),從而改寫 experiment
的提交歷史,使它成為 master
分支的直接下游,如圖 2-29 所示:
圖 2-29. 把 C3 里產生的改變到 C4 上重演一遍。
現在回到 master
分支,進行一次快進合並(見圖 2-30):
圖 2-30. master 分支的快進。
現在的 C3' 對應的快照,其實和普通的三方合並,即上個例子中的 C5 對應的快照內容一模一樣了。雖然最后整合得到的結果沒有任何區別,但衍合能產生一個更為整潔的提交歷史。如果視察一個衍合過的分支的歷史記錄,看起來會更清楚:仿佛所有修改都是在一根線上先后進行的,盡管實際上它們原本是同時並行發生的。
一般我們使用衍合的目的,是想要得到一個能在遠程分支上干凈應用的補丁 — 比如某些項目你不是維護者,但想幫點忙的話,最好用衍合:先在自己的一個分支里進行開發,當准備向主項目提交補丁的時候,根據最新的 origin/master
進行一次衍合操作然后再提交,這樣維護者就不需要做任何整合工作(實際上是把解決分支補丁同最新主干代碼之間沖突的責任,化轉為由提交補丁的人來解決),只需根據你提供的倉庫地址作一次快進合並,或者直接采納你提交的補丁。
請注意,合並結果中最后一次提交所指向的快照,無論是通過衍合,還是三方合並,都會得到相同的快照內容,只不過提交歷史不同罷了。衍合是按照每行的修改次序重演一遍修改,而合並是把最終結果合在一起。
b.有趣的衍合
衍合也可以放到其他分支進行,並不一定非得根據分化之前的分支。以圖 2-31 的歷史為例,我們為了給服務器端代碼添加一些功能而創建了特性分支 server
,然后提交 C3 和 C4。然后又從 C3 的地方再增加一個 client
分支來對客戶端代碼進行一些相應修改,所以提交了 C8 和 C9。最后,又回到 server
分支提交了 C10。
圖 2-31. 從一個特性分支里再分出一個特性分支的歷史。
假設在接下來的一次軟件發布中,我們決定先把客戶端的修改並到主線中,而暫緩並入服務端軟件的修改(因為還需要進一步測試)。這個時候,我們就可以把基於 server
分支而非 master
分支的改變(即 C8 和 C9),跳過 server
直接放到 master
分支中重演一遍,但這需要用 git rebase
的 --onto
選項指定新的基底分支 master
:
$ git rebase --onto master server client
這好比在說:“取出 client
分支,找出 client
分支和 server
分支的共同祖先之后的變化,然后把它們在 master
上重演一遍”。它的結果如圖 2-32 所示(譯注:雖然 client
里的 C8, C9 在 C3 之后,但這僅表明時間上的先后,而非在 C3 修改的基礎上進一步改動,因為 server
和 client
這兩個分支對應的代碼應該是兩套文件,雖然這么說不是很嚴格,但應理解為在 C3 時間點之后,對另外的文件所做的 C8,C9 修改,放到主干重演。):
圖 2-32. 將特性分支上的另一個特性分支衍合到其他分支。
現在可以快進 master
分支了(見圖 2-33):
# step 1 $ git checkout master # step 2 $ git merge client
圖 2-33. 快進 master 分支,使之包含 client 分支的變化。
現在我們決定把 server
分支的變化也包含進來。我們可以直接把 server
分支衍合到 master
,而不用手工切換到 server
分支后再執行衍合操作 — git rebase [主分支] [特性分支]
命令會先取出特性分支 server
,然后在主分支 master
上重演:
$ git rebase master server
於是,server
的進度應用到 master
的基礎上,如圖 2-34 所示:
圖 2-34. 在 master 分支上衍合 server 分支。
然后就可以快進主干分支 master
了:
# step 1 $ git checkout master # step 2 $ git merge server
現在 client
和 server
分支的變化都已經集成到主干分支來了,可以刪掉它們了。最終我們的提交歷史會變成圖 2-35 的樣子:
# step 1 $ git branch -d client # step 2 $ git branch -d server
圖 2-35. 最終的提交歷史
這里需要強調一點的是,使用衍合的時候必須遵守一條准則:
一旦分支中的提交對象發布到公共倉庫,就千萬不要對該分支進行衍合操作。
三、小結
讀到這里,首先我表示對你的耐心感到贊許,而這個時候的你應該已經學會了如何創建分支並切換到新分支,在不同分支間轉換,合並本地分支,把分支推送到共享服務器上,使用共享分支與他人協作,以及在分享之前進行衍合。
碼字不易,如果這篇文章對你有用,請點贊支持一把,你們的支持將是我寫作最大的動力!