原文:https://www.escapelife.site/posts/da89563c.html
簡單地說,Git 究竟是怎樣的一個系統呢?請注意接下來的內容非常重要,若你理解了 Git 的思想和基本工作原理,用起來就會知其所以然,遊刃有餘。在學習 Git 時,請儘量理清你對其它版本管理系統已有的認識,如 CVS、Subversion 或 Perforce, 這樣能幫助你使用工具時避免發生混淆。儘管 Git 用起來與其它的版本控制系統非常相似, 但它在對信息的存儲和認知方式上卻有很大差異,理解這些差異將有助於避免使用中的困惑。

執行完成了 git init 命令,究竟做了什麼呢?
執行完成如下命令之後,我們可以得到下圖所示的內容,右側的就是 Git 為我們創建的代碼倉庫,其中包含了用於版本管理所需要的內容。
#左邊執行$mkdirgit-demo$cdgit-demo&&gitinit$rm-rf.git/hooks/*.sample#右邊執行$watch-n1-dfind.
我們這裡可以一起看下生成的 .git 目錄的結構如何:
➜tree.git.git├──HEAD├──config├──description├──hooks├──info│ └──exclude├──objects│ ├──info│ └──pack└──refs├──heads└──tags.git/config - 當前代碼倉庫本地的配置文件
本地配置文件(.git/config)和全局配置文件(~/.gitconfig)
通過執行如下命令,可以將用戶配置記錄到本地代碼倉庫的配置文件中去
git config user.name "demo"
git config user.email "demo@demo.com"
.git/objects - 當前代碼倉庫代碼的存儲位置
blob 類型
commit 類型
tree 類型
.git/info - 當前倉庫的排除等信息
➜cat./.git/info/exclude#gitls-files--others--exclude-from=.git/info/exclude#Linesthatstartwith'#'arecomments.#ForaprojectmostlyinC,thefollowingwouldbeagoodsetof#excludepatterns(uncommentthemifyouwanttousethem):#*.[oa]#*~.git/hooks - 當前代碼倉庫默認鈎子腳本
./.git/hooks/commit-msg.sample./.git/hooks/pre-rebase.sample./.git/hooks/pre-commit.sample./.git/hooks/applypatch-msg.sample./.git/hooks/fsmonitor-watchman.sample./.git/hooks/pre-receive.sample./.git/hooks/prepare-commit-msg.sample./.git/hooks/post-update.sample./.git/hooks/pre-merge-commit.sample./.git/hooks/pre-applypatch.sample./.git/hooks/pre-push.sample./.git/hooks/update.sample.git/HEAD - 當前代碼倉庫的分支指針
➜cat.git/HEADref:refs/heads/master.git/refs - 當前代碼倉庫的頭指針
#均無內容➜ll.git/refstotal0drwxr-xr-x2escapestaff64BNov2320:39headsdrwxr-xr-x2escapestaff64BNov2320:39tags➜ll.git/refs/heads➜ll.git/refs/tags.git/description - 當前代碼倉庫的描述信息
➜cat.git/descriptionUnnamedrepository;editthisfile'description'tonametherepository.add 之後發生了什麼執行完成了 git add 命令,究竟做了什麼呢?
執行完成如下命令之後,我們可以得到下圖所示的內容,我們發現右側新增了一個文件,但是 Git 目錄裡面的內容絲毫沒有變化。這是因為,我們現在執行的修改默認是放在工作區的,而工作區裡面的修改不歸 Git 目錄去管理。
而當我們執行 git status 命令的時候,Git 又可以識別出來現在工作區新增了一個文件,這裡怎麼做到的呢?—— 詳見[理解 blob 對象和 SHA1]部分
而當我們執行 git add 命令讓 Git 幫助我們管理文件的時候,發現右側新增了一個目錄和兩個文件,分別是 8d 目錄、index 和 0e41.. 文件。
#左邊執行$echo"hellogit">helle.txt$gitstatus$gitaddhello.txt#右邊執行$watch-n1-dfind.

