Skip to content

git基礎功能研究

前言

這是一篇關於git工具的教學書翻譯與整理
只會挑部分重點翻譯與整理,並且附上該章節的連結

此篇只專注在基本使用與基本底層探討,也就是涵蓋教學書的第1、2、3、10章

目錄
第一章:基礎介紹
1-1:版本控制系統
1-2:關於Git
1-3:Git設定檔簡介
第二章:基本使用
2-1:git記錄變動
2-2:查看commit記錄
2-3:利用git復原備份
2-4:git遠端管理
2-5 git tag:幫commit貼標籤
第三章:分支介紹
3-1基礎介紹
3-2分支合併
3-3雜項知識
第四章:git底層
4-1 SHA-1演算法 與 git物件
4-2 git REF: 節點的名字

1. 基礎介紹

本章介紹git的特性與本質

1-1 版本控制系統

原文
Git是一種版本控制系統,所謂版本控制系統(以下簡稱VCS Version control system),即為用一個系統去紀錄"檔案"隨時間的變化,以供未來需要回到特定的版本。
“檔案"可以是任何型態的,在此書是指程式原始碼。

使用VCS,可以以少量額外成本(overhead)紀錄所有版本,使我們可以比較各版本差異找到問題、優化方向,並且在出問題、丟失檔案時回復上一狀態。

本地端版本控制(Local Version Control)

一般人常使用複製檔案於不同資料夾 (如果夠聰明的話甚至會用時間戳記(time stamp)進行資料夾命名)的方式進行本地端版本控制,但是這非常容易導致錯誤,比如開錯資料夾導致寫入錯誤的檔案、覆蓋掉錯的檔案…

程式設計師為了解決此問題,設計了版本資料庫(version database)
(如下圖,使用者每次只會看到一個版本的檔案,並且可以在各版本間切換、修改版本…)

通常程式碼版本控制會記錄各個版本間的差異(稱為patch sets),而非整個版本,並且這些紀錄會以特殊格式記在硬碟裡,可以藉由加入或刪除這些差異達到版本切換。
(Note:在下一小節會介紹git的實作方式與此方法略有不同)

此類型工具如: Revision Control System

中心化版本控制系統 (Centralized Version Control Systems)

在版本控制的發展路上,下一個遇到的問題就是多人合作,於是如下圖的中心化版本控制架構應運而生。

這個架構優缺點類似於任何有中央伺服器的架構,優點為管理方便,甚至可以由管理者(Administrators)以權限進行分工,缺點即為當中央伺服器出問題,維修期間造成工作無法進行下去,甚至有檔案丟失的風險。

此類型工具如: CVS, Subversion, and Perforce

分散式版本控制系統 (Distributed Version Control Systems)

單一紀錄的缺點能被DVCS解決,我們的主角Git也是此類型的一員
下圖為其架構

這個類型的版本控制在給予各個端點檔案時,不會只給一個版本的狀態,而是複製整個版本的歷史,這讓每個端點都是一個完整的備份,並且合作也能更有效率。

此類型工具如: Git,Mercurial,Bazaar,Darcs

1-2 關於Git

原文
儘管Git與其他VCS相似,但很多概念是創新且獨有的,以下各別介紹

一, 儲存快照(snapshot)、而非差異

在上一小節的概述中,提到其他Local VCS會使用紀錄"檔案差異"的方式實作,但Git則是紀錄"檔案快照",意即每一個版本都是紀錄所有檔案在那一刻的樣子,並且當Git發現檔案沒有改變時,會使用指向原始檔,也因此git類似於mini filesystem。

二, 資料完整性(Integrity)

Git在儲存時,都會進行驗證,任何改動都不會被Git忽略,這保證只要Git有紀錄,就不會出現資料丟失、不一致。

Git會使用SHA-1的方式對檔案內容進行hash,會得到一個40長度的16進位碼
比如:
24b9da6552252987aa493b52f8696cd6d3b00373
而Git儲存所有東西都是以SHA-1的格式。

