Git Merge Dry Run photo by Jennifer Duda

Git 面試題

前言

我發現一件有趣的事,不少開發者朋友都知道 Git 這個工具的重要性,但卻又不會花太多時間去了解它到底是個什麼樣的工具。以工程師的謀生技能來說,Git 不需要像演算法、資料結構或其它程式語言、開發框架要學那麼深入,不少人平日的 Git 操作似乎只要會 addcommitpushpull,再加上會開分支跟合併就夠用了。在許多公司的職缺列表中,Git 都只是一個「加分條件」,所以就算不會用或不太熟好像也不會影響飯碗。

嗯…的確目前在職場上是這樣沒錯,而且把 Git 當 FTP 用也不能說不對,但對於開發人員來說,特別對於團隊合作來說,不正確的使用方法有時候會意外的造成檔案混亂或消失,而且也可能會造成團隊其它同事的困擾,甚至造成更大的災難。

以下列出我在面試的時候可能會拿來問面試者的題目,這些題目是我認為一位「自認自己會用 Git 的開發者」應該..應該啦,要能夠答得出來的,供大家參考。如果您在面試的時候被問過其它有趣的問題,也歡迎在下方留言。

免責聲明:

  1. 這不是面試考古題,所以大家要去面試還請多多準備,面試沒通過請不要來問我為什麼這些題目都沒有考。
  2. 承上,如果有機會來敝公司面試,我也不一定會考這些題目。

觀念題

問:git push origin master 這個指令是在做些什麼事?

這個指令會把你手邊的 master 分支的內容,推一份到 origin 這個地方(可能是 GitHub、GitLab 或是公司內部的 Git 伺服器),並且在 origin 這個地方形成一個同名的 master 分支。

但很多人不知道的是,其實 push 指令的完整型態長這樣:

$ git push origin master:master

意思就是「把本地的 master 分支的內容,推一份到 origin 上,並且在 origin 上建立一個 master 分支」,所以如果把指令調整成這樣:

$ git push origin master:cat

意思就會變成「把本地的 master 分支的內容,推一份到 origin 上,並且在 origin 上建立一個 cat 分支」。

參考資料 如何 Push 上傳到 GitHub?

問:請說明 git clonegit fetchgit pull 這三個指令有什麼不同?

git clone

clone 指令會把線上的專案,「整個」複製一份到你的電腦裡,並且在你的電腦裡建立相對應的標案及目錄(包括 .git 目錄),通常這個指令只會在一開始的時候使用,clone 之後要再更新的話,通常是執行 git fetchgit pull 指令。

git fetch

假設遠端節點叫做 origin,當執行 git fetch 指令的時候,Git 會比對本機與遠端(在這邊就是 origin)專案的差別,會「下載 origin 上面有但我本機目前還沒有」的內容下來,並且在本地端形成相對應的分支。

不過,fetch 指令只做下載,並不會進行合併。

git pull

pull 指令其實做的事情跟 fetch 是一樣的,差別只在於 fetch 只有把檔案抓下來,但 pull 不只抓下來,還會順便進行合併。也就是說,本質上,git pull 其實就等於 git fetch 加上 merge 指令。

問:Git 裡的 HEAD 是什麼東西?

HEAD 本身是一個指標,它通常會指向某一個本地端分支或是其它 commit,所以你也可以把 HEAD 當做是目前所在的分支(current branch)。

參考資料 【冷知識】HEAD 是什麼東西?

問:合併過的分支你通常會怎麼處理?為什麼?

分支這玩意兒,本質上就只是一個 40 個字元大小的檔案,刪掉分支,檔案或 commit 也不會因此而消失,所以合併過的分支要刪掉或留著做紀念都可以,反正即使留下這個分支,也就只佔了你硬碟 40 個位元組(Bytes)的空間罷了(幾十、幾百 G 的影片檔都不嫌浪費空間了…)。

參考資料 合併過的分支要留著嗎?

問:刪除已經合併過的分支會發生什麼事?

