Try   HackMD

Git 版本控管實戰:新手進階篇

Git 內部架構解析

版本控管(Version Control)

  • 完整記錄軟體變化的過程(人、事、時、地、物)
  • 紀錄版本變化而𧗠生出
    • 查詢歷史紀錄
    • 復原變更
    • 比對差異
    • 標記版本
    • 變更追蹤
  • 多人版控進一步衍生出
    • 協同作業
    • 分支合併
    • 版控流程
    • 發行管理

分散式版本控管(DVCS)

  • 優點
    • 本地端的工作區會保有完整的儲存庫
      • 每個人都擁有一分完整的儲存庫備分(Full Backup) => 分散式
      • 不需要 Server 支援即可運作版控 => 不用網路連線,大幅節省開發時間
      • 操作都在本地儲存庫中 => 快、離線的版本、完整的歷史紀錄
    • 擁有強悍的合併追蹤能力
      • 取得他人變更版本後,可透過合併方式進行整合
      • 合併多人的版本只要有存取共用儲存庫的權限或管道即可
  • 缺點
    • 無法鎖定版控策略(僅能使用合併策略)==> 專注在自己的分支
    • 無法針對專案進行精細的權限控管
      • 若要鎖定特定資料夾,可透過 Submodules 實現 (Owner 完整的 repo,其他人則在 Submodules 內進行)

工作區(Workspace / Work Tree)

https://gitbook.tw/chapters/using-git/working-staging-and-repository.html

  • 頻繁異動的開發目錄
  • 在工作區執行任意 git 命令
  • 內含 .git 隱藏資料夾(本地儲存庫)
  • 沒有 .git 目錄 => git init
  • 刪掉 .git 目錄 => 脫離版控
  • 擁有 .git 目錄 == 擁有所有原始碼
  • 工作區底下檔案的狀態
    • untracked
    • tracked
      • unmodified
      • modified
      • staged

儲存庫(Repository)

  • 本地儲存庫(Local Repository)
    • 預設位於 .git 資料夾
  • 遠端儲存庫(Remote Repository)
    • 儲存庫 (Bare Repository)
    • 實際上是將本地的 .git 資料夾上傳到遠端儲存庫
    • 透過 git clone --bare 僅下載遠端儲存庫回來
  • 共用儲存庫(Shared Repository)
    • git init --bare 建立共用儲存庫
    • 並不是僅限於git Server,只要是能遠端同步的工具,都可以當作遠端儲存庫使用
      • Dropbox、Google Drive etc
      • 再用 git clone file:///path/to/repo.git/ checkout 即可

--bare

--mirror

了解 Git 資料結構

