ProGit 讀書筆記 Table of Contents

Table of Contents

  • 一、記錄每次更新到倉庫
    * 移除文件
  • 二、查看提交歷史
    * 限制輸出長度
  • 三、撤消操作
    * 取消暫存的文件
    * 撤消對文件的修改
  • 四、遠程倉庫的使用
    * 查看遠程倉庫
    * 添加遠程倉庫
    * 從遠程倉庫中抓取與拉取
  • 五、打標籤
    * 列出標籤
    * 附註標籤
    * 輕量標籤
    * 後期打標籤
    * 共享標籤
    * 檢出標籤
  • 六、分支的新建與合併
    * 新建分支
    * 合併分支
  • 七、分支管理
  • 八、遠程分支
    * 推送
    * 跟蹤分支
    * 拉取
    * 刪除遠程分支
  • 九、變基
    * 變基的基本操作
    * 更有趣的變基例子
    * 變基的風險 & 用變基解決變基
  • 十、分佈式工作流程
  • 十一、向一個項目貢獻
    * 提交準則
    * 私有管理團隊
    * 派生的公開項目
  • 十二、選擇修訂版本
    * 引用日誌
    * 祖先引用
    * 提交區間
    * 多點
    * 三點
  • 十三、交互式暫存
  • 十四、儲藏與清理
    * 儲藏工作
    * 創造性的儲藏
    * 從儲藏創建一個分支
  • 十五、搜索
    * Git Grep
  • 十六、重寫歷史
    * 修改最後一次提交
    * 修改多個提交信息
    * 重新排序提交
    * 壓縮提交
    * 拆分提交
    * 核武器級選項:filter-branch
    * 從每一個提交移除一個文件
    * 全局修改郵箱地址
  • 十七、重置揭密
    * 三棵樹 HEAD, Index, Working Directory
    * 重置的作用
    * 通過路徑來重置
    * 壓縮
    * 檢出
    * 總結
  • 十八、高級合併
    * 合併衝突
  • 十九、使用 Git 調試
    * 文件標註
    * 二分查找
  • 二十、配置 Git
    * 外部的合併與比較工具
    * 格式化與多餘的空白字符
  • 二十一、Git 屬性
    * 合併策略
  • 二十二、Git 鉤子
    * 客戶端鉤子
  • 二十三、Git 對象
    * 樹對象
    * 提交對象
  • 二十四、Git 引用
    * HEAD 引用
  • 二十五、維護與數據恢復
    * 數據恢復
    * 移除對象

一、記錄每次更新到倉庫

移除文件

  1. 要從 Git 中移除某個文件,就必須要從暫存區域移除,然後提交。可以用 git rm 命令完成此項工作,並連帶從工作目錄中刪除指定的文件,這樣以後就不會出現在未跟蹤文件清單中了。

  2. 如果刪除之前修改過並且已經放到暫存區域的話,git rm 必須要用強制刪除選項 -f

  3. 如果想讓文件保留在磁盤,但是並不想讓 Git 繼續跟蹤。當你忘記添加 .gitignore 文件,不小心把一個很大的日誌文件或一堆 .a 這樣的編譯生成文件添加到暫存區時,使用 --cached 選項:

    $ git rm --cached README
    
  4. 要在 Git 中對文件改名,可以使用 Git 的 mv 命令

    $ git mv file_from file_to
    

二、查看提交歷史

限制輸出長度

  1. git log--author 選項顯示指定作者的提交,用 --grep 選項搜索提交說明中的關鍵字。(請注意,如果要得到同時滿足這兩個選項搜索條件的提交,就必須用 --all-match 選項。否則,滿足任意一個條件的提交都會被匹配出來)

  2. 另一個非常有用的篩選選項是 -S,可以列出那些添加或移除了某些字符串的提交。比如說,你想找出添加或移除了某一個特定函數的引用的提交,你可以這樣使用:

    $ git log -Sfunction_name
    
  3. 限制 git log 輸出的選項

三、撤消操作

  • 提交完了才發現漏掉了幾個文件沒有添加,或者提交信息寫錯了。此時,可以運行帶有 --amend 選項的提交命令嘗試重新提交:

    $ git commit --amend
    

取消暫存的文件

  • 使用 git reset HEAD <file>... 來取消暫存

撤消對文件的修改

  1. 使用 git checkout -- <file>..." 來撤銷對文件的修改

  2. 你需要知道 git checkout -- [file] 是一個危險的命令,這很重要。你對那個文件做的任何修改都會消失 - 你只是拷貝了另一個文件來覆蓋它

四、遠程倉庫的使用

查看遠程倉庫

  1. git remote -v 會顯示需要讀寫遠程倉庫使用的 Git 保存的簡寫與其對應的 URL

  2. 想要查看某一個遠程倉庫的更多信息,可以使用 git remote show [remote-name] 命令。它會列出遠程倉庫的 URL 與跟蹤分支的信息。這些信息非常有用,它告訴你正處於 master 分支,並且如果運行 git pull,就會抓取所有的遠程引用,然後將遠程 master 分支合併到本地 master 分支


    這個命令會列出當你在特定的分支上執行 git push 會自動地推送到哪一個遠程分支。它也會列出哪些遠程分支不在你的本地,哪些遠程分支已經從服務器上移除了,還有當你執行 git pull 時哪些分支會自動合併

  3. 如果因爲一些原因想要移除一個遠程倉庫 - 你已經從服務器上搬走了或不再想使用某一個特定的鏡像了,又或者某一個貢獻者不再貢獻了 - 可以使用 git remote rm <remote_name>