分支本身就像是指標或貼紙一樣的東西,它指著或貼在某個 commit 上面,分支並不是目錄或檔案的複製品(但在有些版控系統的確是)。在 Git 裡,刪除分支就像是你把包裝盒上的貼紙撕下來,貼紙撕掉了,盒子並不會就這樣跟著消失。所以,當你刪除合併過的分支不會發生什麼事,也不會造成檔案或目錄跟著被刪除的狀況。

問:git diff 這個指令是做什麼用的?

這個指令可以用來比對兩個 commit 之間的差異,舉個例子來說:

$ git diff e37078e d4d8d9d

這樣就可以比對 e37078ed4d8d9d 這兩個 commit 的差別。如果只給一個 SHA1 的話:

$ git diff e37078e

這樣就會比對 e37078e 跟目前 HEAD 所指的這個 commit 之間的差異。而如果完全都沒有給 SHA1 值的話:

$ git diff

Git 會比對目前「工作目錄」與「暫存區」之間的差異,也就是說,在你執行 git add 之前,這個指令可以看看你改了什麼內容。如果已經執行了 git add 指令把修改加到暫存區的話,可再加個 --cached 參數:

$ git diff --cached

這樣便可比對目前暫存區與 HEAD 所指向的那個 commit 之間的差異。

問:在使用 Git 的時候,你通常會怎麼使用標籤(Tag)?

通常在開發軟體有完成特定的里程碑,例如軟體版號 1.0.0 或是 beta、release 之類的,這時候就適合使用標籤來做標記。例如:

$ git tag 1.0.0

就會在目前這個 commit 打上一個 1.0.0 的標籤。

參考資料 使用標籤

問:你有在 GitHub 上用過 fork 這個功能嗎?它跟 git branch 指令有什麼不同?

GitHub 的 fork 功能是指「可以把這個專案整個複製一份到指定的帳號下」,fork 之後在 GitHub 上的呈現就會長得像這樣:

Git Fork

待修改 commit 推上去後,就可以透過 Pull Request(PR)的流程與原作互動,把你的 commit 貢獻到原專案。

git branch 則是指在專案裡開一個分支來進行修改,分支完成後透過 git mergegit rebase 指令進行合併。

參考資料 與其它開發者的互動 - 使用 Pull Request(PR)

問:用一般方式的合併,跟使用 rebase 方式合併,有什麼不同?各有何優缺點?

一般的合併方式,有些情況(非快轉合併)會產生一個額外的 commit 來接合兩邊分支,而 rebase 合併分支跟一般的合併分支的明顯差別,就是使用 rebase 方式合併分支不會有這個合併的 Commit。

如果就以最後的的結果來說,檔案內容來說是沒什麼差別,但在 Git 的歷史紀錄上來說就有一些差別,誰 rebase 誰,會造成歷史紀錄上先後順序不同的差別。例如 cat 分支 rebase 到 dog 分支的話,表示 cat 分支會被接到 dog 分支的後面;反之如果是 dog 分支 rebase 到 cat 上的話,表示 dog分支 會被接到 cat 分支的後面。

使用 rebase 的好處,是整理出來的歷史紀錄不會有合併用的 commit,看起來比較乾淨(也是有些人不覺得這乾淨多少),另外歷史紀錄的順序可以依照誰 rebase 誰而決定先後關係(不過這點不一定是優點或缺點,端看整理的人而定);缺點就是它相對的比一般的合併來得沒那麼直覺,一個不小心可能會弄壞掉而且還不知道怎麼 reset 回來,或是發生衝突的時候就會停在一半,對不熟悉 rebase 的人來說也許是個困擾。

通常在還沒有推出去但感覺得有點亂(或太瑣碎)的 commit,我會先使用 rebase 來整理分支後再推出去。rebase 等於是在修改歷史,這個行為會做出平行時空,修改已經推出去的歷史可能會對其它人帶來困擾,所以對於已經推出去的內容,請不要任意使用 rebase。

參考資料 1. 合併分支、2. 另一種合併方式(使用 rebase)

問:合併發生衝突(Conflict)的時候,你會怎麼處理?

不要這樣

「有問題,當然是解決掉提出問題的人啊!」

應該這樣

