中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

wordpress導(dǎo)航調(diào)用代碼湖南專業(yè)的關(guān)鍵詞優(yōu)化

wordpress導(dǎo)航調(diào)用代碼,湖南專業(yè)的關(guān)鍵詞優(yōu)化,成都圖紙?jiān)O(shè)計(jì)公司,建站資源免費(fèi)文章摘于GitHub博主geeeeeeeeek 文章目錄 1.1 Git 簡易指南創(chuàng)建新倉庫工作流添加與提交推送改動 1.2 創(chuàng)建代碼倉庫git init用法討論裸倉庫 例子 git clone用法討論倉庫間協(xié)作 例子用法討論栗子 1.3 保存你的更改git add用法討論緩存區(qū) 栗子 git commit用法討論記錄快照&#xf…

文章摘于GitHub博主geeeeeeeeek

文章目錄

  • 1.1 Git 簡易指南
    • 創(chuàng)建新倉庫
    • 工作流
    • 添加與提交
    • 推送改動
  • 1.2 創(chuàng)建代碼倉庫
    • git init
      • 用法
      • 討論
        • 裸倉庫
      • 例子
    • git clone
      • 用法
      • 討論
        • 倉庫間協(xié)作
      • 例子
      • 用法
      • 討論
      • 栗子
  • 1.3 保存你的更改
    • git add
      • 用法
      • 討論
        • 緩存區(qū)
      • 栗子
    • git commit
      • 用法
      • 討論
        • 記錄快照,而不是記錄差異
      • 栗子
  • 1.4 檢查倉庫狀態(tài)
    • git status
      • 用法
      • 討論
        • 忽略文件
      • 栗子
    • git log
      • 用法
      • 討論
      • 栗子
  • 1.5 檢出之前的提交
    • 檢出之前的提交
    • git checkout
      • 用法
      • 討論
      • 栗子
        • 查看之前的版本
        • 檢出文件
  • 1.6 回滾錯(cuò)誤的修改
    • 回滾錯(cuò)誤的修改
    • git revert
      • 用法
      • 討論
        • 撤銷(revert)和重設(shè)(reset)對比
      • 栗子
    • git reset
      • 用法
      • 討論
        • 不要重設(shè)公共歷史
      • 栗子
        • 取消文件緩存
        • 移除本地修改
    • git clean
      • 用法
      • 討論
      • 栗子
  • 1.7 重寫項(xiàng)目歷史
    • 概述
    • git commit --amend
      • 用法
      • 討論
        • 不要修復(fù)公共提交
      • 栗子
    • git rebase
      • 用法
      • 討論
        • 不要 rebase 公共歷史
      • 栗子
    • git rebase -i
      • 用法
      • 討論
      • 栗子
    • git reflog
      • 用法
      • 討論
      • 栗子
  • 2.1 保持代碼同步
  • 保持同步
    • git remote
      • 用法
      • 討論
        • 名為 origin 的遠(yuǎn)程連接
        • 倉庫的 URL
      • 栗子
    • git fetch
      • 用法
      • 討論
        • 遠(yuǎn)程分支
      • 栗子
    • git pull
      • 用法
      • 討論
        • 基于 Rebase 的 Pull
      • 栗子
    • git push
      • 用法
      • 討論
        • 強(qiáng)制推送
        • 只推送到裸倉庫
      • 栗子
  • 2.2 創(chuàng)建Pull Request
      • 剖析一個(gè) Pull Request
    • Pull Request是如何工作的
      • Feature 分支工作流中的 Pull Request
      • GitFlow 工作流中的 Pull Request
      • Fork 工作流中的 Pull Request
    • 栗子
      • Mary fork了官方項(xiàng)目
      • Mary 克隆了她的 GitHub 倉庫
      • Mary 開發(fā)了一個(gè)新功能
      • Mary 將 feature 分支推送到了她的 GitHub 倉庫
      • Mary 創(chuàng)建了一個(gè) Pull Request
      • John 審查了這個(gè) Pull Request
      • Mary 添加了一個(gè)后續(xù)提交
      • John 接受了 Pull Request
    • 接下來怎么做?
  • 2.4 使用分支
  • 使用分支
    • git branch
      • 用法
      • 討論
        • 分支的頂端
      • 栗子
        • 創(chuàng)建分支
        • 刪除分支
    • git checkout
      • 用法
      • 討論
        • 分離的 `HEAD`
      • 例子
    • git merge
      • 用法
      • 討論
        • 解決沖突
      • 例子
        • 快速向前合并
        • 三路合并
  • 2.5 常見工作流比較
    • 中心化的工作流
    • 如何工作
      • 管理沖突
    • 栗子
      • 一人初始化了中央倉庫
      • 所有人將倉庫克隆到本地
      • John 在開發(fā)他的功能
      • Mary 在開發(fā)她的功能
      • John 發(fā)布了他的功能
      • Mary as試圖發(fā)布她的功能
      • Mary在John的提交之上rebase
      • Mary 解決了合并沖突
      • Mary 成功發(fā)布了她的分支
    • 接下來該怎么做
    • Feature 分支的工作流
    • 如何工作
      • Pull Request
    • 栗子
      • Mary 開始了一個(gè)新功能
      • Mary 去吃飯了
      • Mary 完成了她的工作
      • Bill 收到了 Pull Request
      • Mary 作了修改
      • Mary 發(fā)布了她的功能
      • 同時(shí),John 以同樣的方式工作著
    • 接下來該怎么做
    • GitFlow 工作流
    • 如何工作
      • 歷史分支
      • 功能分支
      • 發(fā)布分支
      • 維護(hù)分支
    • 栗子
      • 創(chuàng)建一個(gè)開發(fā)分支
      • Mary 和 John 開始了新功能
      • Mary 完成了她的功能
      • Mary 開始準(zhǔn)備發(fā)布
      • Mary完成了她的發(fā)布
      • 終端用戶發(fā)現(xiàn)了一個(gè) bug
    • 接下來該怎么做
    • Fork 工作流
    • 如何工作
      • 中央倉庫
      • Fork 工作流中的分支
    • 栗子
      • 項(xiàng)目維護(hù)者初始化了中央倉庫
      • 開發(fā)者 fork 倉庫
      • 開發(fā)者將 fork 的倉庫克隆到本地
      • 開發(fā)者進(jìn)行自己的開發(fā)
      • 開發(fā)者發(fā)布他們的功能
      • 項(xiàng)目維護(hù)者整合他們的功能
      • 開發(fā)者和中央倉庫保持同步
    • 接下來該怎么做
  • 3. Git圖解
    • 基本用法
    • 約定
    • 命令詳解
      • Diff
      • Commit
      • Checkout
      • HEAD 標(biāo)識處于分離狀態(tài)時(shí)的提交操作
      • Reset
      • Merge
      • Cherry Pick
      • Rebase
  • 4.1 代碼合并:Merge、Rebase 的選擇
    • 概述
      • Merge
      • Rebase
      • 交互式的 rebase
    • Rebase 的黃金法則
      • 強(qiáng)制推送
    • 工作流
      • 本地清理
      • 將上游分支上的更改并入feature分支
      • 用 Pull Request 進(jìn)行審查
      • 并入通過的功能分支
    • 總結(jié)
  • 4.2 代碼回滾:Reset、Checkout、Revert 的選擇
    • 提交層面的操作
      • Reset
      • Checkout
      • Revert
    • 文件層面的操作
      • Reset
      • Checkout
    • 總結(jié)
  • 4.3 Git log 高級用法
    • 格式化 Log 輸出
      • Oneline
      • Decorate
      • Diff
      • Shortlog
      • Graph
      • 自定義格式
    • 過濾提交歷史
      • 按數(shù)量
      • 按日期
      • 按作者
      • 按提交信息
      • 按文件
      • 按內(nèi)容
      • 按范圍
      • 過濾合并提交
    • 總結(jié)
  • 4.4 Git 鉤子:自定義你的工作流
  • 概述
      • 安裝鉤子
      • 腳本語言
      • 鉤子的作用域
    • 本地鉤子
      • pre-commit
      • prepare-commit-msg
      • commit-msg
      • post-commit
      • post-checkout
      • pre-rebase
    • 服務(wù)端鉤子
      • pre-receive
      • update
      • post-receive
    • 總結(jié)
  • 4.5 Git提交引用和引用日志
    • 哈希字串
    • 引用
      • 指定引用
    • 打包引用目錄
    • 特殊的引用
    • refspec
    • 相對引用
    • 引用日志
    • 總結(jié)


1.1 Git 簡易指南

在這里插入圖片描述

創(chuàng)建新倉庫

創(chuàng)建新文件夾,打開,然后執(zhí)行 git init 以創(chuàng)建新的 git 倉庫。

下面每一步中,你都可以通過 git status 來查看你的git倉庫狀態(tài)。

工作流

你的本地倉庫由 Git 維護(hù)的三棵「樹」組成。
第一個(gè)是你的 工作目錄,它持有實(shí)際文件;
第二個(gè)是 緩存區(qū)(Index),它像個(gè)緩存區(qū)域,臨時(shí)保存你的改動;
最后是 HEAD,指向你最近一次提交后的結(jié)果。

事實(shí)上,第三個(gè)階段是 commit history 的圖。HEAD 一般是指向最新一次 commit 的引用?,F(xiàn)在暫時(shí)不必究其細(xì)節(jié)。

添加與提交

你可以計(jì)劃改動(把它們添加到緩存區(qū)),使用如下命令:

git add < filename >
git add *

這是 Git 基本工作流程的第一步。使用如下命令以實(shí)際提交改動:

git commit -m "代碼提交信息"

現(xiàn)在,你的改動已經(jīng)提交到了 HEAD,但是還沒到你的遠(yuǎn)端倉庫。

在開發(fā)時(shí),良好的習(xí)慣是根據(jù)工作進(jìn)度及時(shí) commit,并務(wù)必注意附上有意義的 commit message。創(chuàng)建完項(xiàng)目目錄后,第一次提交的 commit message 一般為「Initial commit」。

推送改動

你的改動現(xiàn)在已經(jīng)在本地倉庫的 HEAD 中了。執(zhí)行如下命令以將這些改動提交到遠(yuǎn)端倉庫:

git push origin master

可以把 master 換成你想要推送的任何分支。

如果你還沒有克隆現(xiàn)有倉庫,并欲將你的倉庫連接到某個(gè)遠(yuǎn)程服務(wù)器,你可以使用如下命令添加:

git remote add origin <server>

如此你就能夠?qū)⒛愕母膭油扑偷剿砑拥姆?wù)器上去了。

這里 origin 是 <server> 的別名,取什么名字都可以,也可以在 push 時(shí)將 <jserver> 替換為 origin。但為了以后 push 方便,我們第一次一般都會先 remote add。
如果你還沒有 Git 倉庫,可以在 Github 等代碼托管平臺上創(chuàng)建一個(gè)空(不要自動生成 README.md)的倉庫,然后將代碼 push 到遠(yuǎn)端倉庫。


1.2 創(chuàng)建代碼倉庫

這一章簡要地帶你了解一些最重要的 Git 命令。在這節(jié)中,介紹開始一個(gè)新的版本控制項(xiàng)目需要的所有工具,后面的包含了每天都會用到的Git操作。

在這節(jié)之后,你應(yīng)該能夠創(chuàng)建一個(gè)新的 Git 倉庫,緩存你的項(xiàng)目以免丟失,以及查看你項(xiàng)目的歷史。

git init

git init 命令創(chuàng)建一個(gè)新的 Git 倉庫。它用來將已存在但還沒有版本控制的項(xiàng)目轉(zhuǎn)換成一個(gè) Git 倉庫,或者創(chuàng)建一個(gè)空的新倉庫。大多數(shù)Git命令在未初始化的倉庫中都是無法使用的,所以這就是你運(yùn)行新項(xiàng)目的第一個(gè)命令了。

運(yùn)行 git init 命令會在你項(xiàng)目的根目錄下創(chuàng)建一個(gè)新的 .git 目錄,其中包含了你項(xiàng)目必需的所有元數(shù)據(jù)。除了 .git 目錄之外,已經(jīng)存在的項(xiàng)目不會被改變(就像 SVN 一樣,Git 不強(qiáng)制每個(gè)子目錄中都有一個(gè) .git 目錄)。

用法

git init

將當(dāng)前的目錄轉(zhuǎn)換成一個(gè) Git 倉庫。它在當(dāng)前的目錄下增加了一個(gè) .git 目錄,于是就可以開始記錄項(xiàng)目版本了。

git init <directory>

在指定目錄創(chuàng)建一個(gè)空的 Git 倉庫。運(yùn)行這個(gè)命令會創(chuàng)建一個(gè)名為 directory,只包含 .git 子目錄的空目錄。

git init --bare <directory>

初始化一個(gè)裸的 Git 倉庫,但是忽略工作目錄。共享的倉庫應(yīng)該總是用 --bare 標(biāo)記創(chuàng)建(見下面的討論)。一般來說,用 —bare 標(biāo)記初始化的倉庫以 .git 結(jié)尾。比如,一個(gè)叫my-project的倉庫,它的空版本應(yīng)該保存在 my-project.git 目錄下。

討論

和 SVN 相比,git init 命令是一個(gè)創(chuàng)建新的版本控制項(xiàng)目非常簡單的途徑。Git 不需要你創(chuàng)建倉庫,導(dǎo)入文件,檢查正在修改的拷貝。你只需要 cd 到你的項(xiàng)目目錄下,運(yùn)行 git init,你就有了一個(gè)功能強(qiáng)大的 Git 倉庫。

但是,對大多數(shù)項(xiàng)目來說,git init 只需要在創(chuàng)建中央倉庫時(shí)執(zhí)行一次——開發(fā)者通常不會使用 git init 來創(chuàng)建他們的本地倉庫。他們往往使用 git clone 來將已存在的倉庫拷貝到他們的機(jī)器中去。

裸倉庫

-—bare 標(biāo)記創(chuàng)建了一個(gè)沒有工作目錄的倉庫,這樣我們在倉庫中更改文件并且提交了。中央倉庫應(yīng)該總是創(chuàng)建成裸倉庫,因?yàn)橄蚍锹銈}庫推送分支有可能會覆蓋已有的代碼變動。將-—bare看成是用來將倉庫標(biāo)記為儲存設(shè)施,而不是一個(gè)開發(fā)環(huán)境。也就是說,對于所有的 Git 工作流,中央倉庫是裸倉庫,開發(fā)者的本地倉庫是非裸倉庫。

在這里插入圖片描述

例子

因?yàn)?git clone 創(chuàng)建項(xiàng)目的本地拷貝更為方便,git init 最常見的使用情景就是用于創(chuàng)建中央倉庫:

ssh <user>@<host>cd path/above/repogit init --bare my-project.git

首先,你用SSH連入存放中央倉庫的服務(wù)器。然后,來到任何你想存放項(xiàng)目的地方,最后,使用 -—bare 標(biāo)記來創(chuàng)建一個(gè)中央存儲倉庫。開發(fā)者會將 my-project.git 克隆到本地的開發(fā)環(huán)境中。

git clone

git clone 命令拷貝整個(gè) Git 倉庫。這個(gè)命令就像 svn checkout 一樣,除了「工作副本」是一個(gè)完備的Git倉庫——它包含自己的歷史,管理自己的文件,以及環(huán)境和原倉庫完全隔離。

為了方便起見,clone 自動創(chuàng)建了一個(gè)名為 origin 的遠(yuǎn)程連接,指向原有倉庫。這讓和中央倉庫之間的交互更加簡單。

用法

git clone <repo>

將位于 <repo> 的倉庫克隆到本地機(jī)器。原倉庫可以在本地文件系統(tǒng)中,或是通過 HTTP 或 SSH 連接的遠(yuǎn)程機(jī)器。

git clone <repo> <directory>

將位于 <repo> 的倉庫克隆到本地機(jī)器上的 <directory> 目錄。

討論

如果項(xiàng)目在遠(yuǎn)程倉庫已經(jīng)設(shè)置完畢,git clone 是用戶獲取開發(fā)副本最常見的方式。和 git init相似,clone 通常也是一次性的操作——只要開發(fā)者獲得了一份工作副本,所有版本控制操作和協(xié)作管理都是在本地倉庫中完成的。

倉庫間協(xié)作

這一點(diǎn)很重要,你要理解 Git 中「工作副本」的概念和 SVN 倉庫 check out 下來的「工作副本」是很不一樣的。和 SVN 不同的是,Git 不會區(qū)分工作副本和中央倉庫——它們都是功能完備的 Git 倉庫。

這就使得 Git 的協(xié)作和 SVN 截然不同。SVN 依賴于中央倉庫和工作副本之間的關(guān)系,而 Git 協(xié)作模型是基于倉庫和倉庫之間的交互的。相對于 SVN 的提交流程,你可以在 Git 倉庫之間 pushpull 提交。

在這里插入圖片描述

在這里插入圖片描述

當(dāng)然,你也完全可以給予某個(gè)特定的倉庫一些特殊的含義。比如,指定某個(gè) Git 倉庫為中央倉庫,你就可以用 Git 進(jìn)行中央化的工作流。重點(diǎn)是,這是通過約定實(shí)現(xiàn)的,而不是寫死在版本控制系統(tǒng)本身。

例子

下面這個(gè)例子演示用 SSH 用戶名 john 連接到 example.com,獲取遠(yuǎn)程服務(wù)器上中央倉庫的本地副本:

git clone ssh://john@example.com/path/to/my-project.gitcd my-project# 開始工作

第一行命令在本地機(jī)器的 my-project 目錄下初始化了一個(gè)新的 Git 倉庫,并且導(dǎo)入了中央倉庫中的文件。接下來,你 cd 到項(xiàng)目目錄,開始編輯文件、緩存提交、和其它倉庫交互。同時(shí)注意 .git 拓展名克隆時(shí)會被去除。它表明了本地副本的非裸狀態(tài)。

git config

git config 命令允許你在命令行中配置你的 Git 安裝(或是一個(gè)獨(dú)立倉庫)。這個(gè)命令定義了所有配置,從用戶信息到倉庫行為等等。一些常見的配置命令如下所列。

用法

git config user.name <name>

定義當(dāng)前倉庫所有提交使用的作者姓名。通常來說,你希望使用 --global 標(biāo)記設(shè)置當(dāng)前用戶的配置項(xiàng)。

git config --global user.name <name>

定義當(dāng)前用戶所有提交使用的作者姓名。

git config --global user.email <email>

定義當(dāng)前用戶所有提交使用的作者郵箱。

git config --global alias.<alias-name> <git-command>

為Git命令創(chuàng)建一個(gè)快捷方式(別名)。

git config --system core.editor <editor>

定義當(dāng)前機(jī)器所有用戶使用命令時(shí)用到的文本編輯器,如 git commit。<editor> 參數(shù)用編輯器的啟動命令(如 vi)替代。

git config --global --edit

用文本編輯器打開全局配置文件,手動編輯。

討論

所有配置項(xiàng)都儲存在純文本文件中,所以 git config 命令其實(shí)只是一個(gè)提供便捷的命令行接口。通常,你只需要在新機(jī)器上配置一次 Git 安裝,以及,你通常會想要使用 --global 標(biāo)記。

Git 將配置項(xiàng)保存在三個(gè)單獨(dú)的文件中,允許你分別對單個(gè)倉庫、用戶和整個(gè)系統(tǒng)設(shè)置。

  • /.git/config – 特定倉庫的設(shè)置。

  • ~/.gitconfig – 特定用戶的設(shè)置。這也是 --global 標(biāo)記的設(shè)置項(xiàng)存放的位置。

  • $(prefix)/etc/gitconfig – 系統(tǒng)層面的設(shè)置。

當(dāng)這些文件中的配置項(xiàng)沖突時(shí),本地倉庫設(shè)置覆蓋用戶設(shè)置,用戶設(shè)置覆蓋系統(tǒng)設(shè)置。如果你打開期中一份文件,你會看到下面這些:

[user]name = John Smithemail = john@example.com[alias]st = statusco = checkoutbr = branchup = rebaseci = commit[core]editor = vim

你可以用 git config 手動編輯這些值。

栗子

你在安裝 Git 之后想要做的第一件事是告訴它你的名字和郵箱,個(gè)性化一些默認(rèn)設(shè)置。一般初始的設(shè)置過程看上去是這樣的:

# 告訴Git你是誰git config --global user.name "John Smith"git config --global user.email john@example.com# 選擇你喜歡的文本編輯器git config --global core.editor vim# 添加一些快捷方式(別名)git config --global alias.st statusgit config --global alias.co checkoutgit config --global alias.br branchgit config --global alias.up rebasegit config --global alias.ci commit

它會生成上一節(jié)中所說的 ~/.gitconfig 文件。


1.3 保存你的更改

“保存”這個(gè)概念在 Git 等版本控制系統(tǒng)和 Word 等文本編輯應(yīng)用中不太一樣。傳統(tǒng)軟件里的“保存”在 Git 里被叫做“提交”(commit)。 我們常說的的保存可以理解成在文件系統(tǒng)中覆蓋一個(gè)已有的文件或者創(chuàng)建一個(gè)新的文件。而在 Git 中,提交這個(gè)操作作用于若干個(gè)文件和目錄。

在 Git 和 SVN 里保存更改也不一樣。SVN 提交或檢入(check-in)將會推送到遠(yuǎn)端的中央服務(wù)器。也就是說 SVN 的提交需要聯(lián)網(wǎng)才能完全“保存”項(xiàng)目更改。Git 提交可以在本地完成,然后再使用git push -u origin master命令推送到遠(yuǎn)端服務(wù)器。這兩種方法的區(qū)別體現(xiàn)了兩種架構(gòu)設(shè)計(jì)的本質(zhì)區(qū)別。Git 是一個(gè)分布式的應(yīng)用,而 SVN 是一個(gè)中心化的應(yīng)用。分布式應(yīng)用一般來說更可靠,因?yàn)樗鼈儾淮嬖谥醒敕?wù)器這樣的單點(diǎn)故障。

git add、git statusgit commit這三個(gè)命令通常一起使用,將 Git 項(xiàng)目當(dāng)前的狀態(tài)保存成一份快照。

Git 還有另一個(gè)保存機(jī)制:“儲藏”(stash)。儲藏是一個(gè)臨時(shí)的儲存區(qū)域,保存還沒準(zhǔn)備好提交的更改。儲藏操作作用于工作目錄,三個(gè)文件樹中的第一棵。它有很多用法,訪問 git stash 頁面了解更多。

Git 倉庫可以通過設(shè)置忽略一些文件或目錄。Git 將不會保存這些文件的任何更改。Git 有多種方式管理忽略文件列表。訪問 git ignore 頁面了解更多 Git 忽略文件設(shè)置。

git add

git add 命令將工作目錄中的變化添加到暫存區(qū)。它告訴 Git 你想要在下一次提交時(shí)包含這個(gè)文件的更新。但是,git add 不會實(shí)質(zhì)上地影響你的倉庫——在你運(yùn)行 git commit 前更改都還沒有真正被記錄。

使用這些命令的同時(shí),你還需要 git status 來查看工作目錄和暫存區(qū)的狀態(tài)。

用法

git add <file>

<file> 中的更改加入下次提交的緩存。

git add <directory>

<directory> 下的更改加入下次提交的緩存。

git add -i

開始交互式的緩存,你可以選擇文件的一部分加入到下次提交緩存。它會向你展示一堆更改,等待你輸入一個(gè)命令。y 將這塊更改加入緩存,n 忽略這塊更改,s 將它分割成更小的塊,e 手動編輯這塊更改,以及 q 退出。

討論

git addgit commit 這兩個(gè)命令組成了最基本的 Git 工作流。每一個(gè) Git 用戶都需要理解這兩個(gè)命令,不管他們團(tuán)隊(duì)的協(xié)作模型是如何的。我有一千種方式可以將項(xiàng)目版本記錄在倉庫的歷史中。

在一個(gè)只有編輯、緩存、提交這樣基本流程的項(xiàng)目上開發(fā)。首先,你要在工作目錄中編輯你的文件。當(dāng)你準(zhǔn)備備份項(xiàng)目的當(dāng)前狀態(tài)時(shí),你通過 git add 來緩存更改。當(dāng)你對緩存的快照滿意之后,你通過 git commit 將它提交到你的項(xiàng)目歷史中去。

在這里插入圖片描述

git add 命令不能和 svn add 混在一起理解,后者將文件添加到倉庫中。而 git add 發(fā)生于更抽象的 更改 層面。也就是說,git add 在每次你修改一個(gè)文件時(shí)都需要被調(diào)用,而 svn add 只需要每個(gè)文件調(diào)用一次。這聽上去很多余,但這樣的工作流使得一個(gè)項(xiàng)目更容易組織。

緩存區(qū)

緩存區(qū)是 Git 更為獨(dú)特的地方之一,如果你是從 SVN(甚至是 Mercurial)遷移而來,那你可得花點(diǎn)時(shí)間理解了。你可以簡單地把它想成是工作目錄和項(xiàng)目歷史之間的緩沖區(qū)。

緩存允許你在實(shí)際提交到項(xiàng)目歷史之前,將相關(guān)的更改組合成一份高度專注的快照,而不是將你上次提交以后產(chǎn)生的所有更改一并提交。也就是說你可以更改各種不相關(guān)的文件,然后回過去將它們按邏輯切分,將相關(guān)的更改添加到緩存,一份一份提交。在任何修改控制系統(tǒng)中,很重要的一點(diǎn)是提交必須是原子性的,以便于追蹤 bug,并用最小的代價(jià)回滾更改。

栗子

當(dāng)你開始新項(xiàng)目的時(shí)候,git addsvn import 類似。為了創(chuàng)建當(dāng)前目錄的初始提交,使用下面兩個(gè)命令:

git add .
git commit

當(dāng)你項(xiàng)目設(shè)置好之后,新的文件可以通過路徑傳遞給 git add 來添加:

git add hello.py
git commit

上面的命令同樣可以用于記錄已有文件的更改。重復(fù)一次,Git 不會區(qū)分緩存的更改來自新文件,還是倉庫中已有的文件。

git commit

git commit命令將緩存的快照提交到項(xiàng)目歷史。提交的快照可以認(rèn)為是項(xiàng)目安全的版本,Git 永遠(yuǎn)不會改變它們,除非你這么要求。和 git add 一樣,這是最重要的 Git 命令之一。

盡管和它和 svn commit 名字一樣,但實(shí)際上它們毫無關(guān)聯(lián)。快照被提交到本地倉庫,不會和其他 Git 倉庫有任何交互。

用法

git commit

提交已經(jīng)緩存的快照。它會運(yùn)行文本編輯器,等待你輸入提交信息。當(dāng)你輸入信息之后,保存文件,關(guān)閉編輯器,創(chuàng)建實(shí)際的提交。

git commit -m "<message>"

提交已經(jīng)緩存的快照。但將 <message> 作為提交信息,而不是運(yùn)行文本編輯器。

git commit -a

提交一份包含工作目錄所有更改的快照。它只包含跟蹤過的文件的更改(那些之前已經(jīng)通過 git add 添加過的文件)。

討論

快照總是提交到 本地 倉庫。這一點(diǎn)和 SVN 截然不同,后者的工作拷貝提交到中央倉庫。而 Git 不會強(qiáng)制你和中央倉庫進(jìn)行交互,直到你準(zhǔn)備好了。就像緩存區(qū)是工作目錄和項(xiàng)目歷史之間的緩沖地帶,每個(gè)開發(fā)者的本地倉庫是他們貢獻(xiàn)的代碼和中央倉庫之間的緩沖地帶。

這一點(diǎn)改變了 Git 用戶基本的開發(fā)模型。Git 開發(fā)者可以在本地倉庫中積累一些提交,而不是一發(fā)生更改就直接提交到中央倉庫。這對于 SVN 風(fēng)格的協(xié)作有著諸多優(yōu)點(diǎn):更容易將功能切分成原子性的提交,讓相關(guān)的提交組合在一起,發(fā)布到中央倉庫之前整理好本地的歷史。開發(fā)者得以在一個(gè)隔離的環(huán)境中工作,直到他們方便的時(shí)候再整合代碼。

記錄快照,而不是記錄差異

SVN 和 Git 除了使用上存在巨大差異,它們底層的實(shí)現(xiàn)同樣遵循截然不同的設(shè)計(jì)哲學(xué)。SVN 追蹤文件的 變化 ,而 Git 的版本控制模型基于 快照 。比如說,一個(gè) SVN 提交由倉庫中原文件相比的差異(diff)組成。而 Git 在每次提交中記錄文件的 完整內(nèi)容 。

在這里插入圖片描述

這讓很多 Git 操作比 SVN 來的快得多,因?yàn)槲募哪硞€(gè)版本不需要通過版本間的差異組裝得到——每個(gè)文件完整的修改能立刻從 Git 的內(nèi)部數(shù)據(jù)庫中得到。

Git 的快照模型對它版本控制模型的方方面面都有著深遠(yuǎn)的影響,從分支到合并工具,再到協(xié)作工作流,以至于影響了所有特性。

栗子