添加遠程倉庫

  • 運行 git remote add <shortname> <url> 添加一個新的遠程 Git 倉庫

從遠程倉庫中抓取與拉取

  1. 如果你想拉取 Paul 的倉庫中有但你沒有的信息,可以運行 git fetch pb

  2. 如果你有一個分支設置爲跟蹤一個遠程分支,可以使用 git pull 命令來自動的抓取然後合併遠程分支到當前分支。默認情況下,git clone 命令會自動設置本地 master 分支跟蹤克隆的遠程倉庫的 master 分支(或不管是什麼名字的默認分支)。運行 git pull 通常會從最初克隆的服務器上抓取數據並自動嘗試合併到當前所在的分支。

  3. 另一種簡單的方法是使用 git pull --rebase 命令而不是直接 git pull。又或者你可以自己手動完成這個過程,先 git fetch,再 git rebase

  4. 如果你習慣使用 git pull ,同時又希望默認使用選項 --rebase,你可以執行這條語句 git config --global pull.rebase true 來更改 pull.rebase 的默認配置。

五、打標籤

  • Git 使用兩種主要類型的標籤:輕量標籤(lightweight)與附註標籤(annotated)

列出標籤

  • 列出已有的標籤是非常簡單直觀的。只需要輸入 git tag

    $ git tag 
    v0.1 
    v1.3
    

附註標籤

  1. 創建一個附註標籤最簡單的方式是當你在運行 tag 命令時指定 -a 選項:

    $ git tag -a v1.4 -m 'my version 1.4' 
    $ git tag 
    v0.1
    v1.3
    v1.4
    
  2. git show 命令可以看到標籤信息與對應的提交信息

輕量標籤

  • 輕量標籤本質上是將提交校驗和存儲到一個文件中 - 沒有保存任何其他信息。創建輕量標籤,不需要使用 -a-s-m 選項,只需要提供標籤名字:

    $ git tag v1.5
    $ git tag
    v0.1 
    v1.3 
    v1.4 
    v1.5
    

後期打標籤

  • 假設在 v1.2 時你忘記給項目打標籤,也就是在 “updated rakefile” 提交。你可以在之後補上標籤。要在哪個提交上打標籤,你需要在命令末尾指定提交的校驗和:

    $ git tag -a v1.2 9fceb02
    

共享標籤

  1. 默認情況下,git push 命令並不會傳送標籤到遠程倉庫服務器上。在創建完標籤後你必須顯式地推送標籤到共 享服務器上。

    git push origin [tag_name]
    
  2. 如果想要一次性推送很多標籤,也可以使用帶有 git push origin --tags,這將會把所有不在遠程倉庫服務器上的標籤全部推送。

檢出標籤

  • 在 Git 中你並不能真的檢出一個標籤,因爲它們並不能像分支一樣來回移動。如果你想要工作目錄與倉庫中特定的標籤版本完全一樣,可以使用 git checkout -b [branchname] [tagname]在特定的標籤上創建一個新分支

    $ git checkout -b version2 v2.0.0 
    Switched to a new branch 'version2'
    

六、分支的新建與合併

新建分支

  1. 在你切換分支之前,保持好一個乾淨的狀態。有一些方法:保存進度(stashing) 和 修補提交(commit amending))

  2. 在合併的時候,你應該注意到了"快進(fast-forward)"這個詞。由於當前 master 分支所指向的提交是你當前提交的直接上游,所以 Git 只是簡單的將指針向前移動。換句話說,當你試圖合併兩個分支時,如果順着一個分支走下去能夠到達另一個分支,那麼 Git 在合併兩者的時候,只會簡單的將指針向前推進 (指針右移),因爲這種情況下的合併操作沒有需要解決的分歧。

合併分支

  1. 你的開發歷史從一個更早的地方開始分叉開來(diverged)。因爲,master 分支所在提交併不是 iss53 分支所在提交的直接祖先,Git 不得不做一些額外的工作。出現這種情況的時候,Git 會使用兩個分支的末端所指的快照(C4 和 C5)以及這兩個分支的工作祖先(C2),做一個簡單的三方合併。 高亮 [57]

  2. Git 將此次三方合併的結果做了一個新的快照並且自動創建一個新的提交指向它。這個被稱作一次合併提交,它的特別之處在於他有不止一個父提交 高亮 [58]

七、分支管理

  1. 如果需要查看每一個分支的最後一次提交,可以運行 git branch -v 命令

      $ git branch -v 
      iss53 93b412c fix javascript issue 
    
  • master 7a98805 Merge branch 'iss53'
    testing 782fd34 add scott to the author list in the readmes
    
    
  1. 如果分支包含了還未合併的工作,嘗試使用 git branch -d 命令刪除它時會失敗,如果真的想要刪除分支並丟掉那些工作,可以使用 -D 選項強制刪除。

