1. 程式人生 > 實用技巧 >Git、Github、Gitkraken 學習筆記

Git、Github、Gitkraken 學習筆記

《Git、Github、Gitkraken 學習筆記》

一、寫在前面

1、參考資料

本文參考 《Pro Git》 一書。

在官網有免費線上版可供閱讀:https://git-scm.com/book/en/v2

未看章節:

  • 伺服器上的 Git
  • Git 內部原理 - 引用規範

2、符號備註

  • 本文出現 【重點】 處,表示為知識的重點,可以著重看待。

二、起步

1、版本控制

(1)什麼是版本控制

版本控制(Revision control)是一種記錄一個或若干檔案內容變化,以便將來查閱特定版本修訂情況的系統。

功能:

  • 記錄
  • 回退
  • 比較
  • ……
(2)版本控制系統(VCS)的發展

① 手動備份

② 本地版本控制系統

其中最流行的一種叫做修訂控制系統(Revision Control System,簡稱 RCS)。工作原理是在硬碟上儲存補丁集(補丁是指檔案修訂前後的變化);通過應用所有的補丁,可以重新計算出各個版本的檔案內容。

集中化的版本控制系統(Centralized Version Control Systems,簡稱 CVCS)

特點:客戶端只需取出最新的檔案進行工作。

產品:CVS、Subversion(SVN)、SVN 以及 Perforce 等

好處:

  • 可以協同工作
  • 支援許可權管理
  • 好管理。管理一個 CVCS 要遠比在各個客戶端上維護本地 VCS 來得輕鬆容易

缺點:

  • 單點故障整個系統就癱瘓了
  • 必須聯上 CVCS 的那臺中央伺服器才能提交

分散式版本控制系統(Distributed Version Control System,簡稱 DVCS)

特點:客戶端需要把程式碼倉庫完整地映象下來,包括完整的歷史記錄,然後進行工作。

這就是分散式的特點。

產品:Git、Mercurial、Bazaar 以及 Darcs 等

好處:

  • 既有集中化的版本控制系統的優點,也可避免其缺點

    所以能上 git 就別用 svn 那種了。

  • 實現更復雜的工作流
  • 對檔案和提交的完整性保證的更好。(例如 Git 提交的內容或者元資訊只要修改了,commit-id 就會變)
  • 因為操作幾乎都在本地執行,所以速度很快,效能更高

    即使是跟遠端倉庫的互動(例如 fetch / push),git 也比 SVN 要快。僅在 clone 時,因為 git 正在下載整個歷史記錄,而不僅僅是最新版本(這也是分散式的必要),所以比 SVN 要慢。但基本上操作 Git 比SVN 快一兩個數量級。

  • 在 Git 中任何已提交的東西幾乎都是可以恢復的。

壞處:

  • 還是有的,不存在沒有缺點的技術,但本人不敢班門弄斧,具體可以參考網上別人的總結。

(3)Git 與其他版本控制系統的三大區別

分散式

參考上面 分散式版本控制系統 的敘述。

快照流【重點】

這是 Git 和其它版本控制系統(包括 SVN 和近似工具)的最主要差別,即在於 對待資料的方法

1、其它版本系統

  • (1)存每個版本完整的檔案(存在重複)
  • (2)基於差異(delta-based) 的版本控制,以檔案變更列表的方式儲存資訊。

2、Git

  • (1)對當時的全部檔案建立一個快照並儲存這個快照的索引(基於SHA-1)。 為了效率,如果檔案沒有修改,Git 不再重新儲存該檔案,而是隻保留一個連結指向之前儲存的檔案。

具體原理涉及 git 物件(三大物件),下面會有詳細介紹。

  • (2)基於快照流

好處:

  • 讓 git 的倉庫體量更小,效能更好。

開源

可免費使用。

2、SCM - 軟體配置管理

(1)什麼是軟體配置管理

如果你留心的話,可以發現 git 的官網地址不是 git.com 而是 git-scm.com,這個 scm 是什麼意思呢?

軟體配置管理(Software Configuration Management,簡稱:SCM),又稱軟體形態管理、或軟體建構管理,簡稱軟體形管。界定軟體的組成專案,對每個專案變更進行管控(版本控制),並維護不同專案之間的版本關係,以使軟體在開發過程中任一時間的內容都可以被追溯,包括某幾個具有重要意義的數個組合,例如某一次交付給客戶的軟體內容。

摘自維基百科。

(2)軟體配置管理(SCM)跟版本控制系統(VCS)有啥區別?
  • SCM 包括了 VSC。軟體配置管理是一個廣義的術語,涵蓋了構建,打包和部署軟體所需的所有過程。
  • VSC 只是軟體,而 SCM 不是

3、Git 誕生歷史

Linux 核心開源專案有著為數眾多的參與者。絕大多數的 Linux 核心維護工作都花在了提交補丁和儲存歸檔的繁瑣事務上(1991-2002年間)。到 2002 年,整個專案組開始啟用一個專有的分散式版本控制系統 BitKeeper 來管理和維護程式碼。

到了 2005 年,開發 BitKeeper 的商業公司同 Linux 核心開源社群的合作關係結束,他們收回了 Linux 核心社群免費使用 BitKeeper 的權力。 這就迫使 Linux 開源社群(特別是 Linux 的締造者 Linus Torvalds)基於使用 BitKeeper 時的經驗教訓,開發出自己的版本系統,即 Git

據說 Linus 只花了兩週時間自己用C寫出了 git。

4、安裝

以 CentOS 為例:

yum install git

寫本文時,最新版本為 v2.27.0

5、幫助

(1)命令列

可以隨時執行 git help <command> 命令來瞭解。

(2)官方檔案

https://git-scm.com/docs

6、配置

(1)配置檔案

按優先順序從低到高排列(級別高的會覆蓋級別低的):

  • 1、/etc/gitconfig 檔案: 所有 OS 使用者 + 所有倉庫

git config --system

由於它是系統配置檔案,因此你需要管理員或超級使用者許可權來修改它。

  • 2、~/.gitconfig~/.config/git/config 檔案:當前 OS 使用者 + 所有倉庫

git config --global

  • 3、當前倉庫 Git 目錄中的 config 檔案(即 .git/config):當前 OS 使用者 + 當前倉庫

git config --local or git config(預設)

(2)檢視配置
# 檢視所有原始配置(以及他們所在的配置檔案)
git config --list --show-origin # 檢視所有配置(會存在優先順序不同而覆蓋的情況,下同)
git config --list # 檢視具體某個配置
git config <key>
(3)常用配置

① 使用者資訊(建議設定全域性)

第一件事就是設定你的使用者名稱和郵件地址。

$ git config --global user.name "xjnotxj"
$ git config --global user.email [email protected]

② 文字編輯器

git config --global core.editor vim

這個值剛安裝 git 的是空,Git 會呼叫你通過環境變數 $VISUAL 或 $EDITOR 設定的文字編輯器, 如果沒有設定,預設則會呼叫 vi 來建立和編輯你的提交以及標籤資訊。

更多的編輯器如何設定,見:https://git-scm.com/book/zh/v2/附錄-C%3A-Git-命令-設定與配置

(4)你需要知道的配置(但不用改)

① 處理不同 OS 的換行規則

注意:換行處理只針對文字檔案,而非二進位制檔案。

通過 core.autocrlf 配置。

關於不同 OS 的換行規則 ,參考我的舊文:《關於“編碼”的方方面面》

② 修復空白

通過 core.whitespace 配置來探測和修正多餘空白字元問題。

預設被開啟的三個選項是:

  • blank-at-eol,查詢行尾的空格
  • blank-at-eof,盯住檔案底部的空行
  • space-before-tab,警惕行頭 tab 前面的空格

7、在其它環境中使用 Git

(1)GUI

① 為什麼要用 GUI?

只有在命令列模式下你才能執行 Git 的 所有 命令,而大多數的 GUI 軟體只實現了 Git 所有功能的一個子集降低操作難度

② 用什麼 GUI

1、內建 GUI

gitk - 在 git 倉庫下執行 gitk 命令即可開啟。

2、第三方 GUI

本文以 gitkraken 為例(下文如果提到 GUI,預設指的就是它)(參見下文還有會單獨一章介紹 gitkraken)。

本人之前在 mac 上用的 tower,後來才換到了 gitkraken,感覺明顯好用多了,推薦。

更多 第三方 GUI 列表,可見:https://git-scm.com/download/gui/mac

(2)IDE

① 支援哪些?

Visual Studio Code / Visual Studio / Eclipse / IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git

② Visual Studio Code

Visual Studio Code 的官方教程:https://code.visualstudio.com/Docs/editor/versioncontrol

③ 其它

(3)編輯器

Sublime Text

(4)命令列

① 環境變數

Git 總是在一個 shell 中執行,並藉助一些 shell 環境變數來決定它的執行方式。

略。

② 在 Bash 中

1、效果:

2、如何實現

略。

③ 在 Zsh 中(我本人用的就是這個)

1、效果:

可使用 "oh-my-zsh" (推薦)

2、如何實現

略,詳細可見:https://git-scm.com/book/zh/v2/附錄-A%3A-在其它環境中使用-Git-Zsh-中的-Git

④ 在 PowerShell 中

8、在你的應用中嵌入 Git

(1)方法一

直接嵌入 shell,執行 git 命令。

(2)方法二

使用第三方庫:

  • for c
  • for java
  • for go
  • ……

三、Git 基礎知識

1、獲取 Git 倉庫

(1)方法一 - git init

git init 將尚未進行版本控制的本地目錄轉換為 Git 倉庫。

該命令將建立一個名為 .git 的子目錄。

(2)方法二 - git clone

① 介紹

git clone 從其它遠端地址克隆一個已存在的 Git 倉庫。

② 協議

支援:

  • https:// 協議
  • git:// 協議

適用場景:

  • 對於 Github 來說,通常對於公開專案可以優先分享基於 HTTPS 的 URL,因為使用者克隆專案不需要有一個 GitHub 帳號

    HTTPS URL 與你貼到瀏覽器裡檢視專案用的地址是一樣的。

  • 如果你分享 SSH URL,使用者必須有一個帳號並且上傳 SSH 金鑰才能訪問你的專案。

③ 操作

1、指定分支

# git clone 不指定分支 (預設為 master)
git clone http://10.1.1.11/service/tmall-service.git # git clone 指定分支
git clone -b dev http://10.1.1.11/service/tmall-service.git

注:不管指不指定分支,git clone 都是整個倉庫拉下來,只是拉下來後預設建立的跟蹤分支不同

跟蹤分支的概念下面會說。

GitKraken clone 後會把所有遠端分支都建立一個本地分支。

2、重新命名

# clone 下來重新命名專案
git clone https://github.com/libgit2/libgit2 mylibgit

④ 結果

把遠端倉庫整個給 clone 下來。

包含:

  • 分支
  • 標籤
  • log

不包含:

  • 暫存區
  • stash
  • reflog
(3)[拓展] 協議 與 憑證儲存

如果你使用的是 SSH 方式連線遠端,並且設定了一個沒有口令的金鑰,這樣就可以在不輸入使用者名稱和密碼的情況下安全地傳輸資料。

然而,這對 HTTP 協議來說是不可能的 —— 每一個連線都是需要使用者名稱和密碼的。 這在使用雙重認證的情況下會更麻煩,因為你需要輸入一個隨機生成並且毫無規律的 token 作為密碼。

幸運的是,Git 擁有一個憑證系統來處理這個事情。

略。

(4)[拓展] 協議的底層

Git 可以通過兩種主要的方式在版本庫之間傳輸資料:“啞(dumb)”協議“智慧(smart)”協議

知道常用的預設的是智慧協議就好。

略。

2、基本操作

(1)常用操作

① 檔案的四種狀態

② 三類區域(三個階段)

  • 工作區
  • 暫存區

    SVN 就沒有暫存區的概念。

  • Git 目錄