下面這個(gè)栗子假設(shè)你編輯了 hello.py 文件的一些內(nèi)容,并且準(zhǔn)備好將它提交到項(xiàng)目歷史。首先,你需要用 git add 緩存文件,然后提交緩存的快照。

git add hello.py
git commit

它會打開一個(gè)文件編輯器(可以通過 git config 設(shè)置) 詢問提交信息,同時(shí)列出將被提交的文件。

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#modified: hello.py

Git 對提交信息沒有特定的格式限制,但約定俗成的格式是:在第一行用 50 個(gè)以內(nèi)的字符總結(jié)這個(gè)提交,留一空行,然后詳細(xì)闡述具體的更改。比如:

Change the message displayed by hello.py- Update the sayHello() function to output the user's name
- Change the sayGoodbye() function to a friendlier message

注意,很多開發(fā)者傾向于在提交信息中使用一般現(xiàn)在時(shí)態(tài)。這樣看起來更像是對倉庫進(jìn)行的操作,讓很多改寫歷史的操作更加符合直覺。


1.4 檢查倉庫狀態(tài)

git status

git status 命令顯示工作目錄和緩存區(qū)的狀態(tài)。你可以看到哪些更改被緩存了,哪些還沒有,以及哪些還未被 Git 追蹤。status 的輸出 不會 告訴你任何已提交到項(xiàng)目歷史的信息。如果你想看的話,應(yīng)該使用 git log 命令。

用法

git status

列出已緩存、未緩存、未追蹤的文件。

討論

git status 是一個(gè)相對簡單的命令。 它告訴你 git addgit commit 的進(jìn)展。status 信息還包括了添加緩存和移除緩存的相關(guān)指令。樣例輸出顯示了三類主要的 git status 輸出:

# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#modified: hello.py
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#modified: main.py
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#hello.pyc
忽略文件

未追蹤的文件通常有兩類。它們要么是項(xiàng)目新增但還未提交的文件,要么是像 .pyc、.obj、.exe 等編譯后的二進(jìn)制文件。顯然前者應(yīng)該出現(xiàn)在 git status 的輸出中,而后者會讓我們困惑究竟發(fā)生了什么。

因此,Git 允許你完全忽略這些文件,只需要將路徑放在一個(gè)特定的 .gitignore 文件中。所有想要忽略的文件應(yīng)該分別寫在單獨(dú)一行,* 字符用作通配符。比如,將下面這行加入項(xiàng)目根目錄的.gitignore文件可以避免編譯后的Python模塊出現(xiàn)在git status中:

*.pyc

栗子

在提交更改前檢查倉庫狀態(tài)是一個(gè)良好的實(shí)踐,這樣你就不會不小心提交什么奇怪的東西。這個(gè)例子顯示了緩存和提交快照前后的倉庫狀態(tài):

# Edit hello.py
git status
# hello.py is listed under "Changes not staged for commit"
git add hello.py
git status
# hello.py is listed under "Changes to be committed"
git commit
git status
# nothing to commit (working directory clean)

第一個(gè) status 的輸出顯示文件還未緩存。git add 操作會影響第二個(gè) git status,最后的 status 輸出告訴你已經(jīng)沒有可以提交的東西了——工作目錄和最近的提交一致。一些 Git 命令(比如 git merge)需要工作目錄整潔,以免意外覆蓋更改。

git log

git log 命令顯示已提交的快照。你可以列出項(xiàng)目歷史,篩選,以及搜索特定更改。git status 允許你查看工作目錄和緩存區(qū),而 git log 只作用于提交的項(xiàng)目歷史。

在這里插入圖片描述

log 輸出可以有很多種自定義的方式,從簡單地篩選提交,到用完全自定義的格式顯示。其中一些最常用的 git log 配置如下所示。

用法

git log

使用默認(rèn)格式顯示完整地項(xiàng)目歷史。如果輸出超過一屏,你可以用 空格鍵 來滾動,按 q 退出。

git log -n <limit>

<limit> 限制提交的數(shù)量。比如 git log -n 3 只會顯示 3 個(gè)提交。

git log --oneline

將每個(gè)提交壓縮到一行。當(dāng)你需要查看項(xiàng)目歷史的上層情況時(shí)這會很有用。

git log --stat

除了 git log 信息之外,包含哪些文件被更改了,以及每個(gè)文件相對的增刪行數(shù)。

git log -p

顯示代表每個(gè)提交的一堆信息。顯示每個(gè)提交全部的差異(diff),這也是項(xiàng)目歷史中最詳細(xì)的視圖。

git log --author="<pattern>"

搜索特定作者的提交。<pattern> 可以是字符串或正則表達(dá)式。

git log --grep="<pattern>"

搜索提交信息匹配特定 <pattern> 的提交。<pattern> 可以是字符串或正則表達(dá)式。

git log <since>..<until>

只顯示發(fā)生在 <since><until> 之間的提交。兩個(gè)參數(shù)可以是提交 ID、分支名、HEAD 或是任何一種引用。

git log <file>

只顯示包含特定文件的提交。查找特定文件的歷史這樣做會很方便。

git log --graph --decorate --oneline

還有一些有用的選項(xiàng)。--graph 標(biāo)記會繪制一幅字符組成的圖形,左邊是提交,右邊是提交信息。--decorate 標(biāo)記會加上提交所在的分支名稱和標(biāo)簽。--oneline 標(biāo)記將提交信息顯示在同一行,一目了然。

討論

git log 命令是 Git 查看項(xiàng)目歷史的基本工具。當(dāng)你要尋找項(xiàng)目特定的一個(gè)版本或者弄明白合并功能分支時(shí)引入了哪些變化,你就會用到這個(gè)命令。

commit 3157ee3718e180a9476bf2e5cab8e3f1e78a73b7
Author: John Smith

大多數(shù)時(shí)候都很簡單直接。但是,第一行需要解釋下。commit 后面 40 個(gè)字的字符串是提交內(nèi)容的 SHA-1 校驗(yàn)總和(checksum)。它有兩個(gè)作用。一是保證提交的正確性——如果它被損壞了,提交會生成一個(gè)不同的校驗(yàn)總和。第二,它是提交唯一的標(biāo)識 ID。

這個(gè) ID 可以用于 git log 這樣的命令中來引用具體的提交。比如,git log 3157e..5ab91 會顯示所有ID在 3157e5ab91 之間的提交。除了校驗(yàn)總和之外,分支名、HEAD 關(guān)鍵字也是常用的引用提交的方法。HEAD 總是指向當(dāng)前的提交,無論是分支還是特定提交也好。

~字符用于表示提交的父節(jié)點(diǎn)的相對引用。比如,3157e~1 指向 3157e 前一個(gè)提交,HEAD~3 是當(dāng)前提交的回溯3個(gè)節(jié)點(diǎn)的提交。

所有這些標(biāo)識方法的背后都是為了讓你對特定提交進(jìn)行操作。git log 命令一般是這些交互的起點(diǎn),因?yàn)樗屇阏业侥阆胍奶峤弧?/p>

栗子

用法 一節(jié)提供了 git log 很多的栗子,但請記住,你可以將很多選項(xiàng)用在同一個(gè)命令中:

git log --author="John Smith" -p hello.py

這個(gè)命令會顯示 John Smith 作者對 hello.py 文件所做的所有更改的差異比較(diff)。

…句法是比較分支很有用的工具。下面的栗子顯示了在 some-feature 分支而不在 master 分支的所有提交的概覽。

git log --oneline master..some-feature

1.5 檢出之前的提交

檢出之前的提交

git checkout

git checkout 這個(gè)命令有三個(gè)不同的作用:檢出文件、檢出提交和檢出分支。在這一章中,我們只關(guān)心前兩種用法。

檢出提交會使工作目錄和這個(gè)提交完全匹配。你可以用它來查看項(xiàng)目之前的狀態(tài),而不改變當(dāng)前的狀態(tài)。檢出文件使你能夠查看某個(gè)特定文件的舊版本,而工作目錄中剩下的文件不變。

用法

git checkout master

回到 master 分支。分支會在下一節(jié)中講到,而現(xiàn)在,你只需要將它視為回到項(xiàng)目「當(dāng)前」?fàn)顟B(tài)的一種方式。

git checkout <commit> <file>

查看文件之前的版本。它將工作目錄中的 <file> 文件變成 <commit> 中那個(gè)文件的拷貝,并將它加入緩存區(qū)。

git checkout <commit>

更新工作目錄中的所有文件,使得和某個(gè)特定提交中的文件一致。你可以將提交的哈希字串,或是標(biāo)簽作為 <commit> 參數(shù)。這會使你處在分離 HEAD 的狀態(tài)。

討論

版本控制系統(tǒng)背后的思想就是「安全」地儲存項(xiàng)目的拷貝,這樣你永遠(yuǎn)不用擔(dān)心什么時(shí)候不可復(fù)原地破壞了你的代碼庫。當(dāng)你建立了項(xiàng)目歷史之后,git checkout 是一種便捷的方式,來將保存的快照「加載」到你的開發(fā)機(jī)器上去。

檢出之前的提交是一個(gè)只讀操作。在查看舊版本的時(shí)候絕不會損壞你的倉庫。你項(xiàng)目「當(dāng)前」的狀態(tài)在 master 上不會變化。在開發(fā)的正常階段,HEAD 一般指向 master 或是其他的本地分支,但當(dāng)你檢出之前提交的時(shí)候,HEAD 就不再指向一個(gè)分支了——它直接指向一個(gè)提交。這被稱為「分離 HEAD」?fàn)顟B(tài)

在另一方面,檢出舊文件不影響你倉庫的當(dāng)前狀態(tài)。你可以在新的快照中像其他文件一樣重新提交舊版本。所以,在效果上,git checkout 的這個(gè)用法可以用來將單個(gè)文件回滾到舊版本 。

栗子

查看之前的版本

這個(gè)栗子假定你開始了一個(gè)瘋狂的實(shí)驗(yàn),但你不確定你是否想要保留它。為了幫助你決定,你想看一看你開始實(shí)驗(yàn)之前的項(xiàng)目狀態(tài)。首先,你需要找到你想要看的那個(gè)版本的 ID。

git log --oneline

假設(shè)你的項(xiàng)目歷史看上去和下面一樣:

b7119f2 繼續(xù)做些喪心病狂的事
872fa7e 做些喪心病狂的事
a1e8fb5 對 hello.py 做了一些修改
435b61d 創(chuàng)建 hello.py
9773e52 初始導(dǎo)入

你可以這樣使用 git checkout 來查看「對 hello.py 做了一些修改」這個(gè)提交:

git checkout a1e8fb5

這讓你的工作目錄和 a1e8fb5 提交所處的狀態(tài)完全一致。你可以查看文件,編譯項(xiàng)目,運(yùn)行測試,甚至編輯文件而不需要考慮是否會影響項(xiàng)目的當(dāng)前狀態(tài)。你所做的一切 都不會 被保存到倉庫中。為了繼續(xù)開發(fā),你需要回到你項(xiàng)目的「當(dāng)前」?fàn)顟B(tài):

git checkout master

這里假定了你默認(rèn)在 master 分支上開發(fā),我們會在以后的分支模型中詳細(xì)討論。

一旦你回到 master 分支之后,你可以使用 git revertgit reset 來回滾任何不想要的更改。

檢出文件

如果你只對某個(gè)文件感興趣,你也可以用 git checkout 來獲取它的一個(gè)舊版本。比如說,如果你只想從之前的提交中查看 hello.py 文件,你可以使用下面的命令:

git checkout a1e8fb5 hello.py

記住,和檢出提交不同,這里 確實(shí) 會影響你項(xiàng)目的當(dāng)前狀態(tài)。舊的文件版本會顯示為「需要提交的更改」,允許你回滾到文件之前的版本。如果你不想保留舊的版本,你可以用下面的命令檢出到最近的版本:

git checkout HEAD hello.py

1.6 回滾錯(cuò)誤的修改

回滾錯(cuò)誤的修改

這章教程提供了和項(xiàng)目舊版本打交道所需要的所有技巧。首先,你會知道如何瀏覽舊的提交,然后了解回滾項(xiàng)目歷史中的公有提交和回滾本地機(jī)器上的私有更改之間的區(qū)別。

git revert

git revert 命令用來撤銷一個(gè)已經(jīng)提交的快照。但是,它是通過搞清楚如何撤銷這個(gè)提交引入的更改,然后在最后加上一個(gè)撤銷了更改的 提交,而不是從項(xiàng)目歷史中移除這個(gè)提交。這避免了Git丟失項(xiàng)目歷史,這一點(diǎn)對于你的版本歷史和協(xié)作的可靠性來說是很重要的。
在這里插入圖片描述

用法

git revert <commit>

生成一個(gè)撤消了 <commit> 引入的修改的新提交,然后應(yīng)用到當(dāng)前分支。

討論

撤銷(revert)應(yīng)該用在你想要在項(xiàng)目歷史中移除一整個(gè)提交的時(shí)候。比如說,你在追蹤一個(gè) bug,然后你發(fā)現(xiàn)它是由一個(gè)提交造成的,這時(shí)候撤銷就很有用。與其說自己去修復(fù)它,然后提交一個(gè)新的快照,不如用 git revert,它幫你做了所有的事情。

撤銷(revert)和重設(shè)(reset)對比

理解這一點(diǎn)很重要。git revert 回滾了「單獨(dú)一個(gè)提交」,它沒有移除后面的提交,然后回到項(xiàng)目之前的狀態(tài)。在 Git 中,后者實(shí)際上被稱為 reset,而不是 revert

在這里插入圖片描述

撤銷和重設(shè)相比有兩個(gè)重要的優(yōu)點(diǎn)。首先,它不會改變項(xiàng)目歷史,對那些已經(jīng)發(fā)布到共享倉庫的提交來說這是一個(gè)安全的操作。至于為什么改變共享的歷史是危險(xiǎn)的,請參閱 git reset 一節(jié)。

其次,git revert 可以針對歷史中任何一個(gè)提交,而 git reset 只能從當(dāng)前提交向前回溯。比如,你想用 git reset 重設(shè)一個(gè)舊的提交,你不得不移除那個(gè)提交后的所有提交,再移除那個(gè)提交,然后重新提交后面的所有提交。不用說,這并不是一個(gè)優(yōu)雅的回滾方案。

栗子

下面的這個(gè)栗子是 git revert 一個(gè)簡單的演示。它提交了一個(gè)快照,然后立即撤銷這個(gè)操作。

# 編輯一些跟蹤的文件# 提交一份快照
git commit -m "Make some changes that will be undone"# 撤銷剛剛的提交
git revert HEAD

注意第四個(gè)提交在撤銷后依然在項(xiàng)目歷史中。git revert 在后面增加了一個(gè)提交來撤銷修改,而不是刪除它。 因此,第三和第五個(gè)提交表示同樣的代碼,而第四個(gè)提交依然在歷史中,以備以后我們想要回到這個(gè)提交。

git reset

如果說 git revert 是一個(gè)撤銷更改安全的方式,你可以將 git reset 看做一個(gè) 危險(xiǎn) 的方式。當(dāng)你用 git reset 來重設(shè)更改時(shí)(提交不再被任何引用或引用日志所引用),我們無法獲得原來的樣子——這個(gè)撤銷是永遠(yuǎn)的。使用這個(gè)工具的時(shí)候務(wù)必要小心,因?yàn)檫@是少數(shù)幾個(gè)可能會造成工作丟失的命令之一。

git checkout 一樣,git reset 有很多種用法。它可以被用來移除提交快照,盡管它通常被用來撤銷緩存區(qū)和工作目錄的修改。不管是哪種情況,它應(yīng)該只被用于 本地 修改——你永遠(yuǎn)不應(yīng)該重設(shè)和其他開發(fā)者共享的快照。

用法

git reset <file>

從緩存區(qū)移除特定文件,但不改變工作目錄。它會取消這個(gè)文件的緩存,而不覆蓋任何更改。

git reset

重設(shè)緩沖區(qū),匹配最近的一次提交,但工作目錄不變。它會取消 所有 文件的緩存,而不會覆蓋任何修改,給你了一個(gè)重設(shè)緩存快照的機(jī)會。

git reset --hard

重設(shè)緩沖區(qū)和工作目錄,匹配最近的一次提交。除了取消緩存之外,--hard 標(biāo)記告訴 Git 還要重寫所有工作目錄中的更改。換句話說:它清除了所有未提交的更改,所以在使用前確定你想扔掉你所有本地的開發(fā)。

git reset <commit>

將當(dāng)前分支的末端移到 <commit>,將緩存區(qū)重設(shè)到這個(gè)提交,但不改變工作目錄。所有 <commit> 之后的更改會保留在工作目錄中,這允許你用更干凈、原子性的快照重新提交項(xiàng)目歷史。

git reset --hard <commit>

將當(dāng)前分支的末端移到 <commit>,將緩存區(qū)和工作目錄都重設(shè)到這個(gè)提交。它不僅清除了未提交的更改,同時(shí)還清除了 <commit> 之后的所有提交。

討論

上面所有的調(diào)用都是用來移除倉庫中的修改。沒有 --hard 標(biāo)記時(shí) git reset 通過取消緩存或取消一系列的提交,然后重新構(gòu)建提交來清理倉庫。而加上 --hard 標(biāo)記對于作了大死之后想要重頭再來尤其方便。

撤銷(revert)被設(shè)計(jì)為撤銷 公開 的提交的安全方式,git reset被設(shè)計(jì)為重設(shè) 本地 更改。因?yàn)閮蓚€(gè)命令的目的不同,它們的實(shí)現(xiàn)也不一樣:重設(shè)完全地移除了一堆更改,而撤銷保留了原來的更改,用一個(gè)新的提交來實(shí)現(xiàn)撤銷。

不要重設(shè)公共歷史

當(dāng)有 <commit> 之后的提交被推送到公共倉庫后,你絕不應(yīng)該使用 git reset。發(fā)布一個(gè)提交之后,你必須假設(shè)其他開發(fā)者會依賴于它。

移除一個(gè)其他團(tuán)隊(duì)成員在上面繼續(xù)開發(fā)的提交在協(xié)作時(shí)會引發(fā)嚴(yán)重的問題。當(dāng)他們試著和你的倉庫同步時(shí),他們會發(fā)現(xiàn)項(xiàng)目歷史的一部分突然消失了。下面的序列展示了如果你嘗試重設(shè)公共提交時(shí)會發(fā)生什么。origin/master 是你本地 master 分支對應(yīng)的中央倉庫中的分支。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

一旦你在重設(shè)之后又增加了新的提交,Git 會認(rèn)為你的本地歷史已經(jīng)和 origin/master 分叉了,同步你的倉庫時(shí)的合并提交(merge commit)會使你的同事困惑。

重點(diǎn)是,確保你只對本地的修改使用 git reset,而不是公共更改。如果你需要修復(fù)一個(gè)公共提交,git revert 命令正是被設(shè)計(jì)來做這個(gè)的。

栗子

取消文件緩存

git reset 命令在準(zhǔn)備緩存快照時(shí)經(jīng)常被用到。下面的例子假設(shè)你有兩個(gè)文件,hello.pymain.py它們已經(jīng)被加入了倉庫中。

# 編輯了hello.py和main.py# 緩存了目錄下所有文件
git add .# 意識到hello.py和main.py中的修改
# 應(yīng)該在不同的快照中提交# 取消main.py緩存
git reset main.py# 只提交hello.py
git commit -m "Make some changes to hello.py"# 在另一份快照中提交main.py
git add main.py
git commit -m "Edit main.py"

如你所見,git reset 幫助你取消和這次提交無關(guān)的修改,讓提交能夠?qū)W⒂谀骋惶囟ǖ姆秶?/p>

移除本地修改

下面的這個(gè)栗子顯示了一個(gè)更高端的用法。它展示了你作了大死之后應(yīng)該如何扔掉那幾個(gè)更新。

# 創(chuàng)建一個(gè)叫`foo.py`的新文件,增加代碼# 提交到項(xiàng)目歷史
git add foo.py
git commit -m "Start developing a crazy feature"# 再次編輯`foo.py`,修改其他文件# 提交另一份快照
git commit -a -m "Continue my crazy feature"# 決定廢棄這個(gè)功能,并刪除相關(guān)的更改
git reset --hard HEAD~2

git reset HEAD~2 命令將當(dāng)前分支向前倒退兩個(gè)提交,相當(dāng)于在項(xiàng)目歷史中移除剛創(chuàng)建的這兩個(gè)提交。記住,這種重設(shè)只能用在 非公開 的提交中。絕不要在將提交推送到共享倉庫之后執(zhí)行上面的操作。

git clean

git clean 命令將未跟蹤的文件從你的工作目錄中移除。它只是提供了一條捷徑,因?yàn)橛?git status 查看哪些文件還未跟蹤然后手動移除它們也很方便。和一般的 rm 命令一樣,git clean 是無法撤消的,所以在刪除未跟蹤的文件之前想清楚,你是否真的要這么做。

git clean 命令經(jīng)常和 git reset --hard 一起使用。記住,reset 只影響被跟蹤的文件,所以還需要一個(gè)單獨(dú)的命令來清理未被跟蹤的文件。這個(gè)兩個(gè)命令相結(jié)合,你就可以將工作目錄回到之前特定提交時(shí)的狀態(tài)。

用法

git clean -n

執(zhí)行一次git clean的『演習(xí)』。它會告訴你那些文件在命令執(zhí)行后會被移除,而不是真的刪除它。

git clean -f

移除當(dāng)前目錄下未被跟蹤的文件。-f(強(qiáng)制)標(biāo)記是必需的,除非 clean.requireForce 配置項(xiàng)被設(shè)為了 false(默認(rèn)為 true)。它 不會 刪除 .gitignore 中指定的未跟蹤的文件。

git clean -f <path>

移除未跟蹤的文件,但限制在某個(gè)路徑下。

git clean -df

移除未跟蹤的文件,以及目錄。

git clean -xf

移除當(dāng)前目錄下未跟蹤的文件,以及 Git 一般忽略的文件。

討論

如果你在本地倉庫中作死之后想要?dú)瑴幺E,git reset --hardgit clean -f 是你最好的選擇。運(yùn)行這兩個(gè)命令使工作目錄和最近的提交相匹配,讓你在干凈的狀態(tài)下繼續(xù)工作。

git clean 命令對于 build 后清理工作目錄十分有用。比如,它可以輕易地刪除 C 編譯器生成的 .o.exe 二進(jìn)制文件。這通常是打包發(fā)布前需要的一步。-x 命令在這種情況下特別方便。

請牢記,和 git reset 一樣, git clean 是僅有的幾個(gè)可以永久刪除提交的命令之一,所以要小心使用。事實(shí)上,它太容易丟掉重要的修改了,以至于 Git 廠商 強(qiáng)制 你用 -f 標(biāo)志來進(jìn)行最基本的操作。這可以避免你用一個(gè) git clean 就不小心刪除了所有東西。

栗子

下面的栗子清除了工作目錄中的所有更改,包括新建還沒加入緩存的文件。它假設(shè)你已經(jīng)提交了一些快照,準(zhǔn)備開始一些新的實(shí)驗(yàn)。

# 編輯了一些文件
# 新增了一些文件
# 『糟糕』# 將跟蹤的文件回滾回去
git reset --hard# 移除未跟蹤的文件
git clean -df

在執(zhí)行了 reset/clean 的流程之后,工作目錄和緩存區(qū)和最近一次提交看上去一模一樣,而 git status會認(rèn)為這是一個(gè)干凈的工作目錄。你可以重新來過了。

注意,不像 git reset 的第二個(gè)栗子,新的文件沒有被加入到倉庫中。因此,它們不會受到 git reset --hard 的影響,需要 git clean 來刪除它們。


1.7 重寫項(xiàng)目歷史

概述

Git 的主要職責(zé)是保證你不會丟失提交的修改。但是,它同樣被設(shè)計(jì)成讓你完全掌控開發(fā)工作流。這包括了讓你自定義你的項(xiàng)目歷史,而這也創(chuàng)造了丟失提交的可能性。Git 提供了可以重寫項(xiàng)目歷史的命令,但也警告你這些命令可能會讓你丟失內(nèi)容。

這份教程討論了重寫提交快照的一些常見原因,并告訴你如何避免不好的影響。

git commit --amend

git commit --amend 命令是修復(fù)最新提交的便捷方式。它允許你將緩存的修改和之前的提交合并到一起,而不是提交一個(gè)全新的快照。它還可以用來簡單地編輯上一次提交的信息而不改變快照。

在這里插入圖片描述
在這里插入圖片描述

但是,amend 不只是修改了最新的提交——它進(jìn)行了一次替換。對于 Git 來說,這看上去像一個(gè)全新的提交,即上圖中用星號表示的那一個(gè)。在公共倉庫工作時(shí)一定要牢記這一點(diǎn)。

用法

git commit --amend

合并緩存的修改和上一次的提交,用新的快照替換上一個(gè)提交。緩存區(qū)沒有文件時(shí)運(yùn)行這個(gè)命令可以用來編輯上次提交的提交信息,而不會更改快照。

討論

倉促的提交在你日常開發(fā)過程中時(shí)常會發(fā)生。很容易就忘記了緩存一個(gè)文件或者弄錯(cuò)了提交信息的格式。--amend 標(biāo)記是修復(fù)這些小意外的便捷方式。

不要修復(fù)公共提交

git reset這節(jié)中,我們說過永遠(yuǎn)不要重設(shè)和其他開發(fā)者共享的提交。對于修復(fù)也是一樣:永遠(yuǎn)不要修復(fù)一個(gè)已經(jīng)推送到公共倉庫中的提交。

修復(fù)過的提交事實(shí)上是全新的提交,之前的提交會被移除出項(xiàng)目歷史。這和重設(shè)公共快照的后果是一樣的。如果你修復(fù)了其他開發(fā)者在之后繼續(xù)開發(fā)的一個(gè)提交,看上去他們的工作基礎(chǔ)從項(xiàng)目歷史中消失了一樣。對于在這上面的開發(fā)者來說這是很困惑的,而且很難恢復(fù)。

栗子

下面這個(gè)🌰展示了 Git 開發(fā)工作流中的一個(gè)常見情形。我們編輯了一些希望在同一個(gè)快照中提交的文件,但我們忘記添加了其中的一個(gè)。修復(fù)錯(cuò)誤只需要緩存那個(gè)文件并且用 --amend 標(biāo)記提交:

# 編輯 hello.py 和 main.py
git add hello.py
git commit# 意識到你忘記添加 main.py 的更改
git add main.py
git commit --amend --no-edit

編輯器會彈出上一次提交的信息,加入 --no-edit 標(biāo)記會修復(fù)提交但不修改提交信息。需要的話你可以修改,不然的話就像往常一樣保存并關(guān)閉文件。完整的提交會替換之前不完整的提交,看上去就像我們在同一個(gè)快照中提交了 hello.pymain.py。

git rebase

變基(rebase, 事實(shí)上這個(gè)名字十分詭異, 所以在大多數(shù)時(shí)候直接用英文術(shù)語)是將分支移到一個(gè)新的基提交的過程。過程一般如下所示:

在這里插入圖片描述

從內(nèi)容的角度來看,rebase 只不過是將分支從一個(gè)提交移到了另一個(gè)。但從內(nèi)部機(jī)制來看,Git 是通過在選定的基上創(chuàng)建新提交來完成這件事的——它事實(shí)上重寫了你的項(xiàng)目歷史。理解這一點(diǎn)很重要,盡管分支看上去是一樣的,但它包含了全新的提交。

用法

git rebase <base>

將當(dāng)前分支 rebase 到 <base>,這里可以是任何類型的提交引用(ID、分支名、標(biāo)簽,或是 HEAD 的相對引用)。

討論

rebase 的主要目的是為了保持一個(gè)線性的項(xiàng)目歷史。比如說,當(dāng)你在 feature 分支工作時(shí) master 分支取得了一些進(jìn)展:

在這里插入圖片描述

要將你的 feature 分支整合進(jìn) master 分支,你有兩個(gè)選擇:直接 merge,或者先 rebase 后 merge。前者會產(chǎn)生一個(gè)三路合并(3-way merge)和一個(gè)合并提交,而后者產(chǎn)生的是一個(gè)快速向前的合并以及完美的線性歷史。下圖展示了為什么 rebase 到 master 分支會促成一個(gè)快速向前的合并。

rebase 是將上游更改合并進(jìn)本地倉庫的通常方法。你每次想查看上游進(jìn)展時(shí),用 git merge 拉取上游更新會導(dǎo)致一個(gè)多余的合并提交。在另一方面,rebase 就好像是說「我想將我的更改建立在其他人的進(jìn)展之上」。

不要 rebase 公共歷史

和我們討論過的 git commit --amendgit reset 一樣,你永遠(yuǎn)不應(yīng)該 rebase 那些已經(jīng)推送到公共倉庫的提交。rebase 會用新的提交替換舊的提交,你的項(xiàng)目歷史會像突然消失了一樣。

栗子

下面這個(gè)例子同時(shí)使用 git rebase 和 git merge 來保持線性的項(xiàng)目歷史。這是一個(gè)確認(rèn)你的合并都是快速向前的方法。

