git 之五分鐘教程

NO IMAGE

 原貼:http://blog.chinaunix.net/u/24474/showart_257249.html

git 之五分鐘教程

from: http://dieken-qfz.spaces.live.com/blog/cns!586d665c0deb512d!406.entry

git 是一個分散式版本管理工具,關於它大家應該都有所耳聞了,
貌似 Linus 說是 2005 年 4 月 17 日公開的,如今已經 1.4.3.4
了,開發社群非常活躍,一如 Linux (Linus 英明神武,sigh),
git 現在的維護者是 Junio C Hamano,主頁在 http://git.or.cz

關於分散式版本管理工具的定義,我土,感受最深的是每個人都有
自己的程式碼庫,這個跟 SVN 不同,這個區別帶來的好處是跟蹤本地
的修改過程非常方便,SVN 裡頭不同程式碼庫之間不能 switch,麻煩。
但是 SVN 的使用者介面實在是比 git 友好得多,特別是 SVN 建立
在大家普遍比較熟悉的 CVS 模型上。

閒話少說,切入正題。

* git 的四種物件

在 git 中最基本的四種物件為 blob,tree,commit,tag。
  blob   – 即檔案,注意只包含內容,沒有名字,許可權等屬性(例外的是
           包含大小)
  tree   – 所有檔名字以及其屬性的列表,這些屬性只是基本屬性,比如
           許可權,是否符號連結。git 沒有像 svn 那樣豐富的 property 用。
  commit – 表示修改歷史,描述一個個 tree 之間如何聯絡起來的,每一個
           commit 對應有一個 tree —— commit 的修改結果。commit 包含
           作者、提交者、parent commit、tree、log、修改時間、提交時間,
           注意 git 明確區分了 author 和 committer 這兩個角色。
  tag    – 標籤,它可以指向 blob, tree, commit 幷包含簽名,最常見的是
           指向 commit 的 GPG 簽名的標籤。

blob,tree,commit 都是用其儲存內容的 SHA-1 值命名的(不是簡單的對
整個檔案取 SHA-1 值),tag 自然使用的是普通名字。

git 的想法就是 blob 和 tree 中包含的只能是最公共的最基本的資訊,
所以它不會在 blob 和 tree 裡頭存放 svn 那麼多的自定義屬性。

git 也只是定位在 content tracker,這個術語跟 VCS 是有微妙的差別的,
第一是體現在它對 blob 和 tree 的“純資料”的要求上,第二是對分支
合併的處理上,如下所示(a, b, c 表示三個 commit):

   — a —-> master
       /
        b —– c -> test

