Git觀念

Git的特點

  • 分散式開發
    • 每個人都有完整的容器,各自獨立
    • 不需要中央管理
  • 非擠壓合併
    • 合併後仍包含所有被合併分支的commit記錄
    • 也可以用 git merge/pull --squash 強迫擠壓(不推薦)

Git安裝

  • Debian/Ubuntu: 主要: git-coregit-doc
    • 選擇性: git-gui, gitk(圖形化), gitweb ...
  • Windows: Cygwin, msysGit, github on windows

Git常用名稱

  • origin: 遠端(remote) Repository 的別稱,預設即為origin/master分支

    • git clone的時候會自動設定origin
  • master: 本地(local) Repository 的 master分支

    • 名為master的分支通常是專案中的主要分支
  • HEAD

    • 目前分支的最新一個提交
  • 相對名稱

    • HEAD/HEAD^/HEAD^^/HEAD~4
      • 代表目前版本/前一版/前二版/前四版的提交
    • HEAD^1/HEAD^2/ ...
      • 代表第一個父提交,第二個父提交 ......
    • A...B
      • A和B的相對差異
      • 列出A和B的祖先,直到A和B的分歧點
  • git rev-parse HEAD: 取得代稱(HEAD^, HEAD~1, tag)的辨識碼

  • ORIG_HEAD:合併或重設前的HEAD

  • FETCH_HEAD:fetch所抓取的HEAD

  • MERGE_HEAD:合併時,另一個分支的HEAD

設定檔

  1. 容器的設定檔 .git/config
  2. 使用者的設定檔 ~/.gitconfig
  3. 全系統的設定檔 /etc/gitconfig

優先級: 1 > 2 > 3

Git資料結構

Index 和 Data:[Blobs, Tree, Commits, Tags]

  • Data
    • Blob(binary large object): 檔案本身
      • 新增檔案時,看的是檔案的sha1,而不是檔名
        • 若有兩個檔案sha1相同,則git只會有一份blob
      • 打包機制(pack file):找內容相似的檔案,只儲存一份+差異的部分
    • Tree: 目錄資訊,指向Blobs
      • 記錄Blob的辨識碼,檔案的資訊(如檔名),子Tree(子資料夾)等
      • Git可以用Tree快速產生兩個版本間的差異
    • Commit: 更動資訊,新的Commit會指向前一個Commit
      • 包含作者,時間,commit對應的目錄(Tree),commit message
    • Tag: 記錄commit的別名
      • lightweight tag
        • 容器私有的
      • annotated tag
        • 正式的tag,以物件方式儲存
  • Index
    • 即為Staging area(暫存區)
    • 可視為一顆Tree,在git add之後就將新檔案加入Tree中
    • commit時,commit的Tree就是目前的index

版本和Tag都是指標,指向某個commit

分支

  • 可視為指向某個commit的指標
  • 切換的時候工作目錄(容器所在的資料夾)會被改變
  • 用途
    • 分隔 測試、開發、穩定的版本
      - debug分支
      - feature分支
  • 命名時可以分層
    • bug/...
    • release/...
  • 開發時用branch(pointer),完成後可用tag取代(const pointer)
  • 分支是local的資訊
    • 平常上傳只會將目前的HEAD和遠端分支的HEAD同步

分支種類

  • 唯讀
    • 遠端追蹤分支:蒐集遠端每個分支的變更
    • 本地追蹤分支:蒐集本地分支和遠端追蹤分支的變更
  • 本地分支:即平常使用的分支
  • 遠端分支

合併

每次提交合併的時間間隔愈短,每次產生的衝突愈少,愈容易合併。

同一條線上的合併

  • 不會有新提交,只會移動branch指標
  • 目前版本比被合併版本新 → Already up to date
  • 目前版本比被合併版本舊 → fast-forward(快轉)
    • 將目前版本移動到被合併版本的HEAD
    • git只允許fast-forward的push

正常的合併

  1. Resolve(直觀的方法)
    • 只能用在兩個分支的合併
    • 以共同的祖先為基礎,套用被合併版本的變更到目前版本
  2. Recursive(預設方法)
    • 只能用在兩個分支的合併
    • 兩個版本有多個共同祖先的時候,先將所有祖先合併成一個暫時性的版本,再以此版本為基礎用Resolve方法
  3. Octopus
    • 可用在多分支的合併
    • 多次呼叫Recursive,每次處理一個分支

特殊合併

  • Ours
    • 合併時只採用現在版本的變更
      • 但留下歷史記錄(即其他版本的樹仍存在於父節點)
    • 通常用於
      • 已經有其他版本的變更時
      • 只想要別人的歷史記錄,不想要其他人的變更

修改提交