八、遠程分支

  1. 遠程跟蹤分支是遠程分支狀態的引用。它們是你不能移動的本地引用,當你做任何網絡通信操作時,它們會自動移動。

  2. 如果你在本地的 master 分支做了一些工作,然而在同一時間,其他人推送提交到 git.ourcompany.com 並更新了它的 master 分支,那麼你的提交歷史將向不同的方向前進。也許,只要你不與 origin 服務器連接,你的 origin/master 指針就不會移動

  3. 如果要同步你的工作,運行 git fetch origin 命令。這個命令查找 “origin” 是哪一個服務器(在本例中,它是 git.ourcompany.com),從中抓取本地沒有的數據,並且更新本地數據庫,移動 origin/master 指針指向新的、更新後的位置

推送

  1. 當你想要公開分享一個分支時,需要將其推送到有寫入權限的遠程倉庫上。本地的分支並不會自動與遠程倉庫同步 - 你必須顯式地推送想要分享的分支。這樣,你就可以把不願意分享的內容放到私人分支上,而將需要和別人協作的內容推送到公開分支。

  2. 你也可以運行 git push origin serverfix:serverfix,它會做同樣的事 - 相當於它說,“推送本地 的 serverfix 分支,將其作爲遠程倉庫的 serverfix 分支”。
    可以通過這種格式來推送本地分支到一個命名不相同的遠程分支。如果並不想讓遠程倉庫上的分支叫做 serverfix,可以運行 git push origin serverfix:awesomebranch 來將本地的 serverfix 分支推送到遠程倉庫上的 awesomebranch 分支。

  3. 要特別注意的一點是當抓取到新的遠程跟蹤分支時,本地不會自動生成一份可編輯的副本(拷貝)。即是說,這種情況下,不會有一個新的 serverfix 分支 - 只有一個不可以修改的 origin/serverfix 指針。

  4. 可以運行 git merge origin/serverfix 將這些工作合併到當前所在的分支。如果想要在自己的 serverfix 分支上工作,可以將其建立在遠程跟蹤分支之上:

    $ git checkout -b serverfix origin/serverfix
    

跟蹤分支

  1. 從一個遠程跟蹤分支檢出一個本地分支會自動創建一個叫做 “跟蹤分支”(有時候也叫做 “上游分支”)。跟蹤分支是與遠程分支有直接關係的本地分支。如果在一個跟蹤分支上輸入 git pull,Git 能自動地識別去哪個服務器上抓取、合併到哪個分支。

  2. 最簡單的就是,運行 git checkout -b [branch] [remotename]/[branch]。這是一個十分常用的操作,所以 Git 提 供了 --track 快捷方式:

    $ git checkout --track origin/serverfix
    Branch serverfix set up to track remote branch serverfix from origin. 
    Switched to a new branch 'serverfix'
    
  3. 如果想要將本地分支與遠程分支設置爲不同名字,你可以輕鬆地增加一個不同名字的本地分支:

    $ git checkout -b sf origin/serverfix
    
  4. 設置已有的本地分支跟蹤一個剛剛拉取下來的遠程分支,或者想要修改正在跟蹤的上游分支,你可以在任意時間使用 -u 或 --set-upstream-to 選項運行 git branch 來顯式地設置。

    $ git branch -u origin/serverfix
    
  5. 當設置好跟蹤分支後,可以通過 @{upstream}@{u} 快捷方式來引用它。

  6. 如果想要查看設置的所有跟蹤分支,可以使用 git branch-vv 選項。

      $ git branch -vv 
      iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets 
      master 1ae2a45 [origin/master] deploying index fix 
    * serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
    

    如果想要統計最新的領先與落後數字,需要在運行此命令前抓取 所有的遠程倉庫。可以像這樣做:$ git fetch --all; git branch -vv

拉取

  • git pull 在大多數情況下它的含義是一個 git fetch 緊接着一個 git merge 命令。

刪除遠程分支

  • 可以運行帶有 --delete 選項的 git push 命令來刪除一個遠程分支。如果想要從服務器上刪除 serverfix 分支,運行下面的命令:

    $ git push origin --delete serverfix
    To https://github.com/schacon/simplegit
    - [deleted] serverfix
    

九、變基

變基的基本操作

  1. 有一種方法:你可以提取在 C4 中引入的補丁和修改,然後在 C3 的基礎上再應用一次。在 Git 中,這種操作就叫做 變基。你可以使用 rebase 命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。 在上面這個例子中,運行:

    $ git checkout experiment 
    $ git rebase master
    First, rewinding head to replay your work on top of it...
     Applying: added staged command 
    

    它的原理是首先找到這兩個分支(即當前分支 experiment、變基操作的目標基底分支 master)的最近共同祖先 C2,然後對比當前分支相對於該祖先的歷次提交,提取相應的修改並存爲臨時文件,然後將當前分支指向目標基底 C3, 最後以此將之前另存爲臨時文件的修改依序應用。 高亮 [80]

  2. 無論是通過變基,還是通過三方合併,整合的最終結果所指向的快照始終是一樣的,只不過提交歷史不同罷了。變基是將一系列提交按照原有次序依次應用到另一分支上,而合併是把最終結果合在一起。

