--- tags: Git --- # 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` * https://moelove.info/2016/12/04/Git-%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93%E5%92%8C%E8%A3%B8%E4%BB%93%E5%BA%93/ * 在預設的情況下,不管是 `git init` 或 `git clone` 都會一併建立[工作區(Workspace / Work Tree)](#%E5%B7%A5%E4%BD%9C%E5%8D%80%EF%BC%88Workspace--Work-Tree%EF%BC%89) * 包含 tags、local branches,但不包括 remote tracking branches * 若不須要 git workspace,搭配 `--bare` 參數即可乎略(不建立)git work tree * 若在沒有 work tree 的 repo 進行操作,則會出現訊息:`fatal: this operation must be run in a work tree` #### `--mirror` * https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---mirror * https://stackoverflow.com/questions/3959924/whats-the-difference-between-git-clone-mirror-and-git-clone-bare * 含蓋所有的 refs(即包含 tags、local branches、remote tracking branches) ### 了解 Git 資料結構 #### 物件(Object):immutable * [Git Internals - Git Objects](https://git-scm.com/book/en/v2/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`](https://git-scm.com/docs/git-cat-file) 可以知道 git objects 的類型 * `git cat-file` 看到的 ID 為檔案**內容**所運算(SHA-1 hash)而來,若 git object 檔名相同(即 ID 相同),則代表檔案內容一致 * `-t` : type 類型 * `-p` : print 內容 * `-s` : size 檔案大小 * 以下圖為範例 ```shell= $ 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 (因為占空間) * 1g 的檔案,就會產生 1g 的 blob => 使用 [Git Large File Storage (LFS)](https://git-lfs.github.com/) 解決 (需 server 有支援) #### 索引(index) * 記錄有哪些檔案即將要被提交到下一個 commit 版本中 (保存要進儲存庫之前的**所有檔案狀態**) * `git add` 會將 tracked file 記錄到 git index file(git file status => Staged),並且建立一個 blob 物件([Blob 對象](http://www-cs-students.stanford.edu/~blynn/gitmagic/intl/zh_tw/ch08.html#_blob%E5%B0%8D%E8%B1%A1)) * 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 checkout -- . vs git checkout](https://stackoverflow.com/questions/41101998/git-checkout-vs-git-checkout) * [Why do we use double dash in “git checkout -- .”?](https://stackoverflow.com/questions/23208156/why-do-we-use-double-dash-in-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`](https://stackoverflow.com/questions/3528245/whats-the-difference-between-git-reset-mixed-soft-and-hard) ![](https://i.stack.imgur.com/qRAte.jpg) |Reset 模式|所在位置(HEAD)|變更狀態紀錄(INDEX)|工作目錄|說明| |--|--|--|--|--| |soft|changed|unchanged|unchanged|僅移除 **commit** 變成新版未 commit,內容仍是新版的| |mixed|changed|changed|unchanged|index 移除 **staged** 標記,變成 **Modified** or **Untracked**,內容是新版的| |hard|changed|changed|changed|回到上一版版本,其間變更完全移除(接近 svn revert),內容及狀態皆是上一版| ![](https://trello-attachments.s3.amazonaws.com/583c75c90173173906e0b4ce/1023x654/ad3080ce58b146f8f0b5e343771a8a17/_output_gitlifecycle.png) * 取得檔案變更應使用 `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` * 重新 commit 最近的 commit * 修改 commit 的 Git user name、email: * [`--reset-author`](https://git-scm.com/docs/git-commit#git-commit---reset-author) * [`--author=<author>`](https://git-scm.com/docs/git-commit#git-commit---authorltauthorgt):`git commit --amend --author="Author Name <email@address.com>" --no-edit` * https://help.github.com/articles/setting-your-commit-email-address-in-git/ * https://stackoverflow.com/questions/3042437/change-commit-author-at-one-specific-commit * https://stackoverflow.com/a/1320317/8530187 * https://yulun.me/2014/git-tips-change-author-and-email-in-previous-commits/ ## 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 * [台北捷運](https://github.com/othree/taipei-mrt) * 設計師將寫好的版型放到 orphan 分支,前端工程師比對差異進行套版 ### 關於「分支」的真正意義 * 標記"時間維度"的指標 * 「分支 Branch」是一個會隨時間演進的指標,當下時間點的索引狀態 (開發用) * 任何一條分支的異動,都不會影響其他分支 * 「標籤 Tag」是一個不會隨時間演進的指標 (參考用) * 「切換分支 Checkout」等同於「控制時間維度」 ### 檢示所有分支詳細資訊 * https://git-scm.com/docs/git-branch#git-branch--v ### 分支更名(改名) * https://git-scm.com/docs/git-branch#git-branch---move * `git branch --move [<oldbranch>] <newbranch>` * 重新命名 `.git/refs/heads/<oldbranch>` 分支 ### 合併「不同世界」的分支 * `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` * https://ihower.tw/blog/archives/3843 * 加上 rebase 的意思是,會先 1. 把本地 repo 從上次 pull 之後的變更暫存起來 2. 回復到上次 pull 時的情況 3. 套用遠端的變更 4. 最後再套用剛暫存下來的本地變更 * [`git pull --rebase --autostash`](https://git-scm.com/docs/git-pull#git-pull---autostash) * 無法 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` * [What are the differences between `--squash` and `--no-ff --no-commit`?](https://stackoverflow.com/questions/11983749/what-are-the-differences-between-squash-and-no-ff-no-commit) * `--no-commit`:套用合併內容而不直接 commit,待使用者自行 commit * 合併但不 commit (會同時 add to index) * 合併成功 != 建置成功,確定可正常建置再自行 commit * http://www.jianshu.com/p/58a166f24c81 * `--no-ff` 產生的 merge commit 上不會有 diff 資訊,要自行到被併入的 branch 中查看各個 commit #### `--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` ```bash (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` ```bash (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 ### 合併策略 * https://git-scm.com/docs/git-merge#_merge_strategies * https://git-scm.com/docs/git-rebase#_merge_strategies * https://git-scm.com/docs/merge-strategies * https://www.jianshu.com/p/58a166f24c81 * https://www.cnblogs.com/chaoguo1234/p/5347623.html * `git [merge | rebase] [-X ours | theirs] <commit>` * `-X ours`:以目前 head 所在的 branch 為主 * `-X theirs`:以不是 head 所在的 branch 為主 * `git checkout [--ours | theirs]` * https://gitbook.tw/chapters/branch/fix-conflict.html * https://nitaym.github.io/ourstheirs/ * 衝突方向 * 目前 head 指向的分支內容:current change * 對方分支內容:incoming change * 例如: * `(dev)$ git merge master`:dev <- current(在 dev 上建立 merge commit 將 master 內容 merge 至 dev,會把 merge commit 的另一個 parent 指向 master) * `(dev)$ git rebase master`:master <- current(將 dev 上的 commit 一個一個重新 commit 到 master,head 指向 master 且會一個一個移動到後方重新 commit 的新 commit) ### 列出已合併/未合併的分支 * 已合併: `git branch --merged` * 刪除已合併的分支 `git branch --merged | egrep -v "(^\*|master|dev)" | xargs git branch -d` * egrep 排除 當前分支、master、dev * https://stackoverflow.com/questions/6127328/how-can-i-delete-all-git-branches-which-have-been-merged * 未合併: `git branch --no-merged` ## 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`](https://git-scm.com/docs/git-rebase#git-rebase---autostash) ### `git rebase --skip` * https://git-scm.com/docs/git-rebase#git-rebase---skip * https://stackoverflow.com/questions/9539067/what-exactly-does-git-rebase-skip-do * 跳過(略過)目前衝突的 commit,會放棄此 commit 的異動 ### `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](https://git-scm.com/docs/git-rebase#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 -i --root` * 最早可跳至初始 commit 後的狀態 * https://stackoverflow.com/questions/2119480/edit-the-root-commit-in-git * https://git-scm.com/docs/git-rebase#git-rebase---root ### `git rebase --onto` * https://blog.yorkxin.org/2011/07/29/git-rebase * https://git-scm.com/docs/git-rebase * 指定要 rebase 的 base commit * 可用於要 rebase 已 merge 的 branch,解決只用 rebase 可能會無法正確 rebase 的情形 * `git rebase --onto <new base-commit> <current base-commit> <target-branchName>` * `git rebase --onto <new base-commit> <current base-commit>`(rebase 目前的分支) ## `git cherry-pick` * `--no-commit`:先不 commit,可利用此參數來多次 cherry-pick 不同的 commit 內容 * 在真正 commit 之前,都是屬於 cherry-pick 的過程 * 過程中可以 `git cherry-pick --abort` 來中斷 cherry-pick * 可以跨 repository 挑選需要的 commit ### `git cherry-pick` 多個 commit * 多個 commit:`git cherry-pick commit1 commit2` * 多個連續 commit:`git cherry-pick preStartCommit..endCommit` 或 `git cherry-pick startCommit^..endCommit` * https://git-scm.com/docs/git-cherry-pick#git-cherry-pick-codegitcherry-pickmaintmasternextcode * https://stackoverflow.com/questions/1670970/how-to-cherry-pick-multiple-commits * https://www.jianshu.com/p/08c3f1804b36 * https://segmentfault.com/q/1010000010185984 * `preStartCommit..endCommit` 代表從 `preStartCommit`(不包含此 commit 之異動內容)到 `endCommit` 為止的所有 commit * `startCommit^..endCommit` 代表從 `startCommit`(包含此 commit 之異動內容)到 `endCommit` 為止的所有 commit * cherry-pick 整個分支:`git cherry-pick parentBranchName..targetBranchName` ### 反向撿櫻桃 (Revert) * `git revert <commit_id>` ## HEAD * https://git-scm.com/docs/git-rev-parse#_specifying_revisions * https://stackoverflow.com/questions/2221658/whats-the-difference-between-head-and-head-in-git * **HEAD^** 一個 ^ 表示為第一個分支上的前一版,一個 ^ 就代表切換一次分支,後面接的數字則代表往前一版的第幾個分支 * Note: Windows command 需打 `HEAD^^` (`^`為跳脫字元 [Escape Characters](http://www.robvanderwoude.com/escapechars.php)) * **HEAD~** 一個 ~ 表示為前一版,預設都是於左數來第一個分支上往上找,後面接的數字則代表往前幾版 * **HEAD~** vs. **HEAD^** ![](https://i.stack.imgur.com/pDAzG.png) ### detached HEAD * https://www.git-tower.com/learn/git/faq/detached-head-when-checkout-commit ### origin/HEAD * [Re: [問題] 請問 git中 origin/HEAD 指的是](https://www.ptt.cc/bbs/Linux/M.1406900317.A.D51.html) * [How does origin/HEAD get set?](https://stackoverflow.com/questions/8839958/how-does-origin-head-get-set) * origin/HEAD 代表遠端的 default branch,也就是你 clone(與一些其他操作)時預設會切換到的 branch。預設會指到 origin/master ### ORIG_HEAD https://gitbook.tw/chapters/branch/how-git-know-what-current-branch-is.html > 當你在做一些比較「危險」的操作(例如像 `merge`、`rebase` 或 `reset` 之類的),Git 就會把 HEAD 的狀態存放在這裡,讓你隨時可以跳回危險動作之前的狀態 ## 還原/清除工作目錄檔案 ### 還原 * `git reset` 全部 Staged -> Unstaged,可搭配 `hard`、`soft`、`mixed` 等參數 * `git reset [--] <file>` Staged -> Unstaged `--`:代表將 `--` 後方的每個參數皆一率視為檔案路徑 * https://git-scm.com/docs/git-reset#git-reset-Resetasinglefileintheindex * https://stackoverflow.com/questions/7147270/hard-reset-of-a-single-file * https://stackoverflow.com/questions/6561142/difference-between-git-checkout-filename-and-git-checkout-filename/6561160#6561160 * `git reset <commitCode>` 會 checkout 至指定 commit,並將差異還原成 Unstaged 一樣可搭配 hard、soft、mixed 等參數 但要小心每一個參數處理差異的方式並不同 * `git checkout <file>` Staged 中有記錄:以 Staged 來還原 Staged 中沒有記錄:以目前 commit 來還原,Unstaged 內容會消失 ### 清除 * `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 目錄 * 可指定路徑 ## 比對差異 * https://git-scm.com/docs/git-diff ### 不同的 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` 2. 套用 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`](https://git-scm.com/docs/git-stash#git-stash-showltstashgt) * `git stash pop <n>`:n 是離現在最近的第幾個 stash(0-base) ## git show * https://git-scm.com/docs/git-show * [`git show --patch`](https://git-scm.com/docs/git-show#git-show---patch) * [Generating patches with -p](https://git-scm.com/docs/git-show#_generating_patches_with_p) ## 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 遠端儲存庫管理 * `git remote` * [`--verbose`](https://git-scm.com/docs/git-remote#git-remote---verbose) * `git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git` * Syncing a fork * http://fred-zone.blogspot.tw/2015/09/git-fork.html * https://gitbook.tw/chapters/github/pull-from-github.html * https://gitbook.tw/chapters/github/syncing-a-fork.html * https://www.peterdavehello.org/2014/02/update_forked_repository/ * `$ git pull upstream master` * 一般建議使用 `git pull --rebase` * 衝突時,先 `git rebase --abort` 再說 * `git push [<遠端倉庫名稱>] [<本地分支名稱>][:<遠端分支名稱>]` * `git push origin local-name` * `git push origin local-name:remote-name` * https://git-scm.com/docs/git-push#git-push-codegitpushorigincode * https://git-scm.com/docs/git-push#git-push-ltrefspecgt82308203 * [git push --force and how to deal with it](https://evilmartians.com/chronicles/git-push---force-and-how-to-deal-with-it) * [GIT: PUSHING TO A REMOTE BRANCH WITH A DIFFERENT NAME](https://penandpants.com/2013/02/07/git-pushing-to-a-remote-branch-with-a-different-name/) * [將本地分支 push 到遠端](https://git-scm.com/docs/git-push#_description) * https://help.github.com/articles/pushing-to-a-remote/ * `git push --force` * https://git-scm.com/docs/git-push#git-push---force * `git push --force-with-lease` * https://git-scm.com/docs/git-push#git-push---no-force-with-lease * https://blog.csdn.net/wpwalter/article/details/80371264 * `git branch -u <遠端倉庫名稱>[/<遠端分支名稱>] [<本地分支名稱>]` == `git branch --set-upstream-to=<遠端倉庫名稱>[/<遠端分支名稱>] [本地分支名稱]` * [追蹤遠端分支](https://git-scm.com/docs/git-branch#git-branch--ultupstreamgt) * https://zlargon.gitbooks.io/git-tutorial/content/remote/upstream.html * `git checkout -b [<本地分支名稱>] --track <遠端倉庫名稱>[/<遠端分支名稱>]` * 建立新的追蹤分支(tracking branch),為 `git checkout <分支名稱>` 的[預設行為之一](https://git-scm.com/docs/git-checkout#git-checkout-emgitcheckoutemltbranchgt)(如果有同名的遠端分支名稱) * `git push -u <遠端倉庫名稱> <本地分支名稱>` == `git push --set-upstream <遠端倉庫名稱> <本地分支名稱>` * `git branch -u <遠端倉庫名稱>[/<遠端分支名稱>] [<本地分支名稱>]` * 刪除遠端分支 * https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches * https://git-scm.com/docs/git-push#git-push---delete * `git push origin --delete <branchName>` * 會同時刪掉遠端追蹤分支 * 其他人則透過 `git fetch --prune` 刪掉遠端分支 * 刪除本地之遠端追蹤(remote-tracking)分支 * `git branch -r` 列出遠端追蹤分支 * https://git-scm.com/docs/git-branch#git-branch---delete * https://git-scm.com/docs/git-branch#git-branch---remotes * `git branch --delete --remotes origin/<branchName>` ## 標記版本 * 指向特定 commit 版本 * 標籤建立後通常不會再異動 * https://git-scm.com/docs/git-tag ### 建立與刪除輕量標籤 (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 * https://guides.github.com/introduction/flow/ * http://calvert.logdown.com/posts/2014/09/21/understanding-the-github-flow * http://blog.krdai.info/post/17485259496/github-flow ## Git Commit Message * http://karma-runner.github.io/5.0/dev/git-commit-msg.html * http://blog.fourdesire.com/2018/07/03/%E6%92%B0%E5%AF%AB%E6%9C%89%E6%95%88%E7%9A%84-git-commit-message/ * https://juffalow.com/other/write-good-git-commit-message * https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-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 命令列工具](https://git-scm.com/) * [推薦安裝] [TortoiseGit](https://tortoisegit.org/) ( Windows only ) * [推薦安裝] [SourceTree](https://www.sourcetreeapp.com/) * [Visual Studio Code](https://code.visualstudio.com/) * [GitHub Desktop](https://desktop.github.com/) * [Fork](https://git-fork.com/) ( Mac only ) * [SmartGit](http://www.syntevo.com/smartgit/) * [GitExtensions](https://gitextensions.github.io/) ( Windows only ) * [tig](https://jonas.github.io/tig/) (Command Line) * [GitKraken](https://www.gitkraken.com ) ( Commerical ) * [Tower](https://www.git-tower.com/) ( Commerical ) ### 環境建立 * [Git 版本控管實戰:新手進階篇 - 範例版本庫 (Windows)]( https://gist.github.com/doggy8088/491d9da6b2452437f4af487137dd0340) * [Start-Sleep](https://ss64.com/ps/start-sleep.html) 必須使用 PowerShell 才能使用 * [Git 版本控管實戰:新手進階篇 - 範例版本庫 (macOS)]( https://gist.github.com/doggy8088/e535363a8f5c8ef3aeb31a1d234d4937) ### Git 教學影片(課後問卷) * [認識 Git 資料結構中的物件資料庫與物件之間的關係](https://www.youtube.com/watch?v=PZbSRy_ow0U) * [認識 Git 資料結構中的索引與檔案狀態的變化關係](https://www.youtube.com/watch?v=5c_7v0cIFk4) * [如何在 GitHub 使用 Fork / Pull Request 功能 (以 VS2013 為例)](https://www.youtube.com/watch?v=NTLAfy6lcdQ) * [透過 Visual Studio 2013 匯入方案到 Git 儲存庫的正確做法 ](https://www.youtube.com/watch?v=97BVjQyK8ag)