三, 採用三階段架構

對檔案而言,在Git中會經歷三個階段

  1. modified: 修改過但還未commit進git database裡
  2. staged: 已將檔案標記為下一次commit的版本,但還未commit
  3. commited: commit完成,已確實被git紀錄

這三個階段也就是git工作的一個循環,如下圖

  • Working Directory就是你看到的特定版本,是從git database中抓下供修改、查看的。
  • Staging Area是一個放於.git資料夾中的檔案,負責記錄接下來哪些file會被commit,專業名詞也叫index。
  • .git資料夾是最重要的部分,存放所有額外資訊(metadata)、檔案紀錄在這裡,同時當你clone專案時,實際上就是複製這個資料夾下來!

1-3 Git設定檔

原文
設定git的環境會使用git config這個指令,而這個指令其實就是將設定值變數儲存至git的config檔案中。

git設定檔案會在三個地方,分別代表系統層級、user層級、專案層級
可以由–system, –global ,–local(default) 參數來指定位置

檔案位置(以unix-like舉例) 層級 git config參數
[path]/etc/gitconfig 系統 –system
~/.gitconfig 使用者 –global
[project]/.git/config 專案 –local(或不加)

當然通常初次設定會設在–global層級

接下來就可以設定個人資料、編輯器、預設分支名 …
比如設定個人資料

git config --global user.name "hello"  
git config --global user.email test123@gmail.com 

資料會以類似toml格式被記在設定檔中

[user]
	email = test123@gmail.com              
	name = hello                            

(熟知格式後也可以直接到.gitconfig檔案查看與修改)

最後如果加上–list參數就會顯示所有設定(git config --list)
也可以不指定值,就會變成查看該值的內容(如:git config user.name)

眾多設定變數參考這裡
比如從中可以知道,user系列是用於commit呈現作者資料。

2. 基本使用

本章是git指令的基本使用教學

2-1:git記錄變動

原文

創立一個git repository

在開始使用git前,必須要告知git這個資料夾內所有內容需要被監控(track)
如果是要開始新的專案,可以cd到目標資料夾,然後輸入git init
這個指令會創建.git資料夾,並內含所有git所需檔案。

如果是要使用其他人的專案,則可以使用git clone <目標URL>
會將他人當下版本的檔案與.git資料夾複製下來。

記錄變動

在git監控下,會有四個狀態來表達一個檔案被監控的狀態
即 Untracked , Unmodified , Modefied , Stagged

Untracked 是還未被監控的檔案,git add 後進入stagged Modified 是已被記錄的檔案被修改,但改變還未被記錄,git add 後進入stagged Stagged 是已被告知下次會被commit的檔案 Unmodifed 是已被記錄的檔案

其關係與狀態轉移參考下圖

可以使用git status查看各檔案當前的狀態

以下用一個實際例子講解
當使用git init監控一個新資料夾(或是下載別人的專案)後,輸入git status
會得到以下訊息

On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

On branch master表示當前顯示是在master這個分支,關於分支會在後續講解
剩下訊息表明目前所有檔案都是被記錄的(也就是Unmodified狀態)

接著建立一個名叫README的純文字檔案,再輸入git status

On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

nothing added to commit but untracked files present (use "git add" to track)

可以發現新增的README被git發現,並告知你它是Untracked狀態

再來輸入git add README讓此檔案被監控,再輸入git status

On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    new file:   README

此時可以說README已進入staged狀態,也就是準備被commit了

這時假如資料夾內還有另一個檔案CONTRIBUTING.md,我們先對他修改後,再 git add CONTRIBUTING.md,再輸入git status

On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

git也會幫你發現被修改的檔案,並且告訴你是modified狀態

最後有個有趣的現象,當這時再對CONTRIBUTING.md進行修改時,會發現

On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