合併分支會發生衝突,很多時候沒先講好分工範圍的溝通不良造成,或是架構本身沒設計好,要切也切不出去導致大家都在改同一個檔案。通常遇到衝突問題,就把兩邊的人請過來討論一下,到底是該用誰的 code,修正完再 commit 就行了。

參考資料 合併發生衝突了,怎麼辦?

問:試說明 git checkout SHA1git reset SHA1 以及 git revert SHA1 這三個指令有什麼不同?

git checkout SHA1

這個指令會把目前的 HEAD 移到指定的 commit 上,並且把目前的狀態變成那個 commit 時候的樣子,但是不會移動任何分支(也就是分支都停在原來的地方,只有 HEAD 移動而已)。

因此,整個歷史紀錄看起來並沒有什麼變化,只是 HEAD 暫時移到某個地方而已。

git reset SHA1

這個指令會把目前的 HEAD 跟分支都一起移到指定的 commit 上,同時會根據後面追加的參數(常見的有 --mixed--soft--hard),會決定原本那些 commit 的檔案跟目錄的去留。使用預設的 --mixed 會把檔案留在工作目錄,使用 --soft 會把檔案跟目錄留在暫存區,而使用 --hard 則會把拋棄這些變化。

而不管是哪個參數,不只是 HEAD 的位置變了,整個歷史紀錄看起來也會有變化(變短或變長都有可能)。

git revert SHA1

這個指令會產生一個新的 commit,而這個 commit 的目的就是去取消(或該說是 undo)某些 commit 做的事情。

因為本質上還是 commit,所以整個 Git 的歷史紀錄不會變短,只會越 revert 越長。

狀況題

問:如果不小心把還沒合併的 develop 分支刪掉了,該怎麼辦?

不要這樣

「老闆,這是我的辭呈!」(遞)

應該這樣

不用緊張,分支就像是個貼紙的概念,commit 不會因為你把分支刪掉而消失,即使是還沒合併的分支也一樣。少了沒有分支貼紙貼著,Commit 會暫時看不到,只要把分支貼紙再貼回去就行了。

例如,原本的分支圖是這個樣子:

Git 分支刪除前

然後執行刪除 develop 分支:

$ git branch -D develop

它就會變成這個樣子:

Git 分支刪除後

東西還在,只是你現在看不到而已。要讓那些 commit 再次被看到,只要再貼一張分支上去就行了:

$ git branch new_develop 84aa2e6

這樣就會在原本 develop 被撕掉的那個位置上(在這裡也就是 84aa2e6),再貼一張叫做 new_develop 的貼紙,然後就看得到了。

你也許會好奇,那個 84aa2e6 是怎麼找到的。通常在你刪掉分支的時候,畫面上會有提示訊息:

$ git branch -D develop
Deleted branch develop (was 84aa2e6).

如果沒仔細看或是畫面被洗掉找不到了,也可以使用 git reflog 進去裡面翻看看,應該是找得到的。

參考資料 【狀況題】不小心把還沒合併的分支砍掉了,救得回來嗎?

問:不小心用 git reset --hard 指令把檔案處理掉了,有機會救回來嗎?

不要這樣

「就再重新下載一份就好啦(笑)」(好啦.. 也是可以)

應該這樣

放心,基本上東西進了 Git 就跟得罪方丈一樣不容易消失,它們只是以一種你肉眼看不懂的格式存放在 Git 空間裡,你可以透過 git reflog 指令去翻一下被 reset 的那個 Commit 的 SHA1 值,然後再做一次 hard reset 就可以把它救回來了。

你會發現,水能載舟亦能覆舟,git reset 也是 :)

參考資料 不小心使用 hard 模式 Reset 了某個 Commit,救得回來嗎?

問:你什麼時候會使用 git cherry-pick 指令?該怎麼使用?

當某些分支上的某些 commit 是你要的,但你又不想合併那一整個分支(也許是那個分支上其它 commit 是有問題的),便可使用 git cherry-pick 指令去複製想要的 commit 過來接在目前的分支後面。例如:

$ git cherry-pick 6a498ec

這個指令就會去撿 6a498ec 這個 commit 來接在目前這個 commit 後面。而且一次還可以複製好幾個:

