一、基礎(chǔ)部分
1、git add 和 git stage 有什么區(qū)別
在回答這個問題之前需要先了解 git 倉庫的三個組成部分:工作區(qū)(Working Directory)、暫存區(qū)(Stage)和歷史記錄區(qū)(History):
工作區(qū):在 git 管理下的正常目錄都算是工作區(qū),我們平時的編輯工作都是在工作區(qū)完成。
暫存區(qū):臨時區(qū)域。里面存放將要提交文件的快照。
歷史記錄區(qū):git commit 后的記錄區(qū)。
然后是這三個區(qū)的轉(zhuǎn)換關(guān)系以及轉(zhuǎn)換所使用的命令:

然后我們就可以來說一下 git add 和 git stage 了。其實(shí),他們兩是同義的,所以,驚不驚喜,意不意外?這個問題竟然是個陷阱…引入 git stage 的原因其實(shí)比較有趣:是因?yàn)橐?svn add 區(qū)分,兩者的功能是完全不一樣的,svn add 是將某個文件加入版本控制,而 git add 則是把某個文件加入暫存區(qū),因?yàn)樵?git 出來之前大家用 svn 比較多,所以為了避免誤導(dǎo),git 引入了git stage,然后把 git diff --staged 做為 git diff --cached 的相同命令?;谶@個原因,我們建議使用 git stage 以及 git diff --staged。
考察關(guān)鍵點(diǎn):
對 git 工作區(qū)(Working Directory)、暫存區(qū)(Stage)和歷史記錄區(qū)(History)以及轉(zhuǎn)換關(guān)系的了解;
對 git add 和 git stage 的了解。
回答關(guān)鍵點(diǎn):
工作區(qū)(Working Directory)、暫存區(qū)(Stage)和歷史記錄區(qū)(History)以及轉(zhuǎn)換關(guān)系不能少;
git stage 是 git add 的同義指令;
我用 git stage。
git reset、git revert 和 git checkout 有什么區(qū)別
這個問題同樣也需要先了解 git 倉庫的三個組成部分:工作區(qū)(Working Directory)、暫存區(qū)(Stage)和歷史記錄區(qū)(History)。
首先是它們的共同點(diǎn):用來撤銷代碼倉庫中的某些更改。
然后是不同點(diǎn):
首先,從 commit 層面來說:
git reset 可以將一個分支的末端指向之前的一個 commit。然后再下次 git 執(zhí)行垃圾回收的時候,會把這個 commit 之后的 commit 都扔掉。git reset 還支持三種標(biāo)記,用來標(biāo)記 reset 指令影響的范圍:
--mixed:會影響到暫存區(qū)和歷史記錄區(qū)。也是默認(rèn)選項(xiàng);
--soft:只影響歷史記錄區(qū);
--hard:影響工作區(qū)、暫存區(qū)和歷史記錄區(qū)。
注意:因?yàn)?git reset 是直接刪除 commit 記錄,從而會影響到其他開發(fā)人員的分支,所以不要在公共分支(比如 develop)做這個操作。
- git checkout 可以將 HEAD 移到一個新的分支,并更新工作目錄。因?yàn)榭赡軙采w本地的修改,所以執(zhí)行這個指令之前,你需要 stash 或者 commit 暫存區(qū)和工作區(qū)的更改。
- git revert 和 git reset 的目的是一樣的,但是做法不同,它會以創(chuàng)建新的 commit 的方式來撤銷 commit,這樣能保留之前的 commit 歷史,比較安全。另外,同樣因?yàn)榭赡軙采w本地的修改,所以執(zhí)行這個指令之前,你需要 stash 或者 commit 暫存區(qū)和工作區(qū)的更改。
回答關(guān)鍵點(diǎn):
- 對于 commit 層面和文件層面,這三個指令本身功能差別很大。
- git revert 不支持文件層面的操作。
- 不要在公共分支做 git reset 操作。
二、GitFlow 基本流程和你的理解
1、什么是GitFlow?
GitFlow 是由 Vincent Driessen 提出的一個 git操作流程標(biāo)準(zhǔn)。包含如下幾個關(guān)鍵分支:
名稱 說明
- master 主分支
- develop 主開發(fā)分支,包含確定即將發(fā)布的代碼
- feature 新功能分支,一般一個新功能對應(yīng)一個分支,對于功能的拆分需要比較合理,以避免一些后面不必要的代碼沖突
- release 發(fā)布分支,發(fā)布時候用的分支,一般測試時候發(fā)現(xiàn)的 bug 在這個分支進(jìn)行修復(fù)
- hotfix hotfix 分支,緊急修 bug 的時候用
GitFlow 的優(yōu)勢有如下幾點(diǎn):
- 并行開發(fā):GitFlow 可以很方便的實(shí)現(xiàn)并行開發(fā):每個新功能都會建立一個新的 feature 分支,從而和已經(jīng)完成的功能隔離開來,而且只有在新功能完成開發(fā)的情況下,其對應(yīng)的 feature 分支才會合并到主開發(fā)分支上(也就是我們經(jīng)常說的 develop 分支)。另外,如果你正在開發(fā)某個功能,同時又有一個新的功能需要開發(fā),你只需要提交當(dāng)前 feature 的代碼,然后創(chuàng)建另外一個 feature 分支并完成新功能開發(fā)。然后再切回之前的 feature 分支即可繼續(xù)完成之前功能的開發(fā)。
- 協(xié)作開發(fā):GitFlow 還支持多人協(xié)同開發(fā),因?yàn)槊總€ feature 分支上改動的代碼都只是為了讓某個新的 feature 可以獨(dú)立運(yùn)行。同時我們也很容易知道每個人都在干啥。
- 發(fā)布階段:當(dāng)一個新 feature 開發(fā)完成的時候,它會被合并到 develop 分支,這個分支主要用來暫時保存那些還沒有發(fā)布的內(nèi)容,所以如果需要再開發(fā)新的 feature,我們只需要從 develop 分支創(chuàng)建新分支,即可包含所有已經(jīng)完成的 feature 。
- 支持緊急修復(fù):GitFlow 還包含了 hotfix 分支。這種類型的分支是從某個已經(jīng)發(fā)布的 tag 上創(chuàng)建出來并做一個緊急的修復(fù),而且這個緊急修復(fù)只影響這個已經(jīng)發(fā)布的 tag,而不會影響到你正在開發(fā)的新 feature。
然后就是 GitFlow 最經(jīng)典的幾張流程圖,一定要理解:

feature 分支都是從 develop 分支創(chuàng)建,完成后再合并到 develop 分支上,等待發(fā)布。

當(dāng)需要發(fā)布時,我們從 develop 分支創(chuàng)建一個 release 分支

然后這個 release 分支會發(fā)布到測試環(huán)境進(jìn)行測試,如果發(fā)現(xiàn)問題就在這個分支直接進(jìn)行修復(fù)。在所有問題修復(fù)之前,我們會不停的重復(fù)發(fā)布->測試->修復(fù)->重新發(fā)布->重新測試這個流程。
發(fā)布結(jié)束后,這個 release 分支會合并到 develop 和 master 分支,從而保證不會有代碼丟失。

master 分支只跟蹤已經(jīng)發(fā)布的代碼,合并到 master 上的 commit 只能來自 release 分支和 hotfix 分支。
hotfix 分支的作用是緊急修復(fù)一些 Bug。
它們都是從 master 分支上的某個 tag 建立,修復(fù)結(jié)束后再合并到 develop 和 master 分支上。

考察關(guān)鍵點(diǎn)
- GitFlow 包含的分支類型和功能;
- GitFlow 的優(yōu)勢;
- 對 GitFlow feature、release、hotfix 流程的理解。
回答關(guān)鍵點(diǎn)
- GitFlow 的基本內(nèi)容以及優(yōu)勢;
- 對于 feature 流程,都是從 develop 分支發(fā)起,然后通過 PR/MR 的方式合并回 develop 分支;
- 對于 release 流程,則是要注意幾點(diǎn):
- 如果 release 分支上有 bug 需要修復(fù),直接在 release 分支上完成;
- release 分支上的 bug 修復(fù)要持續(xù)通過 PR/MR 的方式合并回 develop 分支;
- 最后確認(rèn)發(fā)版的時候才把 release 分支直接合并到 master 分支。
- 對于 hotfix 流程,則是要注意幾點(diǎn):
- 從 master 分支發(fā)起;
- 修復(fù)完要同時合并到 develop 和 master。
2、解釋下 PR 和 MR 的區(qū)別
PR 和 MR 的全稱分別是 pull request 和 merge request。解釋它們兩者的區(qū)別之前,我們需要先了解一下 Code Review,因?yàn)?PR 和 MR 的引入正是為了進(jìn)行 Code Review。
Code Review 是指在開發(fā)過程中,對代碼的系統(tǒng)性檢查。通常的目的是查找系統(tǒng)缺陷,保證代碼質(zhì)量和提高開發(fā)者自身水平。 Code Review 是輕量級代碼評審,相對于正式代碼評審,輕量級代碼評審所需要的各種成本要明顯低的多,如果流程正確,它可以起到更加積極的效果。
進(jìn)行 Code Review 的原因:
提高代碼質(zhì)量
- 及早發(fā)現(xiàn)潛在缺陷與BUG,降低事故成本。
- 促進(jìn)團(tuán)隊(duì)內(nèi)部知識共享,提高團(tuán)隊(duì)整體水平
- 評審過程對于評審人員來說,也是一種思路重構(gòu)的過程,幫助更多的人理解系統(tǒng)。
然后我們需要了解下 fork 和 branch,因?yàn)檫@是 PR 和 MR 各自所屬的協(xié)作流程。
fork 是 git 上的一個協(xié)作流程。通俗來說就是把別人的倉庫備份到自己倉庫,修修改改,然后再把修改的東西提交給對方審核,對方同意后,就可以實(shí)現(xiàn)幫別人改代碼的小目標(biāo)了。fork 包含了兩個流程:
-
fork 并更新某個倉庫
-
同步 fork
和 fork 不同,branch 并不涉及其他的倉庫,操作都在當(dāng)前倉庫完成。