test 分支從 master 的 a 點上分出來,做了許多修改,然後當 master
分支和 test 分支合併會怎麼樣呢?按照通常的 CVS/SVN 的做法,是建立
一個新的 commit,繼承自 a 和 c,但是 git 則是直接修改 master
使其指向 c,在 git 這稱為 fast forward,整個版本圖變成這樣:

   — a — b — c –> master
                 /
                  `–> test

從圖中看 master 分支和 test 分支分不出主次,這裡頭的含義很值得玩味。

* 選擇物件

blob, tree, commit 都是用其 SHA-1 命名的,如果得到一個 SHA-1
值,但不知道是什麼型別,可以用
        git cat-file -t SHA-1
命令識別,它會輸出 blob, tree, commit (如果給它一個 tag 名字,
它也能輸出 tag) 這樣的字串,然後就可以用
        git cat-file type SHA-1
檢視其內容了,這裡 type 是 blob, tree, commit, tag 中的一個。
注意對於 tree 物件它輸出的是 tree 物件儲存時的樣子,看起來像是
亂碼,所以對於 tree 要用
        git ls-tree SHA-1

另外 git 也可以在需要 tree 物件的地方給它一個 commit 物件
或者一個 tag 物件,這時就是用 commit 或 tag 指向的 tree,
在 git 的手冊中可以看到 tree-ish 這樣的寫法,意思就是可以
從 tree-ish 這個引數得到一個 tree,同樣的,也有 commit-ish.
比如
    git-ls-tree [-d] [-r] [-t] [-z] [–name-only]
        [–name-status] [–full-name] [–abbrev=[<n>]]
        <tree-ish> [paths…]

總結一下,可以用 SHA-1 值指定是哪一個 blob/tree/commit 物件(無需
指出型別),可以用 commit 或者 tag 指定一個 tree,可以用 tag 指定
一個 commit(一般 tag 都是用來指向 commit,雖然它可以指向其它型別
物件)。

在 git 中很頻繁提到 ref 或者 reference,它就是一個名字,包含了
一個 commit。分支名字,tag 其實都是 reference,這可以從它們都
位於 .git/refs 下看出來,區別在於分支名字(就是指代各個分支的
HEAD)指向的目標可以改變,而 tag 不能。

指代一個 tree 中的某個檔案可以用
     tree-ish:path/to/file
的格式,注意 path 前面沒有“/”。

* commit 記法

commit 因為有繼承關係(ancestry),它的選擇方式更特殊些,比如怎麼
選擇它的 parent commit,以及 parent commit 的 parent commit,
雖然這個資訊可以逐步通過 git cat-file commit commit-ish 獲得,
但顯然需要一個簡便的記法來指定,類似於 SVN 的 HEAD、PREV、COMMITTED。

這種 git 獨有的記法在 git-rev-parse(1) 有詳細描述,簡要的說,
commit-ish^n    表示 commit 的第 n 個 直接 parent commit,n 從 1 算起。
                只有在一個 commit 是合併的結果時這個 commit 才可能有
                commit^2, commit^3。
                當 n 等於 1 時可以省略,只寫 ^。
                當 n 等於 0 時就指 commit 本身,這可以用來將一個 tag
                轉成 commit。

這種 ^n 記法可以連續寫,比如 HEAD^2^2 表示 HEAD 的第二個 parent commit
的第二個 parent commit。

commit-ish~n    這種記法是 commit-ish^^^…^ 總共 n 個 ^ 的簡寫,這樣在
                直線歷史上能夠方便的說倒數第 (n 1) 次提交(commit-ish^1
                是倒數第二次)。
                當 n 等於 1 時自然也是可以不寫 1 的。

在接收 commit 集合的命令中,比如 git log,commit 的記法有特殊含義:
git log HEAD   
                表示按照 parent 關係從 HEAD 能夠到達的所有 commit,
                包含 HEAD。

git log ^HEAD~2 HEAD
                表示從 HEAD  能夠到達的所有 commit(包含 HEAD),排除
                能從 HEAD~2 到達的所有 commit(包含 HEAD~2),這樣計算
                的結果就是 HEAD~1 和 HEAD.

這樣的集合記法可以寫多個,比如
git log commit-1 commit-2 ^commit-3 ^commit-4

特殊的,commitA..commitB 是 ^commitA commitB 的簡寫。

這種集合記法在比較兩個分支的區別時很有效,比如想看 test 分支從 master
分支分出來後做了什麼修改,可以用
git log ^master test
這裡不需要從分支點排除,因為按照這種集合相減關係,得到的就是 test 自
master 分出來後所建立的所有 commit。

由於 git 的 fast forward 形式的合併,HEAD^1 並不總是合併前所在的
工作分支,這一點是很容易迷糊人的,如下圖(a,b,c 代表 commit):

   — a —> master
        /
         b —– c —> test

現在 master 從 test 上合併,按照 git 的做法,因為 a 是 c 祖先,而且
master 分支上自 a 之後沒有新的修改,所以 git 直接把 master 指向 c,
而不建立新的 commit:

   — a — b —– c —->master, test

現在 master 和 test 指向同一個 commit,master^1 是 b,而 b 並不在
*原先的* master 分支上。合併後,master 和 test 的歷史是一致的,沒有
區別,這個 CVS 和 SVN 的做法是不同的,按照 CVS 或者 SVN 的做法,
合併後 show log master 看到的應該是 a 和 d(d 的 parent commit 是 a 和 c),
但是 git log master 看到的是 a b c。

git 的這個 fast forward 形式的 merge 很有意思。

* .git 和 index
CVS/SVN 中,程式碼庫和工作拷貝都是分開的,工作拷貝的版本資訊放在每一層
目錄中,這給在工作拷貝中 grep 程式碼不便。git 將程式碼庫和工作拷貝的版本
資訊統一放在工作拷貝的頂層目錄下的 .git/ 中(可以配置使用其它目錄)。

.git/
  ├─branches
  ├─hooks
  ├─info
  ├─objects
  │  ├─info
  │  └─pack
  ├─refs
  │  ├─heads
  │  └─tags
  └─remotes

objects 下面是存放 blob, tree, commit 物件的地方,取 SHA-1的十六進位制
前兩個字元做目錄名,餘下的做檔名, 注意一個物件的 SHA-1 值不是直接
對這裡面的檔案計算 SHA-1 值得到的。

用 SHA-1 引用物件時,一般取其前六個字元,只要能夠唯一識別一個物件即可,

.git 下可以存在 config 檔案,這個是一個庫的配置檔案,使用者可以在自己
的 HOME 目錄下有 .gitconfig 檔案,兩者格式是一樣的,參考 git-repo-config(1)。

.git 下可能還存在一個 logs 目錄,由於 git 的 fast forward 形式的合併
做法,導致不能通過 git log 來獲取一個分支的頭曾經指向哪些 commit,這個
logs 目錄下的檔案就是用來記錄這個資訊的,要想使用它需要在 .git/config
或者 ~/.gitconfig 裡頭設定選項。這些 log 就稱為 reflog,叫 ref 是因為
分支名和標籤都是 commit 的引用,都放在 .git/refs 目錄下。

.git 下還有 index 檔案(對於剛建立的空庫是沒有的),這個檔案就起到 CVS
的 .cvs 目錄或者 SVN 的 .svn 目錄的作用,它記錄了工作拷貝在哪個 commit
上,以及工作拷貝中被 git 管理的檔案的狀態。在 SVN 中,.svn 裡頭儲存了
一份程式碼,稱為 BASE,index 裡頭跟它作用類似,不過它只是記錄了檔名
和其 SHA-1 值、mtime、ctime、所在的裝置號、許可權位、uid、gid、size,
真正的檔案內容都在 .git/objects 裡頭。

index 跟 tree 很類似,除了包含一些資訊用於跟蹤工作拷貝中的檔案狀態,
粗略的講,使用 git 最常打交道的有三個 tree:HEAD 對應的 tree,index
對應的 tree,工作拷貝中的目錄樹。

git 和 svn 對於處理 index 和 BASE 版本的方式不大一樣,SVN 是以工作拷貝為
中心,BASE 在提交前是不會變的,git 則以 index 為中心,在提交前 index 是
可以變的,這一點也很迷糊人,比如,如果你在工作拷貝中修改了檔案,用 git
commit 卻告訴你沒有修改,因為 git commit 是把 index 代表的狀態提交到庫裡
頭,只有你用git-update-index your_file 將工作目錄中的修改反映到 index 裡,
git commit 才會跟直覺的提交行為一致,不過檔案被提交到庫裡的時機是在呼叫
git-update-index 的時候,而非 git commit 的時候,git commit只是依照
index 的狀態建立一個 tree 放入 .git/ojbects 裡頭。可以用 git commit -a
達到 svn commit 的直觀效果。

index 也被稱為 cache,這是早期的叫法。

一份 .gitconfig:
[core]
        fileMode = false
        logAllRefUpdates = true
    compression = 9

[diff]
    color = auto

[pack]
    window = 64

[user]
    email = your_email
    name = your_name

[merge]
        summary = true

* 入門操作
git 的命令分成兩類,低階命令(稱為“plumbing”)和高階命令(稱為“porcelain”),
不算開頭的 git 字首,低階命令的名字 *一般* 是兩個單詞的,高階命令則 *一般* 是
一個單詞的。

低階命令都是一些 C 寫的小程式,直接操作 git 核心的物件如 tree,commit 以
及 index 等,這些是提供給編寫 git 包裝程式比如 cogito 的人使用的,高階命
令是面向終端使用者的,這些命令要麼是對 git 命令的包裝,要麼是提供一些方便的
功能比如統計 log。

一個 git 命令可以寫成 “git-cmd” 或者 “git cmd”,前者在 PATH 中尋找 “git-cmd”
這個命令並執行之,後者則是執行 git 命令,傳入後面的 cmd 等引數,git 這個
程式在 exec-path 中尋找 git-cmd 並執行之,這個 exec-path 可以通過命令列
選項 –exec-path=XXX 或者 GIT_EXEC_PATH 環境變數設定,在安裝 git 時它內部
也會儲存一個預設的 exec-path。這個路徑列表的語法跟 PATH 一致。git 的這種
exec-path 機制能夠讓它很方便的加入更多的命令。

獲取一個 git 命令的幫助可以用 man git-cmd 或者 git –help cmd 或者 git help cmd
或者 git cmd –help,這四種命令效果跟第一條一樣。

(a)
git init-db
在當前目錄下建立 git 庫(.git)

(b)
echo hello > xxx
git add xxx
遞迴將 xxx 加入 git 控制中,執行完後檔案已經被加入庫中,index 也已更新,
這時 git diff 因為 index 跟 工作拷貝一致,所以沒有差別。
// 其實就是用的 git-update-index,這個命令在更新 index 時也會立即把
// 檔案寫入庫中,這點跟 SVN 的 add 不一樣。

(c)
git commit
將 index 例項化為一個 tree,建立一個 commit,這個時候看.git/objects 下面
就有三個物件:blob xxx,一個 tree 和一個commit。

(d)
echo world >> xxx
git diff
比較 index 和 工作拷貝
git diff –cached
比較 index 和前一次提交對應的 tree,這就是 git commit 要提交的東西。
git diff HEAD
比較工作拷貝和前一次提交對應的 tree,這就是 git commit -a 要提交的東西。

(e)
git commit
因為 index 沒有改變,而 commit 只是從 index 建立 tree,所以沒有需要提交
的,提示用 git-update-index.
git update-index xxx
git commit
更新了 index,這下可以提交了, 如果是想提交 index 中所有修改過和刪除了的
檔案,這兩步可以用 git commit -a(這個命令類似於在頂層目錄執行 svn commit)。

(f)
git log -p
檢視 log,-p 表示顯示每一次修改的 diff。

(g)
git branch
顯示所有分支,當前分支前面有一個 *,預設分支叫 master。
git branch test
建立一個分支 test
git checkout test
切換到 test 分支,這兩步可以合併為 git checkout -b test。

(h)
在 test 分支做了有些修改後提交;
git checkout master
回到主分支
git log master..test
檢視 test 分支上修改記錄(參考 “commit 記法” 一節)。

git merge –no-commit “msg” HEAD test
這個命令的引數語法比較怪異。–no-commit 是讓 git 不要馬上
提交,這樣可以檢查一下合併結果是否正確,由於這段時間主幹上
沒有修改,所以這個合併其實就是 fast forward,直接將
master 指向 test 分支的 HEAD,不會建立新的 commit。
// git pull 的行為有點古怪,不推薦使用。

如果不是 fast forward,可以用 git diff 檢查一下然後再
git commit;如果合併後有衝突,git 會把衝突標記寫入檔案,
這個時候開啟這個檔案編輯之,解決衝突後用 git-update-index
更新 index,然後 git commit。

(i)
gitk –all
檢視版本圖。

(j)
git branch -D test
刪除 test 分支。

(k)
git clone git://git.kernel.org/pub/scm/git/git.git
把 git 的開發庫映象到本地的 git 目錄中。

(l)
cd git && git fetch
與 git 的開發庫同步。不建議用 git pull,可以用 git fetch
和 git merge 的組合代替 git pull。

(m)
git status
檢視工作拷貝中的修改情況

(n)
git show commit-ish
檢視一個 commit。

(o)
git reset commit-ish
將當前 HEAD 指向 commit-ish 代表的 commit。
這個命令有三個選項:
–mixed     預設選項,調整 HEAD 並重置 index,保留工作拷貝中的修改,
            修改過的檔案需要再次 git-update-index 以更新 index。
–soft      僅修改 HEAD。
–hard      修改 HEAD,並將 index 和 工作拷貝的狀態對應到 commit-ish
            相應的 tree 上,這會丟失工作拷貝中的修改!

在 git-reset(1) 中有一些這個命令的使用場合說明,可以看看,常用的可能
是第一條:
git reset –soft HEAD^
撤銷最近一次提交。

// 類似於 svn revert 的是 git checkout。

(p)
git revert commit-ish
撤銷庫裡的某次提交,注意跟 svn revert 撤銷工作拷貝的修改不一樣。
如果是撤銷最近的提交,最好用 git reset。

(q)
有點點類似於 svn info 的命令是:
cat .git/remotes/origin

還有一個 git-push(1), 比較的複雜,以及其它的許多許多命令,
比如格式化 patch,將 patch 通過郵件傳送,從 mbox 中提取
patch,等等,這篇小文就不羅嗦了(我也不大會-_-b),可以參考手冊。

* 參考資料
http://www.kernel.org/pub/software/scm/git/docs/
    這個上面的 tutorial 和 Everyday Git 指向兩篇很好的教程,此頁的
FURTHER DOCUMENTATION 中提到的 Discussion 以及 Core tutorial 對了解
git 的內部機制很有幫助。

http://git.or.cz/gitwiki/GitLinks
    很多 git 相關資料的連結。

http://linux.yyz.us/git-howto.html
    Kernel Hackers’ Guide to git, 比較老的一篇教程了。

http://www.gelato.unsw.edu.au/archives/git/0512/13748.html
http://marc.theaimsgroup.com/?l=git&m=113402372012587&w=4
    git for the confused, 雖然有點老了,仍然強烈推薦。