$ git cherry-pick fd23e1c 6a498ec f4f4442

這樣就會一口氣複製 3 個 commit 來接在後面。

另外,如果在使用 git cherry-pick 指令時加上 --no-commit 參數,複製過來的 commit 不會直接接在後面,而只會先放在暫存區。

參考資料 【狀況題】如果你只想要某個分支的某幾個 Commit?

問:在專案中可能有些比較敏感或內容比較機密的檔案(例如 /config/database.yml ),在使用 Git 時,你通常會怎麼處理這類型內容比較敏感的檔案?

不要這樣

$ rm config/database.yml

(這可能跟提議中秋節改掉燒炭烤肉就可以減少輕生念頭的概念差不多吧…)

應該這樣

不只是比較敏感或比較機密檔案,有時候一些程式編譯的中間檔或暫存檔,因為每次只要一編譯就等於產生一次新的檔案,對專案來說通常沒有實質的利用價值,像這樣的檔案其實也不需要讓它進到 Git 裡。這時只要在專案目錄裡放一個 .gitignore 檔案,並且設定想要忽略的規則就行了。

參考資料 【狀況題】有些檔案我不想放在 Git 裡面…

問:想要知道這行程式碼是誰寫的?

不要這樣

在公司的群組軟體 mention 所有人,大喊「他 X 的這行 code 是誰寫的!」

應該這樣

$ git blame 檔案名稱

這個指令會列出這個檔案裡的每一行是誰在什麼時間、哪一次 commit 寫進去的,想賴都賴不掉。

參考資料 【狀況題】等等,這行程式誰寫的?

問:你用過 git push -f 指令嗎?在什麼情境下用的?

有啊,這還滿好用的。

有時候專案 commit 的歷史紀錄真的太亂了,我常會使用 rebase 指令來大刀闊斧的來修正一下,但 rebase 等於是修改已經發生的事實,等於是進入了一個新的平行時空,所以正常來說是推不上去的。這時候就可使用 git push -f 來解決這個問題。不過,使用前別忘了知會一下跟你同一個專案的隊友,請他們到時候以你這份進度為主。

另外,通常開一個分支出去執行任務,但做完發現 commit 太過瑣碎,我會使用 rebase 來整理一下這個分支。雖然 rebase 是修改歷史沒錯,但因為這個歷史影響的範圍只有我自己這個分支,並不會影響其它人正常使用,所以便可安心的使用:

$ git push -f origin feature/my_cat

像這種只用在跟自己有關、不會影響別人的分支上,使用 git push -f 是不會造成什麼問題。但不論如何,使用這個指令還是要多想幾秒鐘,確定指令沒有打錯再按下 Enter 鍵。

參考資料 聽說 git push -f 這個指令很可怕,什麼情況可以使用它

問:在團隊一起工作的時候,如果你推了一個 commit 到大家一起共同的地方,你發現這個 commit 有問題想取消它,你會怎麼做?

不要這樣

先這樣:

$ git reset HEAD^ --hard

再放大絕:

$ git push -f

打完收工!

Git Force Push

應該這樣

如果團隊不只你一個人,千萬不要隨便對共用的分支做 git push -f 啊。如果 commit 已經推上去而且已經有其它隊友在使用,這時候請發一個 revert 指令來取消某個 commit:

$ git revert SHA1

這樣就可以產生一個 commit 去取消你指定的那個 commit 幹的好事。

參考資料 Reset、Revert 跟 Rebase 指令有什麼差別?

問:如果你正在某個分支進行開發,突然被老闆叫去修別的問題,這時候你會怎麼處理手邊的工作?

有幾種做法,一種是直接先 git commit,等要處理的問題解決後再回來這個分支,再 git reset 把 commit 拆開來繼續接著做。

另一種做法,則是使用 git stash 指令,先把目前的進度存在 stash 上,等任務結束後可以再使用 git stash popgit stash apply 把當時的進度再拿出來。

參考資料 【狀況題】手邊的工作做到一半,臨時要切換到別的任務

問:如何把好幾個 Commit 合併成同一個?

不要這樣

