相信大家在平常工作中其實已經基本都開始在使用git了,我自己從開始工作到現在也使用了快2年的git,但是我們真的能算"掌握"git了嗎,其實很多時候我們只是會了git最簡單的一部分,但是對於背後的原理以及一些高階的使用就是一知半解,所以當我們碰到了一些情景需要用到這些高階操作時就會手足無措。這個系列主要就是對git的原理進行剖析並且幫助大家理解和學會如何使用這些高階操作,然後也會對一些工作中的場景例子進行講解總結。這裡假設大家已經瞭解了基本的git的應用,如果不太熟悉的話可以去google一下,對於git的基礎教學其實網上很多材料的,這裡就不再贅述。話不多說,讓我們進入主題。
Git版本迭代
上面這張圖比較了傳統的版本控制系統與git的差別,圖的上半部分代表的就是傳統的版本控制系統,在每個新的版本中儲存的是對前一個版本的的變化(delta),而git採取的是完全不同的辦法,如圖中的下半部分,git的每個新版本都是存著一個快照而非差異。已上圖為例,從版本1到版本2,我們修改了A和C,那麼此時git就會建立新的A1和C1的檔案物件,而因為B沒有變動,所以版本2中的B會指向版本1中的B
Git檔案狀態
在git中,檔案有三個狀態:已修改(modified),已暫存(staged),已提交(committed),如上圖所示,工作區中存放著git控制的檔案,而這些檔案是從物件庫(即版本庫)中檢出的,當我們在工作區對檔案進行修改然後執行git add之後其實就是將修改的檔案放入了暫存區,接著當我們執行git commit時,其實就是將暫存區中的檔案存到圖版本庫之中。這裡要注意的是在從暫存區提交至版本庫時,我們需要放入一個commit的message來描述這次提交的目的,這裡這個message是不能為空的,不然就會見到empty message will abort commit這個警告。
Git中的物件及背後原理
在git中存在4種物件用來幫助我們進行版本管理,它們分別是blob,tree,commit,tag,blob物件用來存放檔案的內容,tree物件用來存放目錄以及檔案資訊,commit物件存放每個commit的資訊,而tag存放的就是跟標籤有關的資訊,這裡要注意的是git只關心檔案的內容,所以在git中不管檔案存在什麼位置叫什麼名字,只要兩個檔案內容是一摸一樣的,那麼背後永遠只有一個blob物件,這個特性使得git能使用我們之前說到的快照功能而又不用擔心存放的內容過大,因為每次新的commit,只要檔案內容不變,其實我們只需指向之前的blob物件,而不需要重複的建立一個新的物件。在我們的專案中,進入.git資料夾下面我們可以看到一個叫objects的資料夾,這個資料夾下就是存放著我們以上說的這些物件檔案,檔名其實就是經過sha1演算法獲得的一串id,而內容就是我們物件的內容經過壓縮演算法後的結果。那麼現在就讓我們用一個例子來講解下我們在做git操作時背後究竟發生了什麼
-
在一個git初始化的空專案中建立一個檔案
echo '123' > index.html
並且通過git add將檔案加入暫存區中,這個時候git就會建立出一個blob物件 -
我們建立一個config資料夾
mkdir config
,這裡就要注意,git不會去在意空的資料夾,所以這裡甚至連工作區的untrack變動都沒有,如果我們真的想建立一個資料夾但是有還沒有打算在裡面放置內容的話,慣例我們可以在資料夾下建立一個.keep或者.gitkeep檔案 -
我們在config目錄中建立一個database.yml檔案
echo 'super-secret-password' > config/database.yml
,然後git add,這時候git就會建立一個新的blob物件 -
執行git commit,這時候git就會開始建立tree物件了,git會先建立一個代表config資料夾的物件並且會指向它包含的blob物件,也就是前面database.yml的blob物件,就著還會建立專案的根目錄的tree物件,指向config的tree物件和index.html的blob物件,最後git還會建立一個commit物件,這個commit物件就指向了根目錄的tree物件。在建立完commit物件之後,就會將現在的master分支指向這個commit物件,同時因為我們現在就處於master分支,所以HEAD指標也是也會指向著master分支
-
修改一下index.html檔案,
echo 'hello world' >> index.html
,並且執行git add,這時候因為index.html發生了改變,所以就會建立一個新的blob物件,執行git commit,這時候因為產生了一個新的blob物件所以上層也會產生一個新的tree物件,由於config目錄並未改變,所以新的tree物件就直接加一個指向之前config的tree物件,接著建立新的commit物件,並把master和HEAD指向這個新的物件 -
在根目錄下建立一個key.txt,內容跟database.yml一樣,
echo 'super-secret-password' > key.txt
,執行git add,此時由於內容相同,git就不會再重複建立一個新的物件了,執行git commit,此時由於根目錄發生變化所以會建立一個新的tree物件,這個物件指向原先的config的tree物件和index.html的blob物件,接著應為多了一個key.txt並且內容與database.yml一致,所以tree也會指向之前database.yml的blob物件,最後建立新的commit物件並將master和HEAD指向這個物件
以上的例子中我們一共建立了10個物件,我們也可以通過執行git count-objects
來查詢當前一共有幾個物件
到這裡其實我們就算是對git背後的物件及原理有一個瞭解了,其實如果我們去到上面提到的objects目錄,然後去執行這些操作就可以看到這些物件檔案是如何被創造出來的了,同時也可以使用我們之後會介紹的git cat-file
指令去檢視型別和內容,有興趣的話可以去實驗一下,相信你就會完全掌握我們上面說的這一切了,最後我們看下面這張圖中,objects目錄下其實是先將物件id的前兩位抽出來當做資料夾名,然後剩下的位數當做檔名來存放物件內容的,為什麼要這麼做呢?因為在一些作業系統下如果在一個資料夾中放了過多的檔案,那麼讀取效率就會變得很差,所以git使用了這種方法來避免這個問題。
Commit
相信大家都知道可以使用git log來檢視之前的一些commit資訊,它的格式如圖所示
這裡可以看到git的commit id是一個摘要值,這個值就是之前提到的commit物件的sha1值,這個跟傳統的版本控制使用數字遞增來標示每個commit是不同的,因為git是分散式的版本控制系統,所以數字的方式是無法處理這樣的情景,因為每個人的主機上都有控制著一份版本庫,每個人都可以在自己的主機上做提交,如果用數字的話那就會出現好幾個commit id是1,2,3....這樣的情況,很明顯就衝突了。另外可以看到在commit id下面有一行顯示作者的姓名和郵箱的,這個姓名和郵箱又是怎麼得來的呢,其實git有三個地方可以設定
- /etc/gitconfig (幾乎不會使用) ,通過
git config --system
設定 - ~/.gitconfig (很常用) 通過
git config --global
設定 - 針對於特定專案,在專案的.git/config檔案中 (很常用) ,通過
git config --local
設定
在配置完之後就會再相應的檔案中看到類似的資訊被加入
如果三個都設定了的話優先順序會是 3 > 2 > 1, 當然3的話只會在特定專案中生效,2只會在當前主機使用者的檔案系統中生效,1則是全域性生效,通過git config user.name
就可以檢視到當前上下文中的資訊
指令
-
重新命名檔案
可以使用
git mv test1.txt test2.txt
,這裡要注意的是,git mv背後其實是先做了mv test1.txt test2.txt
,接著再去執行git rm test1.txt
,再git add test2.txt
,所以假設我們現在執行git reset HEAD test1.txt
的話,這裡會撤銷刪除test1.txt,但是test2.txt依然會在暫存區中,因為背後其實是執行了兩條命令,所以對原始檔的git操作並不影響目標檔案 -
消除當前上下文中已配置的使用者名稱和郵箱
git config --local --unset user.name
-
修改上次提交的訊息
git commit --amend -m 'new message for last commit'
,這個指令同時也會將暫存區中新有的改動一起合併到commit中,但這裡要注意的是如 果有遠端的版本伺服器,儘量不要在已經推送到遠端伺服器之後還去對commit進行修改,因為其他人可能 已經在使用當前遠端上的內容了。另外如果在更新了當前的git的使用者名稱和郵箱後想要修改上一次提交的使用者名稱和郵箱可以執行git commit --amend --reset-author
-
檢視git log
git log -3
檢視最近3條commitgit log --pretty=online
以簡單的一行形式看commit歷史git log --pretty=format:"%h - %an, %ar : %s"
可以自定義format來輸出commit歷史git log --graph
圖形化檢視提交歷史git log --graph --abbrev-commit
提交資訊簡寫git log --author='desmond'
查詢一位作者叫desmond的相關commitgit log --grep='wtf'
查詢commit資訊包含wtf的相關commitgit log -S "elixir"
查詢字串elixir在哪個commit中加到哪個檔案中git log --since="9am" until="12am"
查詢早上9點至12點之間的commitgit log -p test.html
檢視某個檔案的提交記錄,-p會將具體修改的情況一併顯示 -
.gitignore的一些配置方法
*.a
忽略所有.a結尾的檔案!lib.a
lib.a除外/TODO
僅僅會略根目錄下的TODO檔案,不會包括子目錄中的TODObuild/
忽略build目錄下的所有檔案doc/*.txt
忽略doc下的txt結尾檔案,但是doc子目錄中的txt結尾檔案不會包括/*/*.txt
忽略所有一級目錄下的所有txt結尾檔案,但是一級之下的子目錄不會包括/**/*.txt
忽略一級目錄及其子目錄下的txt結尾檔案 需要注意的是.gitignore只會對在設定它之後新增的檔案生效,如果在之前就已經被納入到版本庫的檔案,即使命中也不會生效
-
檢視我們輸入的內容在git的sha1演算法計算過後的sha1值
echo '123' | git hash-object --stdin
-
檢視當前的git配置,包括使用者名稱,郵箱等
git config --list
-
修改git提交時的預設編輯器(預設為vim)
git config --global core.editor emacs
-
刪除掉.gitignore裡面過濾的檔案
git clean -fX
-
只提交檔案中的部分內容
git add -p index.html
,接著就會進入編輯模式可以選擇行的把想要提交的內容保留,此時檔案在git的狀態中就會便拆成兩部分,一部分是保留的修改放在暫存區中,第二部分是保留在工作區的修改 -
檢視SHA1值背後代表的物件
git cat-file SHA1值 -t
可以檢視該SHA1值背後代表的是一個什麼物件(即blob,tree,commit等),同時git cat-file SHA1值 -p
可以檢視物件背後儲存的內容 -
git add 與 git commit 操作合二為一
git commit -am 'hello'
場景
-
從暫存區恢復到修改狀態 & 取消修改狀態
有時候如果我們在git add之後發現想要撤銷回這些修改,我們可以使用
git reset HEAD 檔名
,將暫存區中的修改檔案恢復至修改狀態,如果連修改狀態也不需要呢?那我們就可以使用git checkout -- 檔名
來取消已修改檔案。如果是使用git add將一個新增的檔案放入暫存區中的時候,我們還可以使用git rm --cached 檔名
來撤銷操作,在檔案是新增的時候,這條命令與git reset HEAD
是一樣的效果 -
設定別名
在git中,如果覺得指令太長的話,我們可以通過設定別名來簡化,例如
git config --gloabal alias.co checkout
,這樣在我們使用git checkout
時就可以直接使用git co
了,通常我們還會用br代表branch,st代表status, 或者譬如我們使用git log --oneline --graph
時覺得太長,就可以用一個l來作為別名,git config --gloabal alias.l "log --oneline --graph"
,再比如我們想要設定git預設gui gitk的別名可以使用git config --global alias.ui '!gitk'
這裡加感嘆號是使gitk以外部命令的形式跑,也就是前面不會加一個git,我們通過這個技巧就可以給一些不以git開頭的命令作別名
Q&A
-
git rm 與 rm 有什麼區別呢?
git rm背後做了兩件事,首先就是執行rm命令刪除了檔案,接著它會將被刪除的檔案加入到暫存區中。
-
如果git使用快照的方式,那感覺很浪費空間啊,我可能只改了一個字都會創造一個新的blob物件
從建立快照的角度來說確實有一點,但是git本身自帶了資源回收機制,在發動這個機制時,git會使用高效的方式來壓縮物件並且做索引,我們可以通過手動的去呼叫
git gc
來觸發資源回收這時候我們看到pack資料夾下就會多出來一個pack檔案和index檔案,我們可以再通過呼叫
git verify-pack -v .git/objects/pack/pack-ea00f1558d67a7df25bf9744f3d83a17a7a2bf43.idx
來檢視打包的狀況這裡看到第二個紅框中的物件其實是第一個紅框中的物件修改之後的blob物件,但是他的大小隻有9,為什麼呢,原因是因為它參照了前面的blob物件,也就是說在打包之後,其實使用了delta備份的方式來有效降低大小的,只有在打包前才是快照那樣完整的檔案內容的。
當然除了物件之外,包括像HEAD,branch之類的資訊也會被回收。
那資源回收機制預設是在什麼時候會自動觸發呢
- 當在
./git/objects
目錄的檔案或是打包過的pack檔案過多的時候 - 當執行
git push
的時候
- 當在
-
git add . 和 git add * 區別
git add .
會把本地所有untrack的檔案都加入暫存區,並且會根據.gitignore做過濾,但是git add *
會忽略.gitignore把任何檔案都加入
Tips
- Mac的terminal中可以執行
ctrl + l
來清屏,ctrl + a
可以將游標跳至行頭,ctrl + e
可以跳至行末,cd -
可以回到上一次訪問的目錄 - Homebrew是Mac中很強大的包管理工具,我們可以使用它來安裝很多程式,例如git等
- 推薦一個Homebrew中可以安裝的包叫tree,可以用來看當前目錄結構,在我們學習git的時候還是蠻有用的,並且它可以通過-L引數指定最多要看的層數, etc. tree -L 1(只看一層目錄結構)