理由

  • 大小適當
    • 將大範圍的提交拆解成小的,有主題的變更
    • 結合相似的變更成為一個大的提交
  • 步驟分明
    • 排序使其更合理
    • 移除不必要的提交
  • 註:需要在其他開發者取得你的repository之前修改(git push之前),以避免歷史不同步

方法

  • git rebase

回復(git reset)

  • --soft: 將HEAD還原至指定commit
  • --mixed: 將HEAD和Index(staging area)還原至指定commit,工作目錄不變 (預設選項)
  • --hard: 將HEAD和Index和工作目錄還原至指定commit
  • 被取代的HEAD會放在ORIG_HEAD
1
2
3
4
5
6
git reset [--soft/mixed/hard] [commit]

# 變更上一次的提交
git reset HEAD^ # 此時上一次的提交已經不存在,但是工作目錄中的資料相同
# do some modification
git commit # 會覆蓋上一次的commit

通常用於清除錯誤

  • cherry-pick
    • 拿取指定commit到目前分支
    • git cherry-pick [commit]
    • 使用時機
      • 若有多個branch都找到相同的bug,可以在每個branch都cherry-pick debug的commit
1
2
git checkout release
git cherry-pick develop~2 # get debug commit from develop branch
	- 將某個分支的commit移植到另一個branch
1
2
3
4
5
6
7
8
# in develop: W → X → Y → Z (newest)
git checkout master
git cherry-pick develop^ # Y
git cherry-pick develop~3 # W
git cherry-pick develop~2 # X
git cherry-pick develop # Z
# alternative
git cherry-pick develop^3..develop # from W to Z

容器

  • 裸容器(bare)
    • 沒有工作目錄、目前分支
    • 其他人可以用clone及fetch取得資料,push更新
    • 如github
    • 產生: git init --bare
  • 開發容器

建立備份

自行建立裸容器,並加入remote,即可push, pull了

1
2
3
4
5
6
7
8
# assume mygitrepository is already a git repository
cd /tmp/Backup # for example, you may want to put on server
git clone --bare mygitrepository mygitrepository.git # create bare repository
# backup repository often use .git suffix
cd mygitrepository
git remote add origin /tmp/Backup/mygitrepository
# can use other name to replace "origin"
git remote update # 建立遠端追蹤分支(update remote information)
1
2
3
4
5
ssh myhost.com # build it on server
cd /git
mkdir newrepo.git
cd newrepo.git
git init --shared --bare

Git tag

  • 輕量級(lightweight)
    • 像是沒有更動的分支
    • 指到特定commit的指標
    • 臨時加註標籤
    • git tag [tagname] [commit]
  • 含附註(annotated)
    • 實際存在Git資料庫上的完整物件
    • 具備檢查碼、e-mail和日期,也包含標籤訊息
    • 可以被GNU Privacy Guard (GPG)簽署和驗證
    • git tag -a [tagname] -m [tag message]
    • git tag -a [tagname] [commit]
  • 顯示詳細資訊: git show [tagname]
  • git push指令並不會將標籤傳到遠端伺服器上。必須透過git push origin [tagname]指令
    • push 所有標籤: git push origin --tags

Git Submodule

  • 新增 submodule
    • git submodule add [repository path] [local path]
    • 產生 .gitmodules 文件。這是一個設定檔,保存了專案的remote URL和本地目錄
  • git clone並不會下載submodule,需要自己更新
  • git submodule init
    • 根據 .gitmodules 的名稱和 URL,將這些資訊註冊到 .git/config
  • 更新submodule
    • git submodule update
    • 根據已註冊(也就是 .git/config )的 submodule 進行更新,例如 clone 遺失的 submodule,所以執行這個指令前最好加上 --init
    • update並不一定是submodule的最新版,而是在主repository的目前版本中,submodule所在的版本
  • 將submodule更新到最新版
    • git submodule foreach --recursive git pull origin master
  • git submodule sync
    • 如果 submodule 的 remote URL 有變動,可以在 .gitmodules 修正 URL,然後執行這個指令,便會將 submodule 的 remote URL 更正。

Git Server Hosting

  • gitlab
    • 功能齊全
    • 有開源版,可自架
  • cgit
    • cgit主網站
    • 需開啟cgi功能
      • ./cgit.cgi 可觀看產生的html
    • 安裝成功後在/etc/cgitrc中修改設定,否則404
    • 參考lantw44的設定檔
    • 複製git裸容器
    • 快速複製github中的repository
    • cgitrc詳細設定

其他工具

  • 產生容器的統計資料
    • git quick stats
    • gitstats

Git 常見錯誤

1
Error: The following untracked working tree files would be overwritten by merge
  • 原因:本地端尚未進版本控制的檔案,在remote branch已經在版本控制中,所以報錯
  • 解決方法:先將這些檔案放入stash中,等到pull完再還原