物件(Object):immutable

  • Git Internals - Git Objects
  • 用來保存儲存庫中所有的檔案版本紀錄
  • 類似當下狀態的 snapshot
  • 區分四種物件類型
    • blob
      • 檔案內容
      • git hash-object <fileName> => 根據檔案內容產生物件名稱
    • commit => 提交
      • author 和 committer 可以不一樣(ex. 重新 commit:rebase、commit --amend
    • tree => 目錄
      • 檔案名稱不變,內容改變 => 新的 tree
      • 檔案名稱改變,內容不變 => 新的 tree
    • tag
      • annotated tag 為物件型態(with -a property):以二進制檔案型式儲存
      • lightweight tag 為參考型態(without -a property):以文字檔案型式儲存 (不會產生 tag 物件)
    • 可使用 git cat-file 可以知道 git objects 的類型
      • git cat-file 看到的 ID 為檔案內容所運算(SHA-1 hash)而來,若 git object 檔名相同(即 ID 相同),則代表檔案內容一致
        • -t : type 類型
        • -p : print 內容
        • -s : size 檔案大小
      • 以下圖為範例
        ​​​​​​$ git cat-file -t a821ff3dfa531821967cc0380570bb01356c05a2 ​​​​​​tree ​​​​​​# 放置 git object 之資料夾名稱 = a8 ​​​​​​# git object 檔案名稱(ID) = 21ff3dfa531821967cc0380570bb01356c05a2 ​​​​​​# 使用 sha1 值前 2 碼當作目錄名稱,目的在於提升讀取效率
        ​​​​​​.git
        ​​​​​​|
        ​​​​​​...
        ​​​​​​|
        ​​​​​​|- objects  // 所有資料都在 objects 底下
        ​​​​​​|  |- a8
        ​​​​​​|  |        21ff3dfa531821967cc0380570bb01356c05a2
        ​​​​​​|  |
        ​​​​​​|  |- f5
        ​​​​​​|  |        eea678d87a8664e4c76e12d3ef5c4ff775ad58
        ​​​​​​|  |
        ​​​​​​|  |- fb
        ​​​​​​|  |        8821d9a4cd57a2e6c5e4ae6a6706bcb89f2ce7
        ​​​​​​|  |- info
        ​​​​​​|  '- pack
        ​​​​​​'- refs
        ​​​​​​    |- heads
        ​​​​​​    '- tags
        
      • 頻繁變動的大檔案不應 commit 到 Git (因為占空間)

索引(index)

  • 記錄有哪些檔案即將要被提交到下一個 commit 版本中 (保存要進儲存庫之前的所有檔案狀態)
  • git add 會將 tracked file 記錄到 git index file(git file status => Staged),並且建立一個 blob 物件(Blob 對象
    • working directory => index => object storage
    • 俗話說的好:有 git add 有保佑,有 git add 過都會有檔案的 blob,即使沒有 commit 指向它
  • git commit 時,會建立 commit object file,並指向 git index file 中異動檔案的 blob 物件
  • 屬於一種「可變的」(mutable) 檔案類型
  • 主要位於 .git/index 檔案
  • 包含了所有版控中的檔案(可利用 .gitignore 排除檔案受到 index 版控)
  • 空資料夾是無法加入版控的 (通常會用 .gitkeep 空檔使資料夾加入版控)
  • 工作區與索引的檔案為一對一Mapping
  • 索引有可能錯亂 XD (壞掉時將 index 砍掉重建就好了)

Git 物件結構的優點

  • 有效率的處理大型專案
    • 檔案內容換算 SHA1 Hash => 絕對不可能發產生衝突
  • 歷史紀錄保護
    • 每個版本包含上一個版本的 hash 值
    • 檢查 git 儲存庫的完整性:git fsck
  • 定期的封裝物件
    • 對於不常用的物件會自動進行壓縮處理
      • git gc => 壓縮到 objects/pack 目錄下,同時會將不常用到的物件刪除
        • 使用 git gc --prune=now 會立刻清除 Unreachable 物件
          • 等價於 git gc + git prune --expire=now
      • ex: git add . 後又修改了同一份檔案後再次 git add . ==> 產生兩個 blob 物件,但 commit 僅參考到最後一份 blob 物件,第一份 blob 物件會在 git gc 時被清除

儲存庫、工作目錄與索引的關係圖

   |                            |                       |               |
   |------------------ git commit -a ------------------>|               |
   |                            |                       |               |
   |------- git add (-u) ------>|----- git commit ----->|               |
   |                            |  commit / tree / tag  |               |
   |                            |                       |-- git push -->|
   |                            |                       |               |
工作目錄                        索引                 本地儲存庫      遠端儲存庫
   |                            |                       |               |
   |<---------------------------- git pull -----------------------------|
   |                            |                       |               |
   |                            |                       |<- git fetch --|
   |                            |                       |               |
   |<----------- git checkout <branch_name> ------------|               |
   |                            |                       |               |
   |<-- git checkout -- file ---|                       |               |
   |                            |                       |               |
   |                            |-- git diff --cached --|               |
   |                            |                       |               |
   |--------- git diff ---------|                       |               |
   |                            |                       |               |

git checkout

從索引內取得檔案 / 資料夾內容

--

請參考:還原

Git Reset 應用技巧

  • https://dotblogs.com.tw/wasichris/2016/04/29/225157

  • 主要用途:將當前分支復原變更

  • --mixed | --soft | --hard

    • --mixed [預設值] 工作目錄變更會保留,也就是保留 working directory 中的異動內容 (僅更新索引及指標位置)
    • --soft 工作區目錄變更會保留,同時也保留 index 中的異動內容
    • --hard 工作區檔案內容未 commit 的會被直接還原到指定版本
    • --soft vs. --hard
    Reset 模式 所在位置(HEAD) 變更狀態紀錄(INDEX) 工作目錄 說明
    soft changed unchanged unchanged 僅移除 commit 變成新版未 commit,內容仍是新版的
    mixed changed changed unchanged index 移除 staged 標記,變成 Modified or Untracked,內容是新版的
    hard changed changed changed 回到上一版版本,其間變更完全移除(接近 svn revert),內容及狀態皆是上一版
  • 取得檔案變更應使用 git checkout 而非濫用 git reset

  • git reset -p: 互動式選擇還原區塊

復原最近一次重置 (reset)、合併 (merge) 或 重訂基底 (rebase)

  • git reset --hard ORIG_HEAD: 復原重大變更的前一版

git reflog

  • 利用 git commit object 在 git gc 之前都存在的特性,將因 reset 而沒有指向的 commit 調出來
    • reflog 列出來的內容是做完指令的結果,所以要還原必須再往前找一版

commit --amend

Git 分支合併技巧

分支 (Branching)

  • 一個指向某個 commit id 的 pointer
  • 每一個 commit 是將 index 的內容寫入 respository(請參考索引一節)
  • branch、lightweight tag(非 -a)、HEAD 都是一種指向 commit 的指標(以文字檔案型式儲存著 commit id)
  • 分支圖是從 refs 中找出所有儲存指標的檔案,並將它們所指向的 commit 及其 parents 繪製出來
  • HEAD 指向一個沒有 branch 指向的 commit => 稱為 detached HEAD
  • 分支刪除並不會保留任何紀錄

孤兒分支(orphan)

  • git checkout --orphan <branch_name>
  • 沒有 parent 的 commit object
  • 一個 repo 中可以有多個 root commit
  • 範例:
    • GitHub Pages
    • 台北捷運
    • 設計師將寫好的版型放到 orphan 分支,前端工程師比對差異進行套版

關於「分支」的真正意義

  • 標記"時間維度"的指標
  • 「分支 Branch」是一個會隨時間演進的指標,當下時間點的索引狀態 (開發用)
    • 任何一條分支的異動,都不會影響其他分支
  • 「標籤 Tag」是一個不會隨時間演進的指標 (參考用)
  • 「切換分支 Checkout」等同於「控制時間維度」

檢示所有分支詳細資訊

分支更名(改名)

合併「不同世界」的分支

  • git merge --ff(default)=> 快轉 (Fast Forward):快速合併,直接使用每一次異動的 commit,最好使用在同一分支的情況下,ex. 本地 merge 遠端

    ​​C <-- develop        C <-- develop, test
    ​​|                    |
    ​​B <-- test    ==>    B
    ​​|                    |
    ​​A                    A
    
    • 只有異動 commit 的父 commit 才能使用 Fast Forward 至異動(同一條 branch)
  • git merge --no-ff=> No Fast Forward:將多個異動併為一個 commit

    • 兩條意義不同的分支合併應該使用此合併策略,使線圖清晰
    • master 每一個版本都會是穩定版
    ​​                        M
    ​​                        | \ <-- develop
    ​​                        | o <-- test
    ​​                        | o
    ​​M   <-- develop         | o
    ​​| o <-- test     ===>   o |
    ​​o o                     o |
    ​​o o                     o |
    ​​o /                     o /
    ​​I                       I
    
  • git merge --ff vs. git merge --ff-only

    • --ff:預設行為,若不行使用 Fast Forward 則用自動轉為 No Fast Forward
    • --ff-only:明確要求必須使用 Fast Forward,若不能使用 Fast Forward 時則拒絕操作
  • git pull == git fetch + git merge

    • 所以遠端比本地新旳時候會無法 git push
  • git pull --rebase

  • 無法 Fast Forward 的情形,本地端與遠端版本已經不是同一條分支了

    ​​  O <-- origin/master
    ​​M | <-- master
    ​​o o
    ​​o /
    ​​I
    
    • 解法一:git cherry-pick <commit_id>
    • 解法二:git rebase (等同於 reset 後再做 cherry-pick)

合併沒有共同祖先的分支 (即 orphan branch)

  • 透過 git merge --allow-unrelated-histories <orphan_branch_name> 合併孤兒分支

--no-ff vs. --no-ff --no-commit vs. --squash

--no-ff

(master) $ git merge --no-ff topic

       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /            \
D -- E -- F -- G    master        D -- E -- F -- G -- H  master

--no-ff --no-commit

(master) $ git merge --no-ff --no-commit topic
Automatic merge went well; stopped before committing as requested
(master) $ git status
On branch master
Your branch is up to date with 'origin/master'.

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

	modified:   README.md
	modified:   package.json
...
       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /            \
D -- E -- F -- G    master        D -- E -- F -- G -- H  master

--squash

(master) $ git merge --squash topic
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
(master) $ 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)

	modified:   README.md
	modified:   package.json
...
       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /
D -- E -- F -- G    master        D -- E -- F -- G -- H  master
  • 壓縮合併,但不產生線圖
  • 等價於 cherry-pick & rebase -i squash

合併策略

列出已合併/未合併的分支

Rebase

git rebase 的重新 commit 為:

  1. 建立一個沒有名字的 branch
  2. 在此 branch 提交
  3. 將原 branch 指向此 branch

https://blog.yorkxin.org/2011/07/29/git-rebase

        A -- B -- C  topic                          A' -- B' -- C'  topic
      /                      ===>                  /
D -- E -- F -- G     master        D -- E -- F -- G                 master
  • 在 topic 下指令 git rebase master
  • 移花接木,將 master 的 commit 接在 topic 的 commit之前(不影響 master 之線圖
    1. checkout 到指定 commit
    2. cherry-pick 到一個沒有 branch 指向的 commit
    3. checkout 回自己 commit 後 reset 回沒有名字的 branch
  • rebase 的目的在於避免 merge 衝突
    • 遠端有更新時,應當立刻 rebase!!
  • 通常有遠端的時候才會需要用到
  • git rebase --autostash

git rebase --skip

git rebase -i

  • git rebase -i <base_commit_id>(可修改 base commit 後的 commit)
    • 參數 i 代表:Interactive Mode 互動模式
    • pick = 要這條 commit ,什麼都不改
    • reword = 要這條 commit ,但要改 commit message
    • edit = 要這條 commit,但要改 commit 的內容
      • 修改完 commit 內容後可以再繼續新增其他 commit (於既有歷史紀錄中插入新版本)
    • squash = 要這條 commit,但要跟前面那條合併,並保留這條的 messages
      • 於 commit 時加上 --squash=<commit> 標註要壓縮哪個版本
      • git rebase -i --autosquash 會自動將修正版本接在 commit 版本之後並設為 squash
      • git rebase autosquash
    • fixup = squash + 只使用前面那條 commit 的 message ,捨棄這條 message
      • 於 commit 時加上 --fixup=<commit> 標註要修正哪個版本 (效果同 squash)
    • exec = 執行一條指令(如執行 git 指令)
    • drop = 不要這條 commit
    • 還可以調整 commits 的順序,直接剪剪貼貼,改行的順序就行了
  • rebase 修訂衝突後,應使用 git add <file> 標註衝突已解決,並使用 git rebase --conitnue 完成 (而不是增加額外的commit)
  • 修改第一個初始 commit

git rebase --onto

git cherry-pick

  • --no-commit:先不 commit,可利用此參數來多次 cherry-pick 不同的 commit 內容
    • 在真正 commit 之前,都是屬於 cherry-pick 的過程
    • 過程中可以 git cherry-pick --abort 來中斷 cherry-pick
  • 可以跨 repository 挑選需要的 commit

git cherry-pick 多個 commit

反向撿櫻桃 (Revert)

  • git revert <commit_id>

detached HEAD

origin/HEAD

ORIG_HEAD

https://gitbook.tw/chapters/branch/how-git-know-what-current-branch-is.html

當你在做一些比較「危險」的操作(例如像 mergerebasereset 之類的),Git 就會把 HEAD 的狀態存放在這裡,讓你隨時可以跳回危險動作之前的狀態

還原/清除工作目錄檔案

還原

清除

  • git clean
    刪除 untracked 的檔案
    • git clean -n 只列出將會清除的清單(Dry run, 不搭配其他參數則列出 non-ignored 的 untracked 檔案)
    • git clean -f 執行清除檔案(不搭配其他參數則清除 non-ignored 的 untracked 檔案)
    • 參數 n、f 可以搭配以下參數:
      • git clean -X 要清除 untracked 檔案(ignored files)
      • git clean -x 要清除所有 untracked 檔案(ignored and non-ignored files)
      • git clean -d 要同時清除 untracked 目錄
    • 可指定路徑

比對差異

不同的 git diff 方法

  • 工作目錄(Work Tree) vs 更新索引(Index): git diff
  • 工作目錄(Work Tree) vs 最新版本(HEAD): git diff HEAD
  • 工作目錄(Work Tree) vs 歷史版本(Commits): git diff <commit>
  • 更新索引(Index) vs 最新版本(HEAD): git diff --cached <HEAD> (HEAD 可省略)
  • 更新索引(Index) vs 歷史版本(Commits): git diff --cached <commit>
  • 歷史版本(Commits) vs 歷史版本(Commits): git diff <src-commit> <target-commit>

比對二進位檔案之間的差異

  • git diff --binary
    • 用於產生 patch

比對兩個版本之間的檔案異動清單與狀態

  • git diff --name-only: 僅列出工作目錄與索引間異動的檔案
  • git diff --name-status: 列出工作目錄與索引間異動的檔案及狀態
  • git diff --name-status <src-commit> <target-commit>: 列出兩版本間異動的檔案及狀態
    • 搭配 --diff-filter 指定特定狀態,配合 shell script 自動修正佈署環境檔案

使用 git diff 產生 patch 修補檔與套用修補檔的方法

https://juejin.im/post/5b5851976fb9a04f844ad0f4

  1. 建立 patch: git diff <src-commit> <target-commit> > my-patch.patch
  • 若包含二進位檔案須加上 --binary
  1. 套用 patch: git apply my-patch.patch
  • 套用前可先使用 git apply --check 檢查套用 patch 過程是否會發生衝突

正式環境上版與退版

  • 上版: git apply <patch_file>
  • 退版: git apply --reverse <patch_file>

format-patch vs. diff >

https://yodalee.blogspot.com/2017/03/git-patch.html

常見的 diff 其實也就是git diff 生成的 patch,內容就是:這幾行刪掉,這幾行加上去,用 git diff > commit1.patch 就能輕鬆生成。git patch system 則是用 git format-patch 來產生,它提供比 diff 更豐富的資訊

git stash

  • git stash 也是一種分支(refs/stash)
  • git stash show
  • git stash pop <n>:n 是離現在最近的第幾個 stash(0-base)

git show

git add / checkout patch

https://zlargon.gitbooks.io/git-tutorial/content/advanced/add_checkout_part_of_file.html

檔案 / 資料夾大小寫更名

  1. git config --local core.ignorecase false 將 git 設定為大小寫敏感
  2. git mv client/themeCore/components/core/productCard temp
  3. git mv temp client/themeCore/components/core/ProductCard

Git 遠端儲存庫管理

標記版本

建立與刪除輕量標籤 (lightweight tag)

  • 輕量標籤: 就只是個指標,並不會建立任何物件
  • 建立標籤: git tag <tag_name> <commit>
    • 顯示訊息時只會顯示該版本的 commit 內容
  • 刪除標籤: git tag -d <tag_name>
    • 或手動刪除檔案 .git/refs/tags/<tag_name>

建立與刪除標示標籤 (annotated tag)

  • 建立標籤: git tag -a <tag_name> <commit>
    • 需輸入版本訊息 (使用 tag -n 顯示 tag 訊息)
    • 通常用於 release
  • 刪除標籤: git tag -d <tag_name>

遠端標籤

  • 推送遠端標籤: git push --tags
  • 刪除遠端標籤: git push --delete origin <tag_name>
  • 取得遠端標籤: git fetch
    • 若遠端標籤已不存在,要同時刪除本地標籤: git fetch --prune-tags

取出特定標籤的完整原始碼

  • git checkout <tag_name>: 實際上是把標籤指向的版本取出 (因為標籤不是分支,所以會跑去 detached HEAD)
    • 因此應加上 -b 同時建立分支, ex git checout -b hotfix/6.0.1 6.0.1

Submodule

https://blog.chh.tw/posts/git-submodule/

Git 協同作業實戰 ==> 好的分支合併流程!!

  1. 集中式版控流程
    • 共用儲存庫
    • 類似 SVN
  2. 整合式管理版控流程
    • blessed repository 對每個人都是唯讀
    • devloper 將修改 pull request 給 integration manager,通過後再合併
    • 類似 GitHub fork
    • pull 都從 blessed repository,push 則 push 自己的
      • 透過 git remote set-url --push origin <URL_TO_MY_REPO> 設定
  3. 獨裁者與副手工作流程
    • 類似前一步,只是再加上副手幫忙 code review

認識 GitHub Flow

Git Commit Message

其他

  • 命令列呈現線圖 git log --oneline --graph -n
    • --oneline: 僅顯示第一行訊息,即 commit log
    • --graph: 以線圖呈現
    • -n: 最近幾版
  • 實用的 git alias
    ​​git config --global alias.co   checkout
    ​​git config --global alias.ci   commit
    ​​git config --global alias.st   status
    ​​git config --global alias.sts  "status -s"
    ​​git config --global alias.br   branch
    ​​git config --global alias.re   remote
    ​​git config --global alias.di   diff
    ​​git config --global alias.type "cat-file -t"
    ​​git config --global alias.dump "cat-file -p"
    ​​git config --global alias.lo   "log --oneline"
    ​​git config --global alias.ll   "log --pretty=format:'%h %ad | %s%d [%Cgreen%an%Creset]' --graph --date=short"
    ​​git config --global alias.lg   "log --graph --pretty=format:'%Cred%h%Creset %ad |%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset [%Cgreen%an%Creset]' --abbrev-commit --date=short"
    
    ​​# Windows 限定 (因為要 TortoiseGit)
    ​​git config --global alias.tlog "!start 'C:\PROGRA~1\TortoiseGit\bin\TortoiseGitProc.exe' /command:log /path:."
    

Git 工具

環境建立

Git 教學影片(課後問卷)