## 第?28?章?Git 版本控制系統
**目錄**
[](ch28.html#id3132645)
[版本控制之道](ch28s02.html)
[時間機器](ch28s02.html#id3132023)
[分支控制](ch28s02.html#id3132682)
[協同工作](ch28s02.html#id3132696)
[沖突合并](ch28s02.html#id3132707)
[常見版本控制系統](ch28s02.html#id3132719)
[git 如何工作](ch28s03.html)
[補丁](ch28s03.html#id3132799)
[git 對象](ch28s03.html#id3132818)
[操作級別](ch28s03.html#id3132919)
[初始化](ch28s04.html)
[創建版本庫](ch28s04.html#id3133060)
[版本庫狀態](ch28s04.html#id3133181)
[配置](ch28s04.html#id3133399)
[版本更新](ch28s05.html)
[版本標簽](ch28s05.html#id3133858)
[時間機器](ch28s06.html)
[分支管理](ch28s07.html)
[創建分支](ch28s07.html#id3134364)
[合并分支](ch28s07.html#id3134545)
[處理沖突](ch28s07.html#id3134650)
[通過文件協作](ch28s08.html)
[通過網絡協作](ch28s09.html)
[gitweb](ch28s10.html)
## 版本控制之道
幾乎所有項目[[61](ch28s02.html#ftn.id3132658)],都要使用版本控制,它究竟有什么優勢呢?
### 時間機器
假設你使用的編輯器,不支持刪除,那你就得特別的謹小慎微,甚至是如履薄冰:因為你打錯了字沒法刪除
放松下來,目前我所接觸的所有編譯器中,還沒有變態到這種程度的。
如果編譯器提供了刪除功能,卻沒有 undo,那可能會更可怕:如果你不小心選中了全部文字,手一抖……因為不能 undo,你知道,如果此時不小心按下 delete,你就得從頭來過……你會為可能產生的后果而發抖
然而,命運總在你不想被打擾的時候來敲你可愛的門,在你手抖的剎那,你真的鬼使神差的按了下那個可怕的鍵……你的選擇只能是重新來過或者放棄……
你會比任何時候都希望時間倒流一秒鐘
還好,幾乎所有的編譯器,都會幫你回到一秒種、一分鐘、一小時……之前
假設你想回到一天之前?一覺醒來,你突然想起,有一部分內容其實應該保留下來……但是編輯器在重啟之后,就不能夠再幫你回到以前的任何時間……
這種情況下,版本控制才是你的救命稻草
### 分支控制
如果項目只能朝一個方向發展,你會時常在確定方向的問題上猶豫不決。而使用版本控制,創建一個分支各自發展,在適當的時候合并分支,是最好的解決辦法
### 協同工作
很多項目需要協同工作,版本控制能夠提供協同工作需要的環境,解決協同工作可能產生的問題
### 沖突合并
項目成員可能會對一處內容進行不同的修改,版本控制能夠反饋這些沖突,以便解決它
### 常見版本控制系統
傳統的版本控制系統,需要在服務器上搭建一個中央倉庫,所有成員都是客戶端,具有不同的權限。這種方式適應大教堂開發模式,但比較依賴網絡,且不夠靈活,使用比較廣泛的有cvs、svn等
而分布式版本控制系統,則不需要中央倉庫,所有的成員都可以完全掌控己方的版本控制系統,通過多種方式靈活的協作。這種方式適應集市開發模式,典型的代表是git
* * *
> [[61](ch28s02.html#id3132658)] 項目并不總是意味著開發一個大型軟件;你幫領導打一份文件,其實也是項目
## git 如何工作
盡管 `Linus Torvalds` 將 git 定位為:“傻瓜式的內容跟蹤工具”,但它對不熟悉版本控制的朋友來說,還是過于復雜
所以我們需要先在概念上大概了解,git 是如何工作的
### 補丁
多數版本控制系統,使用補丁來紀錄內容的改動。
當你修改了文件內容,版本控制系統會比較修改后的內容和原來的內容,并使用補丁紀錄下來。 無論是查看版本之間的變化,或者需要回溯原來內容,都需要使用補丁中的內容
### git 對象
工作樹
git將工作目錄稱為:工作樹
索引
工作樹的快照,無論是添加、刪除文件,或者對文件內容進行修改,都需要提交到索引。git只跟蹤被索引的內容
將改動提交到索引,意味著建立一個快照
版本庫
存儲工作樹的各種版本
工作樹中只保存當前內容,各種版本通過補丁的形式存儲在版本庫中
版本名稱
git可以使用“版本ID”和“版本標簽”作為版本名稱
版本ID自動生成,版本標簽用**git tag**命令指定
### 操作級別
git可以在四種級別上實現版本控制:
改動紀錄
改動了文件內容,提交到索引,但未提交到版本庫
該級別的常見操作有:**add diff**
版本紀錄
改動被提交到版本庫后,就成為一個新的版本
該級別的常見操作有:**commit log tag show reset**
其中**reset**、以及分支操作,需要在**commit**之后,**add**之前,沒有待提交改動紀錄的情況下進行
分支
分支為該主線上的系列版本
版本庫
協同工作時,需要合并項目成員的版本庫
該級別常見的操作有:**clone pull push**
## 初始化
### 創建版本庫
git 基于文件夾(工作樹)進行版本控制,在一個文件夾中創建 git版本庫:
```
$ cd project/
$ git init
Initialized empty Git repository in .git/
```
> [](ch28s04.html#git-init-1) 輸出信息:在當前文件夾的 `.git/` 目錄下創建版本庫
將文件提交到 git索引:
```
git add file1 file2 file3 ……
```
更方便的作法是將當前文件夾中的所有文件全部加入到索引中
```
git add .
```
* 可以在 `.gitignore` 文件中設置排除的文件(通常把臨時文件排除)
> 注意:git 只負責管理被索引的文件
此時,文件還沒有被提交到版本庫。向版本庫提交第一個版本:
```
git commit
git commit -m "備注"
```
> [](ch28s04.html#git-init-2) 調用系統默認編輯器編輯備注內容
### 版本庫狀態
使用 **git status** 命令查看版本庫狀態。先創建一個演示版本庫:
```
mkdir sandbox #新建一個文件夾
cd sandbox/ #進入該文件夾
git init #初始化版本庫
touch a b #新建 a b 兩個文件
git add . #將這兩個文件提交到索引
git commit -m "創建git版本庫" #將第一個版本提交到版本庫
```
這時使用 `git status` 查看版本庫狀態:
```
# On branch master
nothing to commit (working directory clean)
```
對文件進行一些操作:
```
vi a #編輯 a
rm b #刪除 b
touch c #新建 c
```
再用 `git status` 查看:
```
# On branch master #在 master 分支上
# Changes to be committed: #已提交到索引,等待提交到版本庫(其實本例中沒有這一段)
# (use "git reset HEAD <file>..." to unstage)
#
# new file: e
# modified: f
#
# Changed but not updated: #改動未提交到索引
# (use "git add/rm <file>..." to update what will be committed)
#
# **modified: a**
# **deleted: b**
#
# Untracked files: #文件未提交到索引
# (use "git add <file>..." to include in what will be committed)
#
# **c**
no changes added to commit (use "git add" and/or "git commit -a")
```
> 注意:如果只是想刪除該文件夾中的版本庫,只要刪除 `.git/` 目錄即可
```
rm -rf .git
```
### 配置
git 初始化后,會在`.git/`目錄下創建一個版本庫,其中`.git/config`為配置文件。
#### 用戶信息
為當前版本庫添加用戶信息[[62](ch28s04.html#ftn.id3133428)]:
```
[user]
name = kardinal
email = 2999am@gmail.com
```
也使用全局用戶信息,在`~/.gitconfig`中寫入上述內容,或者使用命令:
```
git config --global user.name "kardinal"
git config --global user.email 2999am@gmail.com
```
#### 語法高亮
在`~/.gitconfig`文件中添加如下語句,使用容易閱讀的彩色來輸出信息:
```
[color]
branch = auto
diff = auto
status = auto
```
或者自己定義:
```
branch.current # color of the current branch
branch.local # color of a local branch
branch.plain # color of other branches
branch.remote # color of a remote branch
diff # when to color diff output
diff.commit # color of commit headers
diff.frag # color of hunk headers
diff.meta # color of metainformation
diff.new # color of added lines
diff.old # color of removed lines
diff.plain # color of context text
diff.whitespace # color of dubious whitespace
status # when to color output of git-status
status.added # color of added, but not yet committed, files
status.changed # color of changed, but not yet added in the index, files
status.header # color of header text
status.untracked # color of files not currently being tracked
status.updated # color of updated, but not yet committed, files
```
* * *
> [[62](ch28s04.html#id3133428)] 這是必需的,請不要忽略
## 版本更新
現在創建一個 git版本庫:(參見[“初始化”一節](ch28s04.html "初始化"))
```
mkdir sandbox
cd sandbox/
git init
touch test
git add .
git commit -m "創建git版本庫"
```
**git log**查看版本紀錄:
```
commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 14:08:50 2008 +0800
創建 git版本庫
```
> [](ch28s05.html#git-use-1) 版本ID(默認自動生成)
> [](ch28s05.html#git-use-2) 提交者
> [](ch28s05.html#git-use-3) 提交日期
> [](ch28s05.html#git-use-4) 備注
現在對`test`文件作一些修改:
```
增加一行內容
```
**git diff**查看自上次提交以來發生什么改動:
```
diff --git a/test b/test
index e69de29..bae0882 100644
--- a/test
+++ b/test
@@ -0,0 +1 @@
+增加一行內容
```
> [](ch28s05.html#git-use-5) 典型的diff輸出,如果你設置了彩色輸出,這些內容會非常直觀的顯示
接下來,把這次的更新作為新的版本提交
```
git add test
git commit -m "增加了一行內容"
```
> [](ch28s05.html#git-use-6) 將本次更新提交到索引(生成快照)。此時使用**git diff**查看改動紀錄,看不到任何內容;但是仍可以使用**git diff --cached**查看緩存的改動紀錄
> [](ch28s05.html#git-use-7) 提交為新版本后,便不能使用**git diff**查看改動紀錄
> 提示:`git add`提交改動到索引,但并不提交到版本庫。如果不想頻繁的提交新版本,可以使用該命令提交改動到索引,比較和上一次提交的變化。只要不使用`git commit`提交,版本庫中不會有新的版本
使用**git log**查看版本庫紀錄
```
commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 14:28:04 2008 +0800
增加了一行內容
commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 14:08:50 2008 +0800
創建git版本庫
```
如果想查看每個版本的改動紀錄,使用**git log -p**
```
commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 14:28:04 2008 +0800
增加了一行內容
diff --git a/test b/test
index e69de29..bae0882 100644
--- a/test
+++ b/test
@@ -0,0 +1 @@
+增加一行內容
commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 14:08:50 2008 +0800
創建git版本庫
diff --git a/test b/test
new file mode 100644
index 0000000..e69de29
```
每次使用**git add**和**git commit**兩個命令提交版本更新很繁瑣,可以使用**git commit -a**提交(已索引文件的改動)
```
git commit -a -m "一次新的提交"
```
### 版本標簽
使用**git tag**為某一版本創建版本標簽:
```
git tag 1.0 d63e70
git tag 1.1 13aa16
git tag newest HEAD
```
* 版本標簽存儲在`.git/refs/tags/`目錄
使用容易記憶的版本標簽進行操作:
```
git diff 1.0 1.1
git diff 1.0 13aa16
git log 1.0
```
> [](ch28s05.html#git-use-21) 查看1.0和1.1之間的變化
> [](ch28s05.html#git-use-22) 查看1.0紀錄
## 時間機器
在`test`文件中隨意改動,然后提交
```
git commit -a -m "意外改動"
```
`git log`,增加了一條紀錄:
```
commit d9b03125921d20482937f43ea0bdbfbfb7fe1745
Author: kardinal <2999am@gmail.com>
Date: Wed Nov 5 15:18:49 2008 +0800
意外改動
```
使用**git reset**命令回溯到歷史版本:
```
git reset HEAD^
git log
git diff
```
> [](ch28s06.html#git-reset-0) `git reset`默認使用**--mixed**選項
> [](ch28s06.html#git-reset-1) `HEAD`表示當前版本,`HEAD^`表示前一個版本,`HEAD^^`表示前兩個版本,`HEAD~4`表示前四個版本;也可以使用“版本標簽”或“版本ID”來指定版本(只要前幾位就可以了)
> [](ch28s06.html#git-reset-01) 可以看到版本紀錄中最后一次提交已經取消
> [](ch28s06.html#git-reset-02) 可以看到,**--mixed**選項回溯到提交到索引之前的狀態
**git reset --soft**回溯到已提交到索引但未提交到版本庫的狀態
```
git commit -a -m "意外改動"
git reset --soft HEAD^
git log
git diff
git diff --cached
```
> [](ch28s06.html#git-reset-2) 再一次將這些改變提交
> [](ch28s06.html#git-reset-3) 使用**--soft**選項回溯到上一版本
> [](ch28s06.html#git-reset-4) 版本紀錄中已取消該版本
> [](ch28s06.html#git-reset-5) 改動紀錄中沒有任何內容
> [](ch28s06.html#git-reset-6) 改動已被提交到索引,但是未提交到版本庫,所以緩存的改動紀錄還可以查看
> 注意:`git reset` **回溯到**`git add`之前的狀態;`git reset --soft`回溯到`git add`之后的狀態
以上方法回溯到歷史版本,只是回溯版本庫和索引的紀錄,而文件的內容并不會回溯到之前的狀態,使用**git reset --hard**命令,將文件內容也一同回溯
```
git commit -a -m "意外改動"
git reset --hard HEAD^
git log
git diff --cached
cat test
```
> [](ch28s06.html#git-reset-11) ……還得提交一次,誰讓它是“意外改動”
> [](ch28s06.html#git-reset-12) 使用**--hard**選項回溯到上一版本
> [](ch28s06.html#git-reset-13) 版本紀錄中已取消該版本
> [](ch28s06.html#git-reset-14) 沒有任何改動紀錄待提交
> [](ch28s06.html#git-reset-15) 文件內容回溯到上一版本的狀態
**--hard**選項存在一定風險,因為很多情況下,你不能確定內容算不算“意外改動”。這時,可以新建一個分支,在這個分支中進行回溯,處理完成后合并兩個分支,參見[“分支管理”一節](ch28s07.html "分支管理")
## 分支管理
### 創建分支
**git branch**命令查看分支:
```
git branch
* master
```
> [](ch28s07.html#git-branch-1) 不帶選項,默認為查看分支
> [](ch28s07.html#git-branch-2) `*`表示當前分支
> [](ch28s07.html#git-branch-02) `master`為默認分支
新建分支:
```
$ git branch slave
$ git checkout slave
M slave
Switched to branch "slave"
$ git branch
master
* slave
```
> [](ch28s07.html#git-branch-3) `git branch`使用分支名稱作參數,新建分支
> [](ch28s07.html#git-branch-4) `git checkout`,切換到指定分支
> [](ch28s07.html#git-branch-5) 查看分支
> [](ch28s07.html#git-branch-6) 當前分支已變為`slave`
使用如下命令刪除分支:(先不要刪除,后面會用到)
```
git branch -D 分支名稱
```
### 合并分支
使用**git merge**合并分支:
```
編輯 test
git commit -a -m "slave分支"
git checkout master
git diff master slave
git merge slave
```
> [](ch28s07.html#git-branch-10) 增加一點內容
> [](ch28s07.html#git-branch-11) 在當前分支提交此版本
> [](ch28s07.html#git-branch-12) 切換到 master分支
> [](ch28s07.html#git-branch-91) 比較兩個分支
> [](ch28s07.html#git-branch-13) 合并 slave分支 的內容
### 處理沖突
如果沒有沖突的內容,git 會自動處理合并。如果產生沖突(同一行的內容不一致),git 會輸出如下信息:
```
Auto-merged test
CONFLICT (content): Merge conflict in test
Automatic merge failed; fix conflicts and then commit the result.
```
* `test`文件在合并時發生沖突,需要手動處理沖突,然后后再次提交
現在處理沖突,打開`test`文件,有如下內容:
```
<<<<<<< HEAD:test
這是master分支中的一行
=======
這是slave分支中的一行
>>>>>>> slave:test
```
> [](ch28s07.html#git-branch-21) 當前內容信息
> [](ch28s07.html#git-branch-22) 當前內容
> [](ch28s07.html#git-branch-23) 分隔線,分隔沖突的內容
> [](ch28s07.html#git-branch-24) slave分支內容
> [](ch28s07.html#git-branch-25) slave分支:test文件
修改這部分內容,保留正確的,然后提交
> 提示:沖突不只在合并分支時產生。無論何種沖突,處理的方法是一樣的
合并后可以刪除該分支:
```
git brancd -d slave
```
> [](ch28s07.html#git-merge-22) **-D**強行刪除分支;**-d**只有分支內容被合并后才能刪除
## 通過文件協作
git 可以通過補丁文件進行協作(使用 email 傳送補丁文件)
首先通過 **git clone** 創建一個鏡像版本庫,使用 `git branch -a`命令查看所有分支
```
$ git clone http://linuxtoy.org/path [local]
$ cd [local]
$ git branch -a
* master
origin/HEAD
origin/master
```
> [](ch28s08.html#git-net-1) 原始版本庫路徑
> [](ch28s08.html#git-net-2) 鏡像版本庫路徑。它是可選的,如果沒有指定,則使用和發起者同樣的路徑(文件夾名稱)
其中`origin` 為原始版本庫鏡像,在 master 分支上的工作,要生成對于 origin 的補丁,origin 必須與原始版本庫保持一致,不要試圖修改它
```
git fetch origin #更新 origin 分支。如果 origin 分支不是最新的原始版本庫,會產生錯誤的補丁文件
git rebase origin #將工作遷移到最新原始版本庫基礎上
git format-patch origin #生成補丁文件
```
* 使用 **git rebase** 后可能會產生沖突,手動處理
生成的補丁文件為 `0001-[備注].patch`,發起者得到補丁后,使用 **git am** 命令將這個補丁應用到版本庫
```
git checkout -b patched
git am 0001-[備注].patch
git checkout master
git diff master patched
git merge patched
```
> [](ch28s08.html#git-am-11) 為謹慎起見,創建一個名為 “patched” 的分支,切換到此分支
> [](ch28s08.html#git-am-12) 在 “patched” 分支中應用補丁
## 通過網絡協作
git 提供相當靈活的協作方式,最常見的方式為:協作者獲得原始版本庫的鏡像,并在上面工作;發起者從協作者那里獲取更新
協作者通過**git clone**創建一個鏡像版本庫:
```
git clone user@url:~/path [local]
```
網絡對于 git 來說是透明的,凡是可以訪問的位置,如 http、ftp、ssh……,甚至本地路徑,對于 git 來說沒有什么區別。
通過以下命令,創建一個本機原始版本庫`sandbox`的鏡像`project`,是允許的:
```
git clone ~/sandbox project
```
對于沒有指定協議的遠程路徑,git 默認使用 ssh
```
(ssh://)user`@`127.0.0.1`:`~/sandbox
```
使用**git pull**獲取協作者版本庫中的內容:
```
git pull user@127.0.0.1:~/sanbox master[:newest]
```
> [](ch28s09.html#git-pull-1) 版本庫
> [](ch28s09.html#git-pull-2) 分支名稱
> [](ch28s09.html#git-pull-3) 版本名稱(可選。使用版本ID、版本標簽,請不要使用“HEAD”)
> 提示:**git pull** 基于“版本”操作,也就是說,只有提交后才可以進行;這個命令會比較兩個版本的時間戳,只獲取更新的版本
當發起者進行了更新后,協作者應從發起者那里獲取最新的原始版本庫,并將當前工作遷移到最新的原始版本庫基礎上
```
git fetch origin #獲取最新原始版本庫
git rebase origin/master #將工作遷移到最新原始版本庫
```
這時發起者再次使用 **git pull** 從協作者那里獲取更新……
## gitweb
首先配置 web 服務器,使其支持 cgi,參見[“CGI”一節](ch23s03.html#server-cgi "CGI")
將 git 工作樹拷貝到 web 服務器目錄下:
```
cp -r sandbox /home/lighttpd/html/
```
gitweb 通常隨 git 一同安裝,拷貝文件到 git 工作樹
```
cp /usr/share/gitweb/* /home/lighttpd/html/sandbox
```
檢查 `/home/lighttpd/html/sandbox/gitweb.cgi` 文件中的如下語句
```
our $GIT = "/usr/bin/git";
our $projectroot = ".";
```
> [](ch28s10.html#gitweb-0) git 執行文件位置
> [](ch28s10.html#gitweb-1) 項目根目錄,也可以使用絕對路徑,如 `/home/lighttpd/html/sandbox`
修改項目描述,編輯項目根目錄下的 `.git/description` 文件
這樣就建立了一個 gitweb 站點,通過以下地址訪問:
```
http://linuxtoy.org/sandbox/gitweb.cgi
```
如果想通過 http 協議使用,例如:
```
git clone http://linuxtoy.org/sandbox/.git
```
則需要在項目根目錄下執行 **git update-server-info**
- 開源世界旅行手冊
- 授權
- 致謝
- 序言
- 更新紀錄
- 導讀
- 如何寫作科技文檔
- 部分?I.?氣候
- 第?1?章?GUI? CLI?
- 第?2?章?UNIX 縮寫風格
- 第?3?章?版本號的迷霧
- 第?4?章???Vim 還是 Emacs
- 第?5?章???DocBook 還是 TeX
- 第?6?章?完全用 Gnu/Linux 工作
- 第?7?章?病毒
- 第?8?章?磁盤 分區
- 第?9?章?文件系統
- 第?10?章???發行版介紹
- 第?11?章???編程語言
- 第?12?章?無根的根:無名師的 Unix 心傳
- 部分?II.?地理
- 第?13?章?基礎知識
- 第?14?章?命令系統
- 第?15?章?基本系統
- 第?16?章?軟件管理
- 第?17?章?核心工具集
- 第?18?章?編譯工具鏈
- 第?19?章?圖形界面
- 第?20?章?國際化
- 第?21?章???內核
- 第?22?章?Grub
- 第?23?章?服務器
- 第?24?章?Vim 編輯器
- 第?25?章?Emacs 入門
- 第?26?章?正則表達式
- 第?27?章?docbook 指南
- 第?28?章?Git 版本控制系統
- 第?29?章?ConTeXt 入門指南
- 部分?III.?景觀
- 第?30?章?終極 Shell -- ZSH
- 第?31?章?完美工作站 Archlinux
- 第?32?章?組織你的意念:Emacs org mode
- 第?33?章???Zsh+screen
- 第?34?章???gentoo stage3
- 第?35?章???硬件問題
- 第?36?章???網絡設置
- 第?37?章???自制 LiveCD
- 第?38?章?awesome
- 第?39?章?openbox 工作環境
- 第?40?章???Emacs muse
- 第?41?章???寫作工具鏈
- 第?42?章?使用 lftp
- 第?43?章???Firefox 使用技巧
- 第?44?章???FVWM
- 部分?IV.?地質
- 第?45?章?Unix
- 第?46?章???Gnu
- 第?47?章?軟件業自由之神——Richard Stallman
- 第?48?章?Linux
- 第?49?章?GNOME與KDE的戰爭
- 第?50?章???Vim Emacs
- 第?51?章???年代紀
- 第?52?章?我的選擇
- 第?53?章???補遺