這份檔案同時出現在stagged與not staged區,原因在於stagged是在最後修改前的版本,修改後的則出現在not staged區,很顯然此時commit,則最後修改的部分不會被記錄!

最後可以使用git status -s來查看縮短版本的訊息 會得到以下格式的結果

 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

其中各符號代表意義可以參考這裡
比如 ??表示untracked ,M表示modified …

忽略檔案

很多時候,會有不想要git追蹤的檔案,比如C/C++編譯過程的中間檔、非正式記錄檔…
這時可以將他們的檔名格式記錄在.gitignore裡 比如

*.[oa]

這個.gitignore檔告訴git不要監控結尾是.o 或 .a的檔案
而這種檔案匹配語法,叫做glob,教學可以參考這裡
比如
*代表一個或多個任意字
[]內會選擇一個字

此外還有一些特殊符號

  1. 字符 # 代表註解
  2. 字符 !xxx 代表不准忽略xxx檔案,通常用於不想記錄的檔案類型中,某些需要記錄的檔案
  3. 開頭加上/ 表示僅限於當前目錄(不遞迴執行忽略)
  4. 結尾加上/ 表示指定的是一個資料夾

以下是一個例子:

# ignore all .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in any directory named build
build/

git commit

git commit可以讓staged中的狀態被記錄成snapshot
關於git commit:

  1. 如果只打git commit,會開啟預設編輯器,並要求你輸入message
    預設訊息為git status的訊息,預設編輯器可以於config的core.editor指定

  2. git commit -m "訊息" 是最常用的commit指令

  3. git commit 的 -a 參數會跳過staged階段,將所有modified file記錄起來
    (即:不需要打git add)

  4. git commit 的 –amend 參數可以修改上一次的commit

刪除與移動

git rm <檔案>可以幫你刪除檔案並且讓git不再監控它
-f參數在檔案已被staged時強制使用
--cache可以解除git監控並不刪除該檔案 (用於忘記加入.gitignore時)
git mv則可以移動檔案(或改名)

2-2:查看commit記錄

原文 每一次的commit都會由git幫你記錄資訊
可以使用git log來查看

下面是一個git log結果的例子

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    Change version number

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    Remove unnecessary test

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    Initial commit

可以發現每一個訊息由SHA-1碼、作者、時間、commit訊息組成
並且由上至下表示從最新到最舊的commit

眾多選擇

git log有非常多的參數,這些參數可以為你呈現不同的log資訊
以下介紹一些常用的參數

-p (–patch)

可以看各次git commit修改了甚麼部分(列出實際修改部分)
如:

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    Change version number

diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
 spec = Gem::Specification.new do |s|
     s.platform  =   Gem::Platform::RUBY
     s.name      =   "simplegit"
-    s.version   =   "0.1.0"
+    s.version   =   "0.1.1"
     s.author    =   "Scott Chacon"
     s.email     =   "schacon@gee-mail.com"
     s.summary   =   "A simple gem for using Git in Ruby code."

–stat

可以看各次git commit修改了哪些檔案(只列出檔名與增修量)
如:

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    Initial commit

 README           |  6 ++++++
 Rakefile         | 23 +++++++++++++++++++++++
 lib/simplegit.rb | 25 +++++++++++++++++++++++++
 3 files changed, 54 insertions(+)

搜尋commit log

–since 與 –before 參數可以指定特定時間範圍內的訊息
–author則可以指定特定作者的commit
對Code reviewer而言很有用

其他還有指定輸出格式、以文字畫出樹狀結構…
參考這裡
設定alias加上log的整合應用

2-3:利用git復原備份

首先參考書此章內容有些陳舊(2014出版),比如使用checkout回復舊版本,使用reset移除staged狀態,而在2019年後更新了git switchgit restore來部分代替兩個指令。 參考:
原文 git checkout被拆分為git switch 與 git restore
git reset VS git restore

回復版本