# 開始新的功能分支
git checkout -b new-feature master
# 編輯文件
git commit -a -m "Start developing a feature"

在 feature 分支開發(fā)了一半的時(shí)候,我們意識到項(xiàng)目中有一個(gè)安全漏洞:

# 基于master分支創(chuàng)建一個(gè)快速修復(fù)分支
git checkout -b hotfix master
# 編輯文件
git commit -a -m "Fix security hole"
# 合并回master
git checkout master
git merge hotfix
git branch -d hotfix

將 hotfix 分支并回之后 master,我們有了一個(gè)分叉的項(xiàng)目歷史。我們用 rebase 整合 feature 分支以獲得線性的歷史,而不是使用普通的 git merge。

git checkout new-feature
git rebase master

它將 new-feature 分支移到了 master 分支的末端,現(xiàn)在我們可以在 master 上進(jìn)行標(biāo)準(zhǔn)的快速向前合并了:

git checkout master
git merge new-feature

git rebase -i

-i 標(biāo)記運(yùn)行 git rebase 開始交互式 rebase。交互式 rebase 給你在過程中修改單個(gè)提交的機(jī)會,而不是盲目地將所有提交都移到新的基上。你可以移除、分割提交,更改提交的順序。它就像是打了雞血的 git commit --amend 一樣。

用法

git rebase -i <base>

將當(dāng)前分支 rebase 到 base,但使用可交互的形式。它會打開一個(gè)編輯器,你可以為每個(gè)將要 rebase 的提交輸入命令(見后文)。這些命令決定了每個(gè)提交將會怎樣被轉(zhuǎn)移到新的基上去。你還可以對這些提交進(jìn)行排序。

討論

交互式 rebase 給你了控制項(xiàng)目歷史的完全掌控。它給了開發(fā)人員很大的自由,因?yàn)樗麄兛梢蕴峤灰粋€(gè)「混亂」的歷史而只需專注于寫代碼,然后回去恢復(fù)干凈。

大多數(shù)開發(fā)者喜歡在并入主代碼庫之前用交互式 rebase 來完善他們的 feature 分支。他們可以將不重要的提交合在一起,刪除不需要的,確保所有東西在提交到「正式」的項(xiàng)目歷史前都是整齊的。對其他人來說,這個(gè)功能的開發(fā)看上去是由一系列精心安排的提交組成的。

栗子

下面這個(gè)🌰是 非交互式rebase 一節(jié)中🌰的可交互升級版本。

# 開始新的功能分支
git checkout -b new-feature master
# 編輯文件
git commit -a -m "Start developing a feature"
# 編輯更多文件
git commit -a -m "Fix something from the previous commit"# 直接在 master 上添加文件
git checkout master
# 編輯文件
git commit -a -m "Fix security hole"# 開始交互式 rebase
git checkout new-feature
git rebase -i master

最后的那個(gè)命令會打開一個(gè)編輯器,包含 new-feature 的兩個(gè)提交,和一些指示:

pick 32618c4 Start developing a feature
pick 62eed47 Fix something from the previous commit

你可以更改每個(gè)提交前的 pick 命令來決定在 rebase 時(shí)提交移動的方式。在我們的例子中,我們只需要用 squash 命令把兩個(gè)提交并在一起就可以了:

pick 32618c4 Start developing a feature
squash 62eed47 Fix something from the previous commit

保存并關(guān)閉編輯器以開始 rebase。另一個(gè)編輯器會打開,詢問你合并后的快照的提交信息。在定義了提交信息之后,rebase 就完成了,你可以在 git log 輸出中看到那個(gè)提交。整個(gè)過程可以用下圖可視化:

注意縮并的提交和原來的兩個(gè)提交的 ID 都不一樣,告訴我們這確實(shí)是個(gè)新的提交。

最后,你可以執(zhí)行一個(gè)快速向前的合并,來將完善的 feature 分支整合進(jìn)主代碼庫:

git checkout master
git merge new-feature

交互式 rebase 強(qiáng)大的能力可以從整合后的 master 分支看出——額外的 62eed47 提交找不到了。對其他人來說,你就像是一個(gè)天才,用完美數(shù)量的提交完成了 new-feature。這就是交互式提交如何保持項(xiàng)目歷史干凈和合意。

git reflog

Git 用引用日志這種機(jī)制來記錄分支頂端的更新。它允許你回到那些不被任何分支或標(biāo)簽引用的更改。在重寫歷史后,引用日志包含了分支舊狀態(tài)的信息,有需要的話你可以回到這個(gè)狀態(tài)。

用法

git reflog

顯示本地倉庫的引用日志。

git reflog --relative-date

用相對的日期顯示引用日志。(如 2 周前)。

討論

每次當(dāng)前的 HEAD 更新時(shí)(如切換分支、拉取新更改、重寫歷史或只是添加新的提交),引用日志都會添加一個(gè)新條目。

栗子

為了理解 git reflog,我們來看一個(gè)🌰。

0a2e358 HEAD@{0}: reset: moving to HEAD~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to master
c10f740 HEAD@{2}: checkout: moving from master to 2.2

上面的引用日志顯示了 master 和 2.2 的 branch 之間的相互切換。還有對一個(gè)更老的提交的強(qiáng)制重設(shè)。最近的活動用 HEAD@{0} 標(biāo)記在上方顯示。

如果事實(shí)上你是不小心切換回去的,引用日志包含了你意外地丟掉兩個(gè)提交之前 master 指向的提交 0254ea7。

git reset --hard 0254ea7

使用 git reset,就有可能能將master變回之前的那個(gè)提交。它提供了一張安全網(wǎng),以防歷史發(fā)生意外更改。

務(wù)必記住,引用日志提供的安全網(wǎng)只對提交到本地倉庫的更改有效,而且只有移動操作會被記錄。


2.1 保持代碼同步

保持同步

SVN 使用唯一的中央倉庫作為開發(fā)者之間溝通的橋梁,在開發(fā)者的工作拷貝和中央倉庫之間傳遞變更集合(changeset),協(xié)作得以發(fā)生。這和Git的協(xié)作模型有所不同,Git 給予每個(gè)開發(fā)者一份自己的倉庫拷貝,擁有自己完整的本地歷史和分支結(jié)構(gòu)。用戶通常共享一系列的提交而不是單個(gè)變更集合。Git 允許你在倉庫間共享整個(gè)分支,而不是從工作副本提交一個(gè)差異集合到中央倉庫。

下面的命令讓你管理倉庫之間的連接,將分支「推送」到其他倉庫來發(fā)布本地歷史,或是將分支「拉取」到本地倉庫來查看其它開發(fā)者的貢獻(xiàn)。

git remote

git remote 命令允許你創(chuàng)建、查看和刪除和其它倉庫之間的連接。遠(yuǎn)程連接更像是書簽,而不是直接跳轉(zhuǎn)到其他倉庫的鏈接。它用方便記住的別名引用不那么方便記住的 URL,而不是提供其他倉庫的實(shí)時(shí)連接。

例如,下圖顯示了你的倉庫和中央倉庫以及另一個(gè)開發(fā)者倉庫之間的遠(yuǎn)程連接。你可以向 Git 命令傳遞 origin 和 john 的別名來引用這些倉庫,替代完整的 URL。

在這里插入圖片描述

用法

git remote

列出你和其他倉庫之間的遠(yuǎn)程連接。

git remote -v

和上個(gè)命令相同,但同時(shí)顯示每個(gè)連接的 URL。

git remote add <name> <url>

創(chuàng)建一個(gè)新的遠(yuǎn)程倉庫連接。在添加之后,你可以將 <name> 作為 <url> 便捷的別名在其他 Git 命令中使用。

git remote rm <name>

移除名為的遠(yuǎn)程倉庫的連接。

git remote rename <old-name> <new-name>

將遠(yuǎn)程連接從 <old-name> 重命名為 <new-name>

討論

Git 被設(shè)計(jì)為給每個(gè)開發(fā)者提供完全隔離的開發(fā)環(huán)境。也就是說信息并不是自動地在倉庫之間傳遞。開發(fā)者需要手動將上游提交拉取到本地,或手動將本地提交推送到中央倉庫中去。git remote 命令正是將 URL 傳遞給這些「共享」命令的快捷方式。

名為 origin 的遠(yuǎn)程連接

當(dāng)你用 git clone 克隆倉庫時(shí),它自動創(chuàng)建了一個(gè)名為 origin 的遠(yuǎn)程連接,指向被克隆的倉庫。當(dāng)開發(fā)者創(chuàng)建中央倉庫的本地副本時(shí)非常有用,因?yàn)樗峁┝死∩嫌胃暮桶l(fā)布本地提交的快捷方式。這也是為什么大多數(shù)基于 Git 的項(xiàng)目將它們的中央倉庫取名為 origin。

倉庫的 URL

Git 支持多種方式來引用一個(gè)遠(yuǎn)程倉庫。其中兩種最簡單的方式便是 HTTP 和 SSH 協(xié)議。HTTP 是允許匿名、只讀訪問倉庫的簡易方式。比如:

http://host/path/to/repo.git

但是,直接將提交推送到一個(gè) HTTP 地址一般是不可行的(你不太可能希望匿名用戶也能隨意推送)。如果希望對倉庫進(jìn)行讀寫,你需要使用 SSH 協(xié)議:

ssh://user@host/path/to/repo.git

你需要在托管的服務(wù)器上有一個(gè)有效的 SSH 賬戶,但不用麻煩了,Git 支持開箱即用的 SSH 認(rèn)證連接。

栗子

除了 origin 之外,添加你同事的倉庫連接通常會帶來一些便利。比如,如果你的同事 John 在 dev.example.com/john.git 上維護(hù)了一個(gè)公開的倉庫,你可以這樣添加連接:

git remote add john http://dev.example.com/john.git

通過這種方式訪問每個(gè)開發(fā)者的倉庫,中央倉庫之外的協(xié)作變得可能。這給維護(hù)大項(xiàng)目的小團(tuán)隊(duì)帶來了極大的便利。

git fetch

git fetch 命令將提交從遠(yuǎn)程倉庫導(dǎo)入到你的本地倉庫。拉取下來的提交儲存為遠(yuǎn)程分支,而不是我們一直使用的普通的本地分支。你因此可以在整合進(jìn)你的項(xiàng)目副本之前查看更改。

用法

git fetch <remote>

拉取倉庫中所有的分支。同時(shí)會從另一個(gè)倉庫中下載所有需要的提交和文件。

git fetch <remote> <branch>

和上一個(gè)命令相同,但只拉取指定的分支。

討論

當(dāng)你希望查看其他人的工作進(jìn)展時(shí),你需要 fetch。fetch 下來的內(nèi)容表示為一個(gè)遠(yuǎn)程分支,因此不會影響你的本地開發(fā)。這是一個(gè)安全的方式,在整合進(jìn)你的本地倉庫之前,檢查那些提交。類似于 svn update,你可以看到中央倉庫的歷史進(jìn)展如何,但它不會強(qiáng)制你將這些進(jìn)展合并入你的倉庫。

遠(yuǎn)程分支

遠(yuǎn)程分支和本地分支一樣,只不過它們代表這些提交來自于其他人的倉庫。你可以像查看本地分支一樣查看遠(yuǎn)程分支,但你會處于分離 HEAD 狀態(tài)(就像查看舊的提交時(shí)一樣)。你可以把它們視作只讀的分支。如果想要查看遠(yuǎn)程分支,只需要向 git branch 命令傳入 -r 參數(shù)。遠(yuǎn)程分支擁有 remote 的前綴,所以你不會將它們和本地分支混起來。比如,下面的代碼片段顯示了從 origin 拉取之后,你可能想要查看的分支:

git branch -r
# origin/master
# origin/develop
# origin/some-feature

同樣,你可以用尋常的 git checkoutgit log 命令來查看這些分支。如果你接受遠(yuǎn)程分支包含的更改,你可以使用 git merge 將它并入本地分支。所以,不像 SVN,同步你的本地倉庫和遠(yuǎn)程倉庫事實(shí)上是一個(gè)分兩步的操作:先 fetch,然后 merge。git pull 命令是這個(gè)過程的快捷方式。

栗子

這個(gè)例子回顧了同步本地和遠(yuǎn)程倉庫 master 分支的常見工作流:

git fetch origin

它會顯示會被下載的分支:

a1e8fb5..45e66a4 master -> origin/master
a1e8fb5..9e8ab1c develop -> origin/develop
* [new branch] some-feature -> origin/some-feature

在下圖中,遠(yuǎn)程分支中的提交顯示為方塊,而不是圓圈。正如你所見,git fetch 讓你看到了另一個(gè)倉庫完整的分支結(jié)構(gòu)。

在這里插入圖片描述

若想查看添加到上游 master 上的提交,你可以運(yùn)行 git log,用 origin/master 過濾:

git log --oneline master..origin/master

用下面這些命令接受更改并并入你的本地 master 分支:

git checkout master
git log origin/master

我們可以使用 git merge origin/master

git merge origin/master

origin/master 和 master 分支現(xiàn)在指向了同一個(gè)提交,你已經(jīng)和上游的更新保持了同步。

git pull

在基于 Git 的協(xié)作工作流中,將上游更改合并到你的本地倉庫是一個(gè)常見的工作。我們已經(jīng)知道應(yīng)該使用 git fetch,然后是 git merge,但是 git pull 將這兩個(gè)命令合二為一。

用法

git pull <remote>

拉取當(dāng)前分支對應(yīng)的遠(yuǎn)程副本中的更改,并立即并入本地副本。效果和 git fetch 后接 git merge origin/. 一致。

git pull --rebase <remote>

和上一個(gè)命令相同,但使用 git rebase 合并遠(yuǎn)程分支和本地分支,而不是使用 git merge。

討論

你可以將 git pull 當(dāng)做 Git 中對應(yīng) svn update 的命令。這是同步你本地倉庫和上游更改的簡單方式。下圖揭示了 pull 過程中的每一步。

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

你認(rèn)為你的倉庫已經(jīng)同步了,但 git fetch 發(fā)現(xiàn) origin 中 master 的版本在上次檢查后已經(jīng)有了新進(jìn)展。 接著 git merge 立即將 remote master 并入本地的分支。

基于 Rebase 的 Pull

--rebase 標(biāo)記可以用來保證線性的項(xiàng)目歷史,防止合并提交(merge commits)的產(chǎn)生。很多開發(fā)者傾向于使用 rebase 而不是 merge,因?yàn)椤肝蚁胍盐业母姆旁谄渌送瓿傻墓ぷ髦蟆埂T谶@種情況下,與普通的 git pull 相比而言,使用帶有 --rebase 標(biāo)記的 git pull 甚至更像 svn update。

事實(shí)上,使用 --rebase 的 pull 的工作流是如此普遍,以致于你可以直接在配置項(xiàng)中設(shè)置它:

git config --global branch.autosetuprebase always # In git < 1.7.9
git config --global pull.rebase true              # In git >= 1.7.9

在運(yùn)行這個(gè)命令之后,所有的 git pull 命令將使用 git rebase 而不是 git merge。

栗子

下面的栗子演示了如何和一個(gè)中央倉庫的 master branch 同步:

git checkout master
git pull --rebase origin

簡單地將你本地的更改放到其他人已經(jīng)提交的更改之后。

git push

Push 是你將本地倉庫中的提交轉(zhuǎn)移到遠(yuǎn)程倉庫中時(shí)要做的事。它和 git fetch 正好相反,fetch 將提交導(dǎo)入到本地分支,而 push 將提交導(dǎo)出到遠(yuǎn)程分支。它可以覆蓋已有的更改,所以你需要小心使用。這些情況請見下面的討論。

用法

git push <remote> <branch>

將指定的分支推送到 <remote> 上,包括所有需要的提交和提交對象。它會在目標(biāo)倉庫中創(chuàng)建一個(gè)本地分支。為了防止你覆蓋已有的提交,如果會導(dǎo)致目標(biāo)倉庫非快速向前合并時(shí),Git 不允許你 push。

git push <remote> --force

和上一個(gè)命令相同,但即使會導(dǎo)致非快速向前合并也強(qiáng)制推送。除非你確定你所做的事,否則不要使用 --force 標(biāo)記。

git push <remote> --all

將所有本地分支推送到指定的遠(yuǎn)程倉庫。

git push <remote> --tags

當(dāng)你推送一個(gè)分支或是使用 --all 選項(xiàng)時(shí),標(biāo)簽不會被自動推送上去。--tags 將你所有的本地標(biāo)簽推送到遠(yuǎn)程倉庫中去。

討論

git push 最常見的用法是將你的本地更改發(fā)布到中央倉庫。在你積累了一些本地提交,準(zhǔn)備和同事們共享時(shí),你(可以)用交互式 rebase 來清理你的提交,然后推送到中央倉庫去。

在這里插入圖片描述

在這里插入圖片描述

上圖顯示了當(dāng)你本地的 master 分支進(jìn)展超過了中央倉庫的 master 分支,當(dāng)你運(yùn)行 git push origin master 發(fā)布更改時(shí)發(fā)生的事情。注意,git push 和在遠(yuǎn)程倉庫內(nèi)部運(yùn)行 git merge master 事實(shí)上是一樣的。

強(qiáng)制推送

Git 為了防止你覆蓋中央倉庫的歷史,會拒絕你會導(dǎo)致非快速向前合并的推送請求。所以,如果遠(yuǎn)程歷史和你本地歷史已經(jīng)分叉,你需要將遠(yuǎn)程分支 pull 下來,在本地合并后再嘗試推送。這和 SVN 讓你在提交更改集合之前要和中央倉庫同步是類似的。

--force 這個(gè)標(biāo)記覆蓋了這個(gè)行為,讓遠(yuǎn)程倉庫的分支符合你的本地分支,刪除你上次 pull 之后可能的上游更改。只有當(dāng)你意識到你剛剛共享的提交不正確,并用 git commit --amend 或者交互式 rebase 修復(fù)之后,你才需要用到強(qiáng)制推送。但是,你必須絕對確定在你使用 --force 標(biāo)記前你的同事們都沒有 pull 這些提交。

只推送到裸倉庫

此外,你只應(yīng)該推送到那些用 --bare 標(biāo)記初始化的倉庫。因?yàn)橥扑蜁獊y遠(yuǎn)程分支結(jié)構(gòu),很重要的一點(diǎn)是,永遠(yuǎn)不要推送到其他開發(fā)者的倉庫。但因?yàn)槁銈}庫沒有工作目錄,不會發(fā)生打斷別人的開發(fā)之類的事情。

栗子

下面的栗子描述了將本地提交推送到中央倉庫的一些標(biāo)準(zhǔn)做法。首先,確保你本地的 master 和中央倉庫的副本是一致的,提前 fetch 中央倉庫的副本并在上面 rebase。交互式 rebase 同樣是共享之前清理提交的好機(jī)會。接下來,git push 命令將你本地 master 分支上的所有提交發(fā)送給中央倉庫.

git checkout master
git fetch origin master
git rebase -i origin/master
# Squash commits, fix up commit messages etc.
git push origin master

因?yàn)槲覀円呀?jīng)確信本地的 master 分支是最新的,它應(yīng)該導(dǎo)致快速向前的合并,git push 不應(yīng)該拋出非快速向前之類的問題。


2.2 創(chuàng)建Pull Request

Pull Request 是開發(fā)者使用 GitHub 進(jìn)行協(xié)作的利器。這個(gè)功能為用戶提供了友好的頁面,讓提議的更改在并入官方項(xiàng)目之前,可以得到充分的討論。

qq20160127-0

最簡單地來說,Pull Request 是一種機(jī)制,讓開發(fā)者告訴項(xiàng)目成員一個(gè)功能已經(jīng)完成。一旦 feature 分支開發(fā)完畢,開發(fā)者使用 GitHub 賬號提交一個(gè) Pull Request。它告訴所有參與者,他們需要審查代碼,并將代碼并入 master 分支。

但是,Pull Request 不只是一個(gè)通知,還是一個(gè)專注于某個(gè)提議功能的討論版。 如果更改導(dǎo)致了任何問題,團(tuán)隊(duì)成員可以在 Pull Request 下發(fā)布反饋,甚至推送后續(xù)提交來修改這個(gè) Pull Request。所有的活動都在這個(gè) Pull Request里之間追蹤。

在這里插入圖片描述

和其他協(xié)作模型相比,這種共享提交的解決方案形成了更加線性的工作流。SVN 和 Git 都能通過一個(gè)簡單的腳本發(fā)送通知郵件;但是,如果要討論更改,開發(fā)者不得不在郵件里回復(fù)。這會變得愈發(fā)雜亂無章,尤其是后續(xù)提交出現(xiàn)時(shí)。Pull Request 將所有這些功能放入了一個(gè)友好的網(wǎng)頁,在每個(gè) GitHub 倉庫上方都能找到。

剖析一個(gè) Pull Request

當(dāng)你提交一個(gè) Pull Request 的時(shí)候,你做的事情是 請求(request) 另一個(gè)開發(fā)者(比如項(xiàng)目維護(hù)者)來 拉取(pull) 你倉庫中的一個(gè)分支到他們的倉庫。也就是說你需要提供 4 個(gè)信息來完成一個(gè) Pull Request:源倉庫、源分支、目標(biāo)倉庫、目標(biāo)分支。

在這里插入圖片描述

GitHub 會機(jī)智地幫你將一些值設(shè)為默認(rèn)值。但是,取決于你的協(xié)作工作流,你的團(tuán)隊(duì)可能需要設(shè)置不同的值。上圖顯示了一個(gè)請求從 feature 分支合并到官方 master分支的一個(gè) Pull Request,但除此之外還有好多種使用 Pull Request 的方式。

Pull Request是如何工作的

Pull Request 可以和 feature 分支工作流、GitFlow 工作流或者 Fork 工作流一起使用。但 Pull Request 需要兩個(gè)不同的分支或是兩個(gè)不同的倉庫,因此它們不能和中心化的工作流一起使用。在不同的工作流中使用 Pull Request 有些不同,但大致的流程如下:

  1. 開發(fā)者在他們的本地倉庫中為某個(gè)功能創(chuàng)建一個(gè)專門的分支。
  2. 開發(fā)者將分支推送到公共的 GitHub 倉庫。
  3. 開發(fā)者用 GitHub 發(fā)起一個(gè) Pull Request。
  4. 其余的團(tuán)隊(duì)成員審查代碼,討論并且做出修改。
  5. 項(xiàng)目維護(hù)者將這個(gè)功能并入官方的倉庫,然后關(guān)閉這個(gè) Pull Request。

下面的章節(jié)討論 Pull Request 在不同的協(xié)作工作流中有哪些不同。

Feature 分支工作流中的 Pull Request

Feature 分支工作流使用共享的 GitHub 倉庫來管理協(xié)作,開發(fā)者在單獨(dú)的 feature 分支中添加功能。開發(fā)者在將代碼并入主代碼庫之前,應(yīng)該發(fā)起一個(gè) Pull Request 來啟動這個(gè)功能的討論,而不是直接將它們合并到 master

在這里插入圖片描述

在 Feature 分支工作流中只有一個(gè)公共的倉庫,因此 Pull Request 的目標(biāo)和源倉庫永遠(yuǎn)是同一個(gè)。一般來說,開發(fā)者會將他們的 feature分支作為源分支,master 作為目標(biāo)分支。

在收到 Pull Request 之后,項(xiàng)目維護(hù)者將會做出決定。如果這個(gè)功能可以立即發(fā)布,他們只需要將代碼合并進(jìn) master,然后關(guān)閉 Pull Request 即可。但是,如果提議的更改有一些問題,他們可以在 Pull Request 下發(fā)布反饋。后續(xù)提交將會顯示在相關(guān)評論的下方。

你也可以發(fā)布一個(gè)未完成功能的 Pull Request。例如,如果開發(fā)者在實(shí)現(xiàn)一個(gè)特殊的需求時(shí)遇到了問題,同樣可以發(fā)布一個(gè)包含工作進(jìn)展的 Pull Request。其他開發(fā)者可以在這個(gè) Pull Request 后面提供建議,甚至自己發(fā)布后續(xù)的提交來解決這個(gè)問題。

GitFlow 工作流中的 Pull Request

GitFlow 工作流和 Feature 分支工作流類似,但定義了圍繞項(xiàng)目發(fā)布的一個(gè)嚴(yán)格的分支模型。在 GitFlow 工作流之上添加 Pull Request 使得開發(fā)者方便地討論發(fā)布分支或是所在的維護(hù)分支。

在這里插入圖片描述

在 GitFlow 工作流中的 Pull Request 和上一節(jié)中的完全一致:開發(fā)者只需在功能、發(fā)布或是快速修復(fù)分支需要審查時(shí)發(fā)布一個(gè) Pull Request,GitHub 會通知到其余的團(tuán)隊(duì)成員。

功能一般都會合并到 develop 分支,而發(fā)布和快速修復(fù)分支會被同時(shí)合并到 developmaster 當(dāng)中。 Pull Request 可以用來妥善管理這些合并。

Fork 工作流中的 Pull Request

在 Fork 工作流中,開發(fā)者將一個(gè)完成的功能推送到 他們自己的 倉庫,而不是公共的倉庫。在這之后,他們發(fā)布一個(gè) Pull Request,告訴項(xiàng)目維護(hù)者代碼需要審查了。

在這個(gè)工作流中,Pull Request 的通知作用顯得非常有用,因?yàn)轫?xiàng)目維護(hù)者無法獲知其他開發(fā)者什么時(shí)候向他們自己的 GitHub 倉庫中添加了提交。

在這里插入圖片描述

因?yàn)槊總€(gè)開發(fā)者都有他們自己的公共倉庫,Pull Request 的源倉庫和目標(biāo)倉庫不是同一個(gè)。源倉庫是開發(fā)者的公開倉庫,源分支是包含提議更改的那一個(gè)。如果開發(fā)者想要將功能合并到主代碼庫,目標(biāo)倉庫便是官方的項(xiàng)目倉庫,目標(biāo)分支為 master。

Pull Request 還可以用來和官方項(xiàng)目之外的開發(fā)者進(jìn)行協(xié)作。比如說,一個(gè)開發(fā)者正在和同事一起開發(fā)一個(gè)功能,他們可以向 同事的 GitHub 倉庫發(fā)起一個(gè) Pull Request,而不是官方倉庫。他們將 feature 分支同時(shí)作為源分支和目標(biāo)分支。

在這里插入圖片描述

兩個(gè)開發(fā)者可以在 Pull Request 中討論和開發(fā)分支。當(dāng)功能完成時(shí),其中一位可以發(fā)起另一個(gè) Pull Request,請求將功能合并到官方的 master 分支中去。這種靈活性使得 Pull Request 成為了 Fork 工作流中尤為強(qiáng)大的協(xié)作工具。

栗子

下面的🌰演示了如何將 Pull Request 用在 Fork 工作流中。小團(tuán)隊(duì)中的開發(fā)和向一個(gè)開源項(xiàng)目貢獻(xiàn)代碼都可以這樣做。

在這個(gè)栗子中,Mary 是一位開發(fā)者,John 是項(xiàng)目的維護(hù)者。他們都有自己公開的 GitHub 倉庫,John 的倉庫之一便是下面的官方項(xiàng)目。

Mary fork了官方項(xiàng)目

在這里插入圖片描述

為了參與這個(gè)項(xiàng)目,Mary 首先要做的是 fork 屬于 John 的 GitHub 倉庫。她需要注冊登錄 GitHub,找到 John 的倉庫,點(diǎn)擊 Fork 按鈕。

下圖顯示的是 geeeeeeeeek 的 WeChatLuckyMoney 倉庫。

qq20160127-1

選好 fork 的目標(biāo)位置之后,她在服務(wù)端就有了一個(gè)項(xiàng)目的副本。

Mary 克隆了她的 GitHub 倉庫

在這里插入圖片描述

接下來,Mary 需要將她剛剛 fork 的 GitHub 倉庫克隆下來。她在本地會有一份項(xiàng)目的副本。她需要運(yùn)行下面這個(gè)命令:

git clone https://github.com/user/repo.git

請記住,git clone 自動創(chuàng)建了一個(gè)名為 origin 的遠(yuǎn)端連接,指向 Mary 所 fork 的倉庫。

Mary 開發(fā)了一個(gè)新功能

在這里插入圖片描述

在她寫任何代碼之前,Mary 需要為這個(gè)功能創(chuàng)建一個(gè)新的分支。這個(gè)分支將是她隨后發(fā)起 Pull Request 時(shí)要用到的源分支。

git checkout -b some-feature
# 編輯一些代碼
git commit -a -m "新功能的一些草稿"

為了完成這個(gè)新功能,Mary 想創(chuàng)建多少個(gè)提交都可以。如果 feature 分支的歷史有些亂,她可以使用交互式的 rebase 來移除或者拼接不必要的提交。對于大項(xiàng)目來說,清理 feature 的項(xiàng)目歷史使得項(xiàng)目維護(hù)者更容易看清楚 Pull Request 的所處的進(jìn)展。