更有趣的變基例子

  1. 假設你希望將 client 中的修改合併到主分支併發布,但暫時並不想合併 server 中的修改,因爲它們還需要經過更全面的測試。這時,你就可以使用 git rebase 命令的 --onto 選項,選中在 client 分支裏但不在 server 分支裏的修改(即 C8 和 C9),將它們在 master 分支上重演:

    $ git rebase --onto master server client
    

    以上命令的意思是:“取出 client 分支,找出處於 client 分支和 server 分支的共同祖先之後的修改,然 後把它們在 master 分支上重演一遍”。

  2. 接下來你決定將 server 分支中的修改也整合進來。使用 git rebase [basebranch] [topicbranch] 命令可以直接將特性分支(即本例中的 server)變基到目標分支(即master)上。這樣做能省去你先切換到 server 分支,再對其執行變基命令的多個步驟。

    $ git rebase master server
    

變基的風險 & 用變基解決變基

  1. 如果你習慣使用 git pull ,同時又希望默認使用選項 --rebase,你可以執行這條語句 git config --global pull.rebase true 來更改 pull.rebase 的默認配置。

  2. 假如你在那些已經被推送至共用倉庫的提交上執行變基命令,並因此丟棄了一些別人的開發所基於的提交,那就有大麻煩了,你的同事也會因此鄙視你。

十、分佈式工作流程

  • 集成管理者工作流 Git 允許多個遠程倉庫存在,使得這樣一種工作流成爲可能:每個開發者擁有自己倉庫的寫權限和其他所有人倉庫的讀權限。

十一、向一個項目貢獻

提交準則

  1. 首先,你不會想要把空白錯誤(根據 git help diff 的描述,結合下面給出的圖片,空白錯誤是指行尾的空格、Tab 製表符,和行首空格後跟 Tab 製表符的行爲)提交上去。
    Git 提供了一個簡單的方式來檢查這點 - 在提交前,運行 git diff --check,它將會找到可能的空白錯誤並將它們爲你列出來。

  2. 要知道必須合併什麼進入,工作才能推送

    git log --no-merges issue54..origin/master
    

    issue54..origin/master 語法是一個日誌過濾器,要求 Git 只顯示所有在後面分支(在本例中是 origin/master)但不在前面分支(在本例中是 issue54)的提交的列表。

私有管理團隊

  • 需要將在 featureB 分支上合併的工作推送到服務器上的 featureBee 分支。她可以通過指定 本地分支加上冒號(:)加上遠程分支給 git push 命令來這樣做:

    $ git push -u origin featureB:featureBee
    ...
    To jessica@githost:simplegit.git fba9af8..cd685d1 featureB -> featureBee
    

    注意 -u 標記;這是 --set-upstream 的簡寫,該標記會爲之後輕鬆地推送與拉取配置分支

派生的公開項目

  • 因爲你將分支變基了,所以必須爲推送命令指定 -f 選項,這樣才能將服務器上有一個不是它的後代的提交的 featureA 分支替換掉。一個替代的選項是推送這個新工作到服務器上的一個不同分支(可能稱作 featureAv2)。

十二、選擇修訂版本

引用日誌

  1. 當你在工作時, Git 會在後臺保存一個引用日誌(reflog),引用日誌記錄了最近幾個月你的 HEAD 和分支引用所 指向的歷史。 你可以使用 git reflog 來查看引用日誌。

  2. 如果你想查看倉庫中 HEAD 在五次前的所指向的提交,你可以使用 @{n} 來引用 reflog 中輸出的提交記錄。

    $ git show HEAD@{5}
    

    可以運行 git log -g 來查看類似於 git log 輸出格式的引用日誌信息

  3. 引用日誌只存在於本地倉庫,一個記錄你在你自己的倉庫裏做過什麼的日誌。其他人拷貝的倉庫裏的引用日誌不會和你的相同;而你新克隆一個倉庫的時候,引用日誌是空的,因爲你在倉庫裏還沒有操 作。
    git show HEAD@{2.months.ago} 這條命令只有在你克隆了一個項目至少兩個月時纔會有用——如果你是五分鐘前克隆的倉庫,那麼它將不會有結果返回。

祖先引用

  1. 祖先引用是另一種指明一個提交的方式。如果你在引用的尾部加上一個 ^, Git 會將其解析爲該引用的上一個提交。可以使用 git show HEAD^ 來查看上一個提交,也就是 “HEAD 的父提交”

  2. 可以在 ^ 後面添加一個數字——例如 d921970^2 代表 “d921970 的第二父提交”這個語法只適用於合併 (merge)的提交,因爲合併提交會有多個父提交。第一父提交是你合併時所在分支,而第二父提交是你所合併的分支

  3. 另一種指明祖先提交的方法是 ~。同樣是指向第一父提交,因此 HEAD~HEAD^ 是等價的。而區別在於你在後面加數字的時候。HEAD~2 代表 “第一父提交的第一父提交” —— Git 會根據你指定的次數獲取對應的第一父提交。

  4. HEAD~3 也可以寫成 HEAD^^^,也是第一父提交的第一父提交的第一父提交。也可以組合使用這兩個語法 —— 你可以通過 HEAD~3^2 來取得之前引用的第二父提交(假設它是一個合併提交)

提交區間


  1. 想要查看 experiment 分支中還有哪些提交尚未被合併入 master 分支。你可以使用 master..experiment 來讓 Git 顯示這些提交。也就是 “在 experiment 分支中而不在 master 分支中的提交”。
    這可以讓你保持 experiment 分支跟隨最新的進度以及查看你即將合併的內容。

  2. 另一個常用的場景是查看你即將推送到遠端的內容:

    $ git log origin/master..HEAD
    

    這個命令會輸出在你當前分支中而不在遠程 origin 中的提交。
    如果你留空了其中的一邊, Git 會默認爲 HEAD。例如, git log origin/master.. 將會輸出與之前例子相同的結果 —— Git 使用 HEAD 來代替留空的一邊