當在commit前要確認當前各檔案狀態時,會輸入git status
此時其實就會告訴你該怎麼回復上一版本的狀態!

可以知道要使用git restore <file>
(稍舊版本會使用git reset 或是 git checkout 在現版本也是可行的)

  • 如果要解除staged狀態,可以加上--staged參數
  • 如果要回復到更之前的版本,可以加上--source <ref>
    <ref>可以是tag或是commit hash,並且預設是HEAD (上一次commit)

舉例如下:

# 回到上一版本的cat.txt  
git restore cat.txt  

# 回到a09942d版本的dog.txt  
git restore --source a09942d dog.txt

git reset VS git restore

參考上面的文章,簡單來說git reset 與 git restore都是可以回復檔案的危險指令,
但是git reset 會修改commit history , git restore不會,
並且git reset 會全面復原working directory , git restore則需要指定檔案。

git checkout VS git restore , git switch

2019後的git版本將git checkout指令切換為 git restore 與 git switch
原因是社群認為git checkout承擔太多任務導致令人難以區分
所以將針對檔案的切換交給restore , 將對分支的切換交給switch。

2-4 git遠端管理

原文 git身為分散式版本管理系統,很重要的工作是管理遠端專案,以利與他人協作

git remote 管理遠端URL

因為協作或異地備分的需求,git能幫你以變數名記住遠端的server URL

可以使用git remote -v查看
以下是一個例子

# 輸入 git remote -v
bakkdoor  https://github.com/bakkdoor/grit (fetch)
bakkdoor  https://github.com/bakkdoor/grit (push)
cho45     https://github.com/cho45/grit (fetch)
cho45     https://github.com/cho45/grit (push)
origin    git@github.com:mojombo/grit.git (fetch)
origin    git@github.com:mojombo/grit.git (push)

可以看出例子中以協作者名字做為url的小名
這樣以後使用pull 或 push 都不用記整串url

每一次clone別人的專案,都會有預設的origin代表原專案url
當然也可以自己加入新的,使用git remote add <shortname> <url>
以下也是一個例子

$ git remote
origin
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)
pb	https://github.com/paulboone/ticgit (fetch)
pb	https://github.com/paulboone/ticgit (push)

可以看出運用指令加入了新的url小名

未來可以使用git remote remove <url shortcut> 移除 或是 git remote rename <old shortcut> <new shortcut>改名

此外,若想深入察看一個shortcut,可以使用git remote show <url shortcut>

git clone / git fetch / git pull 從遠端得到專案

git clone <url>用於第一次複製遠端的專案,git會幫你複製專案的所有history跟branch,並且自動幫你將master branch 與遠端同步

git fetch <url>則可以將遠端資料抓回來,但不進行合併(merge) ,通常用於同步本地端與遠端資料,url通常會用remote記錄好的小名
如下圖所示

git pull <url>則可以視為 git fetch + git merge

git push 將本地專案傳回遠端

指令格式為 git push <url> <branch>
如最常見的 git push origin master (這兩個shortcut都是在clone時自動設定好的)

另外,使用這個指令必須要在你有該專案的寫入權才能使用,並且當版本被其他人更新過後,得先pull下來、解決conflict,才能再push上去。

2-5 git tag: 幫commit貼標籤

原文 某些commit通常是重要的,比如release版本、完成階段性工作 … 因此通常會使用標籤,除了未來可以直接用標籤名而非SHA-1碼找到該commit之外,也可以添加一些註解

通常會使用v1.0 , v1.1 等等版本號來為標籤命名

標籤種類

在git,標籤有分為兩種,一種是註解用的Annotated Tags,另一種則是單純命名的Lightweight Tags(通常版本號命名,結尾會加上-lw, 比如v1.2-lw)

Annotated Tags會有與commit類似的資訊,包含tag作者、email、時間與message
以下是一個範例

tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    Change version number