Mary 將 feature 分支推送到了她的 GitHub 倉庫

在這里插入圖片描述

在功能完成后,Mary 使用簡單的 git push 將 feature 分支推送到了她自己的 GitHub 倉庫上(不是官方的倉庫):

git push origin some-branch

這樣她的更改就可以被項(xiàng)目維護(hù)者看到了(或者任何有權(quán)限的協(xié)作者)。

Mary 創(chuàng)建了一個(gè) Pull Request

在這里插入圖片描述

GitHub 上已經(jīng)有了她的 feature 分支之后,Mary 可以找到被她 fork 的倉庫,點(diǎn)擊項(xiàng)目簡介下的 New Pull Request 按鈕,用她的 GitHub 賬號創(chuàng)建一個(gè) Pull Request。Mary 的倉庫會被默認(rèn)設(shè)置為源倉庫(head fork),詢問她指定源分支(compare)、目標(biāo)倉庫(base fork)和目標(biāo)分支(base)。

Mary 想要將她的功能并入主代碼庫,所以源分支就是她的 feature 分支,目標(biāo)倉庫就是 John 的公開倉庫,目標(biāo)分支為 master。她還需要提供一個(gè) Pull Request 的標(biāo)題和簡介。

下圖展示的是將 LitoMore/demo-project(源倉庫)的 develop(源分支)合并到 LitoMore/demo-project(目標(biāo)倉庫)的 master(目標(biāo)分支)。

qq20160127-2

在她創(chuàng)建了 Pull Request 之后,GitHub 會給 John 發(fā)送一條通知。

John 審查了這個(gè) Pull Request

qq20160127-3

John 可以在他自己的 GitHub 倉庫下的 Pull Request 選項(xiàng)卡中看到所有的 Pull Request。點(diǎn)擊 Mary 的 Pull Request 會顯示這個(gè) Pull Request 的簡介、feature 分支的提交歷史,以及包含的更改。

如果他認(rèn)為 feature 分支已經(jīng)可以合并了,他只需點(diǎn)擊 Merge Pull Request 按鈕來通過這個(gè) Pull Request,將 Mary 的 feature分支并入他的 master 分支。

但是,在這里栗子中,假設(shè) John 發(fā)現(xiàn)了 Mary 代碼中的一個(gè)小 bug,需要她在合并前修復(fù)。他可以評論整個(gè) Pull Request,也可以評論 feature 分支中某個(gè)特定的提交。

qq20160127-4

Mary 添加了一個(gè)后續(xù)提交

如果 Mary 對這個(gè)反饋感到困惑,她可以在 Pull Request 后回復(fù),把這里當(dāng)做是她的功能的討論版。

為了修復(fù)錯(cuò)誤,Mary 在她的 feature 分支后面添加了另一個(gè)提交,并將它推送到了她的 GitHub 倉庫,就像她之前做的一樣。這個(gè)提交被自動添加到原來的 Pull Request 后面,John 可以在他的評論下方再次審查這些修改。

John 接受了 Pull Request

最后,John 接受了這些修改,將 feature 分支并入了 master 分支,關(guān)閉了這個(gè) Pull Request。功能現(xiàn)在已經(jīng)整合到了項(xiàng)目中,其他在 master 分支上工作的開發(fā)者可以使用標(biāo)準(zhǔn)的 git pull 命令將這些修改拉取到自己的本地倉庫。

接下來怎么做?

你現(xiàn)在應(yīng)該已經(jīng)掌握了如何將你的 Pull Request 整合到你的工作流。記住,Pull Request 不是替代任何 Git 工作流的萬金油,而是一種讓隊(duì)員間協(xié)作錦上添花的工具。


2.4 使用分支

使用分支

這份教程是 Git 分支的綜合介紹。首先,我們簡單講解如何創(chuàng)建分支,就像請求一份新的項(xiàng)目歷史一樣。然后,我們會看到 git checkout 是如何切換分支的。最后,學(xué)習(xí)一下 git merge 是如何整合獨(dú)立分支的歷史。

我們已經(jīng)知道,Git 分支和 SVN 分支不同。SVN 分支只被用來記錄偶爾大規(guī)模的開發(fā)效果,而 Git 分支是你日常工作流中不可缺失的一部分。

git branch

分支代表了一條獨(dú)立的開發(fā)流水線。分支是我們在第二篇中討論過的「編輯/緩存/提交」流程的抽象。你可以把它看作請求全新「工作目錄、緩存區(qū)、項(xiàng)目歷史」的一種方式。新的提交被存放在當(dāng)前分支的歷史中,導(dǎo)致了項(xiàng)目歷史被 fork 了一份。

git branch 命令允許你創(chuàng)建、列出、重命名和刪除分支。它不允許你切換分支或是將被 fork 的歷史放回去。因此,git branchgit checkout、git merge 這兩個(gè)命令通常緊密地結(jié)合在一起使用。

用法

git branch

列出倉庫中所有分支。

git branch <branch>

創(chuàng)建一個(gè)名為 <branch> 的分支。不會 自動切換到那個(gè)分支去。

git branch -d <branch>

刪除指定分支。這是一個(gè)安全的操作,Git 會阻止你刪除包含未合并更改的分支。

git branch -D <branch>

強(qiáng)制刪除指定分支,即使包含未合并更改。如果你希望永遠(yuǎn)刪除某條開發(fā)線的所有提交,你應(yīng)該用這個(gè)命令。

git branch -m <branch>

將當(dāng)前分支命名為 <branch>。

討論

在 Git 中,分支是你日常開發(fā)流程中的一部分。當(dāng)你想要添加一個(gè)新的功能或是修復(fù)一個(gè) bug 時(shí)——不管 bug 是大是小——你都應(yīng)該新建一個(gè)分支來封裝你的修改。這確保了不穩(wěn)定的代碼永遠(yuǎn)不會被提交到主代碼庫中,它同時(shí)給了你機(jī)會,在并入主分支前清理你 feature 分支的歷史。

在這里插入圖片描述

比如,上圖將一個(gè)擁有兩條獨(dú)立開發(fā)線的倉庫可視化,其中一條是一個(gè)不起眼的功能,另一條是長期運(yùn)行的功能。使用分支開發(fā)時(shí),不僅可以同時(shí)在兩條線上工作,還可以保持主要的 master branch 不混入奇怪的代碼。

分支的頂端

Git 分支背后的實(shí)現(xiàn)遠(yuǎn)比 SVN 的模型要輕量。與其在目錄之間復(fù)制文件,Git 將分支存為指向提交的引用。換句話說,分支代表了一系列提交的 頂端 ——而不是提交的 容器 。分支歷史通過提交之間的關(guān)系來推斷。

這使得 Git 的合并模型變成了動態(tài)的。SVN 中的合并是基于文件的,而Git 讓你在更抽象的提交層面操作。事實(shí)上,你可以看到項(xiàng)目歷史中的合并其實(shí)是將兩個(gè)獨(dú)立的提交歷史連接起來。

栗子

創(chuàng)建分支

分支只是指向提交的 指針 ,理解這一點(diǎn)很重要。當(dāng)你創(chuàng)建一個(gè)分支是,Git 只需要創(chuàng)建一個(gè)新的指針——倉庫不會受到任何影響。因此,如果你最開始有這樣一個(gè)倉庫:

在這里插入圖片描述

接下來你用下面的命令創(chuàng)建了一個(gè)分支:

git branch crazy-experiment

倉庫歷史保持不變。你得到的是一個(gè)指向當(dāng)前提交的新的指針:

在這里插入圖片描述

注意,這只會 創(chuàng)建 一個(gè)新的分支。要開始在上面添加提交,你需要用 git checkout 來選中這個(gè)分支,然后使用標(biāo)準(zhǔn)的 git addgit commit 命令。

刪除分支

一旦你完成了分支上的工作,準(zhǔn)備將它并入主代碼庫,你可以自由地刪除這個(gè)分支,而不丟失項(xiàng)目歷史:

git branch -d crazy-experiment

然后,如果分支還沒有合并,下面的命令會產(chǎn)生一個(gè)錯(cuò)誤信息:

error: The branch 'crazy-experiment' is not fully merged.
If you are sure you want to delete it, run 'git branch -D crazy-experiment'.

Git 保護(hù)你不會丟失這些提交的引用,或者說丟失訪問整條開發(fā)線的入口。如果你 真的 想要刪除這個(gè)分支(比如說這是一個(gè)失敗的實(shí)驗(yàn)),你可以用大寫的 -D 標(biāo)記:

git branch -D crazy-experiment

它會刪除這個(gè)分支,無視它的狀態(tài)和警告,因此需謹(jǐn)慎使用。

git checkout

git checkout 命令允許你切換用 git branch 創(chuàng)建的分支。查看一個(gè)分支會更新工作目錄中的文件,以符合分支中的版本,它還告訴 Git 記錄那個(gè)分支上的新提交。將它看作一個(gè)選中你正在進(jìn)行的開發(fā)的一種方式。

在上一篇中,我們看到了如何用 git checkout 來查看舊的提交?!覆榭捶种А购汀笇⒐ぷ髂夸浉碌竭x中的版本/修改」很類似;但是,新的更改 保存在項(xiàng)目歷史中——這不是一個(gè)只讀的操作。

用法

git checkout <existing-branch>

查看特定分支,分支應(yīng)該已經(jīng)通過 git branch 創(chuàng)建。這使得 <existing-branch> 成為當(dāng)前的分支,并更新工作目錄的版本。

git checkout -b <new-branch>

創(chuàng)建并查看 <new-branch>-b 選項(xiàng)是一個(gè)方便的標(biāo)記,告訴Git在運(yùn)行 git checkout <new-branch> 之前運(yùn)行 git branch <new-branch>。

git checkout -b <new-branch> <existing-branch>

和上一條相同,但將 <existing-branch> 作為新分支的基,而不是當(dāng)前分支。

討論

git checkoutgit branch 是一對好基友。當(dāng)你想要創(chuàng)建一個(gè)新功能時(shí),你用 git branch 創(chuàng)建分支,然后用 git checkout 查看。你可以在一個(gè)倉庫中用 git checkout 切換分支,同時(shí)開發(fā)幾個(gè)功能。

每個(gè)功能專門一個(gè)分支對于傳統(tǒng) SVN 工作流來說是一個(gè)巨大的轉(zhuǎn)變。這使得嘗試新的實(shí)驗(yàn)超乎想象的簡單,不用擔(dān)心毀壞已有的功能,并且可以同時(shí)開發(fā)幾個(gè)不相關(guān)的功能。另外,分支可以促進(jìn)了不同的協(xié)作工作流。

分離的 HEAD

現(xiàn)在我們已經(jīng)看到了 git checkout 最主要的三種用法,我們可以討論上一篇中提到的「分離 HEAD」?fàn)顟B(tài)了。

記住,HEAD 是 Git 指向當(dāng)前快照的引用。git checkout 命令內(nèi)部只是更新 HEAD,指向特定分支或提交。當(dāng)它指向分支時(shí),Git 不會報(bào)錯(cuò),但當(dāng)你 check out 提交時(shí),它會進(jìn)入「分離 HEAD」?fàn)顟B(tài)。

有個(gè)警告會告訴你所做的更改和項(xiàng)目的其余歷史處于「分離」的狀態(tài)。如果你在分離 HEAD 狀態(tài)開始開發(fā)新功能,沒有分支可以讓你回到之前的狀態(tài)。當(dāng)你不可避免地 checkout 到了另一個(gè)分支(比如你的更改并入了這個(gè)分支),你將不再能夠引用你的 feature 分支:

在這里插入圖片描述
在這里插入圖片描述

重點(diǎn)是,你應(yīng)該永遠(yuǎn)在分支上開發(fā)——而絕不在分離的 HEAD 上。這樣確保你一直可以引用到你的新提交。不過,如果你只是想查看舊的提交,那么是否處于分離 HEAD 狀態(tài)并不重要。

例子

下面的例子演示了基本的 Git 分支流程。當(dāng)你想要開發(fā)新功能時(shí),你創(chuàng)建一個(gè)專門的分支,切換過去:

git branch new-feature
git checkout new-feature

接下來,你可以和以往一樣提交新的快照:

# 編輯文件
git add <file>
git commit -m "Started work on a new feature"
# 周而復(fù)始…

這些操作都被記錄在 new-feature 上,和 master 完全獨(dú)立。你想添加多少提交就可以添加多少,不用關(guān)心你其它分支的修改。當(dāng)你想要回到「主」代碼庫時(shí),只要 check out 到 master 分支即可:

git checkout master

這個(gè)命令在你開始新的分支之前,告訴你倉庫的狀態(tài)。在這里,你可以選擇并入完成的新功能,或者在你項(xiàng)目穩(wěn)定的版本上繼續(xù)工作。

git merge

合并是 Git 將被 fork 的歷史放回到一起的方式。git merge 命令允許你將 git branch 創(chuàng)建的多條分支合并成一個(gè)。

注意,下面所有命令將更改 并入 當(dāng)前分支。當(dāng)前分支會被更新,以響應(yīng)合并操作,但目標(biāo)分支完全不受影響。也就是說 git merge 經(jīng)常和 git checkout 一起使用,選擇當(dāng)前分支,然后用 git branch -d 刪除廢棄的目標(biāo)分支。

用法

git merge <branch>

將指定分支并入當(dāng)前分支。Git 會決定使用哪種合并算法(下文討論)。

git merge --no-ff <branch>

將指定分支并入當(dāng)前分支,但 總是 生成一個(gè)合并提交(即使是快速向前合并)。這可以用來記錄倉庫中發(fā)生的所有合并。

討論

一旦你在單獨(dú)的分支上完成了功能的開發(fā),重要的是將它放回主代碼庫。取決于你的倉庫結(jié)構(gòu),Git 有幾種不同的算法來完成合并:快速向前合并或者三路合并。

當(dāng)當(dāng)前分支頂端到目標(biāo)分支路徑是線性之時(shí),我們可以采取 快速向前合并 。Git 只需要將當(dāng)前分支頂端(快速向前地)移動到目標(biāo)分支頂端,即可整合兩個(gè)分支的歷史,而不需要“真正”合并分支。它在效果上合并了歷史,因?yàn)槟繕?biāo)分支上的提交現(xiàn)在在當(dāng)前分支可以訪問到。比如,some-featuremaster 分支的快速向前合并會是這樣的:
在這里插入圖片描述
在這里插入圖片描述

但是,如果分支已經(jīng)分叉了,那么就無法進(jìn)行快速向前合并。當(dāng)和目標(biāo)分支之間的路徑不是線性之時(shí),Git 只能執(zhí)行 三路合并 。三路合并使用一個(gè)專門的提交來合并兩個(gè)分支的歷史。這個(gè)術(shù)語取自這樣一個(gè)事實(shí),Git 使用 三個(gè) 提交來生成合并提交:兩個(gè)分支頂端和它們共同的祖先。

在這里插入圖片描述

在這里插入圖片描述

但你可以選擇使用哪一種合并策略時(shí),很多開發(fā)者喜歡使用快速向前合并(搭配 rebase 使用)來合并微小的功能或者修復(fù) bug,使用三路合并來整合長期運(yùn)行的功能。后者導(dǎo)致的合并提交作為兩個(gè)分支的連接標(biāo)志。

解決沖突

如果你嘗試合并的兩個(gè)分支同一個(gè)文件的同一個(gè)部分,Git 將無法決定使用哪個(gè)版本。當(dāng)這種情況發(fā)生時(shí),它會停在合并提交,讓你手動解決這些沖突。

Git 的合并流程令人稱贊的一點(diǎn)是,它使用我們熟悉的「編輯/緩存/提交」工作流來解決沖突。當(dāng)你遇到合并沖突時(shí),運(yùn)行 git status 命令來查看哪些文件存在需要解決的沖突。比如,如果兩個(gè)分支都修改了 hello.py 的同一處,你會看到下面的信息:

# On branch master
# Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)
#
# both modified: hello.py
#

接下來,你可以自己修復(fù)這個(gè)合并。當(dāng)你準(zhǔn)備結(jié)束合并時(shí),你只需對沖突的文件運(yùn)行 git add 告訴 Git 沖突已解決。然后,運(yùn)行 git commit 生成一個(gè)合并提交。這和提交一個(gè)普通的快照有著完全相同的流程,也就是說,開發(fā)者能夠輕而易舉地管理他們的合并。

注意,提交沖突只會出現(xiàn)在三路合并中。在快速向前合并中,我們不可能出現(xiàn)沖突的更改。

例子

快速向前合并

我們第一個(gè)例子演示了快速向前合并。下面的代碼創(chuàng)建了一個(gè)分支,在后面添加了兩個(gè)提交,然后使用快速向前合并將它并入主分支。

# 開始新功能
git checkout -b new-feature master# 編輯文件
git add <file>
git commit -m "開始新功能"# 編輯文件
git add <file>
git commit -m "完成功能"# 合并new-feature分支
git checkout master
git merge new-feature
git branch -d new-feature

對于臨時(shí)存在、用作獨(dú)立開發(fā)環(huán)境而不是組織長期運(yùn)行功能的工具的分支來說,這是一種常見的工作流。

同時(shí)注意,運(yùn)行 git branch -d 時(shí) Git 不應(yīng)該產(chǎn)生錯(cuò)誤提示,因?yàn)?new-feature 現(xiàn)在可以在主分支上訪問了。

三路合并

下一個(gè)例子很相似,但需要進(jìn)行三路合并,因?yàn)?master 在這個(gè)功能開發(fā)時(shí)取得了新進(jìn)展。這是復(fù)雜功能和多個(gè)開發(fā)者同時(shí)工作時(shí)常見的情形。

# 開始新功能
git checkout -b new-feature master# 編輯文件
git add <file>
git commit -m "開始新功能"# 編輯文件
git add <file>
git commit -m "完成功能"# 在master分支上開發(fā)
git checkout master# 編輯文件
git add <file>
git commit -m "在master上添加了一些極其穩(wěn)定的功能"# 合并new-feature分支
git merge new-feature
git branch -d new-feature

注意,Git 現(xiàn)在無法進(jìn)行快速向前合并,因?yàn)闊o法將 master 直接移動到 new-feature。

對大多數(shù)工作流來說,new-feature 會是一個(gè)需要一段時(shí)間來開發(fā)的復(fù)雜功能,這也是為什么同時(shí) master 會有新的提交出現(xiàn)。如果你的分支上的功能像上面的一樣簡單,你會更想將它 rebase 到 master,使用快速向前合并。它會通過整理項(xiàng)目歷史來避免多余的合并提交。


2.5 常見工作流比較

多種多樣的工作流使得在項(xiàng)目中實(shí)施 Git 時(shí)變得難以選擇。這份教程提供了一個(gè)出發(fā)點(diǎn),調(diào)查企業(yè)團(tuán)隊(duì)最常見的 Git 工作流。

閱讀的時(shí)候,請記住工作流應(yīng)該是一種規(guī)范而不是金科玉律。我們希望向你展示所有工作流,讓你融會貫通,因地制宜。

這份教程討論了下面四種工作流:

  • 中心化的工作流
  • 基于功能分支的工作流
  • Gitflow工作流
  • Fork工作流

中心化的工作流

在這里插入圖片描述

過渡到分布式分版本控制系統(tǒng)看起來是個(gè)令人恐懼的任務(wù),但你不必為了利用 Git 的優(yōu)點(diǎn)而改變你現(xiàn)有的工作流。你的團(tuán)隊(duì)仍然可以用以前SVN的方式開發(fā)項(xiàng)目。

然而,使用 Git 來驅(qū)動你的開發(fā)工作流顯示出了一些SVN沒有的優(yōu)點(diǎn)。首先,它讓每個(gè)開發(fā)者都有了自己 本地 的完整項(xiàng)目副本。隔離的環(huán)境使得每個(gè)開發(fā)者的工作獨(dú)立于項(xiàng)目的其它修改——他們可以在自己的本地倉庫中添加提交,完全無視上游的開發(fā),直到需要的時(shí)候。

第二,它讓你接觸到了 Git 魯棒的分支和合并模型。和 SVN 不同,Git 分支被設(shè)計(jì)為一種故障安全的機(jī)制,用來在倉庫之間整合代碼和共享更改。

如何工作

和 Subversion 一樣,中心化的工作流將中央倉庫作為項(xiàng)目中所有修改的唯一入口。和 trunk 不同,默認(rèn)的開發(fā)分支叫做master,所有更改都被提交到這個(gè)分支。這種工作流不需要 master 之外的其它分支。

開發(fā)者將中央倉庫克隆到本地后開始工作。在他們的本地項(xiàng)目副本中,他們可以像SVN一樣修改文件和提交更改;不過,這些新的提交被保存在 本地 ——它們和中央倉庫完全隔離。這使得開發(fā)者可以將和上游的同步推遲到他們方便的時(shí)候。

為了向官方項(xiàng)目發(fā)布修改,開發(fā)者將他們的本地 master 分支「推送」到中央倉庫。這一步等同于 svn commit,除了Git添加的是所有不在中央 master 分支上的本地提交。
在這里插入圖片描述

在這里插入圖片描述

管理沖突

中央倉庫代表官方項(xiàng)目,因此它的提交歷史應(yīng)該被視作神圣不可更改的。如果開發(fā)者的本地提交和中央倉庫分叉了,Git 會拒絕將他們的修改推送上去,因?yàn)檫@會覆蓋官方提交。
在這里插入圖片描述

在開發(fā)者發(fā)布他們的功能之前,他們需要 fetch 更新的中央提交,在它們之上 rebase 自己的更改。這就像是「我想要在其他人的工作進(jìn)展之上添加我的修改」,它會產(chǎn)生完美的線性歷史,就像和傳統(tǒng)的 SVN 工作流一樣。

如果本地修改和上游提交沖突時(shí),Git 會暫停 rebase 流程,給你機(jī)會手動解決這些沖突。Git 很贊的一點(diǎn)是,它將 git statusgit add命令同時(shí)用來生成提交和解決合并沖突。這使得開發(fā)者能夠輕而易舉地管理他們的合并。另外,如果他們改錯(cuò)了什么,Git 讓他們輕易地退出 rebase 過程,然后重試(或者找人幫忙)。

栗子

讓我們一步步觀察一個(gè)普通的小團(tuán)隊(duì)是如何使用這種工作流協(xié)作的。我們有兩位開發(fā)者,John 和 Mary,分別在開發(fā)兩個(gè)功能,他們通過中心化的倉庫共享代碼。

一人初始化了中央倉庫

首先,需要有人在服務(wù)器上創(chuàng)建中央倉庫。如果這是一個(gè)新項(xiàng)目,你可以初始化一個(gè)空的倉庫。不然,你需要導(dǎo)入一個(gè)已經(jīng)存在的 Git 或 SVN 項(xiàng)目。

中央倉庫應(yīng)該永遠(yuǎn)是裸倉庫(沒有工作目錄),可以這樣創(chuàng)建:

ssh user@host git init --bare /path/to/repo.git

但確保你使用的 SSH 用戶名 user、服務(wù)器 host 的域名或 IP 地址、儲存?zhèn)}庫的地址 /path/to/repo.git 是有效的。注意 .git 約定俗成地出現(xiàn)在倉庫名的后面,表明這是一個(gè)裸倉庫。

所有人將倉庫克隆到本地

接下來,每個(gè)開發(fā)者在本地創(chuàng)建一份完整項(xiàng)目的副本。使用 git clone 命令:

git clone ssh://user@host/path/to/repo.git

當(dāng)你克隆倉庫時(shí),Git 自動添加了一個(gè)名為 origin 的遠(yuǎn)程連接,指向「父」倉庫,以便你以后和這個(gè)倉庫交換數(shù)據(jù)。

John 在開發(fā)他的功能

在這里插入圖片描述

在他的本地倉庫中,John 可以用標(biāo)準(zhǔn)的 Git 提交流程開發(fā)功能:編輯、緩存、提交。如果你對緩存區(qū)還不熟悉,你也可以不用記錄工作目錄中每次的變化。于是你創(chuàng)建了一個(gè)高度集中的提交,即使你已經(jīng)在本地做了很多修改。

git status # 查看倉庫狀態(tài)
git add <some-file> # 緩存一個(gè)文件
git commit # 提交一個(gè)文件</some-file>

記住,這些命令創(chuàng)建的是本地提交,John 可以周而復(fù)始地重復(fù)這個(gè)過程,而不用考慮中央倉庫。對于龐大的功能,需要切成更簡單、原子化的片段時(shí),這個(gè)特性就很有用。

Mary 在開發(fā)她的功能

在這里插入圖片描述

同時(shí),Mary 在她自己的本地倉庫用相同的編輯/緩存/提交流程開發(fā)她的功能。和 John 一樣,她不需要關(guān)心中央倉庫的進(jìn)展,她也 完全 不關(guān)心 John 在他自己倉庫中做的事,因?yàn)樗斜镜貍}庫都是私有的。

John 發(fā)布了他的功能

在這里插入圖片描述

一旦 John 完成了他的功能,他應(yīng)該將本地提交發(fā)布到中央倉庫,這樣其他項(xiàng)目成員就可以訪問了。他可以使用git push命令,就像:

git push origin master

記住,origin 是 John 克隆中央倉庫時(shí)指向它的遠(yuǎn)程連接。master 參數(shù)告訴 Git 試著將 originmaster 分支變得和他本地的 master 分支一樣。中央倉庫在 John 克隆之后還沒有進(jìn)展,因此這個(gè)推送如他所愿,沒有產(chǎn)生沖突。

Mary as試圖發(fā)布她的功能

在這里插入圖片描述

John 已經(jīng)成功地將他的更改發(fā)布到了中央倉庫上,看看當(dāng) Mary 試著將她的功能推送到上面時(shí)會發(fā)生什么。她可以使用同一個(gè)推送命令:

git push origin master

但是,她的本地歷史和中央倉庫已經(jīng)分叉了,Git 會拒絕這個(gè)請求,并顯示一段冗長的錯(cuò)誤信息:

error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Git 防止 Mary 覆蓋官方的修改。她需要將 John 的更新拉取到她的倉庫,和她的本地修改整合后,然后重試。

Mary在John的提交之上rebase

在這里插入圖片描述

Mary 可以使用 git pull 來將上游修改并入她的倉庫。這個(gè)命令和 svn update 很像——它拉取整個(gè)上游提交歷史到Mary的本地倉庫,并和她的本地提交一起整合:

git pull --rebase origin master

--rebase 選項(xiàng)告訴 Git,在同步了中央倉庫的修改之后,將 Mary 所有的提交移到 master 分支的頂端,如下圖所示:

在這里插入圖片描述

如果你忽略這個(gè)選項(xiàng)拉取同樣會成功,只不過你每次和中央倉庫同步時(shí)都會多出一個(gè)「合并提交」。在這種工作流中,rebase 和生成一個(gè)合并提交相比,總是一個(gè)更好的選擇。

Mary 解決了合并沖突

在這里插入圖片描述

Rebase 的工作是將每個(gè)本地提交一個(gè)個(gè)轉(zhuǎn)移到更新后的 master 分支。也就是說,你可以一個(gè)個(gè)提交分別解決合并沖突,而不是在一個(gè)龐大的合并提交中解決。它會讓你的每個(gè)提交保持專注,并獲得一個(gè)干凈的項(xiàng)目歷史。另一方面,你更容易發(fā)現(xiàn)bug是在哪引入的,如果有必要的話,用最小的代價(jià)回滾這些修改。

如果 Mary 和 John 開發(fā)的功能沒有關(guān)聯(lián),rebase的過程不太可能出現(xiàn)沖突。但如果出現(xiàn)沖突時(shí),Git 在當(dāng)前提交會暫停 rebase,輸出下面的信息,和一些相關(guān)的指令:

CONFLICT (content): Merge conflict in <some-file>

在這里插入圖片描述

Git 的優(yōu)點(diǎn)在于 每個(gè)人 都能解決他們自己的合并沖突。在這個(gè)例子中,Mary 只需運(yùn)行一下 git status 就可以發(fā)現(xiàn)問題是什么。沖突的文件會出現(xiàn)在未合并路徑中:

# Unmerged paths:
# (use "git reset HEAD <some-file>..." to unstage)
# (use "git add/rm <some-file>..." as appropriate to mark resolution)
#
# both modified: <some-file>

接下來,修改這些文件。如果她對結(jié)果滿意了,和往常一樣緩存這些文件,然后讓 git rebase 完成接下來的工作:

git add <some-file>
git rebase --continue

就是這樣。Git 會繼續(xù)檢查下個(gè)提交,對沖突的提交重復(fù)這個(gè)流程。

如果你這時(shí)候發(fā)現(xiàn)不知道自己做了什么,不要驚慌。只要運(yùn)行下面的命令,你就會回到開始之前的狀態(tài):

git rebase --abort

Mary 成功發(fā)布了她的分支

在這里插入圖片描述

在她和中央倉庫同步之后,Mary 可以成功地發(fā)布她的修改:

git push origin master

接下來該怎么做