多點

  1. Git 允許你在任意引用前加上 ^ 字符或者 --not 來指明你不希望提交被包含其中的分支。因此下列3個命令是等價的:

    $ git log refA..refB 
    $ git log ^refA refB 
    $ git log refB --not refA
    
  2. 這個語法可以幫你在查詢中指定超過兩個的引用,例如想查看所有被 refA 或 refB 包含的但是不被 refC 包含的提交,你可以輸入下面中的任意一個命令

    $ git log refA refB ^refC
    $ git log refA refB --not refC
    

三點

  • 最後一種主要的區間選擇語法是三點,這個語法可以選擇出被兩個引用中的一個包含但又不被兩者同時包含的提交。

    $ git log master...experiment
    F
    E
    D
    C
    

    可以使用參數 --left-right,它會顯示每個提交到底處於哪一側的分支。

    $ git log --left-right master...experiment 
    < F
    < E
    > D
    > C
    

十三、交互式暫存

運行 git add 時使用 -i 或者 --interactive 選項,Git 將會進入一個交互式終端模式

十四、儲藏與清理

儲藏工作

  1. 將新的儲藏推送到棧上,運行 git stashgit stash save

  2. 要查看儲藏的東西,可以使用 git stash list

    $ git stash list
    stash@{0}: WIP on master: 049d078 added the index file 
    stash@{1}: WIP on master: c264051 Revert "added file_size"
    stash@{2}: WIP on master: 21d80a5 added number to log
    
  3. 將你剛剛儲藏的工作重新應用:git stash apply 。如果想要應用其中一個更舊的儲藏,可以通過名字指定它,像這樣:git stash apply stash@{2}。如果不指定一個儲藏,Git 認爲指定的是最近的儲藏.

  4. 可以在一個分支上保存一個儲藏,切換到另一個分支,然後嘗試重新應用這些修改。當應用儲藏時工作目錄中也可以有修改與未提交的文件 - 如果有任何東西不能幹淨地應用,Git 會產生合併衝突。

  5. 文件的改動被重新應用了,但是之前暫存的文件卻沒有重新暫存。想要那樣的話,必須使用 --index 選項來運行 git stash apply 命令,來嘗試重新應用暫存的修改(即存放到相應的暫存區)。

  6. 應用選項只會嘗試應用暫存的工作 - 在堆棧上還有它。可以運行 git stash drop 加上將要移除的儲藏的名字來移除它。
    也可以運行 git stash pop 來應用儲藏然後立即從棧上扔掉它。

創造性的儲藏

  1. 有幾個儲藏的變種可能也很有用。第一個非常流行的選項是 stash save 命令的 --keep-index 選項。它告訴 Git 不要儲藏任何你通過 git add 命令已暫存的東西。
    當你做了幾個改動並只想提交其中的一部分,過一會兒再回來處理剩餘改動時,這個功能會很有用。

  2. 另一個是像儲藏跟蹤文件一樣儲藏未跟蹤文件。默認情況下,git stash 只會儲藏已經在索引中的文件。如果指定 --include-untracked 或 -u 標記,Git 也會儲藏任何創建的未跟蹤文件。

從儲藏創建一個分支

  • 如果儲藏了一些工作,將它留在那兒了一會兒,然後繼續在儲藏的分支上工作,在重新應用工作時可能會有問題。
    如果應用嘗試修改剛剛修改的文件,你會得到一個合併衝突並不得不解決它。
    如果想要一個輕鬆的方式來再次測試儲藏的改動,可以運行 git stash branch 創建一個新分支,檢出儲藏工作時所在的提交,重新在那應用工作,然後在應用成功後扔掉儲藏

    $ git stash branch testchanges 
    Switched to a new branch "testchanges" # On branch testchanges # Changes to be committed:
    
    # (use "git reset HEAD <file>..." to unstage) 
    # 
    # modified: index.html
    # 
    # Changed but not updated:
    # (use "git add <file>..." to update what will be committed)
    # 
    # modified: lib/simplegit.rb 
    # Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)
    

    這是在新分支輕鬆恢復儲藏工作並繼續工作的一個很不錯的途徑。

十五、搜索

Git Grep

  1. Git 提供了一個 grep 命令,你可以很方便地從提交歷史或者工作目錄中查找一個字符串或者正則表達式。
    相比於一些常用的搜索命令比如 grepackgit grep 命令有一些的優點。第一就是速度非常快,第二是你不僅僅可以搜索工作目錄,還可以搜索任意的 Git 樹。

  2. 默認情況下 Git 會查找你工作目錄的文件。你可以傳入 -n 參數來輸出 Git 所找到的匹配行行號

    $ git grep -n gmtime_r 
    compat/gmtime.c:3:#undef gmtime_r 
    compat/gmtime.c:8: return git_gmtime_r(timep, &result); 
    compat/mingw.c:606:struct tm *gmtime_r(const time_t *timep, struct tm *result)
    date.c:429: if (gmtime_r(&now, &now_tm))
    
  3. 你可以使用 --count 選項來使 Git 輸出概述的信息,僅僅包括哪些文件包含匹配以及每個文件包含了多少個匹配。

    $ git grep --count gmtime_r 
    compat/gmtime.c:2
    compat/mingw.c:1  
    date.c:1
    
  4. 如果你想看匹配的行是屬於哪一個方法或者函數,你可以傳入 -p 選項:$ git grep -p gmtime_r *.c

  5. 你還可以使用 --and 標誌來查看複雜的字符串組合,也就是在同一行同時包含多個匹配。

  6. 如果我們想找到 ZLIB_BUF_MAX 常量是什麼時候引入的,我們可以使用 -S 選項來顯示新增和刪除該字符串的提交。

    $ git log -SZLIB_BUF_MAX --oneline
    e01503b zlib: allow feeding more than 4GB in one go 
    ef49a7a zlib: zlib can only process 4GB at a time
    