基本的 Git 工作流程如下:

  • 在工作區中修改檔案。
  • 將你想要下次提交的更改選擇性地暫存,這樣只會將更改的部分新增到暫存區。
  • 提交更新,找到暫存區的檔案,將快照永久性儲存到 Git 目錄。

問:為什麼要有暫存區?

  • 分批遞交。(比如我工作區先提交 A、B 檔案,再提交 C、D 檔案)
  • 分階段提交。(比如我工作區先修改了某檔案的 A 處,再修改這個檔案的 B 處,當兩次提交)
  • 保留一份快照,必要時可回退到 stage 時的狀態。(git checkout -- file.txt)

③ 我的總結

注:

  • 此圖只涵蓋一些日常操作,方便僅我自己快速查閱,具體細節不贅述了。
  • 關於 clone、fetch、pull、push 這些,其實不光是遠端倉庫跟 git 目錄的互動,這裡簡略的寫的。
(2)git add

① 基本操作

git add 是一個多功能命令:

  • 把未跟蹤(新檔案)變成已跟蹤,即放到暫存區
  • 把已修改檔案(已跟蹤)放到暫存區
  • 合併時把有衝突的檔案標記為已解決狀態
  • 等…

可以將這個命令理解為“精確地將內容新增到下一次提交中”而不是“將一個檔案新增到專案中”要更加合適。

注:

  • git add 也可以寫成 git stage(後者含義更準確,前者是歷史遺留)
  • 如果同一個檔案多次被 add(即可能新增、修改、刪除了多次),在暫存區中會合併成一次(最終態)

② 互動式暫存

應用場景:一個檔案你修改了兩處地方,但是你只想 add 一處。

> 注:這裡不多介紹互動式暫存了,因為在命令列裡操作我個人覺得不方便,推薦在 GUI 裡操作。

③ 常見問題

1、為什麼工作區的空資料夾不能被 add ?

原因:git 會忽略空資料夾

解決辦法:在此空資料夾中新建一個空檔案,名為 .gitkeep(此名只是約定俗成)

(3)快速 git add 的方法

git rm - 刪除檔案的快速 add

git rm README.md 

# 相當於

rm README.md
git add README.md

git mv - 重新命名檔案的快速 add

git mv README.md README

相當於

mv README.md README
git rm README.md
git add README

適用條件:上面的命令只適用於已跟蹤檔案。

問:為什麼要用這些命令?

  • 快捷。會自動幫你 git add
  • 安全。如果檔案是已修改 or 已放入暫存區,則會被拒並提示你使用 -f
(4)git commit

① 基本操作

方法一:呼叫編輯器輸入提交資訊

git commit

注:

  • 編輯器中 # 開頭的行都是註釋行,確認提交後會被丟棄。
  • 預設的提交訊息中,開頭有一個空行,供你輸入;接著下面包含了最後一次執行 git status 的輸出(但為註釋狀態)。
  • 可以用 commit.template 來設定 commit 的提交資訊的模板。

方法二:直接命令列裡快速輸入提交資訊

git commit -m 'initial project version'

注:

  • 保持一個好習慣:每次 commit 前 status 一下,看看有沒有需要 add 的。

② commit message

1、規範

示例:

Redirect user to the requested page after login

http://gitlab.xxx.com/production-team/xxx/issues/171

Users were being redirected to the home page after login, which is less
useful than redirecting to the page they had originally requested before
being redirected to the login form. * Store requested path in a session variable
* Redirect to the stored location after successfully logging in the user

格式:

  • 1、第一行的描述不超過50字
  • 2、第二行提供解決了什麼 issue

    如果是 github / gitlab ,直接 # + issues id 即可。

  • 3、第三行詳細解析問題

注:

2、Gitkraken 中的 summary + description

有的 GUI 中會把提交資訊拆分為 summary + description:

其實劃分的規則很簡單:summary 為提交資訊的首行,description 為提交資訊的剩下行。

③ 高階操作

1.0、git commit --amend

作用:這一次提交將代替上一次提交的結果。

適用場景:

  • 有新的變動需要提交,但想要合併到上一個提交裡。
  • 沒有新的變動需要提交,只是想修改上一次提交的提交資訊。

1.1、git commit --amend --no-edit

適用場景:

  • 有新的變動需要提交,但想要合併到上一個提交裡(但提交資訊沿用上一個)。

    適合只是改改上一個提交的錯別字什麼的。

注:--amend 生成的提交本質上是新提交,所有 commit id 是會變的。

2、git commit -a

把所有已跟蹤檔案跳過暫存(無需 add),直接 commit。

這個命令圖快,但是使用需謹慎。

(5)git checkout