說明:

  • 可以看見上半部是tag訊息、下半部會呈現指向的commit的資訊
  • 若是查看lightweight tag的內容,則會只有指向的commit,也就是只有下半部

標籤操作

*查看標籤

使用git tag查看所有標籤,或是使用git tag -l "<pattern>"去搜尋標籤
以下是一個例子

$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1

*創立標籤

若要在此版本建立Annotated Tags 使用 git tag -a 標籤名 -m "訊息" 來完成
而要建立Lightweight Tags
則使用 git tag 標籤名 即可

另外,如果要指定之前版本的tag,只需要在標籤名之後加上該版本的SHA-1碼即可
git tag -a v1.2 9fceb02

*查看標籤訊息

使用 git show <標籤名>即可,前面的例子就是由此指令得到的結果

*刪除標籤

使用 git tag -d <標籤名>即可

*上傳至遠端

對git來說,tag也算一個分支,只是他必定指向一個commit而已,因此當上傳沒有指定他時,並不會自動上傳,需要輸入git push origin <tagname>

3. 分支介紹

3-1 基礎介紹

原文 分支(branching)功能通常存在於每個VCS中,他可以讓你避免搞砸主要開發線的同時開發其他測試功能。

但是git有別於其他系統,git擁有快速、低成本的branching功能,這也是他能在其他VCS中脫穎而出的關鍵功能(killer feature),接下來讓我們從git底層去了解git的分支功能。

三種物件 與 儲存架構

git 在儲存檔案上,會使用到三種物件,亦即commit , tree 與 blob
每個物件都會由SHA-1賦予獨特的一個40字元的編碼。

commit物件

除了會記錄作者、時間、commit訊息…等基本資料,也會記錄專案根目錄的tree物件指標,也就是抽象的snapshot

同時,commit會再用一個指標指向它們的parent,也就是前一狀態的commit物件,有趣的是,commit物件通常只有一個parent,但還是有其他可能
當commit是第一個時,會有0個parent
當commit是被合併後的結果時,會有多個parent

blob物件

記錄檔案內容的物件,是這個物件儲存樹架構的末端節點

tree物件

每個資料夾都會被記錄成tree物件,會紀錄多個指標,指向其內容物

因為資料夾內往往還會有資料夾,所以tree物件可以內含blob與tree物件

用表達式可以寫成 tree := tree | blob

這三種物件可以構成一個專案的儲存架構
下圖是一個例子

談談甚麼是branch

所有的branch在git裡都只是指向某個commit的指標而已! 事實上,tag也是指向commit的指標,所以在上傳遠端時才也需要指定tag,它可以視為只有一個commit的branch。

如下圖

說明:

  • snapshot就是tree物件、blob物件組成的樹狀結構
  • master是一個特別的分支,因為在git init時就會預設好,由於通常大家不怎麼會去改名它,所以通常專案都會用master作為主分支
  • HEAD pointer會指向當前版本的分支pointer作為識別
  • master等分支pointer與HEAD pointer在新的commit物件產生後,會自動移動指向新的commit物件

(重點概念: 分支、tag是指向commit的pointer , HEAD是指向commit pointer 的pointer )

branch創造的流程

以下用一個例子來解說實際上造成分支版本差異的流程
第一步: 創造另一個分支,也就是創造一個指向當前commit的pointer
此時兩個版本未有任何差異。

第二步: 切換到另一個分支,也就是移動HEAD指向新分支pointer。

第三步: 在新的分支進行commit,也就是創建一個新的commit物件,並將它指回原版本,然後也將其資料夾、檔案用tree,blob物件記錄(並且SHA-1 hash過)
此時 兩分支差異 與 一般上下版本 差異是相同的。

第四步: 切換回原分支,再進行一次commit,此時就能發現兩分支會出現差異了!