先這樣:

$ rm -rf .git

把歷史紀錄清掉,再重新初始化:

$ git init

最後再把它一口氣加回去…

$ git add --all
$ git commit -m "add all files"

應該這樣

要把多個 commit 併成同一個,可使用 git rebase 的互動模式(interactive)指令:

$ git rebase -i SHA1

在這個「互動模式」,選擇 squash 選項然後繼續進行 rebase,順利的話,便可把多個 commit 併成同一個。

參考資料 【狀況題】把多個 Commit 合併成一個 Commit

問:假設某位平常沒在用 Git 的主管跟你說:「你可以把剛剛那批更新的檔案給我嗎?」,你會怎麼做?

不要這樣

直接跟主管說「你可以去學一下 Git 嗎?」或是「你可以自己上去 GitHub 看嗎?」

應該這樣

你可以打開圖形介面軟體(例如 SourceTree),一個一個把修改過的檔案挑出來,或是用終端機指令:

$ git diff-tree HEAD --name-only -r --no-commit-id

git diff-tree 指令可以用來比較兩個 commit 之間的差異,後面的 --name-only 是指「只要列出檔名」就好,-r 參數是指目錄、子目錄、子子目錄,全部都列出來。

如果只提供一個 SHA1 給它,Git 就會拿這個 SHA1 的上一次 commit 來跟它進行比較,所以給它 HEAD 參數,就會得到最近這次跟上一次 commit 的差異的檔案列表。

最後,可再搭配 git archive 指令,一口氣可把上面 git diff-tree 列出來的檔案打包成一份壓縮檔,然後就可以拿這包檔案給長官交差囉。

問:空的目錄無法被加入 Git 版控,如果這個目錄是個重要的目錄一定要放進 Git 版控的話,你會怎麼處理?

因為 Git 是根據「檔案的內容」來做版控的,如果目錄裡沒有任何檔案,這個目錄就不會被 Git 看在眼裡(就是無視啦)。只要在裡面隨便放一個檔案就搞定了,但如果沒什麼東西好放,可以放個 .keep 的空檔案在裡面。

參考資料 【狀況題】新增目錄?

問:要怎麼從過去的某個時間點的 Commit,再開一個新的分支執行新任務?

有好幾步的做法:

第一步,先回到過去(例如 657fce7):

$ git checkout 657fce7

接著就可以從這個地方開一個新的分支:

$ git branch my_cat

或是這樣也行:

$ git checkout -b my_cat

但如果要快的話,可以直接一行搞定:

$ git branch my_cat 657fce7

意思就是「在 657fce7 這個 commit 上,貼上一張名為 my_cat 的分支貼紙,然後結束這回合」。

參考資料 過去的某個 Commit 再長一個新的分支

問:當執行 git push 指令之後出現這個訊息:

$ git push origin master
To /tmp/git-kata.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to '/tmp/git-kata.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

是什麼意思?又該如何解決?

不要這樣

「這我懂,只要使用原力…沒有不能解決的問題!」

$ git push -f

Git Force Push

應該這樣

會發生上面這個訊息,是因為有人在你執行這個 push 指令之前,先推了另一份上去,導致你的 commit 沒辦法順利的接在你期望的進度上。這並不見得是發生衝突(Conflict)的情況,是不是衝突等 pull 下來就知道了。這個問題的答案其實就在上面這串錯誤訊息裡,只要先執行 git pull 指令,把線上的進度拉一份下來(通常這時候會在你本機進行合併),確認沒問題之後就可以再推上去了。

參考資料 【狀況題】怎麼有時候推不上去…

問:有時候回到過去的某個時間點的 Commit 的時候會看到這個訊息:

$ git checkout 5bca205
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 5bca205... add database config files

這是什麼意思?又該如何解決?

這一大串看起來有點像錯誤訊息的訊息,意思是目前的 HEAD 並沒有指在某一個本地的分支上,然後就只是「告知」你現在是處於這個狀態罷了,並不是什麼太大的問題。只要讓 HEAD 再回到任何一個本地端的分支就行了,例如:

$ git checkout master

就沒事了。

