講git
的文章不少,可是大部分都是一個套路,講概念,講命令,太多的概念和命令老是讓人有一種稀裏糊塗的感受,講的很對,但彷佛沒能講透,沒有醍醐灌頂的感受,大概是個人悟性差吧。因此這幾天一直在作各類git
的實驗,而且閱讀了一些博客、文檔、資料,綜合下來整理出了這麼一篇文章。注意:html
git
的理解,只是一家之言,聊以分享。git
使用經驗和踩坑經驗。commitID
只保留4位這是一篇比較亂七八糟的文章,不從傳統出發,嘗試用本身的思想去理解git
這一神奇的工具。之前我以爲git
是命運石之門
,咱們在不一樣的時間線(分支)上跳躍,全部的事件都必須且只能發生在時間線上。可是如今我以爲git
是無限的可能性的集合
,一個事件能夠引伸出無限的可能性。而咱們要作的是用工具(branch、tag、reset、rebase、merge....)來組織這些可能性,從而構成一個有序的、向前發展的歷史,引導整個歷史的發展,構建能夠走向將來的工程。git
存檔github
其實吧,版本就是存檔,就是遊戲中的存檔,咱們不斷的推動項目,完成一個又一個任務,而後中途不斷的存檔,就造成了版本迭代。而版本多了,天然就須要管理了,天然就有了版本管理系統。shell
在遊戲中,存檔能夠手動存檔,也能夠到指定點存檔,也能夠自動定場景存檔。在遊戲中,存檔以後造成的東西叫作檔案,而在git
中,叫作commit
。咱們可使用git add
+git commit
完成一個檔案的建立,或者說版本的建立。vim
一個commit
擁有許多的屬性,好比ID
、Message
、Date
、Author:
等等,這些信息都有助於咱們瞭解這個版本,就像遊戲中的存檔會以關卡名/圖片之類的信息來提示你這個存檔所表明的進度,好比使用git log
能夠獲取如下信息:bash
commit 4963 (HEAD -> master)
Author: **********
Date: Thu Jan 10 15:22:12 2019 +0800
版本H
commit 1a42
Author: **********
Date: Thu Jan 10 15:25:01 2019 +0800
版本G
commit 931b
Author: **********
Date: Thu Jan 10 15:24:50 2019 +0800
版本F
....
複製代碼
讀檔工具
既然有存檔,那就有讀檔。遊戲中直接選擇一個檔案就好了,那git
中呢?若是有可視化操做工具,那咱們直接點擊也是能夠的,但如今不使用工具,而使用命令行,該如何呢。讀取一個存檔說白了在git
中就是讀取一個commit
而已,因此咱們可使用git checkout
和git reset
兩個命令來作到,那如何指定版本呢?前面提到的commit
屬性中的ID
能夠幫咱們實現這個目標。ui
git commit
了8個,每一個commit
都添加了一個從a-h
的文件,並在commit
信息中添加版本標記
git checkout
切到版本A,能夠發現,此時只有文件a$ git checkout 401e
Note: checking out '401e'.
...
HEAD is now at 401e1b6 版本A
$ ls
README.md a.txt
複製代碼
git reset
切換到版本G,能夠發現,此時有了a-g
幾個文件了$ git reset 1a42
Unstaged changes after reset:
D b.txt
D c.txt
D d.txt
D e.txt
D f.txt
D g.txt
$ git stash
Saved working directory and index state WIP on (no branch): 1a4268d 版本G
l$ ls
README.md a.txt b.txt c.txt d.txt e.txt f.txt g.txt
複製代碼
總結: 咱們經過commit
的ID
屬性配合其餘命令來達到了任意讀檔的目的,能夠在各類版本中隨意穿梭,快樂的很啊。而讀檔的姿式其實還有不少,但不外乎是對commit
操做方式的不一樣,在git
中,我以爲commit 纔是構成整個版本管理的基本栗子。每個commit
都是獨立的個體,雖然和其餘commit
存在着關聯,可是依舊是獨立的,而咱們在commit
構成節點序列中來回移動和操做,就能夠達到全部對版本管理的目的。this
在上一個章節中,咱們已經能夠依靠一些命令和commit ID
作到在版本中自由的穿梭,可是卻帶來一個問題,那就是commit ID
的記憶難度。commit ID
是hash
值,儘管git
支持只提供前幾位就能匹配到hash
,而且也提供了commit message
來講明commit
,可是依舊存在commit
的辨識和記憶問題。而這個問題,能夠經過別名系統
來解決。spa
所謂的別名系統
,實際上是我本身概括的概念,指的其實就是HEAD
、branch
、tag
這三個東西。在我看來,這三個東西都是同樣的東西,都是別名,也就是標記一個commit
的東西而已,只是在行爲表現上有一些區別。
HEAD
一個倉庫只有一個HEAD
,指向你當前所在的commit
。若是你建立了一個commit
,HEAD
將會指向這個新的commit
。也能夠經過命令,強制HEAD
指向某個commit
,好比reset
、checkout
。也就是不論你在哪一個commit
之上,那麼HEAD
就在哪兒,或者說,其實你能在哪一個commit
,是經過修改HEAD
指向的commit
實現的。
經過修改HEAD
在各個版本之間旋轉跳躍
branch
一開始我以爲這個東西纔是git
的核心,由於建立項目的時候,咱們就處於master
分支之上,而且咱們在工做中,每每也是基於分支工做的。可是後來發現,分支在本質上毫無心義,並不須要真的基於branch
去工做,基於commit
就好了。而branch
只是提供了一個方式來管理這些commit。branch
和HEAD
相同點是隨着新的commit
的建立,branch
指向的commit
會不斷更新,固然前提是你須要在這個branch
所在的commit
上建立新的commit
。而branch
和HEAD
的不一樣點在於HEAD
只能有一個,branch
能夠有多個。
實驗一:用branch
來實現切換版本
$ git log --pretty=oneline
1a42 (HEAD) 版本G
931b 版本F
071d 版本E
0caa 版本D
7855 版本C
1295 版本B
401e 版本A
複製代碼
A-G
分別建立一個分支$ git checkout 1a42 -b G
Switched to a new branch 'G'
$ git checkout 931b -b F
Switched to a new branch 'F'
$ git checkout 071d -b E
Switched to a new branch 'E'
$ git checkout 0caa -b D
Switched to a new branch 'D'
$ git checkout 7855 -b C
Switched to a new branch 'C'
$ git checkout 1295 -b B
Switched to a new branch 'B'
$ git checkout 401e -b A
Switched to a new branch 'A'
$ git log --pretty=oneline
1a42 (HEAD -> G) 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
commit ID
$ git checkout A
Switched to branch 'A'
$ git checkout B
Switched to branch 'B'
$ git checkout C
Switched to branch 'C'
$ git checkout E
Switched to branch 'E'
$ git checkout F
Switched to branch 'F'
$ git checkout G
Switched to branch 'G'
複製代碼
實驗二:分支跟隨新的commit
HEAD -> G
表示HEAD
指向了branch G
,而branch G
指向了版本G
$ git log --pretty=oneline
1a42 (HEAD -> G) 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
commit
$ echo 'h'> h.txt
$ git add h.txt
$ git commit -m '版本H'
[G d346d27] 版本H
1 file changed, 1 insertion(+)
create mode 100644 h.txt
複製代碼
log
,能夠看到HEAD
和G
都指向了版本H
,就是所謂的branch
跟着commit
動,可是它真的是跟着commit
動嗎?$ git log --pretty=oneline
d346 (HEAD -> G) 版本H
1a42 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
實驗三:分支跟着啥動
HEAD
指向版本G
的commit
,而不是分支G
,也就是使用git checkout commitID
,而不是使用git checkout branchName
,能夠看到,此時HEAD
不指向G
,而是HEAD
和G
同時指向了版本H
的commit
。$ git checkout d346 # 版本 H 的 commitID
$ git log --pretty=oneline
d346 (HEAD, G) 版本H
1a42 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
commit
,能夠看到,這個時候分支G
再也不跟着commit
移動了,因此,只有在HEAD
指向branch
的時候,branch
纔會向前移動,也就是隻要HEAD
來到branch
身邊,branch
就會跟着HEAD
跑。$ echo 'i'> i.txt
$ git add i.txt
$ git commit -m "版本I"
[detached HEAD 2e836eb] 版本I
1 file changed, 1 insertion(+)
create mode 100644 i.txt
$ git log --pretty=oneline
2e83 (HEAD) 版本I
d346 (G) 版本H
1a42 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
tag
tag
是比較特殊的一個別名類型,他沒法移動,或者說不推薦移動。一旦一個tag
和指向某個coimmit
,就不但願它移動,由於tag
就是用來標記這個commit
的,他是一個孤獨而忠誠的守望者,而不像branch
,花間游龍似的浪子。
$ git log --pretty=oneline
1a42 (HEAD, G) 版本G
931b (F) 版本F
071d (E) 版本E
0caa (D) 版本D
7855 (C) 版本C
1295 (B) 版本B
401e (A) 版本A
複製代碼
tag
,爲了區別分支名,通通加了個T
$ git tag TA A
$ git tag TB B
$ git tag TC C
$ git tag TD D
$ git tag TE E
$ git tag TF F
$ git tag TG G
$ git log --pretty=oneline
1a42 (HEAD, tag: G, G) 版本G
931b (tag: TF, F) 版本F
071d (tag: TE, E) 版本E
0caa (tag: TD, D) 版本D
7855 (tag: TC, C) 版本C
1295 (tag: TB, B) 版本B
401e (tag: TA, A) 版本A
複製代碼
$ git checkout TA
Previous HEAD position was 1a4268d 版本G
HEAD is now at 401e1b6 版本A
$ git checkout TB
Previous HEAD position was 401e1b6 版本A
HEAD is now at 1295260 版本B
$ git checkout TC
Previous HEAD position was 1295260 版本B
HEAD is now at 7855905 版本C
$ git checkout TD
Previous HEAD position was 7855905 版本C
HEAD is now at 0caa2b7 版本D
$ git checkout TE
Previous HEAD position was 0caa2b7 版本D
HEAD is now at 071d00a 版本E
$ git checkout TF
Previous HEAD position was 071d00a 版本E
HEAD is now at 931b3c9 版本F
$ git checkout TG
Previous HEAD position was 931b3c9 版本F
HEAD is now at 1a4268d 版本G
複製代碼
HEAD
、tag
、branch
,都是一種別名,除了行爲表現上的差異,沒有太大的不一樣,特別是branch
和tag
,不過都只是提供了一種管理commit
的方式。在上一章節中,咱們揭開了別名系統的紅蓋頭,這一章,咱們就開始探索一下分叉的神祕。
和遊戲中的存檔同樣,有時候一個遊戲有許多的選擇,這些選擇指向了不一樣的結果。而做爲遊戲玩家,咱們但願可以走完全部的選擇,以探索更多的遊戲樂趣。因此咱們會在作選擇的時候存檔,而當咱們走完一個選擇,就會讀取這個存檔,繼續往另外一個選擇探索。這個時候,就產生了兩個不一樣的劇情走向,這就是分叉。
在git
中,其實咱們能夠有無數的選擇,每個commit
能夠建立無數的commit
,就會引伸出無數的可能。
版本X
吧$ git log --pretty=oneline
2cae (HEAD) 版本X
....
複製代碼
Y
,而且沿着Y1
一直走到Y3
,這是盡頭$ git log --pretty=oneline
d2e0 (HEAD) 版本Y3
4ca8 版本Y2
fcff 版本Y1
2cae 版本X
...
複製代碼
X
,並選擇另外一個選擇Z
,從Z1
走到Z3
$ git checkout 2cae # 切到`版本X`
$ git log --pretty=oneline
16ff (HEAD) 版本Z3
0ca5 版本Z2
b4a7 版本Z1
2cae 版本X
...
複製代碼
能夠看到,咱們順着兩個選擇一直往下發展,在這發展的過程當中,咱們徹底沒有用到tag
和branch
,也就是爲了印證commit 是構成 git 世界的基本栗子這一說明。
從git log
中,咱們看不到了Y
走向,那Y
真的消失了嗎?不是的,咱們依舊能夠經過Y
的commit ID
來尋回Y
的記錄。固然爲了方便在YZ
中切換,咱們可使用branch
來標記一下YZ
兩個走向,這樣就造成了YZ
兩個branch
了,也就是分叉!
那那些沒有被branch
或者tag
標記的commit
呢?他們會消失嗎?會,也不會。不會是由於不被標記的commit
將變成dangling commit
,我稱之爲遊離的commit
,是git
中最孤獨的存在,只要咱們知道commitID
,就會可喚回它。可是很大的多是咱們永遠不會記得這麼一個不被引用的commit
,因此我呼籲,善待每個commit
。會是由於仍是可能會被回收的,看這裏,git 也有 gc。
和遊戲的存檔不一樣的是,git
中的版本能夠合併,也就是說我能夠在分支Y
中作完任務Y1
、Y2
、Y3
,而後分支Z
中完成任務Z1
、Z2
、Z3
,而後合併這兩個分支,結果回到了X
,可是卻完成了Y1-y3
、Z1-Z3
,並拿到了神器Y
和Z
,這讓boss
怎麼活?
實驗一:使用merge
合併commit
版本O
$ echo O >> o.txt
$ git add o.txt
$ git commit -m '版本O'
[detached HEAD 478fa6d] 版本O
1 file changed, 1 insertion(+)
create mode 100644 o.txt
複製代碼
版本O
建立版本P1
$ echo P >>p1.txt
$ git add p1.txt
$ git commit -m '版本P1'
[detached HEAD a3ab178] 版本P1
1 file changed, 1 insertion(+)
create mode 100644 p1.txt
$ git log --pretty=oneline
a3ab (HEAD) 版本P1
478f 版本O
複製代碼
版本O
建立版本P2
$ git checkout 478f # 版本O 的 commitID
$ echo p2 >> p2.txt
$ git add p2.txt
$ git commit -m '版本P2'
[detached HEAD cbccf52] 版本P2
1 file changed, 1 insertion(+)
create mode 100644 p2.txt
$ git log --pretty=oneline
cbcc (HEAD) 版本P2
478f 版本O
複製代碼
版本P1
到版本P2
$ git merge a3ab # 版本P1 的 commitID
$ git log --pretty=oneline
656a (HEAD) Merge commit 'a3ab' into HEAD
cbcc 版本P2
a3ab 版本P1
478f 版本O
複製代碼
實驗三:使用rebase
合併
切換到版本P2
,在版本P2
中使用rebase
$ git checkout cbcc # 版本P2 的 commitID
....
HEAD is now at cbccf52 版本P2
$ git rebase a3ab # 版本P1 的 commitID
First, rewinding head to replay your work on top of it...
Applying: 版本P2
$ git log --pretty=oneline
3bd7 (HEAD) 版本P2
a3ab 版本P1
478f 版本O
複製代碼
實驗四:使用cherry-pick
合併
版本O
,新建版本P3
$ echo 'p3'>> p3.txt
$ git add p3.txt
$ git commig -m '版本P3'
git: 'commig' is not a git command. See 'git --help'.
The most similar command is
commit
$ git commit -m '版本P3'
[detached HEAD ae09e94] 版本P3
1 file changed, 1 insertion(+)
create mode 100644 p3.txt
$ git log --pretty=oneline
ae09 (HEAD) 版本P3
478f 版本O
複製代碼
版本P2
中使用cherry-pick
合併版本P3
的東西$ git checkout 3bd7 # 版本P2 的commitID
...
HEAD is now at 3bd7820 版本P2
$ git cherry-pick ae09 # 版本P3 的 commitID
[detached HEAD f9dfba2] 版本P3
Date: Sat Jan 12 11:35:27 2019 +0800
1 file changed, 1 insertion(+)
create mode 100644 p3.txt
$ git log --pretty=oneline
f9df (HEAD) 版本P3
3bd7 版本P2
a3ab 版本P1
478f 版本O
複製代碼
注意:合併中的衝突解決
合併的過程當中可能會出現衝突,好比同時拿到神器P1
、P2
,可是在P1
中賣掉了O
以前拿到的裝備S
,而在P2
中則爲S
鑲上了寶石,那麼合併以後要怎麼處理?是賣掉S
?仍是保留鑲寶石的S
?仍是鑲了寶石再賣掉?深井冰啊!我不要面子的啊... 因此這裏就涉及到了合併的衝突解決,這裏再也不贅述,不是我要講的內容。
這個名詞讓人想入菲菲啊,每次項目新成員加入,老是會提醒他們注意要變基....
這裏不去說merge
和rebase
的愛恨情仇,只說一些rebase
的操做,用rebase
來整理commit
。
上面說到commit 是構成 git 世界的基本栗子,因此,咱們須要掌握一些栗子
的操做方式
查看commit
,可使用git log
,若是須要尋回忘記的commit
,可使用reflog
來嘗試看看是否可以找到
$ git log --pretty=oneline
68de (HEAD -> X) Merge branches 'Y' and 'Z' into X
16ff (Z) 版本Z3
...
$ git reflog
23e799e (HEAD) HEAD@{0}: rebase -i (pick): 版本Z
01a10d6 HEAD@{1}: rebase -i (squash): 版本Y
a15dd72 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
b6f2ea3 HEAD@{3}: rebase -i (start): checkout 1004
f4c4ccc HEAD@{4}: rebase -i (abort): updating HEAD
...
複製代碼
建立
建立使用git add
+git commit
就好了
$ echo error > error.txt
$ git add error.txt
$ git commit -m '一個錯誤的版本'
[X bc90774] 一個錯誤的版本
1 file changed, 1 insertion(+)
create mode 100644 error.txt
$ git log --pretty=oneline
bc90 (HEAD -> X) 一個錯誤的版本
68de Merge branches 'Y' and 'Z' into X
...
複製代碼
更新上一個commit
,直接使用git commit --amend
$ echo error2 >> error.txt
$ git add error.txt
$ git commit --amend
// 這裏將打開一個vim窗口
一個錯誤的版本
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Fri Jan 11 17:21:18 2019 +0800
#
# On branch X
# Changes to be committed:
# new file: error.txt
#
// 保存退出以後輸出
[X d5c4487] 一個錯誤的版本
Date: Fri Jan 11 17:21:18 2019 +0800
1 file changed, 2 insertions(+)
create mode 100644 error.txt
複製代碼
要更新歷史中的commit
也是能夠作到的,例如須要在版本X中加入文件x1.txt
,要使用交互式模式
git rebase -i 2e83 # 指向 版本X 的前一個 commit
複製代碼
此時將打開一個交互式窗口
pick 913b571 版本X
pick 0eca5e3 版本Y1
pick 33a9ca3 版本Y2
pick b95b3ca 版本Y3
pick 839c481 版本Z1
pick 6fb6cb3 版本Z2
pick c28d3e0 版本Z3
...
複製代碼
將版本X
前面的pick
改成e
或者edit
,保存,而後退出,這個時候,倉庫將會回到版本X
的狀態,並輸出
Stopped at 913b571... 版本X
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
複製代碼
添加文件x1
$ echo x1 > x1.txt
$ git add x*
$ git commit --amend
// 打開交互式窗口能夠修改 commit message
$ git rebase --comtinue
Successfully rebased and updated detached HEAD.
複製代碼
此時又回會到本來的版本而且多出了文件x1
,就像是在版本X
中就已經加入同樣
插入一個新的commit
,上面的栗子中不使用--amend
,就會在X
和Y1
之間插入一個新的commit
$ git rebase -i 2e83
// 交互式窗口,吧`pick`改成`e`
$ echo x2 > x2.txt
$ git add x2.txt
$ git commit -m '插入一個版本X2'
[detached HEAD 1b4821f] 插入一個版本X2
1 file changed, 1 insertion(+)
create mode 100644 x2.txt
$ git rebase --continue
Successfully rebased and updated detached HEAD.
$ git log --pretty=oneline
30a5 (HEAD) 版本Z3
4b00 版本Z2
cc1d 版本Z1
595e 版本Y3
4456 版本Y2
b6f2 版本Y1
1b48 插入一個版本X2
1004 版本X
複製代碼
刪除
刪除一個分支可使用交互式rebase
,命令:git rebase -i commitID
,這裏的commitID
必須是你要刪除的commit
的前一個commit
。
$ git rebase -i 68de
複製代碼
此時將會打開一個vim
窗口
pick bf2c542 版本Y1
pick 588feec 版本Y2
pick 1b2ae37 版本Y3
pick 38f7cf3 版本Z1
pick 080e442 版本Z2
pick 206a7ae 版本Z3
pick 6b01f70 一個錯誤的版本
# Rebase 2caeda3..6b01f70 onto 2caeda3 (7 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
...
複製代碼
要刪除一個錯誤的版本
,將前面的pick
改成d
或者drop
d 6b01f70 一個錯誤的版本
複製代碼
保存退出,輸出
Successfully rebased and updated refs/heads/X.
複製代碼
合併多個commit
,好比合並Z1-Z3
,打開交互式窗口以後,將Z2
、Z3
的pick
改成s
$ git rebase -i 100468330c7819173760938d9e6d4b02f37ba001
// 打開了交互式窗口
pick bf2c542 版本Y1
pick 588feec 版本Y2
pick 1b2ae37 版本Y3
pick 38f7cf3 版本Z1
s 080e442 版本Z2
s 206a7ae 版本Z3
複製代碼
保存退出之後,又打開交互式窗口,顯示要合併的commit
的message
,這裏能夠修改commit
。
# This is a combination of 3 commits.
# This is the 1st commit message:
版本Z1
# This is the commit message #2:
版本Z2
# This is the commit message #3:
版本Z3
複製代碼
這裏修改成Z
,保存,退出,輸出,能夠看到,Z1-Z3
消失了,取而代之的是Z
,對Y1-Y3
作操做
detached HEAD f4c4ccc] 版本Z
Date: Fri Jan 11 16:27:00 2019 +0800
1 file changed, 3 insertions(+)
create mode 100644 z.txt
Successfully rebased and updated detached HEAD.
$ git log --pretty=oneline
f4c4 (HEAD) 版本Z
595e 版本Y3
4456 版本Y2
b6f2 版本Y1
$ git rebase -i 1004
[detached HEAD 01a10d6] 版本Y
Date: Fri Jan 11 16:24:37 2019 +0800
1 file changed, 3 insertions(+)
create mode 100644 y.txt
Successfully rebased and updated detached HEAD.
$ git rebase --continue
複製代碼
從新排序commit
順序,好比重排版本Y
和版本Z
,交換一下順序就行了
$ git log --pretty=oneline
23e7 (HEAD) 版本Z
01a1 版本Y
$ git rebase -i 1b48
複製代碼
這時候打開交互式窗口,顯示
pick a1942a3 版本Y
pick eeabc6c 版本Z
複製代碼
將它交換順序,保存,退出
pick eeabc6c 版本Z
pick a1942a3 版本Y
複製代碼
查看結果
Successfully rebased and updated detached HEAD.
$ git log --pretty=oneline
a194 (HEAD) 版本Y
eeab 版本Z
複製代碼