十六、重寫歷史

修改最後一次提交

  • 如果你已經完成提交,又因爲之前提交時忘記添加一個新創建的文件,想通過添加或修改文件來更改提交的快照,也可以通過類似的操作來完成。通過修改文件然後運行 git addgit rm 一個已追蹤的文件,隨後運行 git commit --amend 拿走當前的暫存區域並使其做爲新提交的快照。
    使用這個技巧的時候需要小心,因爲修正會改變提交的 SHA-1 校驗和。它類似於一個小的變基 - 如果已經推送了最後一次提交,就不要修正它。

修改多個提交信息

  • 例如,如果想要修改最近三次提交信息,或者那組提交中的任意一個提交信息,將想要修改的最近一次提交的父提交作爲參數傳遞給 git rebase -i命令,即 HEAD~2^HEAD~3。記住 ~3 可能比較容易,因爲你正嘗試修改最後三次提交;
    但是注意實際上指定了以前的四次提交,即想要修改提交的父提交: $ git rebase -i HEAD~3 再次記住這是一個變基命令 - 在 HEAD~3..HEAD 範圍內的每一個提交都會被重寫,無論你是否修改信息。

重新排序提交

壓縮提交

拆分提交

核武器級選項:filter-branch

從每一個提交移除一個文件
  • filter-branch 是一個可能會用來擦洗整個提交歷史的工具。爲了從整個提交歷史中移除一個叫做 passwords.txt 的文件,可以使用 --tree-filter 選項給 filter-branch

    $ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
    Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) 
    Ref 'refs/heads/master' was rewritten
    
全局修改郵箱地址

十七、重置揭密

三棵樹 HEAD, Index, Working Directory

  1. HEAD 是當前分支引用的指針,它總是指向該分支上的最後一次提交。這表示 HEAD 將是下一次提交的父結點。通常,理解 HEAD 的最簡方式,就是將它看做 你的上一次提交 的快照。

  2. 索引是你的 預期的下一次提交。我們也會將這個概念引用爲 Git 的 “暫存區域”,這就是當你運行 git commit 時 Git 看起來的樣子。

重置的作用

  • 如果指定 --mixed 選項,會撤銷一上次 提交,但還會 取消暫存 所有的東西。於是,我們回滾到了所有 git add 和 git commit 的命令執行之前,所有修改都存在於 Working Directory

通過路徑來重置

  • 你還可以給 reset 提供一個作用路徑。若指定了一個路徑,reset 會將它的作用範圍限定爲指定的文件或文件集合。這樣做自然有它的道理,因爲 HEAD 只是一個指針,你無法讓它同時指向兩個提交中各自的一部分。不過索引和工作目錄可以部分更新,所以重置會繼續進行第 2、3 步。
    假如我們運行 git reset file.txt(這其實是 git reset --mixed HEAD file.txt 的簡寫形 式,因爲你既沒有指定一個提交的 SHA-1 或分支,也沒有指定 --soft--hard),它會:
    1. 移動 HEAD 分支的指向 (已跳過)
    2. 讓索引看起來像 HEAD (到此處停止)
      所以它本質上只是將 file.txt 從 HEAD 複製到索引中。

壓縮

檢出

  • 假設我們有 master 和 develop 分支,它們分別指向不同的提交;我們現在在 develop 上(所以 HEAD 指向它)。如果我們運行 git reset master,那麼 develop 自身現在會和 master 指向同一個提交。而如果我們運行 git checkout master 的話,develop 不會移動,HEAD 自身會移動。現在 HEAD 將 會指向 master。