正如你所見,使用一丟丟 Git 命令來復(fù)制一套傳統(tǒng)的 Subversion 開發(fā)環(huán)境也是可行的。這對于從 SVN 轉(zhuǎn)變而來的團(tuán)隊(duì)來說很棒,但這樣沒有利用到 Git 分布式的本質(zhì)。

如果你的團(tuán)隊(duì)已經(jīng)習(xí)慣了中心化的工作流,但希望提高協(xié)作效率,那么探索 Feature 分支工作流 的好處是完全值當(dāng)?shù)摹C總€(gè)功能在專門的獨(dú)立分支上進(jìn)行,在代碼并入官方項(xiàng)目之前就可以啟動圍繞新修改的深度討論。

Feature 分支的工作流

一旦你掌握了 中心化工作流的使用方法,在你的開發(fā)流程中添加功能分支是一個(gè)簡單的方式,來促進(jìn)協(xié)作和開發(fā)者之間的交流。這種封裝使得多個(gè)開發(fā)者專注自己的功能而不會打擾主代碼庫。它還保證 master 分支永遠(yuǎn)不會包含損壞的代碼,給持續(xù)集成環(huán)境帶來了是很大的好處。

封裝功能的開發(fā)使得 Pull Request 的使用成為可能,用來啟動圍繞一個(gè)分支的討論。它給了其他開發(fā)者在功能并入主項(xiàng)目之前參與決策的機(jī)會?;蛘?#xff0c;如果你開發(fā)功能時(shí)卡在一半,你可以發(fā)起一個(gè) Pull Request,向同事尋求建議。重點(diǎn)是,Pull Request 使得你的團(tuán)隊(duì)在評論其他人的工作時(shí)變得非常簡單。

如何工作

Feature 分支工作流同樣使用中央倉庫,master 同樣代表官方的項(xiàng)目歷史。但是,與其直接提交在本地的 master 分支,開發(fā)者每次進(jìn)行新的工作時(shí)創(chuàng)建一個(gè)新的分支。Feature 分支應(yīng)該包含描述性的名稱,比如 animated-menu-items(菜單項(xiàng)動畫)或 issue-#1061。每個(gè)分支都應(yīng)該有一個(gè)清晰、高度集中的目的。

Git 在技術(shù)上無法區(qū)別 master 和功能分支,所以開發(fā)者可以在 feature 分支上編輯、緩存、提交,就和中心化工作流中一樣。

此外,feature 分支可以(也應(yīng)該)被推送到中央倉庫。這使得你和其他開發(fā)者共享這個(gè)功能,而又不改變官方代碼。既然 master 只是一個(gè)“特殊”的分支,在中央倉庫中儲存多個(gè) feature 分支不會引出什么問題。當(dāng)然了,這也是備份每個(gè)開發(fā)者本地提交的好辦法。

Pull Request

除了隔離功能開發(fā)之外,分支使得通過 Pull Request 討論修改成為可能。一旦有人完成了一個(gè)功能,他們不會立即將它并入master。他們將 feature 分支推送到中央服務(wù)器上,發(fā)布一個(gè) Pull Request,請求將他們的修改并入 master。這給了其他開發(fā)者在修改并入主代碼庫之前審查的機(jī)會。

代碼審查是 Pull Request 的主要好處,但他們事實(shí)上被設(shè)計(jì)為成為討論代碼的一般場所。你可以把 Pull Request 看作是專注某個(gè)分支的討論版。也就是說他們可以用于開發(fā)流程之前。比如,一個(gè)開發(fā)者在某個(gè)功能上需要幫助,他只需發(fā)起一個(gè) Pull Request。感興趣的小伙伴會自動收到通知,看到相關(guān)提交中的問題。

一旦 Pull Request 被接受了,發(fā)布功能的行為和中心化的工作流是一樣的。首先,確定你本地的 master 和上游的 master 已經(jīng)同步。然后,將 feature分支并入 master,將更新的 master 推送回中央倉庫。

栗子

下面這🌰演示了代碼審查使用到的 Pull Request,但記住 Pull Request 有多種用途。

Mary 開始了一個(gè)新功能

在這里插入圖片描述

在她開始開發(fā)一個(gè)功能之前,Mary 需要一個(gè)獨(dú)立的分支。她可以用下面的命令創(chuàng)建新分支

git checkout -b marys-feature master

一個(gè)基于 master、名為 marys-feature 的分支將會被checkout,-b 標(biāo)記告訴Git在分支不存在時(shí)創(chuàng)建它。在這個(gè)分支上,Mary和往常一樣編輯、緩存、提交更改,用足夠多的提交來構(gòu)建這個(gè)功能:

git status
git add <some-file>
git commit

Mary 去吃飯了

在這里插入圖片描述

Mary 在早上給她的功能添加了一些提交。在她去吃午飯前,將她的分支推送到中央倉庫是個(gè)不錯(cuò)的想法。這是一種方便的備份,但如果Mary和其他開發(fā)者一起協(xié)作,他們也可以看到她的初始提交了。

git push -u origin marys-feature

這個(gè)命令將 marys-feature 推送到中央倉庫(origin),-u 標(biāo)記將它添加為遠(yuǎn)程跟蹤的分支。在設(shè)置完跟蹤的分支之后,Mary 調(diào)用不帶任何參數(shù)的 git push 來推送她的功能。

Mary 完成了她的工作

在這里插入圖片描述

當(dāng) Mary 吃完午飯回來,她完成了她的功能。在 并入 master 之前,她需要發(fā)布一個(gè) Pull Request,讓其他的團(tuán)隊(duì)成員知道她所做的工作。但首先,她應(yīng)該保證中央倉庫包含了她最新的提交:

git push

然后,她在她的 Git 界面上發(fā)起了一個(gè) Pull Request,請求將 marys-feature 合并進(jìn) master,團(tuán)隊(duì)成員會收到自動的通知。Pull Request 的好處是,評論顯示在相關(guān)的提交正下方,方便討論特定的修改。

Bill 收到了 Pull Request

在這里插入圖片描述

Bill 收到了 Pull Request,并且查看了 marys-feature。他決定在并入官方項(xiàng)目之前做一些小修改,通過 Pull Request 和 Mary 進(jìn)行了溝通。

Mary 作了修改

在這里插入圖片描述

為了做這些更改,Mary 重復(fù)了之前創(chuàng)建功能時(shí)相同的流程,她編輯、緩存、提交、將更新推送到中央倉庫。她所有的活動顯示在 Pull Request 中,Bill 可以一直評論。

如果 Bill 想要的話,也可以將 marys-feature 分支 pull 到他自己的本地倉庫,繼續(xù)工作。后續(xù)的任何提交都會顯示在 Pull Request 上。

Mary 發(fā)布了她的功能

在這里插入圖片描述

一旦 Bill 準(zhǔn)備接受這個(gè) Pull Request,某個(gè)人(Bill 或者 Mary 都可)需要將功能并入穩(wěn)定的項(xiàng)目:

git checkout master
git pull
git pull origin marys-feature
git push

首先,不管是誰在執(zhí)行合并,都要保證他們的 master 分支是最新的。然后,運(yùn)行 git pull origin marys-feature 合并中央倉庫的 marys-feature 副本。你也可以使用簡單的 git merge marys-feature,但之前的命令保證你拉取下來的一定是功能分支最新的版本。最后,更新的 master 需要被推送回 origin。

這個(gè)過程導(dǎo)致了一個(gè)合并提交。一些開發(fā)者喜歡它,因?yàn)樗枪δ芎推溆啻a合并的標(biāo)志。但,如果你希望得到線性的歷史,你可以在執(zhí)行 merge 之前將功能 rebase 到 master 分支的頂端,產(chǎn)生一個(gè)快速向前的合并。

一些界面會自動化接受 Pull Request 的流程,只需點(diǎn)擊一下「Merge Pull Request」。如果你的沒有的話,它至少在合并之后應(yīng)該可以自動地關(guān)閉 Pull Request。

同時(shí),John 以同樣的方式工作著

Mary 和 Bill 一起開發(fā) marys-feature,在 Pull Request 上討論的同時(shí),John 還在開發(fā)他自己的 feature分支。通過將功能用不同分支隔離開來,每個(gè)人可以獨(dú)立地工作,但很容易和其他開發(fā)者共享修改。

接下來該怎么做

為了徹底了解 GitHub 上的功能分支,你應(yīng)該查看使用分支一章?,F(xiàn)在,你應(yīng)該已經(jīng)看到了功能分支極大地增強(qiáng)了中心化工作流中單一 master 分支的作用。除此之外,功能分支還便利了 Pull Request 的使用,在版本控制界面上直接討論特定的提交。GitFlow 工作流是管理功能開發(fā)、發(fā)布準(zhǔn)備、維護(hù)的常見模式。

GitFlow 工作流

下面的 [GitFlow 工作流]源于 nvie 網(wǎng)站上的作者 Vincent Driessen。

GitFlow 工作流圍繞項(xiàng)目發(fā)布定義了一個(gè)嚴(yán)格的分支模型。有些地方比[功能分支工作流]更復(fù)雜,為管理大型項(xiàng)目提供了魯棒的框架。

和功能分支工作流相比,這種工作流沒有增加任何新的概念或命令。它給不同的分支指定了特定的角色,定義它們應(yīng)該如何、什么時(shí)候交流。除了功能分支之外,它還為準(zhǔn)備發(fā)布、維護(hù)發(fā)布、記錄發(fā)布分別使用了單獨(dú)的分支。當(dāng)然,你還能享受到功能分支工作流帶來的所有好處:Pull Request、隔離實(shí)驗(yàn)和更高效的協(xié)作。

如何工作

GitFlow 工作流仍然使用中央倉庫作為開發(fā)者溝通的中心。和[其他工作流]一樣,開發(fā)者在本地工作,將分支推送到中央倉庫。唯一的區(qū)別在于項(xiàng)目的分支結(jié)構(gòu)。

歷史分支

和單獨(dú)的 master 分支不同,這種工作流使用兩個(gè)分支來記錄項(xiàng)目歷史。master 分支儲存官方發(fā)布?xì)v史,develop 分支用來整合功能分支。同時(shí),這還方便了在 master 分支上給所有提交打上版本號標(biāo)簽。

在這里插入圖片描述

工作流剩下的部分圍繞這兩個(gè)分支的差別展開。

功能分支

