相信你們在日常工做中其實已經基本都開始在使用git了,我本身從開始工做到如今也使用了快2年的git,可是咱們真的能算"掌握"git了嗎,其實不少時候咱們只是會了git最簡單的一部分,可是對於背後的原理以及一些高級的使用就是隻知其一;不知其二,因此當咱們碰到了一些情景須要用到這些高級操做時就會手足無措。這個系列主要就是對git的原理進行剖析而且幫助你們理解和學會如何使用這些高級操做,而後也會對一些工做中的場景例子進行講解總結。這裏假設你們已經瞭解了基本的git的應用,若是不太熟悉的話能夠去google一下,對於git的基礎教學其實網上不少材料的,這裏就再也不贅述。話很少說,讓咱們進入主題。html
已上圖爲例,從版本1到版本2,咱們修改了A和C,那麼此時git就會建立新的A1和C1的文件對象,而由於B沒有變更,因此版本2中的B會指向版本1中的Bgit
這裏要注意的是在從暫存區提交至版本庫時,咱們須要放入一個commit的message來描述此次提交的目的,這裏這個message是不能爲空的,否則就會見到empty message will abort commit這個警告。算法
在git中存在4種對象用來幫助咱們進行版本管理,它們分別是blob,tree,commit,tag,blob對象用來存放文件的內容,tree對象用來存放目錄以及文件信息,commit對象存放每一個commit的信息,而tag存放的就是跟標籤有關的信息,這裏要注意的是git只關心文件的內容,因此在git中無論文件存在什麼位置叫什麼名字,只要兩個文件內容是一摸同樣的,那麼背後永遠只有一個blob對象,這個特性使得git能使用咱們以前說到的快照功能而又不用擔憂存放的內容過大,由於每次新的commit,只要文件內容不變,其實咱們只需指向以前的blob對象,而不須要重複的建立一個新的對象。在咱們的項目中,進入.git文件夾下面咱們能夠看到一個叫objects的文件夾,這個文件夾下就是存放着咱們以上說的這些對象文件,文件名其實就是通過sha1算法得到的一串id,而內容就是咱們對象的內容通過壓縮算法後的結果。那麼如今就讓咱們用一個例子來說解下咱們在作git操做時背後究竟發生了什麼vim
在一個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指向這個對象 ui
以上的例子中咱們一共建立了10個對象,咱們也能夠經過執行git count-objects
來查詢當前一共有幾個對象
到這裏其實咱們就算是對git背後的對象及原理有一個瞭解了,其實若是咱們去到上面提到的objects目錄,而後去執行這些操做就能夠看到這些對象文件是如何被創造出來的了,同時也可使用咱們以後會介紹的git cat-file
指令去查看類型和內容,有興趣的話能夠去實驗一下,相信你就會徹底掌握咱們上面說的這一切了,最後咱們看下面這張圖中,objects目錄下實際上是先將對象id的前兩位抽出來當作文件夾名,而後剩下的位數當作文件名來存放對象內容的,爲何要這麼作呢?由於在一些操做系統下若是在一個文件夾中放了過多的檔案,那麼讀取效率就會變得不好,因此git使用了這種方法來避免這個問題。
相信你們都知道可使用git log來查看以前的一些commit信息,它的格式如圖所示
這裏能夠看到git的commit id是一個摘要值,這個值就是以前提到的commit對象的sha1值,這個跟傳統的版本控制使用數字遞增來標示每一個commit是不一樣的,由於git是分佈式的版本控制系統,因此數字的方式是沒法處理這樣的情景,由於每一個人的主機上都有控制着一份版本庫,每一個人均可以在本身的主機上作提交,若是用數字的話那就會出現好幾個commit id是1,2,3....這樣的狀況,很明顯就衝突了。另外能夠看到在commit id下面有一行顯示做者的姓名和郵箱的,這個姓名和郵箱又是怎麼得來的呢,其實git有三個地方能夠設置
git config --system
設置git config --global
設置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 -3
查看最近3條commit
git 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的相關commit
git log --grep='wtf'
查找commit信息包含wtf的相關commit
git log -S "elixir"
查找字符串elixir在哪一個commit中加到哪一個文件中
git log --since="9am" until="12am"
查詢早上9點至12點之間的commit
git log -p test.html
查看某個文件的提交記錄,-p會將具體修改的狀況一併顯示
*.a
忽略全部.a結尾的文件!lib.a
lib.a除外/TODO
僅僅會略根目錄下的TODO文件,不會包括子目錄中的TODObuild/
忽略build目錄下的全部文件doc/*.txt
忽略doc下的txt結尾文件,可是doc子目錄中的txt結尾文件不會包括/*/*.txt
忽略全部一級目錄下的全部txt結尾文件,可是一級之下的子目錄不會包括/**/*.txt
忽略一級目錄及其子目錄下的txt結尾文件 須要注意的是.gitignore只會對在設定它以後新增的文件生效,若是在以前就已經被歸入到版本庫的文件,即便命中也不會生效echo '123' | git hash-object --stdin
git config --list
git config --global core.editor emacs
git clean -fX
git add -p index.html
,接着就會進入編輯模式能夠選擇行的把想要提交的內容保留,此時文件在git的狀態中就會便拆成兩部分,一部分是保留的修改放在暫存區中,第二部分是保留在工做區的修改
git cat-file SHA1值 -t
能夠查看該SHA1值背後表明的是一個什麼對象(即blob,tree,commit等),同時git cat-file SHA1值 -p
能夠查看對象背後保存的內容
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開頭的命令道別名
git rm背後作了兩件事,首先就是執行rm命令刪除了文件,接着它會將被刪除的文件加入到暫存區中。
從建立快照的角度來講確實有一點,可是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 .
會把本地全部untrack的文件都加入暫存區,而且會根據.gitignore作過濾,可是git add *
會忽略.gitignore把任何文件都加入
ctrl + l
來清屏, ctrl + a
能夠將光標跳至行頭,ctrl + e
能夠跳至行末,cd -
能夠回到上一次訪問的目錄