事實上,HEAD 不是只要有指到某個分支就好,如果指到的是一個遠端分支,例如 origin/master 的話,一樣會出現 detached HEAD 的訊息。

參考資料 detached HEAD 是怎麼一回事?

問:有時候在切換分支的時候會看到這個訊息:

$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
	index.html
Please commit your changes or stash them before you switch branches.
Aborting

然後沒辦法順利的切換分支,這時該怎麼解決?

如同錯誤訊息提示的「Please commit your changes or stash them before you switch branches.」,如果你剛剛做的修改還想要留著,可以先 commit 把東西存下來,待稍後再回來 reset 繼續處理,或是使用 git stash,稍後再回來使用 git stash popgit stash apply 拿回來繼續處理。

如果這些修改不想要了,除了可使用 git reset HEAD index.html 或是 git checkout index.html 把這些修改取消掉之外,也可在 git checkout 的時候多加個 -f 參數,直接硬切過去:

$ git checkout master -f

問:剛剛把 feature/develop 這個分支合併到 master 分支(如圖),該怎麼取消剛剛這次的合併呢?

合併前:
Git Merge Before

合併後:
Git Merge After

不要這樣

「先把剛剛的有修改檔案 copy 出來放在別的地方,然後重新 clone 一份專案下來,最後再把剛剛的那些檔案丟進去,結案!」

應該這樣

其實不用重新下載一份啦,只要把分支變成「合併前的樣子」就行了。以上面這個圖例來說,合併前 master 的 commit 是 bb92e0e,所以只要這樣:

$ git reset bb92e0e --hard

就可以變成合併前的樣子。或是,你不想輸入 bb92e0e 這麼難記的數字,也可以使用「代名詞」加上「相對定位」的用法:

$ git reset HEAD^ --hard

它就會變成 HEAD(bfd8e86)的「前一個」commit,也就是 bb92e0e 的樣子,也就等於取消了剛剛這次的合併了。

參考資料 【狀況題】剛才的 Commit 後悔了,想要拆掉重做)

問:剛剛把 feature/develop rebase 到了 master 分支(如圖),該怎麼取消剛剛這次的 rebase 動作?

rebase 合併前:
Git Rebase Before

rebase 合併後:
Git Rebase After

不要這樣

「就再去下載一份就好啦!呵呵」(好啦,老實說我也幹過這種事…)

應該這樣

rebase 不比一般的 merge,如果直接使用 git reset HEAD^ --hard 是不會達到你想要的效果的。有個比較簡單的方式,是可以透過 ORIG_HEAD 這個紀錄點切回 rebase 之前的狀態:

  $ git reset ORIG_HEAD --hard

這個 ORIG_HEAD 會記錄最近危險操作(例如 reset、rebase、merge 等操作)之前的紀錄點,但只會記錄最後一次。所以萬一不小心被洗掉的話,可以再去翻一下 git reflog,應該也是可以找得到 rebase 之前的 commit 的 SHA1 值,然後再執行這個指令:

$ git reset REBASE前的SHA1值 --hard

就可以取消剛剛這次的 rebase 了。

參考資料 另一種合併方式(使用 rebase)

問:如果你發現某位豬隊友使用了 git push -f 把線上的東西蓋掉了,該怎麼辦?

不要這樣

Flip Desk

應該這樣

Git 是一種分散式的版控系統,你或是其它隊友應該都有之前的進度,所以只要由你或是有正確進度的隊友再次進行 git push -f 指令一次,把正確的內容強迫推上去,蓋掉前一次的 git push -f 所造成的災難。

當然,完事之後,也找那位同事好好聊聊,看看是不是有什麼困難才會在沒通知隊友的情況下選擇使用 git push -f 指令。

參考資料 【狀況題】聽說 git push -f 這個指令很可怕

其它

問:公司裡目前沒在用 Git 或是其它版控系統,你會怎麼說服主管或老闆做這件事?

這題沒有標準答案,請自行發揮,歡迎把您的答案寫在下方的留言裡跟大家分享。或是,你希望我去幫忙說服也沒問題,歡迎來信聊聊看怎麼著手進行 :)

Comments