實際指令

  • 創造新分支使用git branch <branch_name>

  • 查看所有分支使用git branch
    此外還可以加上-v參數查看branch最近的commit 加上--merged--no-merged列出有被merge過或沒被merge過的分支 (方便刪除)

  • 切換分支使用git switch <branch_name> (舊版本用 git checkout <branch_name>)

  • 刪除分支git branch -d <branch_name>

  • 改分支名git branch --move <old_branch> <new_branch>

3-2 分支合併

原文

接下來將探討如何將分支合併,比如解決一個bug,或完成開發版本 需要與原版本結合時 可以使用

能用git merge <branch_name>完成這項工作
並且使用這個指令的分支為合併主線,將會移動合併主線的pointer到commit結果

用以下例子說明

Fast-Forward

若想要將master與hotfix合併,會先切換到master,然後輸入git merge hotfix
得到以下結果

Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

就會看到一種merge類型:Fast Forward
這表示git合併的方式不會創造新的snapshot,單純的將master往下移動到hotfix的位置,如下圖

這會發生在合併的兩個分支有上下關係的時候。

完成合併後,可以將hotfix這個分支(再次強調,分支只是一個指標)移除。

Three-way merge

若想要將上圖的iss53與master合併,則一樣先切換到master,再輸入git merge iss53
得到以下結果

Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

此時commit history會變更為以下情形

git 因為發現iss53與master指向的commit並不是上下關係(祖孫關係),因此會採用"雙方的共同祖先"(C2) 與 master(C4)、iss53(C3) 進行三方合併,並且將新的結果紀錄為新的commit

merge conflict

多數情況git會自動的完成merge,但當兩個分支修改了相同的地方,(比如修改了同一行、或刪除了另一個分支的檔案),這時git會提醒你需要手動解決merge conflict

首先,會收到類似以下的訊息,告知你自動merge失敗了,然後git會 暫停整個過程,留下部分merge結果與衝突的檔案等待修正

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此時可以輸入git status查看狀況,比如以下結果

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

就可以發現在哪個檔案、又為甚麼發生conflict

接著打開發生問題的檔案,找到conflict處,會看到以下內容

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.htm

conflict記錄的格式都會是如此,以=======區分兩個分支的內容
在此例子中,HEAD內容在上半部,iss53內容在下半部
這時就可以讓你進行手動選擇要留下哪些內容

(記得改完也要刪除 <<<<<<< >>>>>>> ======= 部分)

最後再記得進行git add git commit 即可完成merge

merge conflict 進階技巧

1.放棄merge
當merge發生衝突,又想晚一點再處理
可以使用 git merge --abort

2.忽略空白
很常發生merge衝突原因出在沒意義的空白上,因此git提供兩種指令
-X ignore-all-space
忽略所有的空白差異

-X ignore-space-change 則是將一個空白與多個空白視為相同

3.選擇一邊
如果發生二進位檔案,或是已經確定整個檔案該使用哪一邊
可以在merge conflict發生後
使用 git checkout --ours <file> 來選擇合併主線的檔案
或是 git checkout --theirs <file>來選擇合併對象的檔案

3-3 雜項知識 - 遠端分支、rebase

原文

遠端分支

當從遠端拉下一個被git監控的專案,git會自動幫你建立遠端分支(remote-tracking branch),格式為<remote>/<branch>,比如常見的origin/master,並且也會幫你建立預設的master分支

如下圖

可以觀察到在遠端看到的只會是該專案的master
但拉回本地端後,則會自動建立遠端的master(origin/master)與本地端的master

遠端的分支(Again,一個commit pointer)在本地端是無法主動移動的,其功能類似tag,告訴使用者(上一次更新後)遠端的進度在哪裡,只有在下一次遠端pull回來更新後,遠端分支才會移動到他的位置

至於同步遠端的方法與指令,在2-4遠端管理已說明過

rebase

rebase基本上作用與merge是相同的,也就是結合兩個分支

但是使用rebase可以得到更乾淨的歷史紀錄
如下圖

