0、導讀 本文適合對git有過接觸,但知其然不知其所以然的小伙伴,也適合想要學習git的初學者,通過這篇文章,能讓大家對git有豁然開朗的感覺,在寫作程序中,我力求通俗易懂,深入淺出,不堆砌概念,你能夠從本文中了解以下知識:
- Git是什么
- Git能夠解決哪些問題
- Git的實作原理
請注意,本文的闡述邏輯是:Git是什么——>Git要解決的根本問題是什么——>git是如何解決這些問題的,
1、Git是什么? Git是一種分布式版本控制系統, 有人要問了,什么是“版本控制”?Git又為什么被冠以“分布式”的名頭呢?這兩個問題我們一一解答, 版本控制這個說法多少有一點抽象,事實上,版本控制這件事兒我們一直在做,只是平時不這么稱呼,舉一個栗子,boss讓你寫一個策劃案,你先完成了一稿,之后又有了一些新的想法,但是并不確定新的想法是否能得到boss的認可,于是你保存了一個初稿,之后在初稿的基礎上另存了一個檔案,做了部分修改完成了一個修改稿,OK,這時你的策劃案就有了兩個版本——初稿和修改稿,如果boss對修改稿不滿意,你可以很輕易的把初稿拿出來交差, 在這個簡單的程序中,你已經執行了一個簡單的版本控制操作——把檔案保存為初稿和修改稿的程序就是版本控制, 學術點說,版本控制就是對檔案變更程序的管理,說白了,版本控制就是要把一個檔案或一些檔案的各個版本按一定的方式管理起來,目的是需要用到某個版本的時候可以隨時拿出來,另一個個問題,為什么說Git是“分布式”版本控制系統呢?
這里的“分布式”是相對于“集中式”來說的,把資料集中保存在服務器節點,所有的客戶節點都從服務節點獲取資料的版本控制系統叫做集中式版本控制系統,比如svn就是典型的集中式版本控制系統,
與之相對,Git的資料不止保存在服務器上,同時也完整的保存在本地計算機上,所以我們稱Git為分布式版本控制系統,
Git的這種特性帶來許多便利,比如你可以在完全離線的情況下使用Git,隨時隨地提交專案更新,而且你不必為單點故障過分擔心,即使服務器宕機或資料損毀,也可以用任何一個節點上的資料恢復專案,因為每一個開發節點都保存著完整的專案檔案鏡像,
2、Git能夠解決哪些問題? 就像上文舉的例子一樣,在未接觸版本控制系統之前,大多人會通過保存專案或檔案的備份來達到版本控制的目的,通常你的檔案或檔案夾名會設定成“XXX-v1.0”、“XXX-v2.0”等, 這是一種簡單的辦法,但過于簡單,這種方式無法詳細記錄版本附加資訊,難以應付復雜專案或長期更新的專案,缺乏版本控制約定,對協作開發無能為力,如果你不慎使用了這種方式,那么稍稍過一段時間你就會發現連自己都不知道每個版本間的區別,版本控制形同虛設, Git能夠為我們解決版本控制方面的大多數問題,利用Git
- 我們可以為每一次變更提交版本更新并且備注更新的內容;
- 我們可以在專案的各個歷史版本之間自如切換;
- 我們可以一目了然的比較出兩個版本之間的差異;
- 我們可以從當前的修改中撤銷一些操作;
- 我們可以自如的創建分支、合并分支;
- 我們可以和多人協作開發;
- 我們可以采取自由多樣的開發模式,
所以,如果問“Git能夠解決哪些問題?”我們可以簡單的回答:Git解決了版本控制方面的很多問題,但最核心的是它很好的解決了版本狀態存盤(即檔案變更程序存盤)的問題,
3、Git的實作原理 我們說到,Git很好的解決了版本狀態記錄的問題,在此基礎上實作了版本切換、差異比較、分支管理、分布式協作等等炫酷功能,那么,這一節我們就先從最根本的講起,看看Git是如何解決版本狀態記錄(即檔案變更程序記錄)問題的, 我們都有版本記錄的經驗,比如在檔案撰寫的關鍵點上保留一個備份,或在需要對檔案進行修改的時候“另存”一次,這都是很好的習慣,也是版本狀態記錄的一種常用方式,事實上,Git采取了差不多的方式, 在我們向Git系統提交一個版本的時候,Git會把這個版本完整保存下來,這是不是和“另存”有異曲同工之妙呢?不同之處在于存盤方式,在Git系統中一旦一個版本被提交,那么它就會被保存在“Git資料庫”中, 3.1 Git資料庫 我們提到了“Git資料庫”,這是什么玩意兒呢?為了能夠說清楚Git資料庫的概念,我們暫且引入三個Git指令,通過這三個命令,我們就能一探git資料庫的究竟,- git init 用于創建一個空的git倉庫,或重置一個已存在的git倉庫
- git hash-object git底層命令,用于向Git資料庫中寫入資料
- git cat-file git底層命令,用于查看Git資料庫中資料
|
$ git init GitTest Initialized empty Git repository in /home/mp/Workspace/GitTest/.git/ |
|
$ cd GitTest
$ ls
$ find .git
.git .git/HEAD .git/config .git/objects .git/objects/info .git/objects/pack .git/refs .git/refs/heads .git/refs/tags .git/hooks .git/hooks/commit-msg.sample .git/hooks/post-update.sample .git/hooks/update.sample .git/hooks/pre-rebase.sample .git/hooks/pre-applypatch.sample .git/hooks/fsmonitor-watchman.sample .git/hooks/applypatch-msg.sample .git/hooks/pre-receive.sample .git/hooks/pre-push.sample .git/hooks/prepare-commit-msg.sample .git/hooks/pre-commit.sample .git/info .git/info/exclude .git/branches .git/description |
|
$ echo "version 1" | git hash-object -w --stdin 83baae61804e65cc73a7201a7252750c76066a30 $ find .git/objects/ -type f |
|
$ git cat-file -t 83baa
blob $ git cat-file -p 83baa version 1 |
| $ echo "version 1" > file.txt $ git hash-object -w file.txt 83baae61804e65cc73a7201a7252750c76066a30 |
此時,執行
|
$ find .git/objects -type f .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 |
接下來,我們修改file.txt的內容,執行
|
$ echo "version 2" > file.txt
$ git hash-object -w file.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 |
我們發現,.git/objects下多出了一個檔案,這是我們新保存進資料庫的file.txt,接下來,我們執行git cat-file搞清楚這兩條資料的內容分別是什么,執行
| $git cat-file -p 83baa version 1 $git cat-file -p 1f7a7a version 2 |
我們發現,file.txt的變更程序被完整的記錄下來了, 當前的file.txt中保存的內容是“version 2”,如果我們想把檔案恢復到修改為“version 2”之前的狀態,只需執行
|
$ cat file.txt version 2 $ git cat-file -p 83baa > file.txt $ cat file.txt version 1 |
file.txt的內容成功恢復到了修改前的狀態,變成了“version 1”,這其實就是版本回滾的實質,
OK,檔案變更狀態跟蹤的道理就是這么簡單, 但做到這一步還遠遠不算完美,至少有以下幾方面的問題:
- 第一,無法記錄檔案名的變化;
- 第二,無法記錄檔案夾的變化;
- 第三,記憶每一個版本對應的hash值無聊且乏味且不可能;
- 第四,無法得知檔案的變更時序;
- 第五,缺少對每一次版本變化的說明,
Git通過樹(tree)物件將資料(blob)物件組織起來,這很類似于一種檔案系統——blob物件對應檔案內容,tree物件對應檔案的目錄和節點,一個樹(tree)物件包含一潭訓多條記錄,每條記錄含有一個指向blob物件或tree物件的SHA-1指標,以及相應的模式、型別、檔案名, 有了樹物件,我們就可以將檔案系統任何時間點的狀態保存在git資料庫中,這是不是很激動人心呢?你的一個復雜的專案可能包含成百上千個檔案和檔案目錄,有了樹物件,這一切都不是問題, 創建樹物件 通常,Git根據某一時刻暫存區所表示的狀態創建并記錄一個對應的樹物件,如此重復便可以依次記錄一系列的樹物件,Git的暫存區是一個檔案——.git/index,下面,我們通過創建樹物件的程序來認識暫存區和樹物件, 為了創建一個樹物件,我們需要通過暫存一些檔案來創建一個暫存區,為此我們引入兩個命令:
- git update-index git底層命令,用于創建暫存區
- git ls-files --stage git底層命令,用于查看暫存區內容
- git write-tree git底層命令,用于將暫存區內容寫入一個樹物件
OK,萬事俱備,我們將file.txt的第一個版本放入暫存區,執行
| $ find .git/index find: ‘.git/index’: No such file or directory $ git update-index --add file.txt $ find .git/index .git/index $ cat .git/index DIRC[???$?;?[???$?;?A???? ???a?Ne?s? rRu vjfile.txt??3%A??,I? ?` $ find .git/objects/ -type f .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 $ git ls-files --stage 100644 83baae61804e65cc73a7201a7252750c76066a30 0 file.txt $ git write-tree 391a4e90ba882dbc9ea93855103f6b1fa6791cf6 $ find .git/objects/ -type f .git/objects/39/1a4e90ba882dbc9ea93855103f6b1fa6791cf6 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 |
| $ git cat-file -t 391a4e tree $ git cat-file -p 391a4e 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 file.txt |
| 83baae61804e65cc73a7201a7252750c76066a30 |
以上我們添加了一個已經存在在git資料庫中的檔案到暫存區,如果我們新建一個未曾保存到git資料庫的檔案存入暫存區,進而保存為tree物件,會有什么不同嗎?我們試試看,執行
|
$ echo "new file" > new $ git ls-files --stage |
這說明兩個問題:
- 如果添加git資料庫中尚未存盤的資料到暫存區,則在執行update-index的時候,會同時把該資料保存到git資料庫,
- 添加檔案進入暫存區的操作是追加操作,之前已經加入暫存區的檔案依然存在——很多人會有誤區,認為變更提交之后,暫存區就清空了,
| $ git cat-file -p 228e49 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 file.txt 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new |
| $ mkdir new_dir $ git update-index --add new_dir error: new_dir: is a directory - add files inside instead fatal: Unable to process path new_dir |
OK,接下來,我們在新建的檔案夾下寫入一個檔案,再嘗試將這一檔案加入暫存區,執行
|
$ echo "file in new dir" > new_dir/new $ git write-tree $ git cat-file -p 06564b |
從執行結果可見,檔案夾new_dir對應一個tree物件,
至此,在git資料庫中,我們可以完整的記錄檔案的狀態、檔案夾的狀態;并且可以把多個檔案或檔案夾組織在一起,記錄他們的變更程序,我們離一個完善的版本控制系統似乎已經不遠了,而這一切實作起來又是如此簡單——我們只是通過幾個命令操作git資料庫就完成了這些功能, 接下來,我們只要把資料庫中各個版本的時序關系記錄下來,再把對每一個版本更新的注釋記錄下來,不就完成了一個邏輯簡單、功能強大、操作靈活的版本控制系統嗎? 那么,如何記錄版本的時序關系,如何記錄版本的更新注釋呢?這就要引入另一個git資料物件——提交物件(commit object), 3.5 利用提交物件(commit object)記錄版本間的時序關系和版本注釋 commit物件能夠幫你記錄什么時間,由什么人,因為什么原因提交了一個新的版本,這個新的版本的父版本又是誰, git提供了底層命令commit-tree來創建提交物件(commit object),我們需要為這個命令指定一個被提交的樹物件的hash鍵值,以及該提交物件的父提交物件(如果是第一次提交,不需要指定父物件), 我們嘗試將之前創建的樹物件提交為commit 物件,執行
|
$ git write-tree
cb0fbcc484a3376b3e70958a05be0299e57ab495 $ git commit-tree cb0fbcc -m "first commit" 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f $ git cat-file 7020a97 tree cb0fbcc484a3376b3e70958a05be0299e57ab495 author john <[email protected]> 1537961478 +0800 committer john <[email protected]> 1537961478 +0800 first commit |
|
$ echo "new version" > file.txt
$ git update-index file.txt
$ git write-tree 848e967643b947124acacc3a2d6c5a13c549231c $ git commit-tree 848e96 -p 7020a97 -m "second commit" e838c8678ef789df84c2666495663060c90975d7 $ git cat-file -p e838c tree 848e967643b947124acacc3a2d6c5a13c549231c parent 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f author john <[email protected]> 1537962442 +0800 committer john <[email protected]> 1537962442 +0800 second commit |
| $ echo "another version" > file.txt $ git update-index file.txt $ git write-tree 92867fcc5e0f78c195c43d1de25aa78974fa8103 $ git commit-tree 92867 -p e838c -m "third commit" 491404fa6e6f95eb14683c3c06d10ddc5f8e883f $ git cat-file -p 49140 tree 92867fcc5e0f78c195c43d1de25aa78974fa8103 parent e838c8678ef789df84c2666495663060c90975d7 author john <[email protected]> 1537963274 +0800 committer john <[email protected]> 1537963274 +0800 third commit |
| $ git log 49140 commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f Author: john <[email protected]> Date: Wed Sep 26 20:01:14 2018 +0800 third commit commit e838c8678ef789df84c2666495663060c90975d7 Author: john <[email protected]> Date: Wed Sep 26 19:47:22 2018 +0800 second commit commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f Author: john <[email protected]> Date: Wed Sep 26 19:31:18 2018 +0800 first commit |
git add 和 git commit 命令時, Git 所做的實質作業是將被改寫的檔案保存為資料物件,更新暫存區,記錄樹物件,最后創建一個指明了頂層樹物件和父提交的提交物件, 這三種主要的 Git 物件——資料物件、樹物件、提交物件——最初均以單獨檔案的形式保存在 .git/objects 目錄下,
然而,小問題依然存在,截止目前為止,我們對版本和資料物件的操作都是基于hash鍵值的,這些毫無直觀含義的字串讓人很頭疼,不會有人愿意一直急著最新提交對應的hash鍵值的,git不會允許這樣的問題存在的,它通過引入“參考(references)”來解決這一問題,
3.6 Git的參考Git的參考(references)保存在.git/refs目錄下,git的參考類似于一個指標,它指向的是某一個hash鍵值, 創建一個參考實在再簡單不過,我們只需把一個git物件的hash鍵值保存在以參考的名字命名的檔案中即可, 執行
| $ echo "491404fa6e6f95eb14683c3c06d10ddc5f8e883f" > .git/refs/heads/master $ cat .git/refs/heads/master 491404fa6e6f95eb14683c3c06d10ddc5f8e883f |
| $ git log 491404 commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f (HEAD -> master) Author: john <[email protected]> Date: Wed Sep 26 20:01:14 2018 +0800 third commit commit e838c8678ef789df84c2666495663060c90975d7 Author: john <[email protected]> Date: Wed Sep 26 19:47:22 2018 +0800 second commit commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f Author: john <[email protected]> Date: Wed Sep 26 19:31:18 2018 +0800 first commit $ git log master commit 491404fa6e6f95eb14683c3c06d10ddc5f8e883f (HEAD -> master) Author: john <[email protected]> Date: Wed Sep 26 20:01:14 2018 +0800 third commit commit e838c8678ef789df84c2666495663060c90975d7 Author: john <[email protected]> Date: Wed Sep 26 19:47:22 2018 +0800 second commit commit 7020a97c0e792f340e00e1bb8edcbafcc4dfb60f Author: john <[email protected]> Date: Wed Sep 26 19:31:18 2018 +0800 first commit |
| $ git update-ref refs/heads/master 49140 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/222250.html
標籤:其他