我們這裡重點看下,生成的 8d 這個目錄以及下面的文件。而其名稱的由來是因為 Git 對其進行了一個叫做 SHA1 的 Hash 算法,用於將文件內容或者字符串變成這麼一串加密的字符。
#查看objects的文件類型$gitcat-file-t8d0e41blob#查看objects的文件內容$gitcat-file-p8d0e41hellogit#查看objects的文件大小$gitcat-file-s8d0e4110#拼裝起來blob10\0hellogit現在我們就知道了,執行 git add 命令將文件從工作區添加到暫存區裡面,Git 會把幫助我們生成一些 Git 的對象,它存儲的是文件的內容和文件類型並不存儲文件名稱。
為了驗證我們上述的說法,我們可以添加同樣的內容到另一個文件,然後進行提交,來觀察 .git 目錄的變化。我們發現,右側的 objects 目錄並沒有新增目錄和文件。這就可以證明,blob 類型的 object 只存儲的是文件的內容,如果兩個文件的內容一致的話,則只需要存儲一個 object 即可。
話說這裡 object 為什麼沒有存儲文件名稱呢?這裡因為 SHA1 的 Hash 算法計算哈希的時候,本身就不包括文件名稱,所以取什麼名稱都是無所謂的。那問題來了,就是文件名的信息都存儲到哪裡去了呢?—— 詳見[理解 blob 對象和 SHA1]部分
#左邊執行$echo"hellogit">tmp.txt$gitaddtmp.txt#右邊執行$watch-n1-dfind.
了解 Git 的 blob 對象和 SHA1 之前的關係和對應計算!
Hash 算法是把任意長度的輸入通過散列算法變化成固定長度的輸出,根據算法的不同,生成的長度也有所不同。
Hash 算法:
MD5 - 128bit - 不安全 - 文件校驗
SHA1 - 160bit(40位) - 不安全 - Git 存儲
SHA256 - 256bit- 安全 - Docker 鏡像
SHA512 - 512bit - 安全
但是,當我們使用工具對上述文件內容進行 SHA1 計算的時候,會發現並沒有我們在 .git 目錄裡面看到的那樣,這是為什麼呢?
➜echo"hellogit"|shasumd6a96ae3b442218a91512b9e1c57b9578b487a0b-這裡因為 Git 工具的計算方式,是使用類型 長度 \0 內容的方式進行計算的。這裡,我們算了下文件內容只有九位,但是這裡是十位,這裡因為內容裡面有換行符的存在導致的。現在我們就可以使用 git cat-file 命令來拼裝 Git 工具存儲的完整內容了。
➜ls-lhhello.txt-rw-r--r--1escapestaff10BNov2321:12hello.txt➜echo"blob10\0hellogit"|shasum8d0e41234f24b6da002d962a26c2495ea16a425f-#拼裝起來blob10\0hellogit
當我們使用 cat 命令來查看 object 對象裡面的內容的時候,發現看着像是一串亂碼。其實這是 Git 工具將文件的原始內容進行一個壓縮,然後再存儲到 object 對象裡面。奇怪的是,我們發現壓縮之後的內容反而比原始內容還大!
這是因為其進行了壓縮,存儲了一些壓縮相關的信息。上例所示的比原始文件大,是因為我們創建的內容實在是太小了。當我們常見一個比較大的文件時,就會看到壓縮之後的文件大小遠小於原始文件的。
➜cat.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425fxKOR04`HWH,6A%➜ls-lh.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f-r--r--r--1escapestaff26BNov2321:36.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f➜file.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f:VAXCOFFexecutablenotstripped-version16694其實,我們這裡也是可以通過 Python 代碼來獲取二進制 object 對象的內容的。
importzlibcontents=open('0e41234f24b6da002d962a26c2495ea16a425f','rb').read()zlib.decompress(contents)
聊聊工作區和暫存區,以及文件如何在工作區和緩存區之間同步的問題。
之前的章節我們也聊到了,當我們執行 git status 命令的時候,Git 工具怎麼知道我們有一個文件沒有追蹤,以及文件名的信息都存儲到哪裡去了?
這一切的答案,都要從工作區和索引區講起。Git 根據其存儲的狀態不同,將對應狀態的「空間」分為工作區、暫存區(也可稱為索引區)和版本區三類。具體示例,可以參考下圖。

而更加深層次的理解,就要從執行 git add 命令後生成相關的 object 對象,但是其存儲的是文件的類容、大小和內容,並不包含文件名稱的信息。而文件名稱相關的信息就包含在生成的 index 文件(索引文件)裡面。
當我們直接查看 index 文件裡面的內容,發現使我們無法理解的亂碼,但是通過基本的輸出,我們可以看到其文件名稱。要想查看 index 文件的內容,可以通過 Git 提供的相關命令進行查看。
#左邊執行$echo"file1">file1.txt$gitaddfile1.txt$cat.git/index$gitls-files#列出當前暫存區的文件列表信息$gitls-files-s#列出當前暫存區文件的詳細信息#右邊執行$watch-n1-dtree.git
當添加文件的時候,文件或目錄會從工作區流向暫存區,加之一些其他操作,會導致工作區和暫存區是會有一定差別的。這就會導致,當我們執行 git status 的結果就是兩者的差別。
經過如下操作,會使工作區和暫存區和的內容不一致了,通過命令我們也是可以查看區別的。當我們使用 add 命令將新文件添加到暫存區的時候,會發現這下就一致了。
#左邊執行$gitstatus$echo"file2">file2.txt$gitls-files-s$gitstatus$gitaddfile2.txt$gitls-files-s$gitstatus#右邊執行$watch-n1-dtree.git
如果我們這裡去修改一個文件的話,很顯然這個時候我們的工作區和暫存區又不一致了。當我們使用命令去查看文件狀態的時候,發現一個文件被修改了,而 Git 是怎麼知道的呢?咳咳,就是通過查找 index 文件的內容,找到對應文件名稱以及其內部引用的 object 對象,與工作區的文件內容進行對比而來的。
#左邊執行$gitls-files-s$echo"file.txt">file1.txt$gitstatus#右邊執行$watch-n1-dtree.git
而這個時候,我們再使用 git add 命令將其修改內容保存至暫存區的話,會發現對應文件的 object 的 blob 對象的引用值發生改變了。這時可以發現,objects 目錄下面有三個對象了,其中 file1.txt 占了兩個,但是文件卻只有兩個。通過命令查看對應 blob 對象的內容,發現各有不同。
#左邊執行$gitls-files-s$gitaddfile1.txt$gitls-files-s#右邊執行$watch-n1-dtree.git
執行完成了 git commit 命令,究竟做了什麼呢?
Git 倉庫中的提交記錄保存的是你的目錄下所有文件的快照,就像是把整個目錄複製,然後再粘貼一樣,但比複製粘貼優雅許多!Git 希望提交記錄儘可能地輕量,因此在你每次進行提交時,它並不會盲目地複製整個目錄。條件允許的情況下,它會將當前版本與倉庫中的上一個版本進行對比,並把所有的差異打包到一起作為一個提交記錄。Git 還保存了提交的歷史記錄。這也是為什麼大多數提交記錄的上面都有父節點的原因。
當我們使用 add 命令將工作區提交到暫存區,而暫存區其實保存的是當前文件的一個狀態,其中包括有哪些目錄和文件,以及其對應的大小和內容等信息。但是我們最終是需要將其提交到代碼倉庫(本地)的,而其命令就是 git commit 了。

而當我們執行 git commit 命令的時候,究竟都發生了什麼呢?可以看到當提交之後,.git 目錄中生成了兩個信息的 object 對象,其中 logs 和 refs 目錄都有新的文件生成。通過如下操作,我們可以查看到其提交的類型和對應內容。
#左邊執行$gitcommit-m"1stcommit"$gitcat-file-t6e4a700#查看commit對象的類型$gitcat-file-p6e4a700#查看commit對象的內容$gitcat-file-t64d6ef5#查看tree對象的類型$gitcat-file-p64d6ef5#查看tree對象的內容#右邊執行$watch-n1-dtree.git
這樣我們就理解了,當我們執行 git commit 命令之後,會生成一個 commit 對象和一個 tree 對象。commit 對象內容裡面包含了一個 tree 對象和相關提交信息,而 tree 對象裡面則包含了這次我們提交版本裡面的文件狀態(文件名稱和 blob 對象),這樣我們就知道了這次提交的變動了。

我們這次提交之後,處理 objects 目錄發生變動之外,還有一些其他的變化。比如 logs 和 refs 的目錄有所變化。我們查看 refs 目錄裡面的內容,發現其指向了 6e4a70 這個 commit 對象,即當前 master 分支上面最新的提交就是這個 6e4a70 了。
而這個 6e4a70 這個 commit 對象,有一個 HEAD 的指向,就是 .git 目錄下的 HEAD 文件。其實質就是一個指針,其永遠指向我們當前工作的分支,即這裡我們工作在 master 分支上。當我們切換分支的時候,這個文件的指向也會隨機改變的。
#左邊執行$cat.git/refs/heads/master$cat.git/HEAD#右邊執行$watch-n1-dtree.git
執行完成了 git commit 命令,究竟做了什麼呢?
當我們再次對 file2.txt 文件的內容進行變更、添加以及提交之後,發現在提交的時候,查看的 commit 對象的內容時,其包含有父節點的 commit 信息。而對於理解的話,可以看看下面的這個提交流程圖。
#左邊執行$echo"file2.txt">file2.txt$gitstatus$gitaddfile2.txt$gitls-files-s$gitcat-file-p0ac9638$gitcommit-m"2ndcommit"$gitcat-file-pbab53ff$gitcat-file-p2f07720#右邊執行$watch-n1-dtree.git

在 Git 中空文件夾是不算在追蹤範圍內的,而且添加文件夾並不會增加 object 對象。當我們查看 index 內容的時候,會發現文件名稱是包含相對路徑的。
而當我們通過 commit 命令提交之後,會發現生成了三個 object 對象,因為 commit 操作不會生成 blob 對象,所以分別是一個 commit 對象和兩個 tree 對象。可以發現,tree 對象裡面有包含了一個目錄的 tree,其裡面包含對象文件內容。
下圖所示的文件狀態,可以體會到 Git 中版本的概念。即 commit 對象指向一個該版本中的文件目錄樹的根(tree),然後 tree 在指向 blob 對象(文件)和 tree 對象(目錄),這樣就可以無限的往復下去形成一個完整的版本。
#左邊執行$mkdirfloder1$echo"file3">floder1/file3.txt$gitaddfloder1$gitls-files-s$gitcommit-m"3rdcommit"$gitcat-file-p1711e01$gitcat-file-p9ab67f8#右邊執行$watch-n1-dtree.git
總結一下,Git 裡面的文件狀態和如何切換。
現在,我們已經基本理解了文件如何在工作區、暫存區以及代碼倉庫之間進行狀態的跟蹤和同步。在 Git 的操作中,文件的可能狀態有哪些,以及如何進行狀態切換的,我們這裡一起總結一下!


執行完成了 git branch 命令,究竟做了什麼呢?
到底什麼是分支?分支切換又是怎麼一回事?我們通過查看 Git 的官方文檔,就可以得到,分支就是一個有名字的(master/dev)指向 commit 對象的一個指針。
我們在初始化倉庫的時候,提供會默認給我們分配一個叫做 master 的分支(在最新的版本默認倉庫已經變更為 main 了),而 master 分支就是指向最新的一次提交。為什麼需要給分支起名字呢?就是為了方便我們使用和記憶,可以簡單理解為 alias 命令的意義一致。

有了上述基礎,我們就需要考慮下,分支到底是如何實現和工作的。要實現一個分支,我們最基本需要解決兩個問題,第一個就是需要存儲每一個分支指向的 commit,第二個問題就是在切換分支的時候幫助我們標識當前分支。
在 Git 中,它有一個非常特殊的 HEAD 文件。而 HEAD 文件是一個指針,其有一個特性就是總會指向當前分支的最新的一個 commit 對象。而這個 HEAD 文件正好,解決了我們上面提出的兩個問題。
當我們從 master 切換分支到 dev 的時候,HEAD 文件也會隨即切換,即指向 dev 這個指針。設計就是這麼美麗,不愧是鬼才,好腦袋。


執行完成了 git branch 命令,究竟做了什麼呢?
這裡我們可以看到分支切換之後,HEAD 指向發生變動了。
#左邊執行$gitbranch$gitbranchdev$ll.git/refs/heads$cat.git/refs/heads/master$cat.git/refs/heads/dev$cat.git/HEAD$gitcheckoutdev$cat.git/HEAD#右邊執行$glo=gitlog
這裡需要注意的是,即使我們刪除了分支,但是該分支上一些特有的對象並不會被刪除的。這些對象其實就是我們俗稱的垃圾對象,還有我們多次使用 add 命令所產生的也有垃圾對象,而這些垃圾對象怎麼清除和回收呢?後續,我們會涉及到的。
#左邊執行$echo"dev">dev.txt$gitadddev.txt$gitcommit-m"1stcommitfromdevbranch"$gitcheckoutmaster$gitbranch-ddev$gitbranch-Ddev$gitcat-file-t861832c$gitcat-file-p861832c$gitcat-file-p680f6e9$gitcat-file-p38f8e88#右邊執行$glo=gitlog
我們一起聊一聊,checkout 和 commit 的操作!
我們執行 checkout 命令的時候,其不光可以切換分支,而且可以切換到指定的 commit 上面,即 HEAD 文件會指向某個 commit 對象。在 Git 裡面,將 HEAD 文件沒有指向 master 的這個現象稱之為 detached HEAD。
這裡不管 HEAD 文件指向的是分支名稱也好,是 commit 對象也罷,其實本質都是一樣的,因為分支名稱也是指向某個 commit 對象的。


當我們切換到指定的 commit 的時候,如果需要在對應的 commit 上繼續修改代碼提交的話,可以使用上述圖片中提及的 swtich 命令創建新分支,再進行提交。但是,通常我們都不會着玩,都會使用 checkout 命令來創建新分支的。
$gitcheckout-btmp$gitlog即使可以這樣操作,我們也很少使用。還記得我們上一章節創建的 dev 分支嗎?我們創建了該分支並有了一個新的提交,但是沒有合併到 master 分支就直接刪除了。現在再使用 log 命令查看的話,是看不到了。
實際,真的看不到了嗎?大家要記住,在 Git 裡面任何的操作,比如分支的刪除。它只是刪除了指向某個特定 commit 的指針引用而已,而那個 commit 本身並不會被刪除,即 dev 分支的那個 commit 提交還是在的。
那我們怎麼找到這個 commit 呢?找到之後,我們就可以在上面繼續工作,或者找到之前的文件數據等。
第一種方法:
[費勁不太好,下下策]
在 objects 目錄下面,自己一個一個看,然後切換過去。
第二種方法:
[推薦的操作方式]
使用 Git 提供的 git reflog 專用命令來查找。
該命令的作用就是用於將我們之前的所有操作都記錄下來。


當我們執行 diff 命令之後,Git 的邏輯它們是怎麼對比出來的呢?
就在本節中中,我們使用上節的倉庫,修改文件內容之後,看看 diff 命令都輸出了哪些內容呢?我們這裡一起來看看,研究研究!
$echo"hello">file1.txt$gitdiff$gitcat-file-p42d9955$gitcat-file-pce01362#下述命令原理也是一樣的$gitdiff--cached$gitdiffHEAD
如何將我們本地的倉庫和遠程服務器上面的倉庫關聯起來呢?
初始化倉庫
$gitinit$gitaddREADME.md$gitcommit-m"firstcommit"關聯遠程倉庫
當我們使用上述命令來關聯遠程服務器倉庫的時候,我們本地 .git 目錄也是會發生改變的。通過命令查看 .git/config 文件的話,可以看到配置文件中出現了 [remote] 字段。
#關聯遠程倉庫$gitremoteaddorigingit@github.com:escapelife/git-demo.git➜cat.git/config[core]repositoryformatversion=0filemode=truebare=falselogallrefupdates=trueignorecase=trueprecomposeunicode=true[remote"origin"]url=git@github.com:escapelife/git-demo.gitfetch=+refs/heads/*:refs/remotes/origin/*推送本地分支
當我們執行如下命令,將本地 master 分支推送到遠程 origin 倉庫的 master 分支。之後,我們登陸 GitHub 就可以看到推送的文件及目錄內容了。
推送分支內容的時候,會列舉推送的 objects 數量,並將其內容進行壓縮,之後推送到我們遠程的 GitHub 倉庫,並且創建了一個遠程的 master 分支(origin 倉庫)。
#推送本地分支$gitpush-uoriginmaster推送之後,我們可以發現,本地的 .git 生成了一些文件和目錄,它們都是什麼呢?如下所示,會新增四個目錄和兩個文件,皆為遠程倉庫的信息。當我們通過命令查看 master 這個文件的內容時,會發現其也是一個 commit 對象。此時與我們本地 master 分支所指向的一致。而其用於表示遠程倉庫的當前版本,用於和本地進行區別和校對的。
➜tree.git├──logs│ ├──HEAD│ └──refs│ ├──heads│ │├──dev│ │├──master│ │└──tmp│└──remotes#新增目錄│ └──origin#新增目錄│ └──master#新增文件└──refs├──heads│ ├──dev│ ├──master│ └──tmp├──remotes#新增目錄│ └──origin#新增目錄│ └──master#新增文件└──tags遠程倉庫存儲代碼使用 GitLab 來了解遠程倉庫的服務器到底是如何存儲,我們的代碼的!
當我們編寫完代碼之後,將其提交到對應的遠程服務器上面,其存儲結構和我們地址是一模一樣的。如果我們仔細想想的話,不一樣的話才見怪了。
Git 本來就是代碼的分發平台,無中心節點,即每個節點都是主節點,所以其存儲的目錄結構都是一直的。這樣,不管哪一個節點的內容發生丟失或缺失的話,我們都可以通過其他節點來找到。而 Git 服務器就是一個可以幫助我們,實時都可以找到的節點,而已。
【Linux常用命令速查手冊】關注【入門小站】,後台回復 「1001」 自取。
近期熱文
6張圖帶你搞懂TCP為什麼是三次握手?
NAT穿透是如何工作的
CentOS7詳細安裝教程--圖文介紹超詳細
VMware中安裝Windows11操作系統
明明還有空間,硬盤卻寫不進去了!
Docker疑難雜症匯總一
35個非常實用的Shell拿來就用腳本實例!
Docker疑難雜症匯總(二)
11個Git實用技巧記錄!
再回顧一下什麼是SSH
9個日常實用Shell腳本