每個(gè)新功能都放置在自己的分支中,可以[在備份/協(xié)作時(shí)推送到中央倉庫]。但是,與其合并到 master,功能分支將開發(fā)分支作為父分支。當(dāng)一個(gè)功能完成時(shí),它將被[合并回 develop。功能永遠(yuǎn)不應(yīng)該直接在 master 上交互。

在這里插入圖片描述

注意,功能分支加上 develop 分支就是我們之前所說的功能分支工作流。但是,GitFlow 工作流不止于此。

發(fā)布分支

在這里插入圖片描述

一旦 develop分支的新功能足夠發(fā)布(或者預(yù)先確定的發(fā)布日期即將到來),你可以從 develop 分支 fork 一個(gè)發(fā)布分支。這個(gè)分支的創(chuàng)建開始了下個(gè)發(fā)布周期,只有和發(fā)布相關(guān)的任務(wù)應(yīng)該在這個(gè)分支進(jìn)行,如修復(fù) bug、生成文檔等。一旦準(zhǔn)備好了發(fā)布,發(fā)布分支將合并進(jìn) master,打上版本號的標(biāo)簽。另外,它也應(yīng)該合并回 develop,后者可能在發(fā)布啟動之后有了新的進(jìn)展。

使用一個(gè)專門的分支來準(zhǔn)備發(fā)布確保一個(gè)團(tuán)隊(duì)完善當(dāng)前的發(fā)布,其他團(tuán)隊(duì)可以繼續(xù)開發(fā)下一個(gè)發(fā)布的功能。它還建立了清晰的開發(fā)階段(比如說,「這周我們準(zhǔn)備 4.0 版本的發(fā)布」,而我們在倉庫的結(jié)構(gòu)中也能看到這個(gè)階段)。

通常我們約定:

  • develop 創(chuàng)建分支
  • 合并進(jìn) master 分支
  • 命名規(guī)范 release-* or release/*

維護(hù)分支

在這里插入圖片描述

維護(hù)或者「緊急修復(fù)」分支用來快速給產(chǎn)品的發(fā)布打上補(bǔ)丁。這是唯一可以從 master 上 fork 的分支。一旦修復(fù)完成了,它應(yīng)該被并入 masterdevelop 分支(或者當(dāng)前的發(fā)布分支),master 應(yīng)該打上更新的版本號的標(biāo)簽。

有一個(gè)專門的 bug 修復(fù)開發(fā)線使得你的團(tuán)隊(duì)能夠處理 issues,而不打斷其他工作流或是要等到下一個(gè)發(fā)布周期。你可以將維護(hù)分支看作在 master 分支上工作的臨時(shí)發(fā)布分支。

栗子

下面的栗子演示了這種工作流如何用來管理發(fā)布周期。假設(shè)你已經(jīng)創(chuàng)建了中央倉庫。

創(chuàng)建一個(gè)開發(fā)分支

你要做的第一步是為默認(rèn)的 master 分支創(chuàng)建一個(gè)互補(bǔ)的 develop 分支。最簡單的辦法是[在本地創(chuàng)建一個(gè)空的 develop 分支],將它推送到服務(wù)器上:

git branch develop
git push -u origin develop

這個(gè)分支將會包含項(xiàng)目中所有的歷史,而 master 將包含不完全的版本。其他開發(fā)者應(yīng)該[將中央倉庫克隆到本地],創(chuàng)建一個(gè)分支來追蹤 develop 分支:

git clone ssh://user@host/path/to/repo.git
git checkout -b develop origin/develop

現(xiàn)在所有人都有了一份歷史分支的本地副本。

Mary 和 John 開始了新功能

我們的栗子從 John 和 Mary 在不同分支上工作開始。他們都要為自己的功能創(chuàng)建單獨(dú)的分支。[他們的功能分支都應(yīng)該基于develop],而不是 master

git checkout -b some-feature develop

他們都使用「編輯、緩存、提交」的一般約定來向功能分支添加提交:

git status
git add <some-file>
git commit

Mary 完成了她的功能

在添加了一些提交之后,Mary 確信她的功能以及準(zhǔn)備好了。如果她的團(tuán)隊(duì)使用 Pull Request,現(xiàn)在正是發(fā)起 Pull Request 的好時(shí)候,請求將她的功能并入 develop 分支。否則,她可以向下面一樣,將它并入本地的 develop 分支,推送到中央倉庫:

git pull origin develop
git checkout develop
git merge some-feature
git push
git branch -d some-feature

第一個(gè)命令在嘗試并入功能分支之前確保 develop 分支已是最新。注意,功能絕不該被直接并入 master。沖突的處理方式和[中心化工作流]相同。

Mary 開始準(zhǔn)備發(fā)布

當(dāng) John 仍然在他的功能分支上工作時(shí),Mary s開始準(zhǔn)備項(xiàng)目的第一個(gè)官方發(fā)布。和開發(fā)功能一樣,她新建了一個(gè)分支來封裝發(fā)布的準(zhǔn)備工作。這也正是發(fā)布的版本號創(chuàng)建的一步:

git checkout -b release-0.1 develop

這個(gè)分支用來整理提交,充分測試,更新文檔,為即將到來的發(fā)布做各種準(zhǔn)備。它就像是一個(gè)專門用來完善發(fā)布的功能分支。

一旦 Mary 創(chuàng)建了這個(gè)分支,推送到中央倉庫,這次發(fā)布的功能便被鎖定了。不在 develop 分支中的功能將被推遲到下個(gè)發(fā)布周期。

Mary完成了她的發(fā)布

一旦發(fā)布準(zhǔn)備穩(wěn)妥,Mary 將它并入 masterdevelop,然后刪除發(fā)布分支。合并回 develop 很重要,因?yàn)榭赡芤呀?jīng)有關(guān)鍵的更新添加到了發(fā)布分支上,而開發(fā)新功能需要用到它們。同樣的,如果 Mary 的團(tuán)隊(duì)重視代碼審查,現(xiàn)在將是發(fā)起 Pull Request 的完美時(shí)機(jī)。

git checkout master
git merge release-0.1
git push
git checkout develop
git merge release-0.1
git push
git branch -d release-0.1

發(fā)布分支是功能開發(fā)(develop)和公開發(fā)布(master)之間的過渡階段。不論什么時(shí)候?qū)⑻峤徊⑷?master 時(shí),你應(yīng)該為提交打上方便引用的標(biāo)簽:

git tag -a 0.1 -m "Initial public release" master
git push --tags

Git 提供了許多鉤子,即倉庫中特定事件發(fā)生時(shí)被執(zhí)行的腳本。當(dāng)你向中央倉庫推送 master 分支或者標(biāo)簽時(shí),你可以配置一個(gè)鉤子來自動化構(gòu)建公開發(fā)布。

終端用戶發(fā)現(xiàn)了一個(gè) bug

正式發(fā)布之后,Mary 回過頭來和 John 一起為下一個(gè)發(fā)布開發(fā)功能。這時(shí),一個(gè)終端用戶開了一個(gè) issue 抱怨說當(dāng)前發(fā)布中存在一個(gè) bug。為了解決這個(gè) bug,Mary(或 John)從 master 創(chuàng)建了一個(gè)維護(hù)分支,用幾個(gè)提交修復(fù)這個(gè) issue,然后直接合并回 master。

git checkout -b issue-#001 master
# Fix the bug
git checkout master
git merge issue-#001
git push

和發(fā)布分支一樣,維護(hù)分支包含了 develop 中需要的重要更新,因此 Mary 同樣需要執(zhí)行這個(gè)合并。接下來,她可以[刪除這個(gè)分支]了:

git checkout develop
git merge issue-#001
git push
git branch -d issue-#001

接下來該怎么做

現(xiàn)在,希望你已經(jīng)很熟悉[中心化的工作流]、[功能分支工作流]和 GitFlow 工作流。你應(yīng)該已經(jīng)可以抓住本地倉庫、推送/拉取模式,和 Git 魯棒的分支和合并模型的無限潛力。

請記住,教程中呈現(xiàn)的工作流只是可行的實(shí)踐——而非工作中使用 Git 的金科玉律。因此,盡情地取其精華,去其糟粕吧。不變的是要讓 Git 為你所用,而不是相反。

Fork 工作流

Fork 工作流和教程中討論的其它工作流截然不同。與其使用唯一的服務(wù)端倉庫作為「中央」代碼庫,它給予 每個(gè) 開發(fā)者一個(gè)服務(wù)端倉庫。也就是說每個(gè)貢獻(xiàn)者都有兩個(gè) Git 倉庫,而不是一個(gè):一個(gè)私有的本地倉庫和一個(gè)公開的服務(wù)端倉庫。

Fork 工作流的主要優(yōu)點(diǎn)在于貢獻(xiàn)可以輕易地整合進(jìn)項(xiàng)目,而不需要每個(gè)人都推送到單一的中央倉庫。開發(fā)者推送到他們 自己的 服務(wù)端倉庫,只有項(xiàng)目管理者可以推送到官方倉庫。這使得管理者可以接受任何開發(fā)者的提交,卻不需要給他們中央倉庫的權(quán)限。

結(jié)論是,這種分布式的工作流為大型、組織性強(qiáng)的團(tuán)隊(duì)(包括不可信的第三方)提供了安全的協(xié)作方式。它同時(shí)也是開源項(xiàng)目理想的工作流。

如何工作

和其它 Git 工作流一樣,Fork 工作流以一個(gè)儲存在服務(wù)端的官方公開項(xiàng)目開場。但新的開發(fā)者想?yún)⑴c項(xiàng)目時(shí),他們不直接克隆官方項(xiàng)目。

取而代之地,他們 fork 一份官方項(xiàng)目,在服務(wù)端創(chuàng)建一份副本。這份新建的副本作為他們私有的公開倉庫——沒有其他開發(fā)者可以在上面推送,但他們可以從上面拉取修改(在后面我們會討論為什么這一點(diǎn)很重要)。在他們創(chuàng)建了服務(wù)端副本之后,開發(fā)者執(zhí)行 git clone 操作,在他們的本地機(jī)器上復(fù)制一份。這是他們私有的開發(fā)環(huán)境,正如其他工作流中一樣。

當(dāng)他們準(zhǔn)備好發(fā)布本地提交時(shí),他們將提交推送到自己的公開倉庫——而非官方倉庫。然后,他們向主倉庫發(fā)起一個(gè) Pull Request,讓項(xiàng)目維護(hù)者知道一個(gè)更新做好了合并的準(zhǔn)備。如果貢獻(xiàn)的代碼有什么問題的話,Pull Request 可以作為一個(gè)方便的討論版。

我為了將功能并入官方代碼庫,維護(hù)者將貢獻(xiàn)者的修改拉取到他們的本地倉庫,確保修改不會破壞項(xiàng)目,將它[合并到本地的 master 分支,然后將 master 分支[推送]到服務(wù)端的官方倉庫。貢獻(xiàn)現(xiàn)在已是項(xiàng)目的一部分,其他開發(fā)者應(yīng)該從官方倉庫拉取并同步他們的本地倉庫。

中央倉庫

「官方」倉庫這個(gè)概念在 Fork 工作流中只是一個(gè)約定,理解這一點(diǎn)很重要。從技術(shù)的角度,Git 并看不出每個(gè)開發(fā)者和官方的公開倉庫有什么區(qū)別。事實(shí)上,官方倉庫唯一官方的原因是,它是項(xiàng)目維護(hù)者的倉庫。

Fork 工作流中的分支

所有這些個(gè)人的公開倉庫只是一個(gè)在開發(fā)者之間共享分支的約定。每個(gè)人仍然可以使用分支來隔離功能,就像在[功能分支工作流] 和 [GitFlow 工作流中]一樣。唯一的區(qū)別在于這些分支是如何開始的。在 Fork 工作流中,它們從另一個(gè)開發(fā)者的本地倉庫拉取而來,而在功能分支和 GitFlow 分支它們被推送到官方倉庫。

栗子

項(xiàng)目維護(hù)者初始化了中央倉庫

和任何基于 Git 的項(xiàng)目一樣,第一步是在服務(wù)端創(chuàng)建一個(gè)可以被所有項(xiàng)目成員訪問到的官方倉庫。一般來說,這個(gè)倉庫同時(shí)還是項(xiàng)目維護(hù)者的公開倉庫。

[公開的倉庫應(yīng)該永遠(yuǎn)是裸的],不管它們是否代表官方代碼庫。所以項(xiàng)目維護(hù)者應(yīng)該運(yùn)行下面這樣的命令來設(shè)置官方倉庫:

ssh user@host
git init --bare /path/to/repo.git

GitHub 同時(shí)提供了一個(gè)圖形化界面來替代上面的操作。這和教程中其它工作流設(shè)置中央倉庫的流程完全一致。如果有必要的話,項(xiàng)目維護(hù)者應(yīng)該將已有的代碼庫推送到這個(gè)倉庫中。

開發(fā)者 fork 倉庫

在這里插入圖片描述

接下來,所有開發(fā)者需要 fork 官方倉庫。你可以用 SSH 到服務(wù)器,運(yùn)行 git clone 將它復(fù)制到服務(wù)器的另一個(gè)地址—— fork 其實(shí)只是服務(wù)端的 clone。但同樣地,GitHub上開發(fā)者只需點(diǎn)一點(diǎn)按鈕就可以 fork 倉庫。

在這步之后,每個(gè)開發(fā)者應(yīng)該都有了自己的服務(wù)端倉庫。像官方倉庫一樣,所有這些倉庫都應(yīng)該是裸倉庫。

開發(fā)者將 fork 的倉庫克隆到本地

接下來開發(fā)者需要克隆他們自己的公開倉庫。他們可以用熟悉的 git clone 命令來完成這一步。

我們的栗子假設(shè)使用他們使用 GitHub 來托管倉庫。記住,在這種情況下,每個(gè)開發(fā)者應(yīng)該有他們自己的 GitHub 賬號,應(yīng)該用下面的命令克隆服務(wù)端倉庫:

git clone https://user@github.com/user/repo.git

而教程中的其他工作流使用單一的 origin 遠(yuǎn)程連接,指向中央倉庫,Fork 工作流需要兩個(gè)遠(yuǎn)程連接,一個(gè)是中央倉庫,另一個(gè)是開發(fā)者個(gè)人的服務(wù)端倉庫。你可以給這些遠(yuǎn)端取任何名字,約定的做法是將 origin 作為你 fork 后的倉庫的遠(yuǎn)端(運(yùn)行 git clone 是會自動創(chuàng)建)和 upstream 作為官方項(xiàng)目。

git remote add upstream https://github.com/maintainer/repo

你需要使用上面的命令來創(chuàng)建上游倉庫的遠(yuǎn)程連接。它使得你輕易地保持本地倉庫和官方倉庫的進(jìn)展同步。注意如果你的上游倉庫開啟了認(rèn)證(比如它沒有開源),你需要提供一個(gè)用戶名,就像這樣:

git remote add upstream https://user@bitbucket.org/maintainer/repo.git

它需要用戶從官方代碼庫克隆或拉取之前提供有效的密碼。

開發(fā)者進(jìn)行自己的開發(fā)

在他們剛克隆的本地倉庫中,開發(fā)者可以編輯代碼、[提交更改],和其它分支中一樣[創(chuàng)建分支]:

git checkout -b some-feature
# 編輯代碼
git commit -a -m "Add first draft of some feature"

他們所有的更改在推送到公開倉庫之前都是完全私有的。而且,如果官方項(xiàng)目已經(jīng)向前進(jìn)展了,他們可以用 git pull 獲取新的提交:

git pull upstream master

因?yàn)殚_發(fā)者應(yīng)該在專門的功能分支開發(fā),這一般會[產(chǎn)生一個(gè)快速向前的合并]

開發(fā)者發(fā)布他們的功能

在這里插入圖片描述

一旦開發(fā)者準(zhǔn)備好共享他們的新功能,他們需要做兩件事情。第一,他們必須將貢獻(xiàn)的代碼推送到自己的公開倉庫,讓其他開發(fā)者能夠訪問到。他們的 origin 遠(yuǎn)端應(yīng)該已經(jīng)設(shè)置好了,所以他們只需要:

git push origin feature-branch

這和其他工作流不同之處在于,origin 遠(yuǎn)端指向開發(fā)者個(gè)人的服務(wù)端倉庫,而不是主代碼庫。

第二,他們需要通知項(xiàng)目維護(hù)者,他們想要將功能并入官方代碼庫。GitHub 提供了一個(gè)「New Pull Request」按鈕,跳轉(zhuǎn)到一個(gè)網(wǎng)頁,讓你指明想要并入主倉庫的分支。一般來說,你希望將功能分支并入上游遠(yuǎn)端的 master 分支。

項(xiàng)目維護(hù)者整合他們的功能

當(dāng)項(xiàng)目維護(hù)者收到 Pull Request 時(shí),他們的工作是決定是否將它并入官方的代碼庫。他們可以使用下面兩種方式之一:

  1. 直接檢查 Pull Request 中檢查代碼
  2. 將代碼拉取到本地倉庫然后手動合并

第一個(gè)選項(xiàng)更簡單,讓維護(hù)者查看修改前后的差異,在上面評論,然后通過圖形界面執(zhí)行合并。然而,如果 Pull Request 會導(dǎo)致合并沖突,第二個(gè)選項(xiàng)就有了必要。在這個(gè)情況中,維護(hù)者需要從開發(fā)者的服務(wù)端倉庫 fetch 功能分支,合并到他們本地的 master 分支,然后解決沖突:

git fetch https://bitbucket.org/user/repo feature-branch
# 檢查修改
git checkout master
git merge FETCH_HEAD

一旦修改被整合進(jìn)本地的 master,維護(hù)者需要將它推送到服務(wù)器上的官方倉庫,這樣其他開發(fā)者也可以訪問它:

git push origin master

記住,維護(hù)者的 origin 指向他們的公開倉庫,也就是項(xiàng)目的官方代碼庫。開發(fā)者的貢獻(xiàn)現(xiàn)在完全并入了項(xiàng)目。

開發(fā)者和中央倉庫保持同步

因?yàn)橹鞔a庫已經(jīng)取得了新的進(jìn)展,其他開發(fā)者應(yīng)該和官方倉庫同步:

git pull upstream master

接下來該怎么做

如果你從 SVN 遷移而來,Fork 工作流看上去是一個(gè)比較大的轉(zhuǎn)變。但不要害怕——它只是在 Feature 分支工作流之上引入了一層抽象。貢獻(xiàn)的代碼發(fā)布到開發(fā)者在服務(wù)端自己的倉庫,而不是在唯一的中央倉庫中直接共享分支。

這篇文章解釋了一次代碼貢獻(xiàn)是如何從一個(gè)開發(fā)者流入官方的 master 分支的,但相同的方法可以用在將代碼貢獻(xiàn)整合進(jìn)任何倉庫。比如,如果你團(tuán)隊(duì)的一部分成員在一個(gè)特定功能上協(xié)作,他們可以用自己約定的行為共享修改——而不改變主倉庫。

這使得 Fork 工作流對于松散的團(tuán)隊(duì)來說是個(gè)非常強(qiáng)大的工具。任何開發(fā)者都可以輕而易舉地和其他開發(fā)者共享修改,任何分支都能高效地并入主代碼庫。


3. Git圖解

此頁圖解 git 中的最常用命令。如果你稍微理解 git 的工作原理,這篇文章能夠讓你理解的更透徹。

基本用法

在這里插入圖片描述

上面的四條命令在工作目錄、stage 緩存(也叫做索引)和 commit 歷史之間復(fù)制文件。

  • git add files 把工作目錄中的文件加入 stage 緩存
  • git commit 把 stage 緩存生成一次 commit,并加入 commit 歷史
  • git reset -- files 撤銷最后一次 git add files,你也可以用 git reset 撤銷所有 stage 緩存文件
  • git checkout -- files 把文件從 stage 緩存復(fù)制到工作目錄,用來丟棄本地修改

你可以用 git reset -pgit checkout -pgit add -p 進(jìn)入交互模式,也可以跳過 stage 緩存直接從 commit歷史取出文件或者直接提交代碼。

在這里插入圖片描述

  • git commit -a 相當(dāng)于運(yùn)行 git add 把所有當(dāng)前目錄下的文件加入 stage 緩存再運(yùn)行 git commit。
  • git commit files 進(jìn)行一次包含最后一次提交加上工作目錄中文件快照的提交,并且文件被添加到 stage 緩存。
  • git checkout HEAD -- files 回滾到復(fù)制最后一次提交。

約定

后文中以下面的形式使用圖片:

在這里插入圖片描述

綠色的5位字符表示提交的 ID,分別指向父節(jié)點(diǎn)。分支用橙色顯示,分別指向特定的提交。當(dāng)前分支由附在其上的 _HEAD_ 標(biāo)識。

這張圖片里顯示最后 5 次提交,_ed489_ 是最新提交。 _master_ 分支指向此次提交,另一個(gè) _maint_ 分支指向祖父提交節(jié)點(diǎn)。

命令詳解

Diff

有許多種方法查看兩次提交之間的變動,下面是其中一些例子。

在這里插入圖片描述

Commit

提交時(shí),Git 用 stage 緩存中的文件創(chuàng)建一個(gè)新的提交,并把此時(shí)的節(jié)點(diǎn)設(shè)為父節(jié)點(diǎn)。然后把當(dāng)前分支指向新的提交節(jié)點(diǎn)。下圖中,當(dāng)前分支是 _master_。

在運(yùn)行命令之前,_master_ 指向 _ed489_,提交后,_master_ 指向新的節(jié)點(diǎn)_f0cec_ 并以 _ed489_ 作為父節(jié)點(diǎn)。

即便當(dāng)前分支是某次提交的祖父節(jié)點(diǎn),Git 會同樣操作。下圖中,在 _master_ 分支的祖父節(jié)點(diǎn) _maint_ 分支進(jìn)行一次提交,生成了 _1800b_。

這樣,_maint_ 分支就不再是 _master_ 分支的祖父節(jié)點(diǎn)。此時(shí),[merge] 或者 [rebase] 是必須的。

如果想更改一次提交,使用 git commit --amend。Git 會使用與當(dāng)前提交相同的父節(jié)點(diǎn)進(jìn)行一次新提交,舊的提交會被取消。

在這里插入圖片描述

另一個(gè)例子是[分離HEAD提交],在后面的章節(jié)中介紹。

Checkout

git checkout 命令用于從歷史提交(或者 stage 緩存)中拷貝文件到工作目錄,也可用于切換分支。

當(dāng)給定某個(gè)文件名(或者打開 -p 選項(xiàng),或者文件名和-p選項(xiàng)同時(shí)打開)時(shí),Git 會從指定的提交中拷貝文件到 stage 緩存和工作目錄。比如,git checkout HEAD~ foo.c 會將提交節(jié)點(diǎn) _HEAD~_(即當(dāng)前提交節(jié)點(diǎn)的父節(jié)點(diǎn))中的 foo.c 復(fù)制到工作目錄并且加到 stage 緩存中。如果命令中沒有指定提交節(jié)點(diǎn),則會從 stage 緩存中拷貝內(nèi)容。注意當(dāng)前分支不會發(fā)生變化。

在這里插入圖片描述

當(dāng)不指定文件名,而是給出一個(gè)(本地)分支時(shí),那么 _HEAD_ 標(biāo)識會移動到那個(gè)分支(也就是說,我們「切換」到那個(gè)分支了),然后 stage 緩存和工作目錄中的內(nèi)容會和 _HEAD_ 對應(yīng)的提交節(jié)點(diǎn)一致。新提交節(jié)點(diǎn)(下圖中的 a47c3)中的所有文件都會被復(fù)制(到 stage 緩存和工作目錄中);只存在于老的提交節(jié)點(diǎn)(ed489)中的文件會被刪除;不屬于上述兩者的文件會被忽略,不受影響。

在這里插入圖片描述

如果既沒有指定文件名,也沒有指定分支名,而是一個(gè)標(biāo)簽、遠(yuǎn)程分支、SHA-1 值或者是像 _master~3_ 類似的東西,就得到一個(gè)匿名分支,稱作 _detached HEAD_(被分離的 _HEAD_ 標(biāo)識)。這樣可以很方便地在歷史版本之間互相切換。比如說你想要編譯 1.6.6.1 版本的 Git,你可以運(yùn)行 git checkout v1.6.6.1(這是一個(gè)標(biāo)簽,而非分支名),編譯,安裝,然后切換回另一個(gè)分支,比如說 git checkout master。然而,當(dāng)提交操作涉及到「分離的 HEAD」時(shí),其行為會略有不同,詳情見在下面。

在這里插入圖片描述

HEAD 標(biāo)識處于分離狀態(tài)時(shí)的提交操作

當(dāng) _HEAD_ 處于分離狀態(tài)(不依附于任一分支)時(shí),提交操作可以正常進(jìn)行,但是不會更新任何已命名的分支。你可以認(rèn)為這是在更新一個(gè)匿名分支。

在這里插入圖片描述

一旦此后你切換到別的分支,比如說 _master_,那么這個(gè)提交節(jié)點(diǎn)(可能)再也不會被引用到,然后就會被丟棄掉了。注意這個(gè)命令之后就不會有東西引用 _2eecb_。

在這里插入圖片描述

但是,如果你想保存這個(gè)狀態(tài),可以用命令 git checkout -b name 來創(chuàng)建一個(gè)新的分支。

在這里插入圖片描述

Reset

git reset 命令把當(dāng)前分支指向另一個(gè)位置,并且有選擇的變動工作目錄和索引。也用來在從歷史commit歷史中復(fù)制文件到索引,而不動工作目錄。

如果不給選項(xiàng),那么當(dāng)前分支指向到那個(gè)提交。如果用 --hard 選項(xiàng),那么工作目錄也更新,如果用 --soft 選項(xiàng),那么都不變。

在這里插入圖片描述

如果沒有給出提交點(diǎn)的版本號,那么默認(rèn)用 _HEAD_。這樣,分支指向不變,但是索引會回滾到最后一次提交,如果用 --hard 選項(xiàng),工作目錄也同樣。

在這里插入圖片描述

如果給了文件名(或者 -p 選項(xiàng)), 那么工作效果和帶文件名的checkout差不多,除了索引被更新。

在這里插入圖片描述

Merge

git merge 命令把不同分支合并起來。合并前,索引必須和當(dāng)前提交相同。如果另一個(gè)分支是當(dāng)前提交的祖父節(jié)點(diǎn),那么合并命令將什么也不做。

另一種情況是如果當(dāng)前提交是另一個(gè)分支的祖父節(jié)點(diǎn),就導(dǎo)致 _fast-forward_ 合并。指向只是簡單的移動,并生成一個(gè)新的提交。

在這里插入圖片描述

否則就是一次真正的合并。默認(rèn)把當(dāng)前提交(_ed489_ 如下所示)和另一個(gè)提交(_33104_)以及他們的共同祖父節(jié)點(diǎn)(_b325c_)進(jìn)行一次三方合并。結(jié)果是先保存當(dāng)前目錄和索引,然后和父節(jié)點(diǎn) _33104_ 一起做一次新提交。

在這里插入圖片描述

Cherry Pick

git cherry-pick 命令「復(fù)制」一個(gè)提交節(jié)點(diǎn)并在當(dāng)前分支做一次完全一樣的新提交。

在這里插入圖片描述

Rebase

git rebase 是合并命令的另一種選擇。合并把兩個(gè)父分支合并進(jìn)行一次提交,提交歷史不是線性的。rebase 在當(dāng)前分支上重演另一個(gè)分支的歷史,提交歷史是線性的。

本質(zhì)上,這是線性化的自動的 cherry-pick。

在這里插入圖片描述

上面的命令都在 _topic_ 分支中進(jìn)行,而不是 _master_ 分支,在 _master_ 分支上重演,并且把分支指向新的節(jié)點(diǎn)。注意舊提交沒有被引用,將被回收。

要限制回滾范圍,使用 --onto 選項(xiàng)。下面的命令在 _master_ 分支上重演當(dāng)前分支從 _169a6_ 以來的最近幾個(gè)提交,即 _2c33a_。

在這里插入圖片描述

同樣有 git rebase --interactive 讓你更方便的完成一些復(fù)雜操作,比如丟棄、重排、修改、合并提交。


4.1 代碼合并:Merge、Rebase 的選擇

git rebase 這個(gè)命令經(jīng)常被人認(rèn)為是一種 Git 巫術(shù),初學(xué)者應(yīng)該避而遠(yuǎn)之。但如果使用得當(dāng)?shù)脑?#xff0c;它能給你的團(tuán)隊(duì)開發(fā)省去太多煩惱。在這篇文章中,我們會比較 git rebase 和類似的 git merge 命令,找到 Git 工作流中 rebase 的所有用法。

概述

你要知道的第一件事是,git rebasegit merge 做的事其實(shí)是一樣的。它們都被設(shè)計(jì)來將一個(gè)分支的更改并入另一個(gè)分支,只不過方式有些不同。

想象一下,你剛創(chuàng)建了一個(gè)專門的分支開發(fā)新功能,然后團(tuán)隊(duì)中另一個(gè)成員在 master 分支上添加了新的提交。這就會造成提交歷史被 fork 一份,用 Git 來協(xié)作的開發(fā)者應(yīng)該都很清楚。

在這里插入圖片描述

現(xiàn)在,如果 master 中新的提交和你的工作是相關(guān)的。為了將新的提交并入你的分支,你有兩個(gè)選擇:merge 或 rebase。

Merge

將 master 分支合并到 feature 分支最簡單的辦法就是用下面這些命令:

git checkout feature
git merge master

或者,你也可以把它們壓縮在一行里。

git merge master feature

feature 分支中新的合并提交(merge commit)將兩個(gè)分支的歷史連在了一起。你會得到下面這樣的分支結(jié)構(gòu):

在這里插入圖片描述

Merge 好在它是一個(gè)安全的操作。現(xiàn)有的分支不會被更改,避免了 rebase 潛在的缺點(diǎn)(后面會說)。

另一方面,這同樣意味著每次合并上游更改時(shí) feature 分支都會引入一個(gè)外來的合并提交。如果 master 非常活躍的話,這或多或少會污染你的分支歷史。雖然高級的 git log 選項(xiàng)可以減輕這個(gè)問題,但對于開發(fā)者來說,還是會增加理解項(xiàng)目歷史的難度。

Rebase

作為 merge 的替代選擇,你可以像下面這樣將 feature 分支并入 master 分支:

git checkout feature
git rebase master

它會把整個(gè) feature 分支移動到 master 分支的后面,有效地把所有 master 分支上新的提交并入過來。但是,rebase 為原分支上每一個(gè)提交創(chuàng)建一個(gè)新的提交,重寫了項(xiàng)目歷史,并且不會帶來合并提交。

在這里插入圖片描述

rebase最大的好處是你的項(xiàng)目歷史會非常整潔。首先,它不像 git merge 那樣引入不必要的合并提交。其次,如上圖所示,rebase 導(dǎo)致最后的項(xiàng)目歷史呈現(xiàn)出完美的線性——你可以從項(xiàng)目終點(diǎn)到起點(diǎn)瀏覽而不需要任何的 fork。這讓你更容易使用 git log、git bisectgitk 來查看項(xiàng)目歷史。

不過,這種簡單的提交歷史會帶來兩個(gè)后果:安全性和可跟蹤性。如果你違反了 rebase 黃金法則,重寫項(xiàng)目歷史可能會給你的協(xié)作工作流帶來災(zāi)難性的影響。此外,rebase 不會有合并提交中附帶的信息——你看不到 feature 分支中并入了上游的哪些更改。

交互式的 rebase

交互式的 rebase 允許你更改并入新分支的提交。這比自動的 rebase 更加強(qiáng)大,因?yàn)樗峁┝藢Ψ种咸峤粴v史完整的控制。一般來說,這被用于將 feature 分支并入 master 分支之前,清理混亂的歷史。

-i 傳入 git rebase 選項(xiàng)來開始一個(gè)交互式的rebase過程:

git checkout feature
git rebase -i master

它會打開一個(gè)文本編輯器,顯示所有將被移動的提交:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

這個(gè)列表定義了 rebase 將被執(zhí)行后分支會是什么樣的。更改 pick 命令或者重新排序,這個(gè)分支的歷史就能如你所愿了。比如說,如果第二個(gè)提交修復(fù)了第一個(gè)提交中的小問題,你可以用 fixup 命令把它們合到一個(gè)提交中:

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

保存后關(guān)閉文件,Git 會根據(jù)你的指令來執(zhí)行 rebase,項(xiàng)目歷史看上去會是這樣:

在這里插入圖片描述

忽略不重要的提交會讓你的 feature 分支的歷史更清晰易讀。這是 git merge 做不到的。

Rebase 的黃金法則

當(dāng)你理解 rebase 是什么的時(shí)候,最重要的就是什么時(shí)候 不能 用 rebase。git rebase 的黃金法則便是,絕不要在公共的分支上使用它。

比如說,如果你把 master 分支 rebase 到你的 feature 分支上會發(fā)生什么:

在這里插入圖片描述

這次 rebase 將 master 分支上的所有提交都移到了 feature 分支后面。問題是它只發(fā)生在你的代碼倉庫中,其他所有的開發(fā)者還在原來的 master 上工作。因?yàn)?rebase 引起了新的提交,Git 會認(rèn)為你的 master 分支和其他人的 master 已經(jīng)分叉了。

同步兩個(gè) master 分支的唯一辦法是把它們 merge 到一起,導(dǎo)致一個(gè)額外的合并提交和兩堆包含同樣更改的提交。不用說,這會讓人非常困惑。

所以,在你運(yùn)行 git rebase 之前,一定要問問你自己「有沒有別人正在這個(gè)分支上工作?」。如果答案是肯定的,那么把你的爪子放回去,重新找到一個(gè)無害的方式(如 git revert)來提交你的更改。不然的話,你可以隨心所欲地重寫歷史。

強(qiáng)制推送

如果你想把 rebase 之后的 master 分支推送到遠(yuǎn)程倉庫,Git 會阻止你這么做,因?yàn)閮蓚€(gè)分支包含沖突。但你可以傳入 --force 標(biāo)記來強(qiáng)行推送。就像下面一樣:

# 小心使用這個(gè)命令!
git push --force

它會重寫遠(yuǎn)程的 master 分支來匹配你倉庫中 rebase 之后的 master 分支,對于團(tuán)隊(duì)中其他成員來說這看上去很詭異。所以,務(wù)必小心這個(gè)命令,只有當(dāng)你知道你在做什么的時(shí)候再使用。

僅有的幾個(gè)強(qiáng)制推送的使用場景之一是,當(dāng)你在想向遠(yuǎn)程倉庫推送了一個(gè)私有分支之后,執(zhí)行了一個(gè)本地的清理(比如說為了回滾)。這就像是在說「哦,其實(shí)我并不想推送之前那個(gè) feature 分支的。用我現(xiàn)在的版本替換掉吧?!雇瑯?#xff0c;你要注意沒有別人正在這個(gè) feature 分支上工作。

工作流

rebase 可以或多或少應(yīng)用在你們團(tuán)隊(duì)的 Git 工作流中。在這一節(jié)中,我們來看看在 feature 分支開發(fā)的各個(gè)階段中,rebase 有哪些好處。

第一步是在任何和 git rebase 有關(guān)的工作流中為每一個(gè) feature 專門創(chuàng)建一個(gè)分支。它會給你帶來安全使用 rebase 的分支結(jié)構(gòu):

在這里插入圖片描述

本地清理

在你工作流中使用 rebase 最好的用法之一就是清理本地正在開發(fā)的分支。隔一段時(shí)間執(zhí)行一次交互式 rebase,你可以保證你 feature 分支中的每一個(gè)提交都是專注和有意義的。你在寫代碼時(shí)不用擔(dān)心造成孤立的提交——因?yàn)槟愫竺嬉欢苄迯?fù)。

調(diào)用 git rebase 的時(shí)候,你有兩個(gè)基(base)可以選擇:上游分支(比如 master)或者你 feature 分支中早先的一個(gè)提交。我們在「交互式 rebase」一節(jié)看到了第一種的例子。后一種在當(dāng)你只需要修改最新幾次提交時(shí)也很有用。比如說,下面的命令對最新的 3 次提交進(jìn)行了交互式 rebase:

git checkout feature
git rebase -i HEAD~3

通過指定 HEAD~3 作為新的基提交,你實(shí)際上沒有移動分支——你只是將之后的 3 次提交重寫了。注意它不會把上游分支的更改并入到 feature 分支中。

在這里插入圖片描述

如果你想用這個(gè)方法重寫整個(gè) feature 分支,git merge-base 命令非常方便地找出 feature 分支開始分叉的基。下面這段命令返回基提交的 ID,你可以接下來將它傳給 git rebase

git merge-base feature master

交互式 rebase 是在你工作流中引入 git rebase 的的好辦法,因?yàn)樗挥绊懕镜胤种?。其他開發(fā)者只能看到你已經(jīng)完成的結(jié)果,那就是一個(gè)非常整潔、易于追蹤的分支歷史。

但同樣的,這只能用在私有分支上。如果你在同一個(gè) feature 分支和其他開發(fā)者合作的話,這個(gè)分支是公開的,你不能重寫這個(gè)歷史。

用帶有交互式的 rebase 清理本地提交,這是無法用 git merge 命令代替的。

將上游分支上的更改并入feature分支

在概覽一節(jié),我們看到了 feature 分支如何通過 git mergegit rebase 來并入上游分支。merge 是保留你完整歷史的安全選擇,rebase 將你的 feature 分支移動到 master 分支后面,創(chuàng)建一個(gè)線性的歷史。

git rebase 的用法和本地清理非常類似(而且可以同時(shí)使用),但之間并入了 master 上的上游更改。

記住,rebase 到遠(yuǎn)程分支而不是 master 也是完全合法的。當(dāng)你和另一個(gè)開發(fā)者在同一個(gè) feature 分之上協(xié)作的時(shí)候,你會用到這個(gè)用法,將他們的更改并入你的項(xiàng)目。

比如說,如果你和另一個(gè)開發(fā)者 John 往 feature 分支上添加了幾個(gè)提交,在從 John 的倉庫中 fetch 之后,你的倉庫可能會像下面這樣:

在這里插入圖片描述

就和并入 master 上的上游更改一樣,你可以這樣解決這個(gè) fork:要么 merge 你的本地分支和 John 的分支,要么把你的本地分支 rebase 到 John 的分支后面。

在這里插入圖片描述
在這里插入圖片描述

注意,這里的 rebase 沒有違反 rebase 黃金法則,因?yàn)橹挥心愕谋镜胤种系?commit 被移動了,之前的所有東西都沒有變。這就像是在說「把我的改動加到 John 的后面去」。在大多數(shù)情況下,這比通過合并提交來同步遠(yuǎn)程分支更符合直覺。

默認(rèn)情況下,git pull 命令會執(zhí)行一次merge,但你可以傳入--rebase 來強(qiáng)制它通過rebase來整合遠(yuǎn)程分支。

用 Pull Request 進(jìn)行審查

如果你將 Pull Request 作為你代碼審查過程中的一環(huán),你需要避免在創(chuàng)建 Pull Request 之后使用 git rebase。只要你發(fā)起了 Pull Request,其他開發(fā)者能看到你的代碼,也就是說這個(gè)分支變成了公共分支。重寫歷史會造成 Git 和你的同事難以找到這個(gè)分支接下來的任何提交。

來自其他開發(fā)者的任何更改都應(yīng)該用 git merge 而不是 git rebase 來并入。

因此,在提交 Pull Request前用交互式的 rebase 進(jìn)行代碼清理通常是一個(gè)好的做法。

并入通過的功能分支

如果某個(gè)功能被你們團(tuán)隊(duì)通過了,你可以選擇將這個(gè)分支 rebase 到 master 分支之后,或是使用 git merge 來將這個(gè)功能并入主代碼庫中。

這和將上游改動并入 feature 分支很相似,但是你不可以在 master 分支重寫提交,你最后需要用 git merge 來并入這個(gè) feature。但是,在 merge 之前執(zhí)行一次 rebase,你可以確保 merge 是一直向前的,最后生成的是一個(gè)完全線性的提交歷史。這樣你還可以加入 Pull Request 之后的提交。

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

如果你還沒有完全熟悉 git rebase,你還可以在一個(gè)臨時(shí)分支中執(zhí)行 rebase。這樣的話,如果你意外地弄亂了你 feature 分支的歷史,你還可以查看原來的分支然后重試。

比如說:

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [清理目錄]
git checkout master
git merge temporary-branch

總結(jié)

你使用 rebase 之前需要知道的知識點(diǎn)都在這了。如果你想要一個(gè)干凈的、線性的提交歷史,沒有不必要的合并提交,你應(yīng)該使用 git rebase 而不是 git merge 來并入其他分支上的更改。

另一方面,如果你想要保存項(xiàng)目完整的歷史,并且避免重寫公共分支上的 commit, 你可以使用 git merge。兩種選項(xiàng)都很好用,但至少你現(xiàn)在多了 git rebase 這個(gè)選擇。


4.2 代碼回滾:Reset、Checkout、Revert 的選擇

git reset、git checkoutgit revert 是你的 Git 工具箱中最有用的一些命令。它們都用來撤銷代碼倉庫中的某些更改,而前兩個(gè)命令不僅可以作用于提交,還可以作用于特定文件。

因?yàn)樗鼈兎浅O嗨?#xff0c;所以我們經(jīng)常會搞混,不知道什么場景下該用哪個(gè)命令。在這篇文章中,我們會比較 git resetgit checkoutgit revert 最常見的用法。希望你在看完后能游刃有余地使用這些命令來管理你的倉庫。

在這里插入圖片描述

Git 倉庫有三個(gè)主要組成——工作目錄,緩存區(qū)和提交歷史。這張圖有助于理解每個(gè)命令到底產(chǎn)生了哪些影響。當(dāng)你閱讀的時(shí)候,牢記這張圖。

提交層面的操作

你傳給 git resetgit checkout 的參數(shù)決定了它們的作用域。如果你沒有包含文件路徑,這些操作對所有提交生效。我們這一節(jié)要探討的就是提交層面的操作。注意,git revert 沒有文件層面的操作。

Reset

在提交層面上,reset 將一個(gè)分支的末端指向另一個(gè)提交。這可以用來移除當(dāng)前分支的一些提交。比如,下面這兩條命令讓 hotfix 分支向后回退了兩個(gè)提交。

git checkout hotfix
git reset HEAD~2

hotfix 分支末端的兩個(gè)提交現(xiàn)在變成了懸掛提交。也就是說,下次 Git 執(zhí)行垃圾回收的時(shí)候,這兩個(gè)提交會被刪除。換句話說,如果你想扔掉這兩個(gè)提交,你可以這么做。reset 操作如下圖所示:

把hotfix分支reset到HEAD~2

如果你的更改還沒有共享給別人,git reset 是撤銷這些更改的簡單方法。當(dāng)你開發(fā)一個(gè)功能的時(shí)候發(fā)現(xiàn)「糟糕,我做了什么?我應(yīng)該重新來過!」時(shí),reset 就像是 go-to 命令一樣。

除了在當(dāng)前分支上操作,你還可以通過傳入這些標(biāo)記來修改你的緩存區(qū)或工作目錄:

  • –soft – 緩存區(qū)和工作目錄都不會被改變
  • –mixed – 默認(rèn)選項(xiàng)。緩存區(qū)和你指定的提交同步,但工作目錄不受影響
  • –hard – 緩存區(qū)和工作目錄都同步到你指定的提交

把這些標(biāo)記想成定義 git reset 操作的作用域就容易理解多了。

這些標(biāo)記往往和 HEAD 作為參數(shù)一起使用。比如,git reset --mixed HEAD 將你當(dāng)前的改動從緩存區(qū)中移除,但是這些改動還留在工作目錄中。另一方面,如果你想完全舍棄你沒有提交的改動,你可以使用 git reset --hard HEAD。這是 git reset 最常用的兩種用法。

當(dāng)你傳入 HEAD 以外的其他提交的時(shí)候要格外小心,因?yàn)?reset 操作會重寫當(dāng)前分支的歷史。正如 rebase 黃金法則所說的,在公共分支上這樣做可能會引起嚴(yán)重的后果。

Checkout

你應(yīng)該已經(jīng)非常熟悉提交層面的 git checkout。當(dāng)傳入分支名時(shí),可以切換到那個(gè)分支。

git checkout hotfix

上面這個(gè)命令做的不過是將HEAD移到一個(gè)新的分支,然后更新工作目錄。因?yàn)檫@可能會覆蓋本地的修改,Git 強(qiáng)制你提交或者緩存工作目錄中的所有更改,不然在 checkout 的時(shí)候這些更改都會丟失。和 git reset 不一樣的是,git checkout 沒有移動這些分支。

將 HEAD 從 master 移到 hotfix

除了分支之外,你還可以傳入提交的引用來 checkout 到任意的提交。這和 checkout 到另一個(gè)分支是完全一樣的:把 HEAD 移動到特定的提交。比如,下面這個(gè)命令會 checkout 到當(dāng)前提交的祖父提交。

git checkout HEAD~2

在這里插入圖片描述

這對于快速查看項(xiàng)目舊版本來說非常有用。但如果你當(dāng)前的 HEAD 沒有任何分支引用,那么這會造成 HEAD 分離。這是非常危險(xiǎn)的,如果你接著添加新的提交,然后切換到別的分支之后就沒辦法回到之前添加的這些提交。因此,在為分離的 HEAD 添加新的提交的時(shí)候你應(yīng)該創(chuàng)建一個(gè)新的分支。

Revert

Revert 撤銷一個(gè)提交的同時(shí)會創(chuàng)建一個(gè)新的提交。這是一個(gè)安全的方法,因?yàn)樗粫貙懱峤粴v史。比如,下面的命令會找出倒數(shù)第二個(gè)提交,然后創(chuàng)建一個(gè)新的提交來撤銷這些更改,然后把這個(gè)提交加入項(xiàng)目中。

git checkout hotfix
git revert HEAD~2

如下圖所示:

在這里插入圖片描述

在這里插入圖片描述

相比 git reset,它不會改變現(xiàn)在的提交歷史。因此,git revert 可以用在公共分支上,git reset 應(yīng)該用在私有分支上。

你也可以把 git revert 當(dāng)作撤銷已經(jīng)提交的更改,而 git reset HEAD 用來撤銷沒有提交的更改。

就像 git checkout 一樣,git revert 也有可能會重寫文件。所以,Git 會在你執(zhí)行 revert 之前要求你提交或者緩存你工作目錄中的更改。

文件層面的操作

git resetgit checkout 命令也接受文件路徑作為參數(shù)。這時(shí)它的行為就大為不同了。它不會作用于整份提交,參數(shù)將它限制于特定文件。

Reset

當(dāng)檢測到文件路徑時(shí),git reset 將緩存區(qū)同步到你指定的那個(gè)提交。比如,下面這個(gè)命令會將倒數(shù)第二個(gè)提交中的 foo.py 加入到緩存區(qū)中,供下一個(gè)提交使用。

git reset HEAD~2 foo.py

和提交層面的 git reset 一樣,通常我們使用HEAD而不是某個(gè)特定的提交。運(yùn)行 git reset HEAD foo.py 會將當(dāng)前的 foo.py 從緩存區(qū)中移除出去,而不會影響工作目錄中對 foo.py 的更改。

在這里插入圖片描述

--soft、--mixed--hard 對文件層面的 git reset 毫無作用,因?yàn)榫彺鎱^(qū)中的文件一定會變化,而工作目錄中的文件一定不變。

Checkout

Checkout 一個(gè)文件和帶文件路徑 git reset 非常像,除了它更改的是工作目錄而不是緩存區(qū)。不像提交層面的 checkout 命令,它不會移動 HEAD引用,也就是你不會切換到別的分支上去。

在這里插入圖片描述

比如,下面這個(gè)命令將工作目錄中的 foo.py 同步到了倒數(shù)第二個(gè)提交中的 foo.py

git checkout HEAD~2 foo.py

和提交層面相同的是,它可以用來檢查項(xiàng)目的舊版本,但作用域被限制到了特定文件。

如果你緩存并且提交了 checkout 的文件,它具備將某個(gè)文件回撤到之前版本的效果。注意它撤銷了這個(gè)文件后面所有的更改,而 git revert 命令只撤銷某個(gè)特定提交的更改。

git reset 一樣,這個(gè)命令通常和 HEAD 一起使用。比如 git checkout HEAD foo.py 等同于舍棄 foo.py 沒有緩存的更改。這個(gè)行為和 git reset HEAD --hard 很像,但只影響特定文件。

總結(jié)

你現(xiàn)在已經(jīng)掌握了 Git 倉庫中撤銷更改的所有工具。git reset、git checkoutgit revert 命令比較容易混淆,但當(dāng)你想起它們對工作目錄、緩存區(qū)和提交歷史的不同影響,就會容易判斷現(xiàn)在應(yīng)該用哪個(gè)命令。

下面這個(gè)表格總結(jié)了這些命令最常用的使用場景。記得經(jīng)常對照這個(gè)表格,因?yàn)槟闶褂?Git 時(shí)一定會經(jīng)常用到。

命令作用域常用情景
git reset提交層面在私有分支上舍棄一些沒有提交的更改
git reset文件層面將文件從緩存區(qū)中移除
git checkout提交層面切換分支或查看舊版本
git checkout文件層面舍棄工作目錄中的更改
git revert提交層面在公共分支上回滾更改
git revert文件層面(然而并沒有)

4.3 Git log 高級用法

每一個(gè)版本控制系統(tǒng)的出現(xiàn)都是為了讓你記錄代碼的變化。你可以看到項(xiàng)目的歷史記錄——誰貢獻(xiàn)了什么、bug 是什么時(shí)候引入的,還可以撤回有問題的更改。但是,首先你得知道如何使用它。這也就是為什么會有 git log 這個(gè)命令。

到現(xiàn)在為止,你應(yīng)該已經(jīng)知道如何用 git log 命令來顯示最基本的提交信息。但除此之外,你還可以傳入各種不同的參數(shù)來獲得不一樣的輸出。

git log 有兩個(gè)高級用法:一是自定義提交的輸出格式,二是過濾輸出哪些提交。這兩個(gè)用法合二為一,你就可以找到你項(xiàng)目中你需要的任何信息。

格式化 Log 輸出

首先,這篇文章會展示幾種 git log 格式化輸出的例子。大多數(shù)例子只是通過標(biāo)記向 git log 請求或多或少的信息。

如果你不喜歡默認(rèn)的 git log 格式,你可以用 git config 的別名功能來給你想要的格式創(chuàng)建一個(gè)快捷方式。

Oneline

--oneline 標(biāo)記把每一個(gè)提交壓縮到了一行中。它默認(rèn)只顯示提交ID和提交信息的第一行。git log --oneline 的輸出一般是這樣的:

0e25143 Merge branch 'feature'
ad8621a Fix a bug in the feature
16b36c6 Add a new feature
23ad9ad Add the initial code base

它對于獲得項(xiàng)目的總體情況很有幫助。

Decorate

很多時(shí)候,知道每個(gè)提交關(guān)聯(lián)的分支或者標(biāo)簽很有用。--decorate 標(biāo)記讓 git log 顯示指向這個(gè)提交的所有引用(比如說分支、標(biāo)簽等)。

這可以和另一個(gè)配置項(xiàng)一起使用。比如,執(zhí)行 git log --oneline --decorate 會將提交歷史格式化成這樣:

0e25143 (HEAD, master) Merge branch 'feature'
ad8621a (feature) Fix a bug in the feature
16b36c6 Add a new feature
23ad9ad (tag: v0.9) Add the initial code base

在這個(gè)例子中,你(通過HEAD標(biāo)記)可以看到最上面那個(gè)提交已經(jīng)被 checkout 了,而且它還是 master 分支的尾端。第二個(gè)提交有另一個(gè) feature 分支指向它,以及最后那個(gè)提交帶有 v0.9 標(biāo)簽。

分支、標(biāo)簽、HEAD 還有提交歷史是你 Git 倉庫中包含的所有信息。因此,這個(gè)命令讓你更完整地觀察項(xiàng)目結(jié)構(gòu)。

Diff

git log 提供了很多選項(xiàng)來顯示兩個(gè)提交之間的差異。其中最常用的兩個(gè)是 --stat-p。

--stat 選項(xiàng)顯示每次提交的文件增刪數(shù)量(注意:修改一行記作增加一行且刪去一行),當(dāng)你想要查看提交引入的變化時(shí)這會非常有用。比如說,下面這個(gè)提交在 hello.py 文件中增加了 67 行,刪去了 38 行。

commit f2a238924e89ca1d4947662928218a06d39068c3
Author: John <john@example.com>
Date:   Fri Jun 25 17:30:28 2014 -0500Add a new featurehello.py | 105 ++++++++++++++++++++++++-----------------1 file changed, 67 insertion(+), 38 deletions(-)

文件名后面+和-的數(shù)量是這個(gè)提交造成的更改中增刪的相對比例。它給你一個(gè)直觀的感覺,關(guān)于這次提交有多少改動。如果你想知道每次提交刪改的絕對數(shù)量,你可以將 -p 選項(xiàng)傳入git log。這樣提交所有的刪改都會被輸出:

commit 16b36c697eb2d24302f89aa22d9170dfe609855b
Author: Mary <mary@example.com>
Date:   Fri Jun 25 17:31:57 2014 -0500Fix a bug in the featurediff --git a/hello.py b/hello.py
index 18ca709..c673b40 100644
--- a/hello.py
+++ b/hello.py
@@ -13,14 +13,14 @@ B
-print("Hello, World!")
+print("Hello, Git!")

對于改動很多的提交來說,這個(gè)輸出會變得又長又大。一般來說,當(dāng)你輸出所有刪改的時(shí)候,你是想要查找某一具體的改動,這時(shí)你就要用到 pickaxe 選項(xiàng)。

Shortlog

git shortlog 是一種特殊的 git log,它是為創(chuàng)建發(fā)布聲明設(shè)計(jì)的。它把每個(gè)提交按作者分類,顯示提交信息的第一行。這樣可以容易地看到誰做了什么。

比如說,兩個(gè)開發(fā)者為項(xiàng)目貢獻(xiàn)了 5 個(gè)提交,那么 git shortlog 輸出會是這樣的:

Mary (2):Fix a bug in the featureFix a serious security hole in our frameworkJohn (3):Add the initial code baseAdd a new featureMerge branch 'feature'

默認(rèn)情況下,git shortlog 把輸出按作者名字排序,但你可以傳入 -n 選項(xiàng)來按每個(gè)作者提交數(shù)量排序。

Graph

--graph 選項(xiàng)繪制一個(gè) ASCII 圖像來展示提交歷史的分支結(jié)構(gòu)。它經(jīng)常和 --oneline--decorate 兩個(gè)選項(xiàng)一起使用,這樣會更容易查看哪個(gè)提交屬于哪個(gè)分支:

git log --graph --oneline --decorate
For a simple repository with just 2 branches, this will produce the following:*   0e25143 (HEAD, master) Merge branch 'feature'
|\  
| * 16b36c6 Fix a bug in the new feature
| * 23ad9ad Start a new feature
* | ad8621a Fix a critical security issue
|/  
* 400e4b7 Fix typos in the documentation
* 160e224 Add the initial code base

星號表明這個(gè)提交所在的分支,所以上圖的意思是 23ad9ad16b36c6 這兩個(gè)提交在 topic 分支上,其余的在 master 分支上。

雖然這對簡單的項(xiàng)目來說是個(gè)很好用的選擇,但你可能會更喜歡 gitk 或 SourceTree 這些更強(qiáng)大的可視化工具來分析大型項(xiàng)目。

自定義格式

對于其他的 git log 格式需求,你都可以使用 --pretty=format:"<string>" 選項(xiàng)。它允許你使用像 printf 一樣的占位符來輸出提交。

比如,下面命令中的 %cn、%h%cd 這三種占位符會被分別替換為作者名字、縮略標(biāo)識和提交日期。

git log --pretty=format:"%cn committed %h on %cd"
This results in the following format for each commit:John committed 400e4b7 on Fri Jun 24 12:30:04 2014 -0500
John committed 89ab2cf on Thu Jun 23 17:09:42 2014 -0500
Mary committed 180e223 on Wed Jun 22 17:21:19 2014 -0500
John committed f12ca28 on Wed Jun 22 13:50:31 2014 -0500

完整的占位符清單可以在文檔中找到。

除了讓你只看到關(guān)注的信息,這個(gè) --pretty=format:"<string>" 選項(xiàng)在你想要在另一個(gè)命令中使用日志內(nèi)容是尤為有用的。

過濾提交歷史

格式化提交輸出只是 git log 其中的一個(gè)用途。另一半是理解如何瀏覽整個(gè)提交歷史。接下來的文章會介紹如何用 git log 選擇項(xiàng)目歷史中的特定提交。所有的用法都可以和上面討論過的格式化選項(xiàng)結(jié)合起來。

按數(shù)量

git log 最基礎(chǔ)的過濾選項(xiàng)是限制顯示的提交數(shù)量。當(dāng)你只對最近幾次提交感興趣時(shí),它可以節(jié)省你一頁一頁查看的時(shí)間。

你可以在后面加上 -<n> 選項(xiàng)。比如說,下面這個(gè)命令會顯示最新的 3 次提交:

git log -3

按日期

如果你想要查看某一特定時(shí)間段內(nèi)的提交,你可以使用 --after--before 標(biāo)記來按日期篩選。它們都接受好幾種日期格式作為參數(shù)。比如說,下面的命令會顯示 2014 年 7 月 1 日后(含)的提交:

git log --after="2014-7-1"

你也可以傳入相對的日期,比如一周前("1 week ago")或者昨天("yesterday"):

get log --after="yesterday"

你可以同時(shí)提供--before--after 來檢索兩個(gè)日期之間的提交。比如,為了顯示 2014 年 7 月 1 日到 2014 年 7 月 4 日之間的提交,你可以這么寫:

git log --after="2014-7-1" --before="2014-7-4"

注意 --since、--until 標(biāo)記和 --after、--before 標(biāo)記分別是等價(jià)的。

按作者

當(dāng)你只想看某一特定作者的提交的時(shí)候,你可以使用 --author 標(biāo)記。它接受正則表達(dá)式,返回所有作者名字滿足這個(gè)規(guī)則的提交。如果你知道那個(gè)作者的確切名字你可以直接傳入文本字符串:

git log --author="John"

它會顯示所有作者叫 John 的提交。作者名不一定是全匹配,只要包含那個(gè)子串就會匹配。

你也可以用正則表達(dá)式來創(chuàng)建更復(fù)雜的檢索。比如,下面這個(gè)命令檢索名叫 Mary 或 John 的作者的提交。

git log --author="John\|Mary"

注意作者的郵箱地址也算作是作者的名字,所以你也可以用這個(gè)選項(xiàng)來按郵箱檢索。

如果你的工作流區(qū)分提交者和作者,--committer 也能以相同的方式使用。

按提交信息

按提交信息來過濾提交,你可以使用 --grep 標(biāo)記。它和上面的 --author 標(biāo)記差不多,只不過它搜索的是提交信息而不是作者。

比如說,你的團(tuán)隊(duì)規(guī)范要求在提交信息中包括相關(guān)的issue編號,你可以用下面這個(gè)命令來顯示這個(gè) issue 相關(guān)的所有提交:

git log --grep="JRA-224:"

你也可以傳入 -i 參數(shù)來忽略大小寫匹配。

按文件

很多時(shí)候,你只對某個(gè)特定文件的更改感興趣。為了顯示某個(gè)特定文件的歷史,你只需要傳入文件路徑。比如說,下面這個(gè)命令返回所有和 foo.pybar.py 文件相關(guān)的提交:

git log -- foo.py bar.py

-- 告訴 git log 接下來的參數(shù)是文件路徑而不是分支名。如果分支名和文件名不可能沖突,你可以省略 --。

按內(nèi)容

我們還可以根據(jù)源代碼中某一行的增加和刪除來搜索提交。這被稱為 pickaxe,它接受形如 -S"<string>" 的參數(shù)。比如說,當(dāng)你想要知道 Hello, World! 字符串是什么時(shí)候加到項(xiàng)目中哪個(gè)文件中去的,你可以使用下面這個(gè)命令:

git log -S "Hello, World!"

如果你想用正則表達(dá)式而不是字符串來搜索,你可以使用 -G"<regex>" 標(biāo)記。

這是一個(gè)非常強(qiáng)大的調(diào)試工具,它能讓你定位到所有影響代碼中特定一行的提交。它甚至可以讓你看到某一行是什么時(shí)候復(fù)制或者移動到另一個(gè)文件中去的。

按范圍

你可以傳入范圍來篩選提交。這個(gè)范圍由下面這樣的格式指定,其中 <since><until> 是提交的引用:

git log <since>..<until>

這個(gè)命令在你使用分支引用作為參數(shù)時(shí)特別有用。這是顯示兩個(gè)分支之間區(qū)別最簡單的方式??纯聪旅孢@個(gè)命令:

git log master..feature

其中的 master..feature 范圍包含了在 feature 分支而不在 master 分支中所有的提交。換句話說,這個(gè)命令可以看出從 master 分支 fork 到 feature 分支后發(fā)生了哪些變化。它可以這樣可視化:

在這里插入圖片描述

注意如果你更改范圍的前后順序(feature…master),你會獲取到 master 分支而非 feature 分支上的所有提交。如果 git log 輸出了全部兩個(gè)分支的提交,這說明你的提交歷史已經(jīng)分叉了。

過濾合并提交

git log 輸出時(shí)默認(rèn)包括合并提交。但是,如果你的團(tuán)隊(duì)采用強(qiáng)制合并策略(意思是 merge 你修改的上游分支而不是將你的分支 rebase 到上游分支),你的項(xiàng)目歷史中會有很多外來的提交。

你可以通過 --no-merges 標(biāo)記來排除這些提交:

git log --no-merges

另一方面,如果你只對合并提交感興趣,你可以使用 --merges 標(biāo)記:

git log --merges

它會返回所有包含兩個(gè)父節(jié)點(diǎn)的提交。

總結(jié)

你現(xiàn)在應(yīng)該對使用 git log 來格式化輸出和選擇你要顯示的提交的用法比較熟悉了。它允許你查看你項(xiàng)目歷史中任何需要的內(nèi)容。

這些技巧是你 Git 工具箱中重要的部分,不過注意 git log 往往和其他 Git 命令連著使用。當(dāng)你找到了你要的提交,你把它傳給 git checkout、git revert 或是其他控制提交歷史的工具。所以,請繼續(xù)堅(jiān)持 Git 高級用法的學(xué)習(xí)。


4.4 Git 鉤子:自定義你的工作流

Git 鉤子是在 Git 倉庫中特定事件發(fā)生時(shí)自動運(yùn)行的腳本。它可以讓你自定義 Git 內(nèi)部的行為,在開發(fā)周期中的關(guān)鍵點(diǎn)觸發(fā)自定義的行為。

在這里插入圖片描述

Git 鉤子最常見的使用場景包括推行提交規(guī)范,根據(jù)倉庫狀態(tài)改變項(xiàng)目環(huán)境,和接入持續(xù)集成工作流。但是,因?yàn)槟_本可以完全定制,你可以用 Git 鉤子來自動化或者優(yōu)化你開發(fā)工作流中任意部分。

在這篇文章中,我們會先簡要介紹 Git 鉤子是如何工作的。然后,我們會審視一些本地和遠(yuǎn)端倉庫使用最流行的鉤子。

概述

Git 鉤子是倉庫中特定事件發(fā)生時(shí) Git 自動運(yùn)行的普通腳本。因此,Git 鉤子安裝和配置也非常容易。

鉤子在本地或服務(wù)端倉庫都可以部署,且只會在倉庫中事件發(fā)生時(shí)被執(zhí)行。在文章后面我們會具體地研究各種鉤子。接下來所講的配置對本地和服務(wù)端鉤子都起作用。

安裝鉤子

鉤子存在于每個(gè) Git 倉庫的 .git/hooks 目錄中。當(dāng)你初始化倉庫時(shí),Git 自動生成這個(gè)目錄和一些示例腳本。當(dāng)你觀察 .git/hooks 時(shí),你會看到下面這些文件:

applypatch-msg.sample       pre-push.sample
commit-msg.sample           pre-rebase.sample
post-update.sample          prepare-commit-msg.sample
pre-applypatch.sample       update.sample
pre-commit.sample

這里已經(jīng)包含了大部分可用的鉤子了,但是 .sample 拓展名防止它們默認(rèn)被執(zhí)行。為了安裝一個(gè)鉤子,你只需要去掉 .sample 拓展名。或者你要寫一個(gè)新的腳本,你只需添加一個(gè)文件名和上述匹配的新文件,去掉 .sample 拓展名。

比如說,試試安裝一個(gè) prepare-commit-msg 鉤子。去掉腳本的 .sample 拓展名,在文件中加上下面這兩行:

#!/bin/shecho "# Please include a useful commit message!" > $1

鉤子需要能被執(zhí)行,所以如果你創(chuàng)建了一個(gè)新的腳本文件,你需要修改它的文件權(quán)限。比如說,為了確保 prepare-commit-msg 可執(zhí)行,運(yùn)行下面這個(gè)命令:

chmod +x prepare-commit-msg

接下來你每次運(yùn)行 git commit 時(shí),你會看到默認(rèn)的提交信息都被替換了。我們會在「準(zhǔn)備提交信息」一節(jié)中細(xì)看它是如何工作的?,F(xiàn)在我們已經(jīng)可以定制 Git 的內(nèi)部功能,你只需要坐和放寬。

內(nèi)置的樣例腳本是非常有用的參考資料,因?yàn)槊總€(gè)鉤子傳入的參數(shù)都有非常詳細(xì)的說明(不同鉤子不一樣)。

腳本語言

內(nèi)置的腳本大多是 shell和 PERL 語言的,但你可以使用任何腳本語言,只要它們最后能編譯到可執(zhí)行文件。每次腳本中的 #!/bin/sh 定義了你的文件將被如何解釋。比如,使用其他語言時(shí)你只需要將 path 改為你的解釋器的路徑。

比如說,你可以在 prepare-commit-msg 中寫一個(gè)可執(zhí)行的 Python 腳本。下面這個(gè)鉤子和上一節(jié)的 shell 腳本做的事完全一樣。

#!/usr/bin/env pythonimport sys, oscommit_msg_filepath = sys.argv[1]
with open(commit_msg_filepath, 'w') as f:f.write("# Please include a useful commit message!")

注意第一行改成了 Python 解釋器的路徑。此外,這里用 sys.argv[1] 而不是 $1 來獲取第一個(gè)參數(shù)(這個(gè)也后面再講)。

這個(gè)特性非常強(qiáng)大,因?yàn)槟憧梢杂萌魏文阆矚g的語言來編寫 Git 鉤子。

鉤子的作用域

對于任何 Git 倉庫來說鉤子都是本地的,而且它不會隨著 git clone 一起復(fù)制到新的倉庫。而且,因?yàn)殂^子是本地的,任何能接觸得到倉庫的人都可以修改。

對于開發(fā)團(tuán)隊(duì)來說,這有很大的影響。首先,你要確保你們成員之間的鉤子都是最新的。其次,你也不能強(qiáng)行讓其他人用你喜歡的方式提交——你只能鼓勵他們這樣做。

在開發(fā)團(tuán)隊(duì)中維護(hù)鉤子是比較復(fù)雜的,因?yàn)?.git/hooks 目錄不隨你的項(xiàng)目一起拷貝,也不受版本控制影響。一個(gè)簡單的解決辦法是把你的鉤子存在項(xiàng)目的實(shí)際目錄中(在 .git 外)。這樣你就可以像其他文件一樣進(jìn)行版本控制。為了安裝鉤子,你可以在 .git/hooks 中創(chuàng)建一個(gè)符號鏈接,或者簡單地在更新后把它們復(fù)制到 .git/hooks 目錄下。

在這里插入圖片描述

作為備選方案,Git 同樣提供了一個(gè)模板目錄機(jī)制來更簡單地自動安裝鉤子。每次你使用 git initgit clone 時(shí),模板目錄文件夾下的所有文件和目錄都會被復(fù)制到 .git 文件夾。

所有的下面講到的本地鉤子都可以被更改或者徹底刪除,只要你是項(xiàng)目的參與者。這完全取決于你的團(tuán)隊(duì)成員想不想用這個(gè)鉤子。所以記住,最好把 Git 鉤子當(dāng)成一個(gè)方便的開發(fā)者工具而不是一個(gè)嚴(yán)格強(qiáng)制的開發(fā)規(guī)范。

也就是說,用服務(wù)端鉤子來拒絕沒有遵守規(guī)范的提交是完全可行的。后面我們會再討論這個(gè)問題。

本地鉤子

本地鉤子只影響它們所在的倉庫。當(dāng)你在讀這一節(jié)的時(shí)候,記住開發(fā)者可以修改他們本地的鉤子,所以不要用它們來推行強(qiáng)制的提交規(guī)范。不過,它們確實(shí)可以讓開發(fā)者更易于接受這些規(guī)范。

在這一節(jié)中,我們會探討 6 個(gè)最有用的本地鉤子:

  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • post-checkout
  • pre-rebase

前四個(gè)鉤子讓你介入完整的提交生命周期,后兩個(gè)允許你執(zhí)行一些額外的操作,分別為 git checkoutgit rebase 的安全檢查。

所有帶 pre- 的鉤子允許你修改即將發(fā)生的操作,而帶 post- 的鉤子只能用于通知。

我們也會看到處理鉤子的參數(shù)和用底層 Git 命令獲取倉庫信息的實(shí)用技巧。

pre-commit

pre-commit 腳本在每次你運(yùn)行 git commit 命令時(shí),Git 向你詢問提交信息或者生產(chǎn)提交對象時(shí)被執(zhí)行。你可以用這個(gè)鉤子來檢查即將被提交的代碼快照。比如說,你可以運(yùn)行一些自動化測試,保證這個(gè)提交不會破壞現(xiàn)有的功能。

pre-commit 不需要任何參數(shù),以非0狀態(tài)退出時(shí)將放棄整個(gè)提交。讓我們看一個(gè)簡化了的(和更詳細(xì)的)內(nèi)置 pre-commit 鉤子。只要檢測到不一致時(shí)腳本就放棄這個(gè)提交,就像 git diff-index 命令定義的那樣(只要詞尾有空白字符、只有空白字符的行、行首一個(gè) tab 后緊接一個(gè)空格就被認(rèn)為錯(cuò)誤)。

#!/bin/sh# 檢查這是否是初始提交
if git rev-parse --verify HEAD >/dev/null 2>&1
thenecho "pre-commit: About to create a new commit..."against=HEAD
elseecho "pre-commit: About to create the first commit..."against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi# 使用git diff-index來檢查空白字符錯(cuò)誤
echo "pre-commit: Testing for whitespace errors..."
if ! git diff-index --check --cached $against
thenecho "pre-commit: Aborting commit due to whitespace errors"exit 1
elseecho "pre-commit: No whitespace errors :)"exit 0
fi

使用 git diff-index 時(shí)我們要指出和哪次提交進(jìn)行比較。一般來說是 HEAD,但 HEAD 在創(chuàng)建第一次提交時(shí)不存在,所以我們的第一個(gè)任務(wù)是解決這個(gè)極端情形。我們通過 git rev-parse --verify 來檢查 HEAD 是否是一個(gè)合法的引用。>/dev/null 2>&1 這部分屏蔽了 git rev-parse 任何輸出。HEAD 或者一個(gè)新的提交對象被儲存在 against 變量中供 git diff-index 使用。4b825d... 這個(gè)哈希字串代表一個(gè)空白提交的 ID。

git diff-index --cached 命令將提交和緩存區(qū)比較。通過傳入 -check 選項(xiàng),我們要求它在更改引入空白字符錯(cuò)誤時(shí)警告我們。如果它這么做了,我們返回狀態(tài)1來放棄這次提交,否則返回狀態(tài) 0,提交工作流正常進(jìn)行。

這只是 pre-commit 的其中一個(gè)例子。它恰好使用了已有的 Git 命令來根據(jù)提交帶來的更改進(jìn)行測試,但你可以在 pre-commit 中做任何你想做的事,比如執(zhí)行其它腳本、運(yùn)行第三方測試集、用 Lint 檢查代碼風(fēng)格。

prepare-commit-msg

prepare-commit-msg 鉤子在 pre-commit 鉤子在文本編輯器中生成提交信息之后被調(diào)用。這被用來方便地修改自動生成的 squash 或 merge 提交。

prepare-commit-msg 腳本的參數(shù)可以是下列三個(gè):

  • 包含提交信息的文件名。你可以在原地更改提交信息。
  • 提交類型??梢允切畔?#xff08;-m-F 選項(xiàng)),模板(-t 選項(xiàng)),merge(如果是個(gè)合并提交)或 squash(如果這個(gè)提交插入了其他提交)。
  • 相關(guān)提交的 SHA1 哈希字串。只有當(dāng) -c、-C--amend 選項(xiàng)出現(xiàn)時(shí)才需要。

pre-commit 一樣,以非0狀態(tài)退出會放棄提交。

我們已經(jīng)看過一個(gè)修改提交信息的簡單例子,現(xiàn)在我們來看一個(gè)更有用的腳本。使用 issue 跟蹤器時(shí),我們通常在單獨(dú)的分支上處理 issue。如果你在分支名中包含了 issue 編號,你可以使用 prepare-commit-msg 鉤子來自動地將它包括在那個(gè)分支的每個(gè)提交信息中。

#!/usr/bin/env pythonimport sys, os, re
from subprocess import check_output# 收集參數(shù)
commit_msg_filepath = sys.argv[1]
if len(sys.argv) > 2:commit_type = sys.argv[2]
else:commit_type = ''
if len(sys.argv) > 3:commit_hash = sys.argv[3]
else:commit_hash = ''print "prepare-commit-msg: File: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, commit_hash)# 檢測我們所在的分支
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "prepare-commit-msg: On branch '%s'" % branch# 用issue編號生成提交信息
if branch.startswith('issue-'):print "prepare-commit-msg: Oh hey, it's an issue branch."result = re.match('issue-(.*)', branch)issue_number = result.group(1)with open(commit_msg_filepath, 'r+') as f:content = f.read()f.seek(0, 0)f.write("ISSUE-%s %s" % (issue_number, content))

首先,上面的 prepare-commit-msg 鉤子告訴你如何收集傳入腳本的所有參數(shù)。接下來,它調(diào)用了 git symbolic-ref --short HEAD 來獲取對應(yīng) HEAD 的分支名。如果分支名以 issue- 開頭,它會重寫提交信息文件,在第一行加上 issue 編號。比如你的分支名 issue-224,下面的提交信息將會生成:

ISSUE-224# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch issue-224
# Changes to be committed:
#   modified:   test.txt

有一點(diǎn)要記住的是即使用戶用 -m 傳入提交信息,prepare-commit-msg 也會運(yùn)行。也就是說,上面這個(gè)腳本會自動插入 ISSUE-[#] 字符串,而用戶無法更改。你可以檢查第二個(gè)參數(shù)是否是提交類型來處理這個(gè)情況。

但是,如果沒有 -m 選項(xiàng),prepare-commit-msg 鉤子允許用戶修改生成后的提交信息。所以腳本的目的是為了方便,而不是推行強(qiáng)制的提交信息規(guī)范。如果你要這么做,你需要下一節(jié)所講的 commit-msg 鉤子。

commit-msg

commit-msg 鉤子和 prepare-commit-msg 鉤子很像,但它會在用戶輸入提交信息之后被調(diào)用。這適合用來提醒開發(fā)者他們的提交信息不符合你團(tuán)隊(duì)的規(guī)范。

傳入這個(gè)鉤子唯一的參數(shù)是包含提交信息的文件名。如果它不喜歡用戶輸入的提交信息,它可以在原地修改這個(gè)文件(和 prepare-commit-msg 一樣),或者它會以非 0 狀態(tài)退出,放棄這個(gè)提交。

比如說,下面這個(gè)腳本確認(rèn)用戶沒有刪除 prepare-commit-msg 腳本自動生成的 ISSUE-[#] 字符串。

#!/usr/bin/env pythonimport sys, os, re
from subprocess import check_output# 收集參數(shù)
commit_msg_filepath = sys.argv[1]# 檢測所在的分支
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "commit-msg: On branch '%s'" % branch# 檢測提交信息,判斷是否是一個(gè)issue提交
if branch.startswith('issue-'):print "commit-msg: Oh hey, it's an issue branch."result = re.match('issue-(.*)', branch)issue_number = result.group(1)required_message = "ISSUE-%s" % issue_numberwith open(commit_msg_filepath, 'r') as f:content = f.read()if not content.startswith(required_message):print "commit-msg: ERROR! The commit message must start with '%s'" % required_messagesys.exit(1)

雖然用戶每次創(chuàng)建提交時(shí),這個(gè)腳本都會運(yùn)行。但你還是應(yīng)該避免做檢查提交信息之外的事情。如果你需要通知其他服務(wù)一個(gè)快照已經(jīng)被提交了,你應(yīng)該使用 post-commit 這個(gè)鉤子。

post-commit

post-commit 鉤子在 commit-msg 鉤子之后立即被運(yùn)行 。它無法更改 git commit 的結(jié)果,所以這主要用于通知用途。

這個(gè)腳本沒有參數(shù),而且退出狀態(tài)不會影響提交。對于大多數(shù) post-commit 腳本來說,你只是想訪問你剛剛創(chuàng)建的提交。你可以用 git rev-parse HEAD 來獲得最近一次提交的SHA1哈希字串,或者你可以用 git log -l HEAD 獲取完整的信息。

比如說,如果你需要每次提交快照時(shí)向老板發(fā)封郵件(也許對于大多數(shù)工作流來說這不是個(gè)好的想法),你可以加上下面這個(gè) post-commit 鉤子。

#!/usr/bin/env pythonimport smtplib
from email.mime.text import MIMEText
from subprocess import check_output# 獲得新提交的git log --stat輸出
log = check_output(['git', 'log', '-1', '--stat', 'HEAD'])# 創(chuàng)建一個(gè)純文本的郵件內(nèi)容
msg = MIMEText("Look, I'm actually doing some work:\n\n%s" % log)msg['Subject'] = 'Git post-commit hook notification'
msg['From'] = 'mary@example.com'
msg['To'] = 'boss@example.com'# 發(fā)送信息
SMTP_SERVER = 'smtp.example.com'
SMTP_PORT = 587session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
session.ehlo()
session.starttls()
session.ehlo()
session.login(msg['From'], 'secretPassword')session.sendmail(msg['From'], msg['To'], msg.as_string())
session.quit()

你雖然可以用 post-commit 來觸發(fā)本地的持續(xù)集成系統(tǒng),但大多數(shù)時(shí)候你想用的是 post-receive 這個(gè)鉤子。它運(yùn)行在服務(wù)端而不是用戶的本地機(jī)器,它同樣在任何開發(fā)者推送代碼時(shí)運(yùn)行。那里更適合你進(jìn)行持續(xù)集成。

post-checkout

post-checkout 鉤子和 post-commit 鉤子很像,但它在你用 git checkout 查看引用的時(shí)候被調(diào)用。這是用來清理你的工作目錄中可能會令人困惑的生成文件。

這個(gè)鉤子接受三個(gè)參數(shù),它的返回狀態(tài)不影響 git checkout 命令。

  • HEAD 前一次提交的引用
  • 新的 HEAD 的引用
  • 1 或 0,分別代表是分支 checkout 還是文件 checkout。

Python 程序員經(jīng)常遇到的問題是切換分支后那些之前生成的 .pyc 文件。解釋器有時(shí)使用 .pyc 而不是 .py 文件。為了避免歧義,你可以在每次用 post-checkout 切換到新的分支的時(shí)候,刪除所有 .pyc 文件。

#!/usr/bin/env pythonimport sys, os, re
from subprocess import check_output# 收集參數(shù)
previous_head = sys.argv[1]
new_head = sys.argv[2]
is_branch_checkout = sys.argv[3]if is_branch_checkout == "0":print "post-checkout: This is a file checkout. Nothing to do."sys.exit(0)print "post-checkout: Deleting all '.pyc' files in working directory"
for root, dirs, files in os.walk('.'):for filename in files:ext = os.path.splitext(filename)[1]if ext == '.pyc':os.unlink(os.path.join(root, filename))

鉤子腳本當(dāng)前的工作目錄總是位于倉庫的根目錄下,所以 os.walk('.') 調(diào)用遍歷了倉庫中所有文件。接下來,我們檢查它的拓展名,如果是 .pyc 就刪除它。

通過 post-checkout 鉤子,你還可以根據(jù)你切換的分支來來更改工作目錄。比如說,你可以在代碼庫外面使用一個(gè)插件分支來儲存你所有的插件。如果這些插件需要很多二進(jìn)制文件而其他分支不需要,你可以選擇只在插件分支上 build。

pre-rebase

pre-rebase 鉤子在 git rebase 發(fā)生更改之前運(yùn)行,確保不會有什么糟糕的事情發(fā)生。

這個(gè)鉤子有兩個(gè)參數(shù):fork 之前的上游分支,將要 rebase 的下游分支。如果 rebase 當(dāng)前分支則第二個(gè)參數(shù)為空。以非 0 狀態(tài)退出會放棄這次 rebase。

比如說,如果你想徹底禁用 rebase 操作,你可以使用下面的 pre-rebase 腳本:

#!/bin/sh# 禁用所有rebase
echo "pre-rebase: Rebasing is dangerous. Don't do it."
exit 1

每次運(yùn)行 git rebase,你都會看到下面的信息:

pre-rebase: Rebasing is dangerous. Don't do it.
The pre-rebase hook refused to rebase.

內(nèi)置的 pre-rebase.sample 腳本是一個(gè)更復(fù)雜的例子。它在何時(shí)阻止 rebase 這方面更加智能。它會檢查你當(dāng)前的分支是否已經(jīng)合并到了下一個(gè)分支中去(也就是主分支)。如果是的話,rebase 可能會遇到問題,腳本會放棄這次 rebase。

服務(wù)端鉤子

服務(wù)端鉤子和本地鉤子幾乎一樣,只不過它們存在于服務(wù)端的倉庫中(比如說中心倉庫,或者開發(fā)者的公共倉庫)。當(dāng)和官方倉庫連接時(shí),其中一些可以用來拒絕一些不符合規(guī)范的提交。

這節(jié)中我們要討論下面三個(gè)服務(wù)端鉤子:

  • pre-receive
  • update
  • post-receive

這些鉤子都允許你對 git push 的不同階段做出響應(yīng)。

服務(wù)端鉤子的輸出會傳送到客戶端的控制臺中,所以給開發(fā)者發(fā)送信息是很容易的。但你要記住這些腳本在結(jié)束完之前都不會返回控制臺的控制權(quán),所以你要小心那些長時(shí)間運(yùn)行的操作。

pre-receive

pre-receive 鉤子在有人用 git push 向倉庫推送代碼時(shí)被執(zhí)行。它只存在于遠(yuǎn)端倉庫中,而不是原來的倉庫中。

這個(gè)鉤子在任意引用被更新前被執(zhí)行,所以這是強(qiáng)制推行開發(fā)規(guī)范的好地方。如果你不喜歡推送的那個(gè)人(多大仇 = =),提交信息的格式,或者提交的更改,你都可以拒絕這次提交。雖然你不能阻止開發(fā)者寫出糟糕的代碼,但你可以用 pre-receive 防止這些代碼流入官方的代碼庫。

這個(gè)腳本沒有參數(shù),但每一個(gè)推送上來的引用都會以下面的格式傳入腳本的單獨(dú)一行:

<old-value> <new-value> <ref-name>

你可以看到這個(gè)鉤子做了非常簡單的事,就是讀取推送上來的引用并且把它們打印出來。

#!/usr/bin/env pythonimport sys
import fileinput# 讀取用戶試圖更新的所有引用
for line in fileinput.input():print "pre-receive: Trying to push ref: %s" % line# 放棄推送
# sys.exit(1)

這和其它鉤子相比略微有些不同,因?yàn)樾畔⑹峭ㄟ^標(biāo)準(zhǔn)輸入而不是命令行傳入的。在遠(yuǎn)端倉庫的 .git/hooks 中加上這個(gè)腳本,推送到 master 分支,你會看到下面這些信息打印出來:

b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master

你可以用 SHA1 哈希字串,或者底層的 Git 命令,來檢查將要引入的更改。一些常見的使用包括:

  • 拒絕將上游分支 rebase 的更改
  • 防止錯(cuò)綜復(fù)雜的合并(非快速向前,會造成項(xiàng)目歷史非線性)
  • 檢查用戶是否有正確的權(quán)限來做這些更改(大多用于中心化的 Git 工作流中)
  • 如果多個(gè)引用被推送,在 pre-receive 中返回非 0 狀態(tài),拒絕所有提交。如果你想一個(gè)個(gè)接受或拒絕分支,你需要使用 update 鉤子

update

update 鉤子在 pre-receive 之后被調(diào)用,用法也差不多。它也是在實(shí)際更新前被調(diào)用的,但它可以分別被每個(gè)推送上來的引用分別調(diào)用。也就是說如果用戶嘗試推送到4個(gè)分支,update 會被執(zhí)行 4 次。和 pre-receive 不一樣,這個(gè)鉤子不需要讀取標(biāo)準(zhǔn)輸入。事實(shí)上,它接受三個(gè)參數(shù):

  • 更新的引用名稱
  • 引用中存放的舊的對象名稱
  • 引用中存放的新的對象名稱

這些信息和 pre-receive 相同,但因?yàn)槊看我枚紩謩e觸發(fā)更新,你可以拒絕一些引用而接受另一些。

#!/usr/bin/env pythonimport sysbranch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)# 只放棄當(dāng)前分支的推送
# sys.exit(1)

上面這個(gè)鉤子簡單地輸出了分支和新舊提交的哈希字串。當(dāng)你向遠(yuǎn)程倉庫推送超過一個(gè)分支時(shí),你可以看到每個(gè)分支都有輸出。

post-receive

post-receive 鉤子在成功推送后被調(diào)用,適合用于發(fā)送通知。對很多工作流來說,這是一個(gè)比 post-commit 更好的發(fā)送通知的地方,因?yàn)檫@些更改在公共的服務(wù)器而不是用戶的本地機(jī)器上。給其他開發(fā)者發(fā)送郵件或者觸發(fā)一個(gè)持續(xù)集成系統(tǒng)都是 post-receive 常用的操作。

這個(gè)腳本沒有參數(shù),但和 pre-receive 一樣通過標(biāo)準(zhǔn)輸入讀取。

總結(jié)

在這篇文章中,我們學(xué)習(xí)了如果用 Git 鉤子來修改內(nèi)部行為,當(dāng)倉庫中特定的事件發(fā)生時(shí)接受消息。鉤子是存在于 git/hooks 倉庫中的普通腳本,因此也非常容易安裝和定制。

我們還看了一些常用的本地和服務(wù)端的鉤子。這使得我們能夠介入到整個(gè)開發(fā)生命周期中去。我們現(xiàn)在知道了如何在創(chuàng)建提交或推送的每個(gè)階段執(zhí)行自定義的操作。有了這些簡單的腳本知識,你就可以對 Git 倉庫為所欲為了 :]


4.5 Git提交引用和引用日志

提交是 Git 的精髓所在,你無時(shí)不刻不在創(chuàng)建和緩存提交、查看以前的提交,或者用各種Git命令在倉庫間轉(zhuǎn)移你的提交。大多數(shù)的命令都對同一個(gè)提交操作,而有些會接受提交的引用作為參數(shù)。比如,你可以給 git checkout 傳入一個(gè)引用來查看以前的提交,或者傳入一個(gè)分支名來切換到對應(yīng)的分支。

在這里插入圖片描述

知道提交的各種引用方式之后,Git 的命令就會變得更加強(qiáng)大。在這章中,我們研究提交的各種引用方式,來一窺 git checkout、git branchgit push 等命令的工作原理。

我們還會學(xué)到如何使用 Git 的引用日志查看似乎已被刪除的提交。

哈希字串

引用一個(gè)提交最直接的方式是通過 SHA-1 的哈希字串,這是每個(gè)提交唯一的 ID。你可以在 git log 的輸出中找到提交的哈希字串。

commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date:   Wed Jul 9 16:37:42 2014 -0500一些提交信息

在 Git 命令中傳遞時(shí),你只需要提供足以確定那個(gè)提交的哈希子串即可。比如,你可以這樣用 git show 的命令顯示上面的提交:

git show 0c708f

有時(shí),我們需要把分支、標(biāo)簽或者其他間接的引用轉(zhuǎn)變成對應(yīng)提交的哈希。git rev-parse 命令正是你需要的。下面這個(gè)命令返回 master 分支提交的哈希字串:

git rev-parse master

當(dāng)你寫的自定義腳本中需要將提交引用作為參數(shù)時(shí),這個(gè)命令非常有用。你可以讓 git rev-parse 幫你處理轉(zhuǎn)換,而不用手動做這件事。

引用

ref 是提交的間接引用。你可以把它當(dāng)做哈希字串的別名,但對用戶更友好。這就是 Git 內(nèi)部表示分支和標(biāo)簽的機(jī)制。

引用以一段普通的文本存在于 .git/refs 目錄中,就是我們平時(shí)說的那個(gè) .git。你去 .git/refs 文件夾查看倉庫中的引用。你可以看到下面這樣的結(jié)構(gòu),但具體的文件取決于你的倉庫中有什么分支和標(biāo)簽,以及你的遠(yuǎn)程倉庫。

.git/refs/heads/mastersome-featureremotes/origin/mastertags/v0.9

heads目錄定義了你本地倉庫中的所有分支。每一個(gè)文件名和你的分支名一一對應(yīng),文件中包含一個(gè)提交的哈希字串。這個(gè)就是分支頂端的所在位置。為了驗(yàn)證這一點(diǎn),試試在 Git 根目錄運(yùn)行下面這兩個(gè)命令:

# 輸出`refs/heads/master`文件內(nèi)容
cat .git/refs/heads/master# 查看`master`分支尾端的提交
git log -1 master

cat 命令返回的哈希字串和 git log 命令顯示的哈希字串應(yīng)該是一致的。

如果要改變 master 分支的位置,Git 只需要更改 refs/heads/master 的文件內(nèi)容。同樣地,創(chuàng)建新的分支也只需要將當(dāng)前提交的哈希字串寫入到新的文件中。這也是為什么 Git 分支比 SVN 輕量那么多的其中一個(gè)原因。

tags 目錄也是以相同的方式存儲,只不過其中存的是標(biāo)簽而不是分支。remotes 目錄將你之前用 git remote 命令創(chuàng)建的所有遠(yuǎn)程倉庫以子目錄的形式一一列出。在每個(gè)文件夾中,你可以找到所有 fetch 到本地倉庫的遠(yuǎn)程分支。

指定引用

當(dāng)你向 Git 命令傳入引用的時(shí)候,你既可以指定引用完整的名稱,也可以使用縮寫,然后讓 Git 來尋找匹配。你應(yīng)該已經(jīng)對引用的縮寫很熟悉了,每次你通過名稱引用分支的時(shí)候都會這么做。

git show some-feature

這里的 some-feature 參數(shù)其實(shí)是分支名的縮寫。Git 在使用前將它解析成 refs/heads/some-feature。你也可以在命令行中指定引用的全稱,就像這樣:

git show refs/heads/some-feature

這避免了引用可能產(chǎn)生的所有歧義。這是非常必要的,比如你同時(shí)有一個(gè)標(biāo)簽和分支都叫 some-feature。然而,如果使用正常的命名規(guī)范,你不應(yīng)該有這樣的歧義。

我們會在 refspec 一節(jié)見到更多引用名稱。

打包引用目錄

對于大型倉庫,Git 會周期性地執(zhí)行垃圾回收來移除不需要的對象,將所有引用文件壓縮成單個(gè)文件來獲得更好的性能。你可以使用這個(gè)命令強(qiáng)制垃圾回收來執(zhí)行壓縮:

git gc

這個(gè)命令把 refs 文件夾中所有單獨(dú)的分支和標(biāo)簽移動到了 .git 根目錄下的 packed-refs 文件中。如果你打開這個(gè)文件,你會發(fā)現(xiàn)提交的哈希字串和引用之間的映射關(guān)系:

00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

另一方面,正常的 Git 功能不會受到任何影響。但如果你好奇你的 .git/refs 文件夾為什么是空的,這一節(jié)告訴你了答案。

特殊的引用

除了 refs 文件夾外,.git 根目錄還有一些特殊的引用。如下所示:

  • HEAD – 當(dāng)前所在的提交或分支。
  • FETCH_HEAD – 遠(yuǎn)程倉庫中 fetch 到的最新一次提交。
  • ORIG_HEAD – HEAD 的備份引用,避免損壞。
  • MERGE_HEAD – 你通過 git merge 并入當(dāng)前分支的引用(們)。
  • CHERRY_PICK_HEAD – 你 cherry pick 使用的引用。

這些引用由 Git 在需要時(shí)創(chuàng)建和更新。比如說,git pull 命令首先運(yùn)行 git fetch,而 FETCH_HEAD 引用隨之改變。然后,運(yùn)行 git merge FETCH_HEAD 來將 fetch 到的分支最終并入倉庫。當(dāng)然,你也可以使用其他任何引用,因?yàn)槲蚁嘈拍阋呀?jīng)對 HEAD 很熟悉了。

這些文件包含的內(nèi)容取決于它們的類型和你的倉庫狀態(tài)。HEAD 引用可以包含符號鏈接(指向另一個(gè)引用而不是哈希字串),或是提交的哈希字串。比如說,看看當(dāng)你在 master 分支上時(shí) HEAD 的內(nèi)容:

git checkout master
cat .git/HEAD

這個(gè)命令會輸出 ref: refs/heads/master,也就是說 HEAD 指向 refs/heads/master 這個(gè)引用。這也正是 Git 如何知道現(xiàn)在所在的是 master 分支。如果你要切換分支,HEAD 的內(nèi)容將會被更新到新的分支。但如果你要切換到一個(gè)提交而不是分支,HEAD 會包含一個(gè)提交的哈希而不是符號引用。這就是 Git 如何知道現(xiàn)在 HEAD 處于分離狀態(tài)。

在大多數(shù)情況下,HEAD 是你唯一用得到的引用。其它引用一般只在寫底層腳本,接觸到 Git 內(nèi)部的工作機(jī)制時(shí)才會用到。

refspec

refspec 將本地分支和遠(yuǎn)程分支對應(yīng)起來。我們可以通過它用本地的 Git 命令管理遠(yuǎn)程分支,設(shè)置一些高級的 git pushgit fetch 行為。

refspec 的定義是這樣的:[+]<src>:<dst>。<src> 參數(shù)是本地的源分支,<dst> 是遠(yuǎn)程的目標(biāo)分支??蛇x的 + 號強(qiáng)制遠(yuǎn)程倉庫采用非快速向前的更新策略。

refspec 可以和 git push 一起使用,用來指定遠(yuǎn)程的分支的名稱。比如,下面這個(gè)命令將 master 分支推送到遠(yuǎn)程 origin,就像一般的 git push 一樣,但它使用 qa-master 作為遠(yuǎn)程倉庫中的分支名。對于 QA 團(tuán)隊(duì)來說,這個(gè)方法非常有用。

git push origin master:refs/heads/qa-master

你也可以用 refspec 來刪除遠(yuǎn)程分支。feature 分支的工作流經(jīng)常會遇到這種情況,將 feature 分支推送到遠(yuǎn)程倉庫中(比如說為了備份)。你刪除本地的 feature 分支之后,遠(yuǎn)程的 feature 分支依然存在,雖然現(xiàn)在我們已經(jīng)不再需要它。你可以 push 一個(gè) <src> 參數(shù)為空的 refspec 來刪除它們,就像這樣:

git push origin:some-feature

這非常方便,因?yàn)槟悴恍枰卿浀侥愕倪h(yuǎn)程倉庫然后手動刪除這些遠(yuǎn)程分支。注意,在 Git v1.7.0 之后你可以用 --delete 標(biāo)記代替上面這個(gè)方法。下面這個(gè)命令和上面的命令作用相同:

git push origin --delete some-feature

在 Git 配置文件中增加幾行,你就可以更改 git fetch 的行為。默認(rèn)地,git fetch 會 fetch 遠(yuǎn)程倉庫中所有分支。原因就是 .git/config 文件的這段配置:

[remote "origin"]url = https://git@github.com:mary/example-repo.gitfetch = +refs/heads/*:refs/remotes/origin/*

fetch 這一行告訴 git fetch 從 origin 倉庫中下載所有分支。但是,一些工作流不需要所有分支。比如,很多持續(xù)集成工作流只關(guān)心 master 分支。為了做到這一點(diǎn),我們需要將 fetch 這行改成下面這樣:

[remote "origin"]url = https://git@github.com:mary/example-repo.gitfetch = +refs/heads/master:refs/remotes/origin/master

你還可以類似地修改 git push 的配置。比如,如果你總是將 master 分支推送到 origin 倉庫的 qa-master 分支(就像我們之前做的一樣),你要把配置文件改成這樣:

[remote "origin"]url = https://git@github.com:mary/example-repo.gitfetch = +refs/heads/master:refs/remotes/origin/masterpush = refs/heads/master:refs/heads/qa-master

refspec 給了你完全的掌控權(quán),可以定制 Git 命令如何在倉庫之間轉(zhuǎn)移分支。你可以重命名或是刪除你的本地分支,fetch 或是 push 不同的分支名,修改 git pushgit fetch 的設(shè)置,只對你想要的分支進(jìn)行操作。

相對引用

你還可以通過提交之間的相對關(guān)系來引用。~ 符號讓你訪問父節(jié)點(diǎn)的提交。比如說,下面這個(gè)命令顯示 HEAD 祖父節(jié)點(diǎn)的提交:

git show HEAD~2

但是,面對合并提交(merge commit)的時(shí)候,事情就會變得有些復(fù)雜。因?yàn)楹喜⑻峤挥卸鄠€(gè)父節(jié)點(diǎn),所以你可以找到多條回溯的路徑。對于 3 路合并,第一個(gè)父節(jié)點(diǎn)是你執(zhí)行合并時(shí)的分支,第二個(gè)父節(jié)點(diǎn)是你傳給 git merge 命令的分支。

~ 符號總是選擇合并提交的第一個(gè)父節(jié)點(diǎn)。如果你想選擇其他父節(jié)點(diǎn),你需要用 ^ 符號來指定。比如說,HEAD 是一個(gè)合并提交,下面這個(gè)命令返回 HEAD 的第二個(gè)父節(jié)點(diǎn):

git show HEAD^2

你可以使用不止一個(gè) ^ 來查看超過一層的節(jié)點(diǎn)。比如,下面的命令顯示的是 HEAD 的祖父節(jié)點(diǎn),也就是 HEAD 第二個(gè)父節(jié)點(diǎn)的父節(jié)點(diǎn)。

git show HEAD^2^1

為了闡明 ~^ 是如何工作的,下面這張圖告訴你如何使用相對引用,來指向任意的提交。有的提交可以通過多種方式引用。

在這里插入圖片描述

相對引用在命令中的用法和普通的引用相同。比如,下面所有命令中使用的都是相對引用:

# 只列出合并提交的第二個(gè)父節(jié)點(diǎn)的父節(jié)點(diǎn)
git log HEAD^2# 移除當(dāng)前分支最新的 3 個(gè)提交
git reset HEAD~3# 交互式rebase當(dāng)前分支最新的 3 個(gè)提交
git rebase -i HEAD~3

引用日志

引用日志是 Git 的安全網(wǎng)。它記錄了你在倉庫中做的所有更改,不管你有沒有提交。你也可以認(rèn)為這是你本地更改的完整歷史記錄。運(yùn)行 git reflog 命令查看引用日志。它應(yīng)該會打印出像下面這樣的信息:

400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2
0e25143 HEAD@{1}: commit (amend): 將一些很贊的新特性引入`master`
00f5425 HEAD@{2}: commit (merge): 合并'feature'分支
ad8621a HEAD@{3}: commit: 結(jié)束feature分支開發(fā)

說人話就是:

  • 你剛剛切換到 HEAD~2
  • 你剛剛修改了一個(gè)提交信息
  • 你剛剛把 feature 分支合并到了 master 分支
  • 你剛剛提交了一份緩存

HEAD{<n>} 語法允許你引用保存在日志中的提交。這和上一節(jié)的 HEAD~<n> 引用差不多,不過 <n> 指的是引用日志中的對象,而不是提交歷史。

你可以用辦法回到之前可能已經(jīng)丟失的狀態(tài)。比如,你剛剛用 git reset 方法粉碎了新的 feature 分支。你的引用日志看上去可能會是這樣的:

ad8621a HEAD@{0}: reset: moving to HEAD~3
298eb9f HEAD@{1}: commit: 一些提交信息
bbe9012 HEAD@{2}: commit: 繼續(xù)開發(fā)
9cb79fa HEAD@{3}: commit: 開始新特性開發(fā)

git reset 前的三個(gè)提交現(xiàn)在都成了懸掛的了,也就是說除了引用日志之外沒有辦法再引用到它們?,F(xiàn)在,假設(shè)你意識到了你不應(yīng)該丟掉你全部的工作。你只需要切換到 HEAD@{1} 這個(gè)提交就能回到你運(yùn)行 git reset 之前倉庫的狀態(tài)。

git checkout HEAD@{1}

這會讓你處于 HEAD 分離的狀態(tài)。你可以從這里開始,創(chuàng)建新的分支,繼續(xù)你的工作。

總結(jié)

你現(xiàn)在對 Git 提交的引用應(yīng)該已經(jīng)相當(dāng)熟悉了。我們知道了分支和標(biāo)簽是如何存在于 .git 的子文件夾 refs 中,如何讀取打包的引用文件,如何使用 refspec 來進(jìn)行更高級的 push 和 fetch 操作,如何使用 ~^ 符號來遍歷分支結(jié)構(gòu)。

我們還了解了引用日志,來引用到其他方式已經(jīng)不存在的提交。這是一種很好的恢復(fù)誤刪提交的方法。

它的意義在于:在任何開發(fā)場景下,你都能找到你需要的特定提交。你很容易就可以把這些技巧用在你一有的 Git 知識中,因?yàn)楹芏喑S玫拿疃冀邮芤米鳛閰?shù),包括 git log、git show、git checkout、git resetgit revert、git rebase 等等。


http://m.risenshineclean.com/news/61581.html

相關(guān)文章:

  • 公司網(wǎng)站網(wǎng)址注冊和備案哪里找sem數(shù)據(jù)分析
  • 北京開放疫情最新消息關(guān)鍵詞優(yōu)化精靈
  • 網(wǎng)站做圖標(biāo)鏈接網(wǎng)站域名查詢地址
  • 表情包做舊網(wǎng)站百度競價(jià)推廣賬戶
  • 網(wǎng)站建設(shè)藤設(shè)計(jì)廣告做到百度第一頁
  • 天津外貿(mào)網(wǎng)站建設(shè)公司如何優(yōu)化關(guān)鍵詞的方法
  • 公司做的網(wǎng)站入哪個(gè)會計(jì)科目怎么安裝百度
  • 建設(shè)個(gè)人商城網(wǎng)站seo優(yōu)化知識
  • 創(chuàng)業(yè)如何進(jìn)行網(wǎng)站建設(shè)路由優(yōu)化大師
  • 徐州建設(shè)工程交易網(wǎng)平臺官網(wǎng)網(wǎng)站seo搜索引擎優(yōu)化案例
  • 太原小程序開發(fā)定制學(xué)校seo推廣培訓(xùn)班
  • 中央人民政府網(wǎng)站網(wǎng)址百度圖片
  • 西安專業(yè)網(wǎng)站建設(shè)公司開創(chuàng)集團(tuán)與百度
  • wordpress 遠(yuǎn)程數(shù)據(jù)庫太原百度搜索排名優(yōu)化
  • 網(wǎng)站備案添加域名識圖找圖
  • 網(wǎng)站建設(shè)的優(yōu)點(diǎn)如何搭建自己的網(wǎng)站
  • 手把手教你做網(wǎng)站7seo推廣方案
  • 臨海市城鄉(xiāng)建設(shè)規(guī)劃局網(wǎng)站廣州搜發(fā)網(wǎng)絡(luò)科技有限公司
  • 萬站群cms百度入口提交
  • 用國外服務(wù)器做網(wǎng)站網(wǎng)絡(luò)宣傳怎么做
  • 怎么用ps做網(wǎng)站圖片能讓手機(jī)流暢到爆的軟件
  • 網(wǎng)上服裝定制網(wǎng)站seo優(yōu)化軟件有哪些
  • 提供手機(jī)自適應(yīng)網(wǎng)站建設(shè)優(yōu)化疫情防控 這些措施你應(yīng)該知道
  • 這是我自己做的網(wǎng)站嗎百度怎么發(fā)廣告
  • 旅游酒店網(wǎng)站建設(shè)三葉草gw9356
  • 網(wǎng)站維護(hù) 公司簡介網(wǎng)站免費(fèi)網(wǎng)站免費(fèi)
  • 佛山做網(wǎng)站建設(shè)政府免費(fèi)培訓(xùn)面點(diǎn)班
  • wordpress拉寬seo文章優(yōu)化技巧
  • 最火的網(wǎng)絡(luò)銷售平臺seo顧問合同
  • 高端網(wǎng)站定制方案seo網(wǎng)絡(luò)推廣培訓(xùn)