見上圖。(詳細介紹看下面的 重置揭祕

(6)git reset

見上圖。(詳細介紹看下面的 重置揭祕

(7)git status

git status

功能:

  • 顯示檔案狀態
  • 提供 add commit checkout reset 等命令的建議
  • 顯示分支資訊
  • 等…

git status -sgit status --short

git status -s 跟 git status 的不同:

  • 僅顯示檔案狀態
  • git status 的展示邏輯是先劃分 工作區暫存區,再展示檔案狀態(即同一個檔案可能出現多次);而 git status -s 展示邏輯是先劃分 檔案,再展示檔案狀態(即同一個檔案僅會出現一次)

git status -s 的輸出結果示例:

$ git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt

git status -s 的輸出結果中,每個檔案的可能出現情況:

針對單個檔案 工作區 暫存區 暫存區是 add 狀態後再在工作區操作 暫存區是 修改 狀態後再在工作區操作 暫存區是 刪除 狀態後再在工作區操作
新增檔案 ?? A 空 N/A N/A 會拆分兩個同名檔案顯示(一個是 D空,一個是 ??)
修改檔案 空 M M 空 AM MM N/A
刪除檔案 空 D D 空 AD MD N/A

注:

  • 1、上面的 代表空格
  • 2、如MM左邊為暫存區檔案情況,右邊為工作區檔案情況
  • 3、如果一個檔案重新命名或者移動了路徑,視為刪除
(8)git log - 檢視提交歷史

① 基礎用法

1、基礎:

git log,結果按時間先後排序,每個 commit 包括:

  • commit id
  • 作者的名字和電子郵件地址
  • 提交時間
  • 提交說明

注:作者的名字和電子郵件地址 和 提交時間 都是可以隨意改的,所以並不可信。

2、簡略:

git log --pretty=oneline,結果只有一行,每個 commit 包括:

  • commit id
  • 提交說明(如果太長會擷取顯示)

3、更簡略:

git shortlog,結果只有提交說明。(適合輸出修改日誌(changelog)類檔案)

預設會按作者分好組。

4、詳細:

git log --stat,結果會比 git log 多出:

  • 列出所有被新增/刪除/修改過的檔名
  • 這些檔案,如果是文字檔案,顯示增刪行數;如果是二進位制檔案,顯示增刪位元組大小。(注意檔案的新增刪除,也會視為行數/位元組的變化)

5、更詳細:

git log --patch or git log -p,結果會比 git log 和 git log --stat 多出更多資訊:比如每次提交所引入的差異(按 補丁 的格式輸出),等。

注:這種展示在命令列很亂,推薦用 GUI 來看吧。

6、定製化:

git log --format定製提交記錄的顯示格式。

選項 說明
%H 提交的完整雜湊值
%h 提交的簡寫雜湊值
%T 樹的完整雜湊值
%t 樹的簡寫雜湊值
%P 父提交的完整雜湊值
%p 父提交的簡寫雜湊值
%an 作者名字
%ae 作者的電子郵件地址
%ad 作者修訂日期(可以用 --date=選項 來定製格式)
%ar 作者修訂日期,按多久以前的方式顯示
%cn 提交者的名字
%ce 提交者的電子郵件地址
%cd 提交日期
%cr 提交日期(距今多長時間)
%s 提交說明

[拓展] 作者(author)提交者(committer)的區別是:

作者是最初補丁(patch)的人,而提交者是最後應用補丁的人。

大多數情況兩者是一樣的,也有不一樣:

  • 譬如你在 github 的 web 端修改檔案並 commit,那作者是你,而提交者是 github
  • 如果另一個人用 git cherry-pick, git rebase, git commit --amend, git filter-branch, git format-patch && git am 之類的 git 命令重寫了這個 commit,其實都是新生成了一個commit,那麼新生成的那個 commit 的 author 還是原來的,但 committer 會變成執行這個操作的使用者。可以簡單地理解成 author 是第一作者,committer 是生成 commit 的人。

② 篩選用法

選項 說明
<commit id> 僅顯示這條提交及更早的提交。
-<n> 僅顯示最近的 n 條提交。
--since, --after 僅顯示指定時間之後的提交。
--until, --before 僅顯示指定時間之前的提交。
--author 僅顯示作者匹配指定字串的提交。
--committer 僅顯示提交者匹配指定字串的提交。
--grep 僅顯示提交說明中包含指定字串的提交。
-S 僅顯示新增或刪除內容匹配指定字串的提交。
-- 僅顯示涉及該檔案的提交。

示例:

# 選項可以搭配使用
git log 42d8fc -2 # 可以是時間 or 時段
git log --since=2.weeks
git log --before="2008-11-01" # value 有空格等特殊字元,記得加雙引號
git log --grep="fix bug" # -- 可以指定多個檔案
git log -- foo.py bar.py

③ 針對單個檔案

git log <file>

④ 針對檔案中的某行

git log -L:可以展示程式碼中一行或者一個函式的歷史。

寫法:

git log -L <start>,<end>:<file>

or

git log -L :<funcname>:<file>

示例:

假設我們想檢視 zlib.c 檔案中 git_deflate_bound 函式的每一次變更,我們可以執行 git log -L :git_deflate_bound:zlib.c

注:至於函式的歷史,git 預設只支援 C 語言,其他語言需要單獨配置,這裡不贅述了。

(9)git diff(tool)

① 基本用法

git diff 可以用來分析檔案差異。顯示的格式正是 Unix 通用的 diff 格式。

git diff 不同比較的引數:

git diff 工作區 暫存區 指定 commit 最新 commit
工作區 N/A - - -
暫存區 預設 N/A - -
指定 commit <commit-id> --cached <commit-id> <commit-id><commit-id> -
最新 commit HEAD --cached HEAD <commit-id> N/A

注:

  • 預設是比較所有檔案,加上 -- <path> 是比較具體檔案
  • --cached 別名 --staged(後者的表意更加正確,前者是歷史遺留)

② 高階用法

1、檢查差錯

--check 可以用來檢查多餘的 衝突標記 或 空白。

到底什麼算空白,是根據 core.whitespace 引數來指定的(上面有介紹)。

③ 外掛

命令列這麼看還是不太直觀,git 支援使用外掛(譬如第三方 diff 工具甚至圖形化工具)來比較差異。

1、檢視外掛

git difftool --tool-help 可以檢視你的系統支援哪些 Git Diff 外掛,我的結果如下:

'git difftool --tool=<tool>' may be set to one of the following:
araxis
bc
bc3
emerge
opendiff
vimdiff
vimdiff2
vimdiff3 The following tools are valid, but not currently available:
codecompare
deltawalker
diffmerge
diffuse
ecmerge
examdiff
gvimdiff
gvimdiff2
gvimdiff3
kdiff3
kompare
meld
p4merge
tkdiff
winmerge
xxdiff Some of the tools listed above only work in a windowed
environment. If run in a terminal-only session, they will fail.

這裡我自己會使用我熟悉且好用的 bc/bc3 (即 Beyond Compare)。

2、進行差異比較

用法跟 git diff 一樣,即把 diff 替換成 difftool 即可。

④ GUI - Gitkraken(推薦)

使用 GUI 更方便。

1、選中僅兩個提交 - diff between

結果:兩個檔案之間的差異

2、選中兩個以上提交 - merged diff

結果:這些檔案的修改累計在一起

注意:diff between 和 merged diff 結果並不同。

3、其他操作

(1)拒絕 add - 忽略檔案

① 基本操作

.gitignore

作用:

當 .gitignore 中包含檔案(夾)的路徑時, git add . 並不會 add 它,並且如果你單獨 git add <filename> 的話,也會預設拒絕,並提示你用 -f 才行。

規則:

.gitignore 使用標準的 glob 模式匹配。

在最簡單的情況下,一個倉庫可能只根目錄下有一個 .gitignore 檔案,它遞迴地應用到整個倉庫中。 然而,子目錄下也可以有額外的 .gitignore 檔案。子目錄中的 .gitignore 檔案中的規則只作用於它所在的目錄中。 (Linux 核心的原始碼庫擁有 206 個 .gitignore 檔案。)

注:也可在 git 的配置檔案裡設定想要忽略的檔案,但是不推薦,這樣別人 clone 你的專案,並不會沿用你忽略的設定。

示例:

github 針對一些主流的語言、框架、平臺推出了常用的 .gitignore:https://github.com/github/gitignore, 例如 Node.js 的 https://github.com/github/gitignore/blob/master/Node.gitignore

沒有看到 react。

② 高階操作

1、除錯忽略規則

適用場景:某個不想忽略的檔案被忽略了,但不知道是哪個 .gitignore 檔案的哪一行起的作用。

git check-ignore -v App.class

結果:

.gitignore:3:*.class    App.class

Git會告訴我們,.gitignore 檔案的第3行規則忽略了該檔案,於是我們就可以知道應該修訂哪個規則。

③ 常見問題

1、已經 add 的檔案如何忽略?

還來得及,因為檔案還沒被跟蹤。保證 .gitignore 有此檔案的路徑,並用 git reset 把檔案從暫存區拿下,即可。

2、已經 commit 的檔案如何忽略?

來不及了,因為檔案已經被跟蹤。

  • 方法一:還是要保留檔案,只是要取消追蹤
# 相當於手動刪除 README.md,並 add,接著重新建立跟之前一樣的新檔案 README.md
git rm --cached README.md 修改 .gitignore 新增 README.md 路徑 git commit
  • 方法二:既要取消追蹤,更要工作區刪除檔案
直接手動刪除 README.md,然後 add 

修改 .gitignore 新增 README.md 路徑

git commit

方法一 跟 方法二 的區別僅在:add 後有沒有重新建立跟之前一樣的新檔案 README.md。

3、在上面 問題2 基礎上,如果我想把之前所有涉及這個檔案的 commit 裡的那個檔案都刪除呢?(比如之前的某次 commit 不小心包含了一個很大的檔案,雖然按 問題2 的方法移除了,但它還是在 git 倉庫中的,譬如別人 clone 還是會佔很大地方)

參考下面 重置歷史 介紹的 filter-branch 命令。

(2)工作目錄 + 暫存區的貯藏 - git stash

① 基礎用法

1、貯藏

# 1.0、只貯藏已跟蹤檔案(工作區+暫存區)
git stash
=
git stash push # 1.1、貯藏所有檔案,包括未跟蹤(工作區+暫存區)
git stash # 2、新增說明資訊
git stash save "message…"

貯藏哪類檔案的引數:

git stash 未跟蹤 已跟蹤(未修改) 已跟蹤(已修改) 已跟蹤(已放入暫存區) 忽略的檔案
預設 × N/A ×
--include-untrackedor -u N/A ×
--allor -a N/A

原理:把儲存到一個上。

應用場景:

  • 當你在做一個新功能時,突然要緊急修復一個 bug,那你需要先把手頭的工作先貯藏,之後再恢復。

2、檢視

(1)檢視列表

git stash list

結果:

[email protected]{0}: On master: test -
[email protected]{1}: On master: 123
[email protected]{2}: WIP on master: 3bd050d 111

(2)檢視具體

git stash show [email protected]{0}

3、恢復

# 不保留在 list 中
git stash pop
git stash pop [email protected]{2} # 還保留在 list 中
git stash apply
git stash apply [email protected]{2}

注:

  • 恢復時,之前在暫存區的,會被移到工作區。如果不想這樣(即想原封不動的恢復),可以加上 --index
  • 恢復不需要在當初貯藏的分支
  • 恢復不需要保持工作區和暫存區是 clear 狀態

適用場景:

  • 可以在新分支快速恢復貯藏,並繼續工作:git stash branch testchanges

4、最佳實踐

如果想最好的保留和恢復現場,最佳實踐是:git stash -u / git stash -a 搭配 git stash pop --index / git stash apply --index

5、刪除

(1)具體

git stash drop
git stash drop [email protected]{2}。

(2)所有

git stash clear

6、互動式操作

--patch

這個還是用 GUI 把,不然太繁瑣。

② 其他用法

1、備份

git stash 還可以用來作備份。

適用場景:工作完成準備提交前,先把暫存區的檔案備份下(譬如可以用在另一分支上),可以用 git stash --keep-index,他的效果等於 git stash ,但同時暫存區不會動(但它確實儲存了)。

(3)工作目錄的清理 - git clean

① 使用

對於工作目錄中一些工作或檔案,你想做的也許不是貯藏而是移除。 git clean 命令就是用來幹這個的。

注:這個不可恢復,一個更安全的選項是執行 git stash --all 來移除每一樣東西並存放在棧中。

清理哪類檔案的引數:

git clean 未跟蹤 忽略的檔案
預設 ×
-x

其他引數:

  • -d :清除子目錄
  • -i--interactive :互動式

注意:

git clean 不可恢復,最好

  • 1、使用前先用 --dry-run-n,模擬清理,它會告訴你將要移除什麼。
  • 2、可以先用 git stash 備份下。

4、簽署工作

前面提到 commit 的元資訊,是可以隨便輸入的(比如你可以把 author 隨便改成別人的名字),那豈不是 git 不安全的嗎?

git 可以使用 GPG 來簽署自己的工作,例如:

  • 簽署提交
  • 簽署標籤
  • ………

本人暫時沒用到,這裡不贅述了,感興趣的看:https://git-scm.com/book/zh/v2/Git-工具-簽署工作

5、檢索

(1)git grep

git grep 查詢一個字串或者正則表示式,支援:

  • 工作區(預設)
  • 暫存區
  • 提交歷史
  • 等等

問:針對工作區,我們可以使用 grep 或者 IDE 的搜尋;針對提交歷史,我們可以使用 git log,為什麼還要使用 git grep 呢?

答:

  • 速度非常快
  • 檢索的範圍更廣
(2)其他檢索方式

1、git log 檢索提交歷史。

參考上面的 git log 的介紹。

四、分支

1、分支簡介

git 的分支功能是必殺技特性,使得 Git 從眾多版本控制系統中脫穎而出。

優點:

  • 輕量
  • 快速
  • 簡單

2、分支原理

① 分支

Git 的分支的本質上僅僅是指向提交物件的可變指標

② 當前分支(通過 HEAD)

那如何知道當前分支是哪一個呢?有一個名為 HEAD 的特殊指標

3、使用

(1)預設分支

Git 的預設分支名字是 master

在 git init 的時候就會預設建立它。

(2)分支建立

① 原理

當前所在的提交物件上建立一個指標

② 操作

方法一:只建立分支不切換。

# 預設指向HEAD
git branch testing # 指向具體某個引用
git branch testing master

關於引用是什麼,下面會專門介紹。

方法二:建立分支並切換。

# 預設指向HEAD
git checkout -b testing # 指向具體某個引用
`git checkout -b testing master`

上面的 git checkout -b testing 等同於:

git branch testing
git checkout testing
(3)檢視(當前)分支

① 簡略

git branch

輸出結果:

* master
production
staging
uat

② 詳細

git branch -v

輸出結果:

* master  0936571 [ahead 24] 11
master2 699c90b 123
master3 81a05db 11
staging ba8dee8 快合併

git branch 多包含了:

  • 分支上的最新一次提交(commit id + 提交資訊)
(4)分支切換

原理:HEAD 指標的移動。

git checkout testing

注:工作區和暫存區的內容都會保持跟隨。

(5)刪除分支

原理:刪除指標(所以很快)

git branch -d hotfix

4、git log 涉及分支的用法

  • git log 預設是顯示當前分支下的提交歷史
  • git log --all 可以顯示所有分支下的提交歷史
  • git log --oneline --graph --all 可以顯示所有分支下的提交歷史,並且有圖形化的分支合併展現。(推薦還是 GUI 看吧)
  • --no-merges,不顯示合併提交。
  • --merge,顯示合併提交。

5、分支型別

(1)按穩定性分

長期分支

  • 為不同開發環境建立不同分支( 譬如 staging、uat、production )
  • 為不同穩定性建立的不同分支(譬如 LTS、Current )

主題分支短期分支

主題分支是一種短期分支,它被用來實現單一特性或其相關工作。

  • 不同人在不同分支上獨立工作
  • 建立新分支來 fix bug( 通常這樣的分支起名為 hotfix

6、合併分支

(1)為什麼要合併分支

當你建立新的分支後,隨著後續各分支的提交,會形成分支分叉。那麼我們可能需要合併分支

(2)合併操作

git merge <branch>

示例:

git checkout -b hotfix

# 修改問題

# commit

git checkout master

git merge hotfix
(3)合併結果

上面的合併分支的結果:

Fast-forward 快進合併

情形:如果順著一個分支走下去能夠到達另一個分支,那麼 git 只會簡單的將指標向前推移。

合併前:

合併後:

三方合併

情形:(如下圖),Git 會使用兩個分支的末端所指的快照(C4 和 C5)以及這兩個分支的公共祖先(C2),做一個簡單的三方合併(生成 C6)

合併後如果不需要原分支就可以刪除它了(畢竟已經指向了同一個位置)。

合併前:

合併後:

快進合併和三方合併的區別【重點】

  • 1、快進合比三方合併快速
  • 2、快進合併不會生成新的提交物件,而三方合併會生成新的提交物件(即合併提交)
  • 3、快進合併並不會產生衝突,而三方合併有可能會產生衝突

④ 問:為什麼有時候要用 --no-ff 禁用快進合併?【重點】

git merge --no-ff hotfix

一般建議在主要、重要的分支上,習慣性的帶上--no-ff。只要發生合併,就要有一個單獨的合併節點。 (尤其是修復 bug 的分支)

它的好處有:

  • 1、保持commit資訊的清晰直觀。
  • 2、不利於以後的回滾,見下圖。

示例:

  • 如果不加 --no-ff(圖下方),預設是快進合併,那在 C5 處想要回滾到 HEAD^ ,則回到 C3 ( 這不是我們想要的 )。
  • 而如果加了 --no-ff(圖上方),那在 C5 處想要回滾到 HEAD^ ,則回到 C4 ( 是我們想要的 )。
(4)解決衝突

① 手動解決衝突

解決步驟:

1、合併結果會告訴你存在衝突,並讓你去解決。(衝突的檔案位於工作區)

2、git status 會在 Unmerged paths 中列出衝突的檔名

3、開啟衝突的檔案,會用 會用 <<<<<<< , ======= , 和 >>>>>>> 來標識衝突之處,如下所示:

<<<<<<< HEAD
2222
=======
1111111111
>>>>>>> staging
  • 上面顯示當前所在分支
  • 下面顯示合併進來的分支

4、手動編輯

5、git add 去 mark resolution, git commit 去提交 resolution,才算最終完成衝突的解決。

② 外掛解決衝突

這裡使用到 git mergetool 命令,跟另一個命令 git difftool 有些類似,可以借鑑使用。

1、git mergetool --tool-help 可以檢視你的系統支援哪些 Git merge 外掛(我是 mac,預設為 vimdiff,但我這裡用 Beyond Compare)。

2、git mergetool -t bc,git 會自動開啟 Beyond Compare,然後在裡面手動編輯。

3、編輯好後儲存退出 Beyond Compare,命令列會向你確認:”Was the merge successful“,輸入 y,則完成衝突的解決( git 會自動幫你 add ),最後再 commit。

[拓展]

用 mergetool 的話,會有一個麻煩,就是每次編輯完後,會自動生成 [衝突的檔名].orig備份檔案在我的工作區。

解決辦法:

  • 在 .gitignore 中忽略它
  • 直接修改 git 設定: git config --global mergetool.keepBackup false ,禁止產生備份檔案

手動解決衝突 和 外掛解決衝突 的區別

  • 1、在編輯檔案時,前者只會提供衝突地點兩方的檔案內容;而後者會提供衝突地點三方的檔案內容(即 base + local + remote )
  • 2、在編輯檔案後,前者需要手動 add + commit,而後者(當你在命令列裡確認解決後) git 會自動幫你完成 add,但需要最後手動 commit。

④ GUI - Gitkraken 解決衝突

因為 Gitkraken 免費版不支援編輯衝突檔案,所以略。

(5)高階 - 關於衝突的更多操作

取消解決衝突

git merge --abort or git reset --hard HEAD 可以恢復合併前的狀態(工作區不可恢復,這也是為什麼建議合併前保持工作區是空的狀態的原因了)

檢出(三方)衝突

1、介紹

Git 會提供一個略微不同版本的衝突標記: 不僅僅只給你 “ours”“theirs” 版本,同時也會有 “base” 版本在中間來給你更多的上下文

在上面介紹的外掛解決衝突,用 Gitkraken 也是支援顯示出三方源( 包括 base )。

2、操作

# 單次
git checkout --conflict=diff3 hello.rb or # 永久
git config --global merge.conflictstyle diff3

3、結果

def hello
<<<<<<< ours
puts 'hola world'
||||||| base
puts 'hello world'
=======
puts 'hello mundo'
>>>>>>> theirs
end

快速解決檔案衝突

git 提供一種無需合併的快速方式,你可以選擇留下一邊的修改而丟棄掉另一邊修改。

git checkout --ours hello.rb
git checkout --theirs hello.rb

適用場景:

  • 二進位制檔案衝突時這可能會特別有用,因為可以直接簡單地選擇一邊。

記住衝突 - git rerere

git rerere 是“重用已記錄的衝突解決方案(reuse recorded resolution)”的意思。它允許你讓 Git 記住解決一個塊衝突的方法(在快取中), 這樣在下一次看到相同衝突時,Git 可以為你自動地解決它

具體用法待寫。

(6)高階 - 關於合併的更多操作

① 更多的合併方法

方法一:直接合並,不產生衝突

# 直接合並所有
git merge -Xours branch-name
git merge -Xtheirs branch-name # 直接合並單個檔案
git merge-file --ours filename.txt
git merge-file --theirs filename.txt

方法二:假合併 - “ours” 策略

欺騙 Git 認為那個分支已經合併過。實際上並沒有合併。

$ git merge -s ours branch-name
Merge made by the 'ours' strategy.

適用場景:

假設你有一個分叉的 release 分支並且在上面做了一些你想要在未來某個時候合併回 master 的工作。 與此同時 master 分支上的某些 bugfix 需要向後移植回 release 分支。 你可以合併 bugfix 分支進入 release 分支同時也 merge -s ours 合併進入你的 master 分支 (即使那個修復已經在那兒了)這樣當你之後再次合併 release 分支時,就不會有來自 bugfix 的衝突。

方法三:子樹合併

子樹合併的思想是你有兩個專案,並且其中一個對映到另一個專案的一個子目錄,或者反過來也行。 當你執行一個子樹合併時,Git 通常可以自動計算出其中一個是另外一個的子樹從而實現正確的合併。

② 更多的合併選項

1、忽略空白

  • -Xignore-all-space:在比較行時完全忽略空白修改
  • -Xignore-space-change:第二個選項將一個空白符與多個連續的空白字元視作等價的

你也可以手動處理檔案後再合併,實際上,這比使用 ignore-space-change 選項要更好,因為在合併前真正地修復了空白修改而不是簡單地忽略它們。(在使用 ignore-space-change 進行合併操作後,我們最終得到了有幾行是 DOS 行尾的檔案,反而使提交內容混亂了。)

(7)撤銷合併

場景:當你不小心合併了:

① 方法一:修復引用

git reset --hard HEAD~

結果:

缺點:重寫了歷史,在一個共享的倉庫中這會造成問題的。

② 方法二:還原提交

revert 命令下面會專門介紹。

git revert -m 1 HEAD

“-m 1” 標記指出 “mainline” 需要被保留下來的父結點。

結果:

新的提交 ^M,內容等於 -C3 + -C4(他們的還原)。即 ^M 與 C6 有完全一樣的內容,所以從這兒開始就像合併從未發生過。

[拓展] 問:如果在 topic 分支上又加了個 C7,然後想把 topic 分支再合併到 master 來。怎麼辦?

希望結果: master 能包含 topic 分支的 C3 + C4 + C7 提交。

易錯方法:直接 git merge topic,錯誤,因為之前合併過,所以導致這次合併僅有 C7 的提交

正確方法:執行 git revert ^M,M 與 ^M 抵消了(即 ^^M 等於 C3 與 C4 的修改),這時再 git merge topic 即可。結果見下圖:

(8)檢視 待合併/合併過 的分支

① 檢視哪些分支已經合併到 當前分支/指定分支

git branch --merged / git branch --merged master

在輸出的結果列表中,分支名字前沒有 * 號的可以使用 git branch -d 刪除

② 檢視哪些分支還沒合併到 當前分支/指定分支

git branch --no-merged / git branch --no-merged master

在輸出的結果列表中,git branch -d 是刪除不了的,必須 -D 強制刪除

注意 -d 和 -D 的區別,-d 只是刪除,而 -D 是強制刪除。

7、遠端倉庫

注:遠端倉庫可以在遠端伺服器,也可以在你的本地主機上。(詞語“遠端”只是表示它在別處。)

(1)檢視

① 列表

1、基本

git remote:它會列出每一個遠端倉庫的簡寫。

輸出結果:

# 預設
origin

2、詳細

git remote -v:它會列出每一個遠端倉庫的簡寫 + 對應的 URL + fetch or push。

輸出結果:

origin	https://github.com/xjnotxj/test.git (fetch)
origin https://github.com/xjnotxj/test.git (push)

② 具體詳情

git remote show <remote>,如 git remote show origin。

輸出結果:

* remote origin
Fetch URL: https://github.com/xjnotxj/test.git
Push URL: https://github.com/xjnotxj/test.git
HEAD branch: master
Remote branch:
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (fast-forwardable)
(2)新增

git remote add <shortname> <url>,如 git remote add pb https://github.com/paulboone/ticgit

注:如果你使用 clone 命令克隆了一個倉庫,git 會自動將其新增為遠端倉庫並預設以 “origin” 為簡寫。

(3)修改

① 修改簡寫

git remote rename <old-shortname> <new-shortname>,git remote rename pb paul

② 修改 url

git remote set-url <shortname> <new-url>,如 git remote set-url origin [email protected]:test/thinkphp.git

注:沒找到修改單獨 fetch / push 的 url 的命令,不知道支不支援。待寫。

(4)刪除

git remote rm <shortname>,如 git remote rm origin

8、遠端倉庫的分支

(1)遠端分支

① 介紹

遠端分支(remote branch)就是在遠端倉庫上的分支。

② 操作

1、檢視

git branch -r

還有個更底層的命令:git ls-remote <remote>

輸出結果:

origin/HEAD -> origin/master
origin/master

[拓展]

git branch -a 檢視所有分支(本地+遠端)。

2、刪除

git push origin -d serverfix

3、建立

參考下面的 git push 介紹。

(2)遠端分支的跟蹤

① 概念【重點】

1、遠端跟蹤分支(remote-tracking branch)記錄遠端分支狀態的本地分支

特點:

  • <remote>/<branch> 的形式命名。
  • 只讀(使用者不能隨意移動,除非使用 git fetch 等指令)。
  • 並不能切換過去然後編輯,它只是一個指標。(想要編輯必須建立 跟蹤分支)

2、跟蹤分支(tracking branch) 是一個本地分支,它通過跟遠端跟蹤分支產生關聯,進而間接地跟遠端分支產生關聯。

注意:遠端分支、遠端跟蹤分支 和 跟蹤分支 的區別。

作用:可以方便的進行 pull 和 push(用簡寫形式)(下面會專門介紹)。

3、上游分支(upstream branch),即 跟蹤分支 追蹤的遠端分支。

② 操作

1、建立(遠端跟蹤分支+跟蹤分支)

方法一:git clone

  • 預設只自動建立 master 的 遠端跟蹤分支 + 跟蹤分支
  • 其它的遠端分支只會建立遠端跟蹤分支而沒有跟蹤分支

方法二:git checkout

1、當沒有事先準備好的本地分支,就直接建立跟蹤分支

(1)本地分支名跟隨遠端分支名(需保證沒有重名的本地分支)
git checkout --track origin/serverfix
git checkout serverfix # 簡寫(省去了“origin/”) (2)本地分支名自擬
git checkout -b newBranch origin/serverfix 2、當有事先準備好的本地分支,就轉化為跟蹤分支(也可用於修改跟蹤分支的追蹤) (1)單獨指定
git branch -u origin/serverfix # -u = --set-upstream-to
git branch -u origin/serverfix serverfix2 (2)在想要 push 的時候指定
git push -u origin colin1 (3)在想要 pull 的時候指定
# 並沒有 git pull -u origin colin1
# 操作同(1)單獨指定

2、修改

參考上面的 ”1、建立“ --> ”方法二:git checkout“ --> ”2、當有事先準備好的本地分支,就轉化為跟蹤分支“

3、刪除

  • 只能刪除跟蹤分支(就按普通分支刪除即可)
  • 不能刪除遠端跟蹤分支

4、檢視

git branch -vv:檢視本地分支 or 跟蹤分支(及它的遠端跟蹤分支)

注意:如果遠端跟蹤分支沒有被跟蹤,則不會顯示。

輸出結果:

* colin          d145421 22
develop 17e0c45 [origin/develop] Merge pull request #3 from xjnotxj/master
master 6bd8a8d [origin/master: behind 1] Create blank.yml

輸出結果:

  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
testing 5ea463a trying something new

注:

  • 跟蹤分支上還會顯示與遠端跟蹤分支相比領先和落後的情況(例如 ahead 3, behind 1)。

    這個情況需要經常的 fetch 保持更新(如何 fetch 參考下面的介紹)。

(3)git fetch - 更新 遠端追蹤分支

原理:將遠端分支拉取到對應的遠端跟蹤分支。

# 1、所有 remote 的所有遠端分支
git fetch --all # 2、remote 的所有遠端分支
git fetch # 預設為 origin
=
git fetch origin # 3、remote 的指定遠端分支
git fetch origin branchName

注:

  • 好習慣:定期的執行 git fetch --all,不過如果用 GUI 工具,一般預設它都會自動幫你輪詢頻繁執行。
(4)git pull - 拉取 遠端分支併合並

原理:將遠端分支拉取到對應的遠端跟蹤分支,並與本地分支(譬如跟蹤分支)合併等於 git fetch + git merge)。

# 1、完整寫法

git pull origin next:master # origin 的遠端分支 next fetch 下來並和 master 合併

git pull origin next # 簡寫(如果遠端分支和本地分支都叫 next)

# 2、簡潔寫法(如果配了跟蹤分支)

git pull # 預設 origin + 當前分支
=
git pull origin # 預設 當前分支
(5)git push - 推送

原理:將本地分支(譬如跟蹤分支)推送到遠端分支

# 1、完整寫法

git push origin next:master # origin 的本地分支 next push 到遠端分支 master 上

git push origin next # 簡寫(如果本地分支和遠端分支都叫 next)

# 2、簡潔寫法(如果配了跟蹤分支)

git push # 預設 origin + 當前分支
=
git push origin # 預設 當前分支

前提:

  • 有遠端倉庫的寫入許可權
  • 之前沒有人推送過(最佳實踐:先 pull 再 push

適用場景:

  • 可以分私人分支和公開分支,私有分支不 push。
(6)應用:使用遠端倉庫與別人協作

也可適用於 github 上 fork 專案後,保持更新。

① 長期合作

儲存為源,並建立跟蹤分支,以後方便使用。

git remote add jessica git://github.com/jessica/myproject.git
git fetch jessica
git checkout -b rubyclient jessica/ruby-client

② 短期合作

不儲存,僅臨時使用。

git pull https://github.com/onetimeguy/project # 當前分支

git pull https://github.com/onetimeguy/project master # 指定分支

9、變基

(1)介紹

其實,在 Git 中整合不同分支的修改主要有兩種方法:

  • merge 合併(上面介紹過了)
  • rebase 變基

變基(rebase)可以將提交到某一分支上的所有修改都移至另一分支上

就好像“重新播放(replay)”一樣。

這個比喻生動形象!

(2)基礎用法

① 示例

1、變基前

目標:把 experiment 分支變基到 master 分支上。

2、變基

git checkout experiment
git rebase master

結果:現在提取在 C4 中的修改,然後在 C3 的基礎上應用一次。

3、變基後

目標:把 master 分支往前移,到 experiment 分支的位置。

方法一:使用 merge 的快進合併

git checkout master
git merge experiment

方法二:再次用 rebase(使用 rebase 的快進合併)

git rebase master
git checkout master

② 原理步驟

  • 1、首先找到這兩個分支(即當前分支 experiment、目標分支 master)的最近共同祖先 C2
  • 2、然後對比當前分支相對於 C2 的歷次提交,提取相應的修改並存為臨時檔案
  • 3、然後將當前分支指向目標分支的最新提交 C3
  • 4、最後將之前另存為臨時檔案的修改依序應用
(3)高階操作

--onto

1、介紹

上面說到變基可以將提交到某一分支上的所有修改都移至另一分支上,注意這個“所有修改”。但有時候我們不想要全部。

目的:選中在 client 分支裡但不在 server 分支裡的修改(即下圖的 C8 和 C9),將它們在 master 分支上重放。

結果:讓 client 看起來像直接基於 master 修改一樣

2、操作

(1)變基前

(2)變基

git rebase --onto master server client

(3)變基後

git checkout master
git merge client

互動式【重點】

1、介紹

上面說到 rebase 的功能就像”重新播放“一樣,那在重新播放的時候,我們可以做很多的變化:

  • 刪除提交
  • 修改提交(的提交資訊)
  • 合併提交
  • 拆分提交
  • 重新排序提交

2、用法

(1)命令列

git rebase -i 支援互動式操作。

例如 git rebase -i HEAD~3 表示要修改在 HEAD~3..HEAD 範圍內的提交。

(2)GUI(Gitkraken)(推薦)

跟用命令列差不多,但操作更加直觀便捷。

方法一:互動式操作

支援:

  • 即保留這個提交不變 —— pick(預設)
  • 合併提交 —— squash
  • 修改提交資訊 —— reword
  • 刪除提交 —— drop
  • 排序提交 —— 直接滑鼠拖拽排序位置

沒找到拆分提交在哪。

方法二:快捷操作

是基於上面互動式操作的快捷方法。

(4)衝突

① rebase 關於衝突的操作

  • 如果您希望跳過沖突:git rebase --skip
  • 停止 rebase:git rebase --abort

② rebase vs merge

rebase 跟 merge 一樣,在涉及快進合併上不會有衝突,但是三方合併可能存在衝突

但跟 rebase 的衝突處理操作跟 merge 相比有一些不同

  • 關於解決完成衝突:rebase 解決完後是執行 git rebase --continue,而 merge 解決完後是執行 commit
(5)merge vs rebase

關於二者衝突的相同和不同,看上面一節。這裡不提了。

① 相同點

  • merge 和 rebase 的最終結果沒有任何區別。

② 不同點

見上圖:

  • 執行命令的所在分支不一樣。merge 是在目標分支執行命令,rebase 是在原有分支執行命令(前者拉過來,後者推過去)

  • 在三方合併上,是否生成新的提交物件(即合併提交)。merge 會產生新的提交物件,而 rebase 只會把自己原有的提交物件移過去,而不是生成新的。

  • 在三方合併上,分支指標的變化不同。看上圖。所以 merge 一般完成後不需要再移動分支指標,而 rebase 後,一般需要手動再移動下目標分支的指標(用 merge or rebase)。

  • 產生的提交歷史不同。merge 的提交歷史不變,提交樹保持分叉,而 rebase 會修改提交歷史,提交樹改造成一條直線。

    注意:改造提交歷史有風險

五、標籤

1、適用場景

  • 釋出結點( 譬如版本號:v1.0、v2.0 )

2、分類與建立

(1)輕量標籤(lightweight)

① 原理

輕量標籤只是一個指標,永遠指向一個提交物件(不可移動)。

注意“通常”二字,實際上標籤物件可以指向任何 git 物件。

② 建立

git tag v1.4-lw

(2)附註標籤(annotated)

① 原理

若要建立一個附註標籤,Git 會先建立一個標籤物件,然後記錄一個引用來指向該標籤物件,而不是(像輕量標籤一樣)直接指向提交物件。

所以 附註標籤 跟 輕量標籤 的結果都是引用,但前者中間隔了一個標籤物件。

② 標籤物件的內容

標籤物件很像提交物件,本身帶有元資訊,包括:

  • Tagger
  • Date
  • 標籤資訊

③ 建立

git tag -a v1.4 -m "my version 1.4"

(3)輕量標籤 vs 附註標籤

相同:

  • 建立後,都不可以輕易移動

不同:

  • 建立原理不同(具體看上面附註標籤的原理)
  • 後者比前者多了一些關於標籤的元資訊

3、檢視標籤

(1)列表

注:

  • 預設情況下,標籤不是按時間順序列出,而是按字母排序的。

① 本地

git tag 

# 想要萬用字元匹配可以帶上 -l / --list
git tag -l "v1.8.5*"

② 遠端

git ls-remote --tags origin

(2)具體

git show <tagname>

4、跟遠端互動(共享標籤)

① 拉

git fetch、git pull、git clone 會預設拉取所有標籤到本地倉庫。

# 拉取所有標籤
git pull origin --tags

② 推

git push 預設並不會傳送標籤到遠端倉庫。

那麼如何推送標籤呢:

# 單獨推送一個標籤
git push origin <tagname>。 # 推送所有標籤(把所有不在遠端倉庫上的標籤全部傳送到那裡)
git push origin --tags

5、刪除標籤

① 針對本地

git tag -d <tagname>

② 針對遠端

git push origin -d <tagname>

6、檢出標籤

git checkout <tag-name>

六、Git 內部原理

1、Git 的底層命令和上層命令

  • “底層(plumbing)”命令:這些命令被設計成能以 UNIX 命令列的風格連線在一起,抑或藉由指令碼呼叫,來完成工作。

  • “上層(porcelain)”命令:對使用者更友好的命令。

本文介紹的幾乎大多都是上層命令。

2、.git 目錄

① 介紹

.git 目錄包含了幾乎所有 Git 儲存和操作的東西。

如若想備份或複製一個版本庫,只需把這個目錄拷貝至另一處即可。

② 內容

新初始化的 .git 目錄的典型結構如下:

config
description
HEAD
hooks/
info/
objects/
refs/

重要的:

  • HEAD 檔案:指向目前被檢出的分支
  • index 檔案(尚待建立):儲存暫存區資訊
  • objects 目錄:儲存所有資料內容
  • refs 目錄:儲存指向資料(分支、遠端倉庫和標籤等)的提交物件的指標

次要的:

  • description 檔案:僅供 GitWeb 程式使用,我們無需關心。
  • config 檔案:包含專案特有的配置選項
  • info 目錄:包含一個全域性性排除(global exclude)檔案, 用以放置那些不希望被記錄在 .gitignore 檔案中的忽略模式(ignored patterns)
  • hooks 目錄:包含客戶端或服務端的鉤子指令碼(hook scripts)

3、Git 物件

(1)介紹

Git 物件位於 .git/objects 目錄下。

(2)分類
  • 1、資料物件(blob object):儲存著檔案快照。

  • 2、樹物件(tree object):記錄著目錄結構和資料物件的索引。

樹物件將多個檔案組織到一起,有點像 UNIX 的檔案管理

實際上樹物件屬於默克爾樹(Merkle Tree),優勢是可以快速判斷變化。

注意:資料物件並不存檔名,而是放在樹物件裡儲存。

  • 3、提交物件(commit object):包含著指向樹物件的指標,指向父提交物件的指標,和提交的元資訊。

注意:其中提交物件的指向父物件的指標:首次提交沒有,普通提交有一個,多個分支合併有多個。

  • 4、其他物件

譬如標籤物件(只針對附註標籤)等……

(3)物件之間的關係

1、首次提交:

2、多次提交:

3、多次提交下,資料物件可以重用:

(4)物件的建立
  • 資料物件:git add 時建立
  • 樹物件 + 提交物件:git commit 時建立

注:

  • 每個資料物件一旦建立是不可變的,如果檔案修改了,那會創造一個新的資料物件。
  • 每個commit都是git倉庫的一個快照
(5)檢視物件

① 檢視所有物件 - git count-objects -v

輸出結果

count: 22
size: 88
in-pack: 12
packs: 1
size-pack: 4
prune-packable: 0
garbage: 0
size-garbage: 0
  • count 代表物件的個數
  • size 是物件們佔用的空間(單位 KB)

② 檢視具體物件

git show

(6)物件的清理

① 底層命令

② 高階命令

③ gc

手動執行 git gc,可以清理一些無用的物件

git gc 還有其他功能(下面都會提到):

  • 打包物件
  • 清理 reflog 無用的記錄
(7)物件的打包 —— 包檔案

① 包檔案介紹

  • Git 最初向磁碟中儲存物件時所使用的格式被稱為“鬆散(loose)”物件格式,會使用 zlib 壓縮
  • 但是,Git 會時不時地將多個這些物件打包成一個稱為“包檔案(packfile)”的二進位制檔案,以節省空間和提高效率

② 打包原理

  • 查詢命名及大小相近的檔案打包
  • 只儲存檔案不同版本之間的差異內容(有可能第二個版本完整儲存了檔案內容,而原始的版本反而是以差異方式儲存的——這是因為大部分情況下需要快速訪問檔案的最新版本)

③ 觸發打包的條件

  • 有太多的鬆散物件(如7000 個以上)
  • 有太多的包檔案(50 個包檔案以上)
  • 手動執行 git gc 命令
  • git push 時
  • ……
(8)從 Git 物件 窺視 Git 的實質

還記得在文章開頭我們說過 git 是版本控制(Revision control)的軟體,但這一章瞭解了 git 的底層原理,可以發現,從根本上來講 Git 是一個內容定址(content-addressable)檔案系統,並在此之上提供了一個版本控制系統的使用者介面。

這個內容定址檔案系統的核心部分是一個簡單的鍵值對資料庫(key-value data store)。 你可以用底層命令向 Git 倉庫中插入任意型別的內容,它會返回一個唯一的鍵,通過該鍵可以在任意時刻再次取回該內容。而 Git 物件,正是這樣存進去的。

4、Git 物件的 id 與 引用

(1)物件的 id

上面我們說到 Git 物件的本質是儲存在鍵值對資料庫裡的,那存入的過程中一定會分配 key(即 id)。

① 提交物件的 id

1、介紹

commit id 即提交物件的id(唯一標識),用 SHA-1 表示。

SHA-1 摘要長度是 20 位元組,也就是 160 位。出現重複的概率極低,為 2^80,是 1.2 x 10^24,也就是一億億億。

而 SVN 是遞增的整數。

2、表示 commit id 的方法

方法一:直接寫全 commit id

如:ca82a6dff817ec66f44342007202690a93763949

方法二:只寫 SHA-1 的前幾個字元

如:ca82a6

注:

  • 不得少於 4 個
  • 不能有歧義,否則需要加多字元

    例如,到 2019 年 2 月為止,Linux 核心這個相當大的 Git 專案, 其物件資料庫中有超過 875,000 個提交,包含七百萬個物件,也只需要前 12 個字元就能保證唯一性。

建議:通常用 8 到 10 個字元即可。

[拓展] git log --abbrev-commit 可以在 log 列印中把 commit id 的位數縮短。

② 其他的物件 id

(2)引用是什麼

① 介紹

引用位於 .git 下的 .git/refs 目錄。

如果我們有一個檔案來儲存物件的 id 值,而該檔案有一個簡單的名字,然後用這個名字來替代原始的難記的 id 值會更加簡單。

在 Git 中,這種簡單的名字被稱為“引用(references,或簡寫為 refs)”。

② 引用 vs 指標

可以發現引用很像 c 語言裡指標的概念。

可以形象的說,引用是指向 Git物件 的指標

注:本文會把指標引用混淆使用,其實指的是一個意思。(但具體有什麼細微的差別,我嘗試 google 未果,於是在原書的 github 上發了問( https://github.com/progit/progit2/issues/1460 ),暫且無人回覆,此處等待,待寫。)

(3)引用 之 分支引用

位於:refs/heads 目錄下。

① 使用

git show topic1 表示該分支頂端的提交(下同)。

② 反推

git rev-parse topic1 獲取 commit id

(4)引用 之 標籤引用

位於:refs/tags 目錄下。

① 使用 + ② 反推 跟上面的分支一樣,略。

本身標籤跟分支就很類似。

(5)引用 之 遠端引用

位於:refs/remotes 目錄下。

① 使用

git show origin/master

② 反推

git rev-parse origin/master 獲取 commit id

這個值 commit id 跟遠端倉庫對應的是一樣的

(6)符號引用是什麼

所謂符號引用(symbolic reference),表示它是一個指向引用的引用。

套娃

(7)符號引用 之 HEAD 引用

① 介紹

之前我們在 分支 一章介紹過 HEAD,說他是指向分支引用,代表了當前分支是哪一個。

其實 HEAD 不光可以指向分支引用,(從上面的符號引用的定義來看),HEAD 可以指向任何引用

② HEAD 的建立

在你 init、clone 等命令來初始化專案的時候,HEAD 就會自動建立。

HEAD 無法刪除。

③ HEAD 的移動

1、自動移動

  • git commit 後,HEAD 前進
  • git reset 後,HEAD 後退
  • ……

2、手動移動

使用 checkout 命令。有如下情況:

  • checkout 到具體提交物件時,HEAD 指向該提交物件(直接指向該提交)
  • checkout 本地分支(包含跟蹤分支)時,HEAD 指向該分支引用(間接指向該分支頂端的提交)
  • checkout 標籤時,HEAD 指向該標籤引用(直接指向該標籤引用對應的提交)

    注意,這裡容易理解成是間接。實際上這時 HEAD 跟標籤引用是並行的指向提交物件的(不管是輕量標籤還是附註標籤)。

  • checkout 遠端跟蹤分支時,HEAD 指向該遠端引用(直接指向該遠端跟蹤引用對應的提交)

    注意,這個只適用於這個遠端跟蹤分支沒有被本地追蹤。

上面的 ”直接“/”間接“ 中的 ”直接“,代表了處於 分離頭指標 的狀態。

[拓展] 分離頭指標 detached HEAD【重點】

1、介紹

(根據上面的介紹)只有 checkout 不在 本地分支(包含跟蹤分支)。 才會出現這種情況。

2、風險

拿 commit 舉例。

這時候你正常的 commit 是可以的,但是這個新提交將不屬於任何分支,會造成:

  • 無法訪問(通過 git log 無法查到,除非記得當初它的commit id 才能看到。)
  • 隨時有被刪除的可能( git 會認為這是個沒用的提交,可能在 gc 的時候刪掉 )

如果你真的需要在分離頭指標狀態下 commit(例如你想基於這個標籤的版本修復某個 bug),那麼可以在此標籤的基礎上建立一個新分支

④ 反推

git symbolic-ref HEAD 獲取引用 name(如 refs/heads/master

要想進一步獲取引用指向的 commit id,可以再執行:git rev-parse refs/heads/master

(8)祖先引用

① 介紹

引用(符號引用)除了可以表示自身,還能搭配 ^~ 來進行祖先引用

② 使用

下面以 HEAD 為例。

1、~ 表示父提交

# 父提交
git show HEAD~ # 父提交的父提交(祖父提交)
git show HEAD~~ # 父提交的父提交的父提交(以此類推)
git show HEAD~3
=
git show HEAD~~~

2、^ 表示當前分支/另一個分支下的父提交

# 當前分支
git show HEAD^
=
git show HEAD^1 # 另一個分支(在沒有另一個分支的情況下(非合併提交),會失敗)
git show HEAD^2

注意:HEAD^3 及其以上,略。

因為貌似 git 只支援兩個分支的合併(即提交物件不會有超過兩個的直接父提交),兩個分支以上的合併也是基於多步驟的兩兩合併來的【待求證】

見下圖(當 HEAD 位於不同地方):

3、^~ 的聯絡

  • HEAD^ = HEAD~
  • 可以組合使用^~(例如 HEAD~3^2、HEAD^2~3)
(9)引用日誌 - git reflog

① 原理

位於 .git/logs/

每當你的 HEAD 所指向的位置發生了變化,Git 就會將這個資訊儲存到引用日誌這個歷史記錄裡。

注意:引用日誌只存在於本地倉庫,當你從遠端倉庫 clone、fetch / pull、push 時,不會涉及引用日誌。

② 使用

1、檢視列表

git reflog

包括這些記錄:

  • clone

  • checkout

  • commit

  • reset

  • discard

  • merge

  • rebase

  • 等等……

輸出示例:

8bd49ac [email protected]{0}: checkout: moving from third to 8bd49ac75fe6fdf0cf5aa66561ed123acb5095cb
43151e5 [email protected]{1}: checkout: moving from a6bbabe31540ca2cb4d2c3ce925e8a26616de4d1 to third
a6bbabe [email protected]{2}: commit: 222
8bd49ac [email protected]{3}: checkout: moving from c43433e2bce4b03d79367553a21dad75ddb78d6c to c43433e2bce4b03d79367553a21dad75ddb78d6c

2、檢視具體

使用 @{n} 來引用 reflog 中輸出的提交記錄。

@{n} 有點類似 HEAD 結合 ^ 和 ~ 的用法,只是前者基於 ref(HEAD)歷史,後者基於提交歷史。

# 當前
$ git show [email protected]{0} # 五次前
$ git show [email protected]{5}

③ 適用場景

  • 恢復、撤銷之前的操作【重點】

    例如:撤銷之前刪除的 commit,可以用 reflog 找到 對應的 commit id,然後用 git reset --hard <commit-id> or git branch recover-branch <commit-id> 等操作建立新分支。

[拓展] 如果 reflog 也沒有之前刪掉的 commit 記錄怎麼辦?

比如你的 reflog 記錄被清了(比如 gc),那可以用 git fsck --full

git fsck 命令用來檢查資料庫的完整性

輸出示例:

Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

dangling commit 後的 SHA-1 就是你要你找的 commit id,恢復辦法參考 reflog 一樣即可。

④ git reflog vs git log

相同點:

  • git reflog 命令絕大多數使用方法跟 git log 一樣(可參考)。

不同點:

  • git reflog 比 git log 相比資訊更豐富,可以看到所有操作記錄。

    從這點看,git log 是 git reflog 的子集。

聯絡:

  • 可以執行 git log -g ,檢視 log 形式資訊的 reflog 內容。

    注意:只是形式是 log ,而內容不是 log。即 git log -g 條目結果不等於 git log,而等於 git reflog。

5、替換物件

① 功能

replace 命令可以讓你在 Git 中指定 某個物件 並告訴 Git:“每次遇到這個 Git 物件時,假裝它是 其它物件”。

② 適用場景

在你用一個不同的提交替換歷史中的一個提交而不想以 git filter-branch 之類的方式重建完整的歷史時,這會非常有用。

③ 使用

七、關於提交物件和提交歷史

1、選擇提交區間

提交區間(即一個或多個提交物件),是基於分支的操作。(即使你傳的不是分支名,而是別的引用,那 git 也會把它當成的假設在這個引用上建立的某分支來看待。

下面的例子都預設為分支名

(1)雙點

① 使用

git log A..B

② 原理

③ 適用場景

  • 檢視 B 分支中還有哪些提交尚未被合併入 A 分支。(譬如,想檢視在 experiment 分支中而不在 master 分支中的提交,你可以使用 git log master..experiment。)

  • 檢視即將 git push 的內容。(git log origin/master..HEAD

    注意:git log origin/master..HEAD = git log origin/master..如果你留空了其中的一邊, Git 會預設為 HEAD

(2)多點

兩點是多點的特殊情況/簡寫形式

$ git log refA..refB
=
$ git log refB ^refA
=
$ git log refB --not refA

② 使用

多點就是可以寫多個,省略兩點的同時,搭配 ^--not

git log A B ^C

git log A B --not C

③ 原理

④ 適用場景

  • 彌補 雙點 不能基於兩個以上分支選取的限制。

例項:檢視所有被 refA 或 refB 包含的但是不被 refC 包含的提交

$ git log refA refB ^refC
=
$ git log refA refB --not refC
(3)三點

① 介紹

git log A...B

② 原理

③ 適用場景

  • 選出被兩個引用之一包含但又不被兩者同時包含的提交。(譬如 git log master...experiment)
  • 解決衝突的時候,回溯源頭可以用到。

[拓展]

三點語法 跟 git log 的引數 --left-right 結合,可以顯示提交是來源哪一邊分支的。

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

2、重置揭密

(1)Git 的三棵樹

” 在我們這裡的實際意思是“檔案的集合”,而不是指特定的資料結構。

  • 這裡的樹也只是個形象的比喻。

Git 作為一個系統,管理並操縱這三棵樹

用途
HEAD 上一次提交的快照,下一次提交的父結點
Index 預期的下一次提交的快照
Directory 沙盒

注:

  • git 的 Index(索引),也稱”暫存區“。本文兩者混用。

三棵樹相互關係:

(2)reset

① 無路徑重置

1、引數

初始狀態:

  • git reset --soft HEAD^

  • git reset [--mixed] HEAD^

  • git reset --hard HEAD^

注:

  • 執行此操作最好還是保持工作區和暫存區的清空(比如 stash 下),避免一些意外情況的發生。
  • 注意寫法:git reset --hard HEAD^ 是對的,git reset HEAD^ --hard 是錯的(坑的是這樣也是可以執行的,等於 --mixed)
  • --hard 是 reset 命令唯一的危險用法,它也是 Git 會真正地銷燬資料的僅有的幾個操作之一。(用的時候一定要小心)

2、原理步驟

步驟(1):移動 HEAD 指標,帶著分支指標一起(若指定了 --soft,則到此停止)

結果:

  • 之前 commit 的改動:打回暫存區(相當於逆操作 git commit)
  • 現有改動【跟之前 commit 的改動不重疊】:暫存區和工作區不受影響;
  • 現有改動【跟之前 commit 的改動重疊】:僅暫存區會自動合併檔案的修改,工作區不受影響;

步驟(2):使索引看起來像 HEAD (若指定 --mixed 或 預設,則到此停止)

結果:

  • 之前 commit 的改動:打回工作區(相當於逆操作 git commit + git add)
  • 現有改動【跟之前 commit 的改動不重疊】:工作區不受影響,暫存區會被打回工作區(相當於逆操作 git add)
  • 現有改動【跟之前 commit 的改動重疊】:工作區+暫存區會一起自動合併檔案的修改,最後落在工作區

步驟(3):使工作目錄看起來像索引(若指定 --hard,則到此停止)

結果:

  • 之前 commit 的改動:刪除

    如果針對的是 HEAD(即當前提交),那 “之前 commit 的改動” 是沒有意義的,可以忽略。

  • 現有改動:暫存區和工作區全部刪除

    這裡討論 “跟之前 commit 的改動重不重疊” 是沒有意義的。

注:

  • 其實刪除可以理解成從工作區再往後打回,但是沒有退路了,就等於刪除了。

3、適用場景

(1)作用於“之前 commit 的改動”

主要是針對 HEAD~ 甚至更早的版本:

  • 回退版本(常用):git reset --hard HEAD~
  • 壓縮提交:git reset --soft HEAD~2,然後再次執行 git commit
  • 拆分提交:git reset HEAD~,然後分多次執行 git add + git commit

(2)作用於“現有改動”

主要是針對 HEAD:

  • 把暫存區打回工作區(常用):git reset HEAD

    即 git add 的相反操作。

  • 清空暫存區和工作區:git reset --hard HEAD

    git reset --hard HEAD 跟 git clean 的區別是,前者清除快取區+工作區,後者只清除工作區。

③ 有路徑重置(即針對具體檔案)

1、引數

git reset file.txt
=
git reset -- file.txt

2、原理【重點】

git reset file.txt 約等於 git reset --mixed HEAD + 指定檔案

為什麼說約等於,具體區別看下面的介紹。

3、跟 ”無路徑重置“ 的區別【重點】

區別(1):原理步驟

  • 步驟1,不同。git reset file.txt 不會移動 HEAD 指標,更不會移動分支指標
  • 步驟2,相同。
  • 步驟3,沒有。(因為 git reset file.txt 相當於 --mixed ,而不是 --hard,自然不會執行到步驟3)

區別(2):適用場景

git 把 git reset file.txt 的引數給限制死了

  • **只能是 HEAD 而不能是 HEAD~ 等其它
  • 只能是 --mixed 而不能是 --hard 和 --soft 等其它

目的就是為了實現”無路徑重置“適用場景中唯一的一個,即 “把暫存區打回工作區”

(3)checkout

前面介紹 “符號引用之 HEAD 引用”,也提到了 checkout 的用法,可去參考。

① 無路徑重置

1、用法

  • git checkout [branch]
  • git checkout [其它引用]

2、原理步驟

步驟:

移動 HEAD 指標

而 reset 會移動 HEAD + 分支的指向

結果:

  • 之前 commit 的改動:刪除

    這一點像 git reset --hard

  • 現有改動【跟之前 commit 的改動不重疊】:暫存區和工作區不受影響;

    這一點像 git reset --soft

  • 現有改動【跟之前 commit 的改動重疊】:git 會 Aborting 並提醒你 commit or stash

    這一點即不像 git reset --hard 那樣自動刪除,也不像 git reset --soft 那樣自動合併。可以說非常的安全。

② 有路徑重置

1、用法

git checkout file
=
git checkout -- file
=
git checkout HEAD -- file

2、原理步驟

git checkout file.txt vs git checkout(無路徑) 的區別:

區別(1)原理步驟

  • 不會移動 HEAD 指標,更不會移動分支指標

區別(2)結果 與 適用場景

這裡就不把 checkout 有路徑 跟 上面提到的 reset 無路徑/有路徑 和 checkout 無路徑 做對比了,這會讓事情變的更復雜。就直接看下面的敘述就好,簡單直接。

把某個檔案恢復到某個提交的樣子,如果你在暫存區或者工作區對這個檔案有改動,則:

  • 改動會被丟失(危險)
  • 會建立新的改動並自動 add 到暫存區

注:

  • 可以看出 git checkout file 跟 git checkout 的差別很大,跟 git reset 和 git reset file 的差別也大。(真的服了這個設計,為了實現功能也不能把命令搞得這麼分裂不統一啊…)
(4)reset vs checkout
HEAD Index Workdir WD Safe?
Commit Level
reset --soft [commit] REF NO NO YES
reset [commit] REF YES NO YES
reset --hard [commit] REF YES YES NO
checkout <commit> HEAD YES YES YES
File Level
reset [commit] <paths> NO YES NO YES
checkout [commit] <paths> NO YES YES NO
  • HEAD 一列中的 “REF” 表示該命令移動了 HEAD 指向的分支引用,而 “HEAD” 則表示只移動了 HEAD 自身。
  • Index、Workdir 列中的的 “YES”、“NO”,表示“之前 commit 的改動”是否會打回。
  • WD Safe? 列,如果它標記為 “NO”,那麼執行該命令之前請考慮一下。
(5)reset 和 checkout 對提交歷史的影響
  • reset:只有 無路徑 + HEAD~ 甚至更早的版本 才會對提交歷史有影響(影響的結果是提交被刪除)
  • checkout:不會

3、撤銷提交

(1)reset

① 用法

reset + 無路徑重置

詳細見之前的介紹,不贅述。

(2)rebase

① 用法

使用 rebase 互動式用法

詳細見之前的介紹,不贅述。

(3)revert

① 用法

git revert HEAD   # 撤銷前一次 commit
git revert HEAD^ # 撤銷前前一次 commit

注:

  • 執行 revert 前工作區和暫存區都得為空(否則 git 會提示並執行不了)
(4)reset vs rebase vs revert

相同:

  • 都可以撤銷某次(某些)提交

不同:

  • reset 和 rebase 是去掉這次提交,revert 是保留這次提交,生成一次新的提交(內容是上一次提交的相反操作)
  • reset 最不靈活,只對於撤銷緊跟 HEAD 的連續著的 N 次提交比較方便,而 rebase 和 revert 可以針對位於中間的隨意某個提交去撤銷。
(5)reset 和 rebase 和 revert 對提交歷史的影響
  • reset 和 rebase 會對提交歷史有影響(影響的結果是提交被刪除)
  • revert 會對提交歷史有影響(影響的結果是提交歷史又新增了)

4、複製+貼上 提交

(1)cherry-pick

使用前:

使用:

git cherry-pick e43a6

使用後:

注:

  • 執行 cherry-pick 前工作區和暫存區都得為空(否則 git 會提示並執行不了)
  • 複製過去的新提交,貼上的時候,因為應用的日期不同(但其他資訊相同),你會得到一個新的 commit id 值
(2)cherry-pick 對提交歷史的影響
  • cherry-pick 會對提交歷史有影響(影響的結果是提交歷史又新增了)

5、修改提交

(1)rebase

rebase 互動式 可以修改提交。

看之前的介紹,不贅述了。

(2)git commit --amend

作用:修改最後一次提交。

git commit --amend

看之前的介紹,不贅述了。

注意:因為提交物件改變了,Git 是有完整性校驗的,所以會 commit id 肯定會改變。

(3)filter-branch

作用:批量提交歷史改寫。

注意:這個命令會修改你的歷史中的每一個提交的 commit id

① 使用建議

  • 因為 filter-branch 改變的太多了,建議在一個測試分支中做這件事。
  • 為了讓 filter-branch 在所有分支上執行,可以給命令傳遞 --all 選項。

② 適用場景

filter-branch 不過多介紹,略,直接說應用。

  • 1、刪除歷史檔案

有人粗心地通過 git add,提交了一個巨大的二進位制檔案,或者一個帶密碼的私密檔案,需要從所有的歷史提交記錄裡刪去。

git filter-branch --tree-filter 'rm -f passwords.txt' HEAD

  • 2、批量修改郵箱地址

你開始工作時忘記執行 git config 來設定你的名字與郵箱地址,或者你想要開源一個專案並且修改所有你的工作郵箱地址為你的個人郵箱地址。

git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "[email protected]" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="[email protected]";
git commit-tree "[email protected]";
else
git commit-tree "[email protected]";
fi' HEAD
(4)rebase、git commit --amend、filter-branch 對提交歷史的影響
  • rebase 會對提交歷史有影響(影響的結果是提交歷史刪除了)
  • git commit --amend 會對提交歷史有影響(影響的結果是提交歷史刪除了)
  • filter-branch 會對提交歷史有影響(影響的結果是提交歷史刪除了)

6、改變提交歷史的風險

(1)有什麼風險

這一章的幾乎每一節,最後一塊都會討論 此操作命令 對提交歷史的影響,為什麼要如此重視,因為提交歷史變動的風險很大。

比如你變基操作後,原有分支會的位置會不見(因為原有分支的修改和指標統統都轉移到了目標分支),所以如果有別人基於原有的分支的這些提交進行開發,就會出錯。

具體會造成什麼樣的錯誤,不贅述了。

(2)什麼會導致風險

就是上面介紹到的關於修改提交歷史的操作,涉及命令:

  • reset
  • rebase
  • git commit --amend
  • filter-branch
(3)怎麼避免風險

建議在本地操作好後再推送你的工作

git 也會有相應的保護措施,譬如你在本地變基了已經被推送的提交,繼而再 push 到遠端,會被拒絕。(如果確信真的沒人用,可以加 -f 來強制 push)

(4)既然有風險,乾脆不要改變提交歷史了?

關於改變提交歷史好不好,仁者見仁智者見智:

  • 有一種觀點認為,提交歷史是真實記錄實際發生過什麼,不要改變它。
  • 另一種觀點則正好相反,他們認為提交歷史是專案過程中發生的事,怎麼方便後來的讀者觀看就怎麼寫。

所以在保證安全的情況下,根據自己的真實的需要,是可以改變的。

八、Git 工具

1、Git 別名

(1)方法一:git 命令 - 不加!

① 適用場景

git 有些命令太長 or 不好記,你可以自定義別名。

② 原理

簡單的替換後執行命令。

③ 示例

# 當要輸入 git commit 時,只需要輸入 git ci。
git config --global alias.ci commit # 當要輸入 git reset HEAD -- 檔名 時,只需要輸入 git unstage 檔名 即可。
git config --global alias.unstage 'reset HEAD --'
(2)方法二:系統命令 - 加!

① 適用場景

然而,你可能想要執行外部命令,而不是一個 Git 子命令,可以在命令前面加入 ! 符號。

② 原理

替換後把開頭的 git 去掉,再執行命令。

③ 示例

# 當要輸入 ls 路徑 時,只需要輸入 git visual 路徑。
git config --global alias.visual '!ls'

2、除錯

適用場景:如果你在追蹤程式碼中的一個 bug,並且想知道是什麼時候引入的

(1)檔案標註(當你知道問題出在哪)

① 檢視每行的直接來源

1、git blame <filename>

可以看到當前版本的某個檔案,每一行分別是:

  • 哪個提交
  • 哪個作者

2、git blame -L 69,82 <filename>

-L 可以指定行數範圍

② 檢視每行的間接來源(真正來源)

1、git blame -C -L 141,153 <filename>

-C 會分析你檔案中從別的地方複製過來的程式碼片段的原始出處。

這個功能很有用。通常來說,你會認為複製程式碼過來的那個提交是最原始的提交,因為那是你第一次在這個檔案中修改了這幾行。但 Git 會告訴你,你第一次寫這幾行程式碼的那個提交才是原始提交,即使這是在另外一個檔案裡寫的。

③ GUI(推薦)

GUI 的 file blame + file history 更直觀更好用。

(2)二分查詢(當你不知道問題出在哪)

① 基本用法

git bisect 命令會對你的提交歷史進行二分查詢來幫助你儘快找到是哪一個提交引入了問題。

使用步驟:

  • 首先執行 git bisect start 來啟動
  • 接著執行 git bisect bad 來告訴系統當前你所在的提交是有問題的
  • 然後你必須使用 git bisect good <good_commit>,告訴 bisect 已知的最後一次正常狀態是哪次提交。這時譬如 Git 發現在你標記為正常的提交(v1.0)和當前的錯誤版本之間有大約12次提交,於是 Git 檢出中間的那個提交。
  • 現在你可以執行測試,看看在這個提交下問題是不是還是存在。然後執行 git bisect good or git bisect bad
  • 當最終找到問題後,你應該執行 git bisect reset 重置你的 HEAD 指標到最開始的位置。

② 高階用法

嫌上面的手動太麻煩,可以引入 bash 指令碼。

略。

3、打包

(1)適用場景
  • 有可能你的網路中斷了,但你又希望將你的提交傳給你的合作者們(通過郵件或者快閃記憶體)。
  • 可能你現在沒有共享伺服器的許可權,
  • 你又希望通過郵件將更新傳送給別人, 卻不希望通過 format-patch 的方式傳輸 40 個提交。
(2)使用

① 打包

# 打包全部
git bundle create repo.bundle HEAD master # 打包增量(提交區間)

具體解釋略。

② 解包

跟 clone 一樣的操作。

git clone repo.bundle repo

結果:得到跟 clone 一樣的結果。

4、歸檔

(1)適用場景
  • 為那些不使用 Git 的人準備。
(2)使用
git archive master --prefix='project/' | gzip > `git describe master`.tar.gz

引數:

  • --prefix:在存檔中的每個檔名前新增字首
  • --format:指定歸檔格式,比如 zip

結果:

解壓後為專案的最新快照

注意與”打包“的不同。

九、Git 高階用法

1、子模組

子模組允許你將一個 Git 倉庫作為另一個 Git 倉庫的子目錄。 它能讓你將另一個倉庫克隆到自己的專案中,同時還保持提交的獨立。

略。

十、自定義 GIT

1、Git 配置

第一章 起步 有提到一些。

略。

2、Git 屬性

(1)介紹

可以針對特定的路徑配置某些設定項,這樣 Git 就只對特定的子目錄或子檔案集運用它們。這些基於路徑的設定項被稱為 Git 屬性

(2)配置檔案
  • .gitattributes 檔案(通常是你的專案的根目錄)。
  • 如果不想讓這些屬性檔案與其它檔案一同提交,你也可以在 .git/info/attributes 檔案中進行設定。

具體設定方法略。

(3)應用

① 過濾器 —— 對比 word 檔案、圖片 等二進位制檔案

1、原理

使用過濾器,把二進位制檔案輸出成文字檔案。

2、例項

  • 以 .docx 結尾的檔案應用“word”過濾器,即 docx2txt。 這樣你的 Word 檔案就能被高效地轉換成文字檔案並進行比較了。
  • 在比較時對影象檔案運用一個過濾器,提煉出 EXIF 資訊——這是在大部分影象格式中都有記錄的一種元資料。

② 關鍵字展開

借鑑的是 SVN 或 CVS 風格的關鍵字展開(keyword expansion)功能。

略。

3、Git 鉤子

(1)介紹

鉤子是什麼就不贅述了。

鉤子位於.git/hooks。把一個正確命名(不帶副檔名)且可執行的檔案放入其中即可被 Git 呼叫。

所有 Git 自帶的示例鉤子指令碼都是用 Perl 或 Bash 寫的。

(2)客戶端鉤子

① 提交工作流鉤子

  • pre-commit 鉤子:在鍵入提交資訊前執行,如果該鉤子以非零值退出,Git 將放棄此次提交
  • prepare-commit-msg 鉤子:在啟動提交資訊編輯器之前,預設資訊被建立之後執行
  • commit-msg 鉤子:接收一個引數,此引數即上文提到的,存有當前提交資訊的臨時檔案的路徑
  • post-commit 鉤子:在整個提交過程完成後執行。 它不接收任何引數

② 電子郵件工作流鉤子

③ 其它鉤子

  • pre-rebase 鉤子:執行於變基之前,以非零值退出可以中止變基的過程
  • post-checkout 鉤子:在 git checkout 成功執行後,會被呼叫
  • post-merge 鉤子:在 git merge 成功執行後,會被呼叫
  • pre-push 鉤子:在 git push 執行期間,會被呼叫
  • 等…
(3)伺服器端鉤子
  • pre-receive

    處理來自客戶端的推送操作時,最先被呼叫的指令碼是 pre-receive。 它從標準輸入獲取一系列被推送的引用。如果它以非零值退出,所有的推送內容都不會被接受。

  • update

    update 指令碼和 pre-receive 指令碼十分類似,不同之處在於它會為每一個準備更新的分支各執行一次。 假如推送者同時向多個分支推送內容,pre-receive 只執行一次,相比之下 update 則會為每一個被推送的分支各執行一次。

  • post-receive

    post-receive 掛鉤在整個過程完結以後執行,可以用來更新其他系統服務或者通知使用者。

(4)客戶端鉤子 和 伺服器端鉤子 的區別
  • push/clone、打包/clone 某個版本庫時,它的客戶端鉤子並不隨同複製。 (如果需要靠這些指令碼來強制維持某種策略,建議你在伺服器端實現這一功能。 )
(5)例項

使用強制策略的一個例子(用 Ruby 寫的):

https://git-scm.com/book/zh/v2/自定義-Git-使用強制策略的一個例子

十一、Git 與其他版本控制系統

1、SVN

(1)橋接

git svn 跟 svn 橋接使用。

(2)遷移

從 svn 遷移到 git。

十二、GitHub

1、基本功能

  • Git 託管
  • 問題追蹤
  • 程式碼審查
  • 等……

2、GitHub Actions

(1)介紹

GitHub Actions 是 GitHub 的持續整合服務。

如果你需要某個 action,不必自己寫複雜的指令碼,直接引用他人寫好的 action 即可,整個持續整合過程,就變成了一個 actions 的組合。這就是 GitHub Actions 最特別的地方。

(2)基本概念
  • 1、workflow (工作流程):持續整合一次執行的過程,就是一個 workflow。
  • 2、job (任務):一個 workflow 由一個或多個 jobs 構成,含義是一次持續整合的執行,可以完成多個任務。
  • 3、step(步驟):每個 job 由多個 step 構成,一步步完成。
  • 4、action (動作):每個 step 可以依次執行一個或多個命令(action)。
(3)使用

GitHub Actions 的配置檔案叫做 workflow 檔案,存放在程式碼倉庫的.github/workflows目錄。

3、GitHub Packages

類似 npm 。

十三、分散式 Git 的工作流(flow)

1、什麼是工作流?

多人協作開發的規範的工作流程

2、按專案複雜度劃分 - 著重在角色(許可權)

(1)集中式工作流

開發者在 push 之前,必須先 pull,這樣才不會有衝突。(即使兩個開發者並沒有編輯同一個檔案。)

(2)整合管理者工作流

  • 1、專案維護者推送到主倉庫。
  • 2、貢獻者克隆此倉庫,做出修改。
  • 3、貢獻者將資料推送到自己的公開倉庫。
  • 4、貢獻者給維護者傳送郵件,請求拉取自己的更新。
  • 5、維護者在自己本地的倉庫中,將貢獻者的倉庫加為遠端倉庫併合並修改。
  • 6、維護者將合併後的修改推送到主倉庫。

這是 GitHub 和 GitLab 等集線器式(hub-based)工具最常用的工作流程。

(3)主管與副主管工作流

  • 1、普通開發者在自己的主題分支上工作,並根據 master 分支進行變基。這裡是主管推送的參考倉庫的 master 分支。
  • 2、副主管將普通開發者的主題分支合併到自己的 master 分支中。
  • 3、主管將所有副主管的 master 分支併入自己的 master 分支中。
  • 4、最後,主管將整合後的 master 分支推送到參考倉庫中,以便所有其他開發者以此為基礎進行變基。

這其實是多倉庫工作流程的變種。一般擁有數百位協作開發者的超大型專案才會用到這樣的工作方式,例如著名的 Linux 核心專案。

但這種工作流程並不常用,只有當專案極為龐雜,或者需要多級別管理時,才會體現出優勢。

3、按不同產品劃分 - 著重在分支

(1)Git flow

① 分支

  • master 分支是主分支(長期分支),因此要時刻與遠端同步;
  • develop 分支是開發分支(長期分支),團隊所有成員都需要在上面工作,所以也需要與遠端同步;
  • feature 分支是開發具體功能的分支,是否推到遠端,取決於你是否和你的小夥伴合作在上面開發;
  • bug 分支只用於在本地修復 bug,就沒必要推到遠端了;
  • hotfix 分支只用於緊急修復遠端 master 分支的 bug;

② 適用場景

這個模式是基於"版本釋出"的,目標是一段時間以後產出一個新版本。

很多網站專案是"持續釋出",程式碼一有變動,就部署一次。這時,master分支和develop分支的差別不大,沒必要維護兩個長期分支。

(2)Github flow

① 分支

它只有一個長期分支,就是 master,此用起來非常簡單。

然後通過向 master 發起一個 pull request(簡稱PR)。

② pull request

pull request 的詳細介紹參考:

[拓展] PR / MR 區別

是一樣的,只是習慣的叫法不同:

  • GitHub、Bitbucket 和碼雲(Gitee.com)選擇 PR - Pull Request 作為這項功能的名稱
  • GitLab 和 Gitorious 選擇 MR - Merge Request 作為這項功能的名稱

③ 適用場景

適用於"持續釋出"。

(3)Gitlab flow

① 分支

它建議在master分支以外,再建立不同的環境分支

  • "開發環境"的分支是master
  • "預發環境"的分支是pre-production
  • "生產環境"的分支是production

② 上游

開發分支是預發分支的"上游",預發分支又是生產分支的"上游"。只有緊急情況,才允許跳過上游,直接合併到下游分支。

上面的流程,適用於"持續釋出"的專案,但對於"版本釋出"的專案,也可以稍加改變 :建議的做法是每一個穩定版本,都要從master分支拉出一個分支,比如2-3-stable、2-4-stable等等。

③ 適用場景

即適用於"持續釋出",也適用於"版本釋出"的專案(見上面剛剛的描述)。

我司用的即這種方法。

十四、GUI - gitkraken

1、安裝

下載地址:https://iusethis.luo.ma/gitkraken/

推薦安裝 v6.5.1。因為更新的版本加了對免費版的限制(例如不能open私有倉庫了,這基本上不升級pro就用不了了)

記得把 127.0.0.1 release.gitkraken.com 寫入你的 host 檔案,這樣就不會自動更新了。

2、配置

(1)配置外部程式

gitkraken 也可以跟 git 一樣,在設定裡配置 open、diff、merge 的外部程式。

3、操作

(1)快捷操作的按鈕

① undo + redo。

這個超好用,可以根據你上一次的操作,軟體就會自動算出對應的撤銷和重做需要執行的命令是啥,而你只需要點選按鈕就行。

不過也不是萬能的,有的複雜操作,undo + redo 是灰掉得(既不支援)。

(2)檔案瀏覽

注意:如果你是在歷史的 commit 裡對檔案做如下(畫紅框的)操作,針對的不是歷史的檔案,還是最新(HEAD 或者說 檢出工作區)的檔案。

這點有點反直覺。

4、其他更多

上文也穿插著介紹不少 Gitkraken 的用法。

十五、寫在最後

1、Git 的缺點

這裡更多的是我自己的“吐槽“,供拋磚引玉。

  • git 重置那塊,git reset 和 git reset file 和 git checkout 和 git checkout file,原理都不是相通的,真的是服了。

2、我之前關於 git 的文章