一、寫在前面
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、
檔案: 所有 OS 使用者 + 所有倉庫/etc/gitconfig
git config --system
由于它是系統配置檔案,是以你需要管理者或超級使用者權限來修改它。
- 2、
或~/.gitconfig
檔案:目前 OS 使用者 + 所有倉庫~/.config/git/config
git config --global
- 3、目前倉庫 Git 目錄中的 config 檔案(即
):目前 OS 使用者 + 目前倉庫.git/config
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 的送出資訊的模闆。commit.template
方法二:直接指令行裡快速輸入送出資訊
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、第三行詳細解析問題
注:
- 使用 line break 分離段落
- 可以使用 emoji,emoji 代表的意思可以參考這個規範:https://gitmoji.carloscuesta.me/
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 -s
(
git 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
定制送出記錄的顯示格式。
選項 | 說明 |
---|---|
| 送出的完整哈希值 |
| 送出的簡寫哈希值 |
| 樹的完整哈希值 |
| 樹的簡寫哈希值 |
| 父送出的完整哈希值 |
| 父送出的簡寫哈希值 |
| 作者名字 |
| 作者的電子郵件位址 |
| 作者修訂日期(可以用 --date=選項 來定制格式) |
| 作者修訂日期,按多久以前的方式顯示 |
| 送出者的名字 |
| 送出者的電子郵件位址 |
| 送出日期 |
| 送出日期(距今多長時間) |
| 送出說明 |
[拓展]
作者(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 的人。
② 篩選用法
選項 | 說明 |
---|---|
| 僅顯示這條送出及更早的送出。 |
| 僅顯示最近的 n 條送出。 |
, | 僅顯示指定時間之後的送出。 |
, | 僅顯示指定時間之前的送出。 |
| 僅顯示作者比對指定字元串的送出。 |
| 僅顯示送出者比對指定字元串的送出。 |
| 僅顯示送出說明中包含指定字元串的送出。 |
| 僅顯示添加或删除内容比對指定字元串的送出。 |
| 僅顯示涉及該檔案的送出。 |
示例:
# 選項可以搭配使用
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 | | | | 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 | √ | √ | × |
or | √ | N/A | √ | √ | × |
or | √ | N/A | √ | √ | √ |
原理:把儲存到一個棧上。
應用場景:
- 當你在做一個新功能時,突然要緊急修複一個 bug,那你需要先把手頭的工作先貯藏,之後再恢複。
2、檢視
(1)檢視清單
git stash list
結果:
stash@{0}: On master: test -
stash@{1}: On master: 123
stash@{2}: WIP on master: 3bd050d 111
(2)檢視具體
git stash show stash@{0}
3、恢複
# 不保留在 list 中
git stash pop
git stash pop stash@{2}
# 還保留在 list 中
git stash apply
git stash apply stash@{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 stash@{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 | 未跟蹤 | 忽略的檔案 |
---|---|---|
預設 | √ | × |
| √ | √ |
其他參數:
-
:清除子目錄-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
-
可以顯示所有分支下的送出曆史,并且有圖形化的分支合并展現。(推薦還是 GUI 看吧)git log --oneline --graph --all
-
,不顯示合并送出。--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 解決完後是執行
,而 merge 解決完後是執行 commitgit rebase --continue
(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 的底層指令和上層指令
-
:這些指令被設計成能以 UNIX 指令行的風格連接配接在一起,抑或藉由腳本調用,來完成工作。“底層(plumbing)”指令
-
:對使用者更友好的指令。“上層(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 最初向磁盤中存儲對象時所使用的格式被稱為
格式,會使用 zlib 壓縮。“松散(loose)”對象
- 但是,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~
- 可以組合使用
和^
(例如 HEAD3^2、HEAD^23)~
(9)引用日志 - git reflog
① 原理
位于
.git/logs/
。
每當你的 HEAD 所指向的位置發生了變化,Git 就會将這個資訊存儲到
引用日志
這個曆史記錄裡。
注意:引用日志隻存在于本地倉庫,當你從遠端倉庫 clone、fetch / pull、push 時,不會涉及引用日志。
② 使用
1、檢視清單
git reflog
包括這些記錄:
- clone
- checkout
- commit
- reset
- discard
- merge
- rebase
- 等等……
輸出示例:
8bd49ac HEAD@{0}: checkout: moving from third to 8bd49ac75fe6fdf0cf5aa66561ed123acb5095cb
43151e5 HEAD@{1}: checkout: moving from a6bbabe31540ca2cb4d2c3ce925e8a26616de4d1 to third
a6bbabe HEAD@{2}: commit: 222
8bd49ac HEAD@{3}: checkout: moving from c43433e2bce4b03d79367553a21dad75ddb78d6c to c43433e2bce4b03d79367553a21dad75ddb78d6c
2、檢視具體
使用
@{n}
來引用 reflog 中輸出的送出記錄。
@{n} 有點類似 HEAD 結合 ^ 和 ~ 的用法,隻是前者基于 ref(HEAD)曆史,後者基于送出曆史。
# 目前
$ git show HEAD@{0}
# 五次前
$ git show HEAD@{5}
③ 适用場景
- 恢複、撤銷之前的操作【重點】
例如:撤銷之前删除的 commit,可以用 reflog 找到 對應的 commit id,然後用
orgit reset --hard <commit-id>
等操作建立新分支。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 的子集。
聯系:
- 可以運作
,檢視 log 形式資訊的 reflog 内容。git log -g
注意:隻是形式是 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 會預設為 HEAD。git log origin/master..
(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^
是錯的(坑的是這樣也是可以運作的,等于 --mixed)git reset HEAD^ --hard
- --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 的改動:删除
如果針對的是
(即目前送出),那 “之前 commit 的改動” 是沒有意義的,可以忽略。HEAD
- 現有改動:暫存區和工作區全部删除
這裡讨論 “跟之前 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 | ||||
| REF | NO | NO | YES |
| REF | YES | NO | YES |
| REF | YES | YES | NO |
| HEAD | YES | YES | YES |
File Level | ||||
| NO | YES | NO | YES |
| 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" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="[email protected]";
git commit-tree "$@";
else
git commit-tree "$@";
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
- 然後你必須使用
,告訴 bisect 已知的最後一次正常狀态是哪次送出。這時譬如 Git 發現在你标記為正常的送出(v1.0)和目前的錯誤版本之間有大約12次送出,于是 Git 檢出中間的那個送出。git bisect good <good_commit>
- 現在你可以執行測試,看看在這個送出下問題是不是還是存在。然後執行
orgit bisect good
git bisect bad
- 當最終找到問題後,你應該執行
重置你的 HEAD 指針到最開始的位置。git bisect reset
② 進階用法
嫌上面的手動太麻煩,可以引入 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”過濾器,即
。 這樣你的 Word 檔案就能被高效地轉換成文本檔案并進行比較了。docx2txt
- 在比較時對圖像檔案運用一個過濾器,提煉出 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 分支
-
隻用于緊急修複遠端 master 分支的 bug;hotfix 分支
② 适用場景
這個模式是基于"版本釋出"的,目标是一段時間以後産出一個新版本。
很多網站項目是"持續釋出",代碼一有變動,就部署一次。這時,master分支和develop分支的差别不大,沒必要維護兩個長期分支。
(2)Github flow
① 分支
它隻有一個長期分支,就是 master,此用起來非常簡單。
然後通過向 master 發起一個 pull request(簡稱PR)。
② pull request
pull request 的詳細介紹參考:
- https://git-scm.com/book/zh/v2/GitHub-對項目做出貢獻
- https://git-scm.com/book/zh/v2/GitHub-維護項目
略
[拓展] 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 的文章
- 《一台電腦上的git同時使用兩個github賬戶》