原本experiment分支指向C4,接著輸入git rebase master
會與git merge master一樣得到一個新的commit,並將合併主體的ref移至新的commit
但不一樣的是合併主體的歷史紀錄會消失,留下更乾淨的線性歷史紀錄

但是當分支存重要的commit紀錄、或是分支有重要的意義(比如開發新功能時),非常不建議使用rebase,這會讓那些紀錄消失

所以,merge還是rebase? 這個問題答案端看你怎麼理解當前的git history
這裡提供一個看法
以備份角度,應當使用merge以留下紀錄,而以作品角度,應當使用rebase以保持整潔。

4. git 底層

學習git底層,除了可以更加瞭解git運作原理以外,也有機會能自己寫script個人化甚至最佳化git的使用。

git會創建.git資料夾紀錄專案的所有歷史、以及輔助用的資料(比如 stage中的檔案)
因此要探究git底層就必須了解.git資料夾

以下是一個新的專案,在輸入git init開始監控後,.git資料夾預設的內容

config
description
HEAD
hooks/
info/
objects/
refs/

*config檔案1-3:Git設定檔簡介已介紹過
會記錄git config所輸入的設定,並以toml格式儲存在此

*description檔案會記錄這個專案的簡介

*hooks資料夾會放置個人化設定的小程式,參考原文書

*HEAD檔案如其名、會寫入HEAD指向的commit的檢查碼

*info資料夾內會有exclude資料夾,放置.gitignore檔案中不想被記錄的檔案pattern

*objects資料夾則是database的核心,存放儲存的三種物件的內容

*refs資料夾則會記錄各個tag , branch , remote … 等參考點

*此外等有一些內容後,也會有 index檔案 紀錄 staging area的資訊

以下將著重介紹比較重要的底層概念

4-1 .SHA-1演算法 與 git物件

參考資料 - 資訊與網路安全技術:第四章 雜湊與亂數演算法 參考資料 - SHA演算法結果一定是固定長度嗎

為甚麼要使用SHA-1

複習:

  1. git會用blob物件紀錄檔案內容、tree物件紀錄資料夾、commit物件紀錄每一次snapshot
  2. git物件都會有一個檢查碼(checksum),由40個16進位數字組成

SHA 為 Secure Hash Algorithm 的縮寫,簡單來說是一個加密演算法,而git在此則把它當作一個壓縮函數,用來節省紀錄成本與檢查碼

SHA-1可以輸入不限長度的訊息,並得到固定160bit長度的輸出,(此輸出也被稱為訊息摘要),這訊息摘要也就是git的檢查碼,檢查碼是由40個16進位組成,也SHA-1加密後的160bit信息摘要

SHA-1具有一個好的hash function應有的特性,不同內容的明文擁有相同輸出的機率很低,所以適合作檢查碼 — 40長度的檢查碼相同 => 基本確認檔案內容相同 ,不用一一核對內容

SHA-1概念上怎麼做的

首先將 以bit表示的明文(內容) + 64bit的長度資訊 + 調整長度至512倍數的部分 每512bit切成一個區塊

附註:

  1. 調整長度是為了讓整個值變成512的倍數,比如今天有136bit的明文、64bit長度資訊, 則需有 512 - (136+64) = 312 位元的調整長度

  2. 長度資訊讓我們知道,演算法加密最長明文可以到 $2^{64}$ bit大小

接著將輸出結果(訊息摘要)設一個初始值,然後透過迭代的使用函數,輸入區塊與訊息摘要,將訊息摘要不斷更新,直到所有區塊完成,最後結果就是160bit的訊息摘要

以下虛擬碼解釋

Output = 67452301EFCDAB8998BADCFE10325476C3D2E1F0  #初始的160bit值
for block in all512LenthBlock:  #對不定長度的明文切割好的每一區塊
    Output = HashFunction(Output , block)

以下圖解