總結

  • 下面的速查表列出了命令對樹的影響。“HEAD” 一列中的 “REF” 表示該命令移動了 HEAD 指向的分支引 用,而`‘HEAD’' 則表示只移動了 HEAD 自身。特別注意 WD Safe? 一列 - 如果它標記爲 NO,那麼運行該命令 之前請考慮一下。


十八、高級合併

合併衝突

  1. git merge --abort 選項會嘗試恢復到你運行合併前的狀態。但當運行命令前,在工作目錄中有未儲藏、未提交的修改時它不能完美處理,除此之外它都工作地很好。

  2. 一個很有用的工具是帶 --conflict 選項的 git checkout。這會重新檢出文件並替換合併衝突標記。如果想要重置標記並嘗試再次解決它們的話這會很有用。 可以傳遞給--conflict 參數 diff3merge(默認選項)。如果傳給它 diff3,Git 會使用一個略微不同版本的衝突標記:不僅僅只給你 “ours” 和 “theirs” 版本,同時也會有 “base” 版本在中間來給你更多的上下文。

    $ git checkout --conflict=diff3 hello.rb
    
    #! /usr/bin/env ruby
    
    def hello 
    <<<<<<< ours
      puts 'hola world' 
    ||||||| base
      puts 'hello world'
    =======
      puts 'hello mundo'
    >>>>>>> theirs 
    end
    
    hello()
    

十九、使用 Git 調試

文件標註

  1. 你可以使用 git blame 標註這個文件,查看這個方法每一行的最後修改時間以及是被誰修改的,可以使用 -L 選項來限制輸出範圍

  2. 有一個很有意思的特性就是你可以讓 Git 找出所有的代碼移動。如果你在 git blame 後面加上一個 -C,Git 會分析你正在標註的文件,並且嘗試找出文件中從別的地方複製過來的代碼片段的原始出處。
    比如,你將 GITServerHandler.m 這個文件拆分爲數個文件,其中一個文件是 GITPackUpload.m。對 GITPackUpload.m 執行帶 -C 參數的blame命令,你就可以看到代碼塊的原始出處

    $ git blame -C -L 141,153 GITPackUpload.m
    

二分查找

  1. 首先執行 git bisect start 來啓動,接着執行 git bisect bad 來告訴系統當前你所在的提交是有問題的。然後你必須告訴 bisect 已知的最後一次正常狀態是哪次提交,使用 git bisect good [good_commit]

    $ git bisect start 
    $ git bisect bad
    $ git bisect good v1.0 
    Bisecting: 6 revisions left to test after this 
    [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo
    

    假設測試結果是沒有問題的,你可以通過 git bisect good 來告訴 Git,然後繼續尋找。
    你再一次執行測試,發現這個提交下是有問題的,因此你可以通過 git bisect bad 告訴 Git

  2. 當你完成這些操作之後,你應該執行 git bisect reset 重置你的 HEAD 指針到最開始的位置,否則你會停留在一個很奇怪的狀態

二十、配置 Git

外部的合併與比較工具

格式化與多餘的空白字符

二十一、Git 屬性

合併策略

  • 通過 Git 屬性,你還能對項目中的特定文件指定不同的合併策略。一個非常有用的選項就是,告訴 Git 當特定文件發生衝突時不要嘗試合併它們,而是直接使用你這邊的內容。

二十二、Git 鉤子

客戶端鉤子

  1. pre-commit 鉤子在鍵入提交信息前運行。它用於檢查即將提交的快照,例如,檢查是否有所遺漏,確保測試運行,以及覈查代碼。如果該鉤子以非零值退出,Git 將放棄此次提交,不過你可以用 git commit --no -verify 來繞過這個環節。你可以利用該鉤子,來檢查代碼風格是否一致(運行類似 lint 的程序)、尾隨空白字符是否存在(自帶的鉤子就是這麼做的),或新方法的文檔是否適當。

  2. post-commit 鉤子在整個提交過程完成後運行。它不接收任何參數,但你可以很容易地通過運行 git log -1 HEAD 來獲得最後一次的提交信息。該鉤子一般用於通知之類的事情。

二十三、Git 對象

  1. 可以通過底層命令 hash-object 來演示上述效果——該命令可 將任意數據保存於 .git 目錄,並返回相應的鍵值。

  2. -w 選項指示 hash-object 命令存儲數據對象;若不指定此選項,則該命令僅返回對應的鍵值。--stdin 選項則指示該命令從標準輸入讀取內容;若不指定此選項,則須在命令尾部給出待存儲文件的路徑。

  3. 可以通過 cat-file 命令從 Git 那裏取回數據。這個命令簡直就是一把剖析 Git 對象的瑞士軍刀。爲 cat-file 指定 -p 選項可指示該命令自動判斷內容的類型,併爲我們顯示格式友好的內容

  4. 利用 cat-file -t 命令,可以讓 Git 告訴我們其內部存儲的任何對象類型,只要給定該對象的 SHA-1 值

樹對象

  1. 所有內容均以樹對象和數據對象的形式存儲,其中樹對象對應了 UNIX 中的目錄項,數據對象則大致上對應了 inodes 或文件內容。一個樹對象包含了一條或多條樹對象記錄(tree entry),每條記錄含有一個指向數據對象或者子樹對象的 SHA-1 指針,以及相應的模式、類型、文件名信息。

  2. master^{tree} 語法表示 master 分支上最新的提交所指向的樹對象。

  3. Git 根據某一時刻暫存區(即 index 區域,下同)所表示的狀態創建並記錄一個對應的樹對象,如此重複便可依次記錄(某個時間段內)一系列的樹對象。
    可以通過底層命令 update-index 爲一個單獨文件創建一個暫存區。
    必須爲上述命令指定 --add 選項,因爲此前該文件並不在暫存區中
    同樣必需的還有 --cacheinfo 選項,因爲將要添加的文件位於 Git 數據庫中,而不是位於當前目錄下。

  4. 文件模式有 100644,表明這是一個普通文件;100755,表示一個可執行文件;120000,表示一個符號鏈接。

  5. 可以通過 write-tree 命令將暫存區內容寫入一個樹對象。此處無需指定 -w 選項——如果某個樹對象此 前並不存在的話,當調用 write-tree 命令時,它會根據當前暫存區狀態自動創建一個新的樹對象

  6. 通過調用 read-tree 命令,可以把樹對象讀入暫存區。本例中,可以通過對 read-tree 指定 --prefix 選項,將一個已有的樹對象作爲子樹讀入暫存區

  7. 如果基於這個新的樹對象創建一個工作目錄,你會發現工作目錄的根目錄包含兩個文件以及一個名爲 bak 的子目錄,該子目錄包含 test.txt 文件的第一個版本

提交對象

  1. 這三個提交對象分別指向之前創建的三個樹對象快照中的一個。現在,如果對最後一個提交的 SHA-1 值運行 git log 命令,會出乎意料的發現,你已有一個貨真價實的、可由 git log 查看的 Git 提交歷史了

  2. 這就是每次我們運行 git add 和 git commit 命令時, Git 所做的實質工作——將被改寫的文件保存爲數據對象,更新暫存區,記錄樹對象,最後創建一個指明瞭頂層樹對象和父提交的提交對象。
    這三種主要的 Git 對象——數據對象、樹對象、提交對象——最初均以單獨文件的形式保存在 .git/objects 目錄下。

二十四、Git 引用

  1. 這基本就是 Git 分支的本質:一個指向某一系列提交之首的指針或引用。

  2. 當運行類似於 git branch (branchname) 這樣的命令時,Git 實際上會運行 update-ref 命令,取得當前 所在分支最新提交對應的 SHA-1 值,並將其加入你想要創建的任何新引用中。

HEAD 引用

  1. HEAD 文件是一個符號引用(symbolic reference),指向目前所在的分支。所謂符號引用,意味着它並不像普通引用那樣包含一個 SHA-1 值——它是一個指向其他引用的指針。

  2. 當我們執行 git commit 時,該命令會創建一個提交對象,並用 HEAD 文件中那個引用所指向的 SHA-1 值設置其父提交字段。

二十五、維護與數據恢復

數據恢復

  1. 最方便最常用的方法,是使用一個名叫 git reflog 的工具。當你正在工作時,Git 會默默地記錄每一次你改變 HEAD 時它的值。每一次你提交或改變分支,引用日誌都會被更新。引用日誌(reflog)也可以通過 git update-ref 命令更新。

  2. 由於引用日誌數據存放在 .git/logs/ 目錄中,現在你已經沒有引用日誌了。這時該如何恢復那次提交?一種方式是使用 git fsck 實用工具,將會檢查數據庫的完整性。如果使用一個 --full 選項運行它,它會向你顯示出所有沒有被其他對象指向的對象

移除對象

  1. 警告:移除對象的操作對提交歷史的修改是破壞性的。它會從你必須修改或移除一個大文件引用最早的樹對象開始重寫 每一次提交。如果你在導入倉庫後,在任何人開始基於這些提交工作前執行這個操作,那麼將不會有任何問題 否則,你必須通知所有的貢獻者他們需要將他們的成果變基到你的新提交上。

  2. 執行 gc 來查看數據庫佔用了多少空間,也可以執行 count-objects 命令來快速的查看佔用空間大小。
    假設你不知道該如何找出哪個文件或哪些文件佔用瞭如此多的空間。如果你執行 git gc 命令,所有的對象將被放入一個包文件中。
    你可以通過運行 git verify-pack 命令,然後對輸出內容的第三列(即文件大小)進行排序,從而找出這個大文件。
    你也可以將這個命令的執行結果通過管道傳送給 tail 命令,因爲你只需要找到列在最後的幾個大對象。
    爲了找出具體是哪個文件,可以使用 revlist 命令,我們在 指定特殊的提交信息格式 中曾提到過。如果你傳遞 --objects 參數給 rev-list 命令, 它就會列出所有提交的 SHA-1、數據對象的 SHA-1 和與它們相關聯的文件路徑。可以使用以下命令來找出你的數據對象的名字

  3. 你必須重寫 7b30847 提交之後的所有提交來從 Git 歷史中完全移除這個文件。爲了執行這個操作,我們 要使用 filter-branch 命令。

    $ git filter-branch --index-filter \ 
    'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
    
    Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz' 
    Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2) 
    Ref 'refs/heads/master' was rewritten
    

    --index-filter 選項類似於在 重寫歷史 中提到的的 --tree-filter 選項,不過這個選項並不會讓命令將修改在硬盤上檢出的文件,而只是修改在暫存區或索引中的文件。
    你必須使用 git rm --cached 命令來移除文件,而不是通過類似 rm file 的命令 - 因爲你需要從索引中移除它,而不是磁盤中。
    還有一個原因是速度 - Git 在運行過濾器時,並不會檢出每個修訂版本到磁盤中,所以這個過程會非常快。如果願意的話,你也可以通過 --tree-filter 選項來完成同樣的任務。
    git rm 命令的 --ignore-unmatch 選項告訴命令:如果嘗試刪除的模式不存在時,不提示錯誤。
    最後,使用 filter-branch 選項來重寫自 7b30847 提交以來的歷史,也就是這個問題產生的地方。否則,這個命令會從最舊的提交開始,這將會花費許多不必要的時間。

  4. 你的歷史中將不再包含對那個文件的引用。不過,你的引用日誌和你在 .git/refs/original 通過 filter-branch 選項添加的新引用中還存有對這個文件的引用,所以你必須移除它們然後重新打包數據庫。在重新打包前需要移除任何包含指向那些舊提交的指針的文件……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章