1
2
3
4
5
git add * # 加入 stage
git stash # 執行 stash(stage會將stage的檔案也放入stash)
git pull ...
git stash pop # 還原 stash
git reset # 回復 stage 狀態

參考資料

大檔案管理 - Git lfs

Large File Storage (LFS) 是 git管理非文字大檔案的擴充套件,可以將大檔案放在放在git repository之外,這樣可以減少repository的大小,讓repository的clone速度變快。

需要注意的是,LFS是一個擴充套件,所以需要額外安裝

lfs的檔案獨立於repository,只用.gitattributes記錄檔案名,所以需要額外的指令來add/push/pull

lfs雖然支援刪除檔案,但在Github中不行,代表所有曾經上傳到Github lfs的檔案都會一直占用著Quota,目前還沒有解決方案,只能刪除原本的repository。
或是使用bfg-repo-cleaner將需要刪除的檔案完全從記錄中移除,然後將清理後的repository重新上傳至Github。

Commit Message 規範

參考Conventional Commit
如果遵守此規則,可以自動產生一個版本的commit message總結(CHANGELOG)和語意版本號;使CD/CI工具自動觸發。

1
[分類] 描述 (備註)

分類可參考下列[1]

  • feat: 新增功能
  • update: 修改功能
  • fix: 修正bug
  • chore: 其它工具的變動(通常是底層library或開發工具)
  • refact: 重構、整理程式碼
  • style: coding style調整 (format、空格或Tab、逗號等)
  • docs: 檔案、註解撰寫
  • build: 打包

描述:這個commit修改了什麼功能,可以用一行的短描述加上詳細描述

備註:可以是人名(commit作者、reviewer或負責人),編號(專案代號,issue流水號),參考資料(相關文件路徑,spec連結)等。
當然commit作者在commit資訊就有了,大多數的資訊寫在issue tracker即可,寫在commit中只是方便搜尋。

e.g.,

1
2
3
4
5
6
[feat(search)] 用關鍵字搜尋網站頁面 (Reviewer: Alice) (#113) (Wiki: PageSearch/Keyword)
(BREAKING CHANGE: database webpage的key從datetime改成id)

[fix] 修正點擊xx按鈕時沒有回應的問題 (Reviewer: Bob) (Fix #114)

[chore]winrar升級至正式版 (#115)

將隱私檔案和git repository分開

隱私檔案:如設定檔、資料庫、內部檔案路徑、金鑰、個人資訊等等

最簡單的方法就是:不要放在repository內,缺點就是沒有版本控制、取得麻煩、可能會遺失。

以下提出一些將隱私檔案放在repository的方法

  1. 開兩個不同的repository,public-repoprivate-repo(不公開),將隱私檔案放在private-repo
    1. 需要使用隱私檔案時,clone private-repo,並合併到public-repo
    2. 缺點
      1. 合併兩個repository困難,這代表兩個repo需要有相同的資料夾結構,不然不易移動
  2. 多一個branchprivate,此branch可以視為完整的repo,包含public的所有內容和隱私檔案
    1. 只要保護 private branch 即可
      1. private branch 設立讀取權限
    2. 小缺點:需要額外維護一個branch
  3. private-repo作為public-repo的submodule
    1. 缺點:代表隱私檔案需要放在同一個資料夾內

將多個repository合併成一個

我目前(2023)有六十多個repository(包含private repo),主因是一個專案就丟一個repository,像是大學的一門課就是一個repo,實在有點難管理。

可以把多個小專案合併成一個,如作業

方法[2]

  1. 要移動的repo
    1. 開新branch
    2. 把所有檔案移動到新的子資料夾
    3. add 並 commit
  2. 要接受合併的repo
    1. 在remote中加入1. 的repo
    2. fetch branch
    3. merge (通常需要--allow-unrelated-histories)

此方法可以保留每一個被合併的repo的歷史

Reference

  • Git-基礎-標籤
  • 版本控制:使用Git
  • git 設定集
  • https://dotblogs.com.tw/hatelove/archive/2011/12/25/introducing-continuous-integration.aspx
  • git: get ready to use it
  • Git Submodule 用法筆記
  • Git 工具 - 子模組 (Submodules)
  • How to delete a file tracked by git-lfs and release the storage quota?
  • Make some files private in a repository

  1. https://tiffrrr.medium.com/git-%E6%98%AF%E6%99%82%E5%80%99%E8%A6%8F%E7%AF%84commit-%E8%A8%8A%E6%81%AF%E4%BA%86-bf02f33ed8f5 ↩︎

  2. https://stackoverflow.com/a/10548919 ↩︎