所以 PR 和 MR 的最大區(qū)別就在于此。
考察關(guān)鍵點(diǎn):
- Code review;
- PR 和 MR 所屬流程的細(xì)節(jié)。
回答關(guān)鍵點(diǎn):
回答這個問題的時候不要單單只說它們的區(qū)別。而是要從 PR 和 MR 產(chǎn)生的原因,分析它們所屬的流程,然后再得出兩者的區(qū)別。
三、進(jìn)階部分
1、### 講一講你知道的 githooks
githooks 是指 git 在執(zhí)行某些特定操作時會觸發(fā)的一系列程序,有點(diǎn)類似數(shù)據(jù)庫中的觸發(fā)器,由用戶編寫。默認(rèn)情況下,這些程序都被放置在 $GIT_DIR/hooks 目錄下,當(dāng)然,我們也可以通過 git 的環(huán)境配置變量 core.hooksPath 來改變這個目錄。
githooks 的類型有很多中,這些 hook 會在各個特定操作時幫你自動完成一些你想要做的操作,比如:在提交代碼到 develop 分支的時候自動打包。
下面是目前支持的 githooks 列表:
-
pre-commit:
由
git commit觸發(fā),可以通過--no-verify選項(xiàng)來跳過,這個 hook 不需要參數(shù),在得到提交消息并開始提交(commit)前被調(diào)用,如果返回非 0,則會導(dǎo)致git commit失敗。當(dāng)默認(rèn)的 pre-commit 鉤子被啟用時,如果它發(fā)現(xiàn)文件尾部有空白行,此次提交就會被終止。
如果進(jìn)行
git commit的命令沒有指定一個編輯器來修改提交信息(commit message),任何的git commithook 被調(diào)用時都會帶上環(huán)境變量GIT_EDITOR=: -
prepare-commit-msg:
執(zhí)行
git commit命令后,在默認(rèn)提交消息準(zhǔn)備好后但編輯器啟動前,這個 hook 就會被調(diào)用。它接受 1 到 3 個參數(shù)。第 1 個是包含了提交消息的文件的名字。第 2 個是提交消息的來源,它可以是:
-
message(如果指定了-m或者-F選項(xiàng)) -
template(如果指定了-t選項(xiàng),或者在git config中開啟了commit.template選項(xiàng)) -
merge(如果本次提交是一次合并,或者存在文件.git/MERGE_MSG) -
squash(如果存在文件.git/SQUASH_MSG) -
commit并且第 3 個參數(shù)是一個提交的 SHA1 值(如果指定了-c,-C或者--amend選項(xiàng))
如果返回值不是 0,那么
git commit命令就會被中止。這個 hook 的目的是用來在工作時編輯信息文件,并且不會被
--no-verify選項(xiàng)跳過。一個非 0 值意味著 hook 工作失敗,會終止提交。它不應(yīng)該用來作為pre-commit鉤子的替代。 -
-
commit-msg:
由
git commit觸發(fā),可以通過--no-verify選項(xiàng)來跳過,接受 1 個參數(shù),這個參數(shù)包含了提交消息的文件的名字,如果返回非 0,則會中止git commit命令。這個 hook 可以用來規(guī)范提交信息,比如把信息格式化成項(xiàng)目定制的標(biāo)準(zhǔn)格式,或者發(fā)現(xiàn)提交信息不符合格式時拒絕這次提交。
-
post-commit:
由
git commit觸發(fā),在提交后被調(diào)用,不能影響git commit的結(jié)果。 -
pre-push:
在
git push運(yùn)行期間,更新了遠(yuǎn)程引用但尚未傳送對象時執(zhí)行。如果返回非 0,將中止推送過程。它接受遠(yuǎn)程分支的名字和位置作為參數(shù),同時從標(biāo)準(zhǔn)輸入中讀取一系列待更新的引用。 你可以在推送開始之前,用它驗(yàn)證對引用的更新操作。 -
pre-receive:
服務(wù)端處理來自客戶端的推送操作時,最先被調(diào)用的 hook。它從標(biāo)準(zhǔn)輸入獲取一系列被推送的引用。如果它以非 0 值退出,所有的推送內(nèi)容都不會被接受。你可以用這個 hook 阻止對
non-fast-forward的更新,或者對該推送所修改的所有引用和文件進(jìn)行訪問控制。 -
update:
和
pre-receivehook 十分類似,不同之處在于它會為每一個準(zhǔn)備更新的分支各運(yùn)行一次。假如推送者同時向多個分支推送內(nèi)容,pre-receive只運(yùn)行一次,相比之下update則會為每一個被推送的分支各運(yùn)行一次。它不會從標(biāo)準(zhǔn)輸入讀取內(nèi)容,而是接受三個參數(shù):引用的分支名;推送前引用所指向內(nèi)容的 SHA-1 值;以及用戶準(zhǔn)備推送內(nèi)容的 SHA-1 值。如果這個 hook 以非 0 值退出,只有相應(yīng)的那一個引用會被拒絕;其余的依然會被更新。 -
post-receive:
在整個過程完結(jié)以后運(yùn)行,可以用來更新其他系統(tǒng)服務(wù)或者通知用戶。它接受與
pre-receive相同的標(biāo)準(zhǔn)輸入數(shù)據(jù)。它的用途包括給某個郵件列表發(fā)信,通知持續(xù)集成服務(wù)器,或者更新缺陷追蹤系統(tǒng) —— 甚至可以通過分析提交信息來決定某個問題是否應(yīng)該被開啟、修改或者關(guān)閉。該腳本無法中止 push 進(jìn)程,不過客戶端在它結(jié)束運(yùn)行之前將保持連接狀態(tài),所以如果你想做其他操作需謹(jǐn)慎使用它,因?yàn)樗鼘⒑馁M(fèi)你很長的一段時間。 -
post-update:
由遠(yuǎn)程倉庫的
git-receive-pack調(diào)用,也是就是本地倉庫完成git push時。這個指令只能用來做通知,不能改變git-receive-pack的結(jié)果。 -
push-to-checkout:
由遠(yuǎn)程倉庫的
git-receive-pack調(diào)用,也是就是本地倉庫完成git push時。如果這個 hook 返回非 0 值,則會中斷git push操作。 -
applypatch-msg & pre-applypatch & post-applypatch:
由
git am觸發(fā),主要用于引入第三方 patch 的時候用,不是很常用。 -
pre-rebase:
由
git rebase觸發(fā),可以用來防止某個分支被 rebase。這個 hook 接受 1 到 2 個參數(shù)。第 1 個參數(shù)是上游分支,第 2 個參數(shù)是將要執(zhí)行 rebase 的分支。 -
post-checkout:
在
git checkout成功運(yùn)行后執(zhí)行。你可以根據(jù)你的項(xiàng)目環(huán)境用它調(diào)整你的工作目錄。包括放入大的二進(jìn)制文件、自動生成文檔或進(jìn)行其他類似這樣的操作。 -
post-merge:
在
git merge成功運(yùn)行后執(zhí)行。你可以用它恢復(fù) git無法跟蹤的工作區(qū)數(shù)據(jù),比如權(quán)限數(shù)據(jù)。這個 hook 也可以用來驗(yàn)證某些在 git控制之外的文件是否存在,這樣你就能在工作區(qū)改變時,把這些文件復(fù)制進(jìn)來。 -
post-rewrite:
這個 hook 被那些會替換提交記錄的命令調(diào)用,比如
git commit --amend和git rebase(不過不包括git filter-branch)。 它唯一的參數(shù)是觸發(fā)重寫的命令名,同時從標(biāo)準(zhǔn)輸入中接受一系列重寫的提交記錄。 這個 hook 的用途很大程度上跟post-checkout和post-merge差不多。 -
sendemail-validate:
這個 hook 由
git send-email觸發(fā),它接受一個參數(shù):包含 e-mail 接受者郵箱的文件名。如果這個 hook 返回非 0 值,git send-email就會被中止。 -
pre-auto-gc:
git 的一些日常操作在運(yùn)行時,偶爾會調(diào)用
git gc --auto進(jìn)行垃圾回收。這個 hook 會在垃圾回收開始之前被調(diào)用,可以用它來提醒你現(xiàn)在要回收垃圾了,或者依情形判斷是否要中斷回收。
2、### git rebase vs git merge vs git cherry pick
首先,我們需要了解一下這三個指令的意義:協(xié)同開發(fā)時往往每個人都會新建自己的分支去做開發(fā),然后再合并到公共分支。因?yàn)槲覀儾惶岢诠卜种Ы鉀Q沖突,所以這時候就涉及到把別人合并到公共分支的 commit 合并到自己分支了。而這三個指令,就可以幫你完成分支合并的操作。
然后,我們要了解這三個指令是怎么做的:
-
git rebase 的做法是先找到兩個分支最近的公共 commit,根據(jù)當(dāng)前分支后續(xù)的 commit,生成一系列文件補(bǔ)丁,然后以被 rebase 的分支的最后一個 commit 記錄為基點(diǎn),逐個應(yīng)用之前準(zhǔn)備好的補(bǔ)丁文件,最后會生成一個新的合并 commit 對象。如圖所示:
所以從我們的角度來看,就好像我們把自己分支的 commit 拼接在了公共分支的提交之后。這樣能產(chǎn)生一個更為整潔的提交歷史。
注意:一旦分支中的 commit 對象已經(jīng)發(fā)布到公共倉庫,就千萬不要對該分支進(jìn)行 rebase 操作,因?yàn)?rebase 是生成了新的 commit,這樣其他人就需要重新再 rebase 一次…
-
git merge 的做法是把兩個分支最新的快照以及二者最新的共同祖先進(jìn)行三方合并,合并的結(jié)果是產(chǎn)生一個新的 commit 對象。如圖所示:
git cherry pick 的做法就比較特殊了,主要用于公共分支已經(jīng)發(fā)生了很大變化,而個人分支 commit 又比較少的情況。開發(fā)人員可以從公共分支創(chuàng)建一個新的分支,然后把自己原來個人分支的 commit 記錄 cherry pick 到新的分支上,操作相對比較簡單。
3、如果工程中有些文件不需要上傳遠(yuǎn)端,該怎么處理?
這個問題考察的是對 .gitignore 的理解。
.gitignore 是配置在倉庫根目錄下的一個文件,作用是用來把一些文件忽略,讓 git 不要跟蹤這些文件。
注意:如果你想把一個已經(jīng)被跟蹤的文件 ignore 掉,這是時候新增的規(guī)則并不會對這個文件產(chǎn)生作用,你需要先用下面的指令把這個文件設(shè)置為不跟蹤:
git rm --cached FILENAME
下面是 .gitignore 文件的基本語法:
- 空白行不匹配任何文件,因此它可以作為提升可讀性的分隔符。
- 以 # 開頭的行作為注釋。
- 可以使用Linux通配符。例如:星號(*)代表任意多個字符,問號(?)代表一個字符,方括號([abc])代表可選字符范圍,大括號({string1,string2,...})代表可選的字符串等。
- 如果名稱的最前面有一個感嘆號(!),表示例外規(guī)則,將不被忽略。
- 如果名稱的最前面是一個路徑分隔符(/),表示要忽略的文件在此目錄下,而子目錄中的文件不忽略。
- 如果名稱的最后面是一個路徑分隔符(/),表示要忽略的是此目錄下該名稱的子目錄,而非文件(默認(rèn)文件或目錄都忽略)。
- 其他連續(xù)的星號被視為無效。
然后是一些官方建議的 gitignore 例子:https://github.com/github/gitignore,請自取。
回答關(guān)鍵點(diǎn)
- .gitignore 文件的基本語法;
- 如何 ignore 已經(jīng)被跟蹤的文件;
- 官方建議 Example。
4、.git 目錄下面都維護(hù)了一些什么信息
大家對這個目錄應(yīng)該不太陌生,但是應(yīng)該很少有人會去探究這個目錄下的內(nèi)容,但是如果一旦清楚的了解過 .git 目錄下的內(nèi)容,你對 git 各個操作的細(xì)節(jié)會更加了解。
首先我們來看一下某個倉庫 .git 目錄下的內(nèi)容:
.
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── config
├── description
├── hooks
│ └── README.sample
├── index
├── info
│ └── exclude
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── develop
│ │ └── master
│ └── remotes
│ └── origin
│ ├── develop
│ └── master
├── objects
│ ├── 01
│ │ ├── 50a03f535237397e5c16fbd4e96f79886889e0
│ │ └── 71e32dd777e045b27923657a58328becd66b56
│ ├── info
│ └── pack
├── refs
│ ├── heads
│ │ ├── develop
│ │ └── master
│ ├── remotes
│ │ └── origin
│ │ ├── develop
│ │ └── master
│ └── tags
└── sourcetreeconfig
這些文件的作用如下:
COMMIT_EDITMSG:保存最近一次 commit message 提供給用戶使用;
FETCH_HEAD:保存本地的 FETCH_HEAD 記錄,用于 git fetch 時和遠(yuǎn)程倉庫的版本號做對比;
HEAD:這個文件包含了一個當(dāng)前 branch 的引用,通過這個文件 git 可以得到下一次 commit 的parent;
config:當(dāng)前倉庫的 git 配置文件;
description:倉庫的描述信息,主要給 gitweb 等 git 托管系統(tǒng)使用;
hooks:這個目錄存放一些 shell 腳本,可以設(shè)置特定的git命令后觸發(fā)相應(yīng)的腳本;
index:這個文件就是暫存區(qū)(stage),是一個二進(jìn)制文件;
info:當(dāng)前倉庫的一些信息,里面有一個 exclude 文件,記錄了被 .gitignore 忽略的文件;
logs:保存所有的更新引用記錄,里面的 refs 文件夾,分文件記錄了各個分支的更新引用記錄;
objects:該目錄存放所有的 git 對象,對象的 SHA1 哈希值的前 2 位是文件夾名稱,后 38 位是對象文件名。
refs:具體的引用,Reference Specification,這個目錄一般包括三個子文件夾,heads、remotes和 tags,比如,heads 中的 master 文件標(biāo)識了當(dāng)前倉庫 master 分支指向的當(dāng)前 commit,以此類推;
sourcetreeconfig:Source Tree 的配置信息。