因此可以看出,就算長度不定,檢查碼也仍會是160bit

更深入說一點

  1. 160bit會被拆成5個32bit的輸入,並各自有計算方式
  2. 每一個block其實還會被擴充成80份32bit,並且17~80份會以前16份(512bit原始區塊資料)製造出來 (這個步驟被稱為訊息擴充,是為了增加保密性)
  3. 事實上每一次HashFunction的計算就會吃入6個暫存器的資料,即5個160bit訊息摘要字串與1份小block
  4. HashFucntion會加入常數(鹽值 salt value)、做shift、以及使用4個邏輯函數去進行雜湊

SHA-1 碰撞

這個小節來探討SHA-1發生碰撞的概率 — 意即不同物件產生同一個160bits的機率

先分析一個特定物件與另一個特定物件碰撞的機率,為 $1/2^{160}$
接著,所有物件都有可能是那兩個"特定物件”,因此要探討n個物件裡,任選兩個,然後碰撞
因此發生在n個物件中,至少發生一次碰撞的機率為${n \choose 2} * 1/2^{160}$

令機率為50%,可以求出n大約等於$2^{80}$

用個例子來說,地球上每個人類,在每秒commit Linux Kernel的檔案(上百萬行程式),也需要花兩年時間才有50%的機率發生一次碰撞

因此基本上可以不用擔心碰撞問題

然而若真的發生碰撞,git會誤以為新的檔案已在資料庫裡,而不更新新的程式

git 底層如何儲存物件

git是以 key-value的資料庫方式去儲存物件的內容,key就是經過SHA-1過後的160bit檢查碼,value就是物件內容

而這個資料庫被放在.git/objects資料夾中

.git/objects會以40個16進位檢查碼的前兩個分資料夾,裡面再放剩下的38個16進位碼長度命名的檔案,檔案內容就是物件的2進位碼

底層指令git hash-object可以讓你將任意內容的檔案紀錄進objects資料庫(資料夾)
也是git commit後的pipe部件之一

以下是一個例子 Example

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

其中 -w 會將結果放入objects資料夾中, –stdin則會將結果輸出在畫面上

底層指令git cat-file可以將檢查碼去資料庫內查詢(注意,不是將檢查碼還原,沒那麼厲害XD,也不是給定任意hash都可以有結果,需要紀錄在資料庫內才行),得到物件的binary紀錄,然後顯示出來

以下是一個例子 Example

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

註: -p 參數可以查看物件內容
-t 參數可以查看物件type ( tree , blob , commit)

git 清理機制

git 有提供 git gc 與 config中有gc.autogc.autopacklimit等參數設定
可以在檔案太大時將重疊的檔案(如上下版本關係)合併(package),以節省空間

正常情況下git會記錄各版本檔案所有內容(即使只加一行,也會重新再記一份),頂多再壓縮一次,這種紀錄方式稱為loose object

當loose object超過7000個,或是你主動使用git gc 指令,git就會幫你將類似的檔案package,讓上下版本只記錄其差異

git實作這套方法的概念為: 尋找上下版本間檔案大小類似、檔名相同者 (所以不宜隨意更改檔名)

4-2 git REF: 節點的名字

複習

  1. git 是一個key-value的資料庫系統
  2. git的key是由SHA-1依物件內容的二進位檔進行加密,value則是二進位檔本身
  3. 每個commit都是一個物件

所以,可以用40長度的檢查碼唯一定位到一個commit,也就是一個版本
branch , tag 也是此原理
(當然,git也允許用縮短版的檢查碼去定位,只要縮短版沒有ambiguous,通常用前7碼)

而git可以幫檢查碼取人類比較好記的代號(就像DNS一樣),比如branch會有master , developed,HEAD代表當前commit…)

這些代號以檔名儲存在.git/refs中,並且這些檔案裡面即紀錄40長度的檢查碼

git 也提供git update-ref指令讓你修改ref內容

git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9