2016年12月31日 星期六

如何在Pull request 被merge/revert 後再送 pull request

自從換了智慧型手機,又搭配1.5G的網路之後,用手機上網的機會大增,在用Firefox 刷Facebook 的時候,很容易跑出廣告來,於是發想把「在Yahoo 台灣大殺四方驚動萬教的人生溫拿勝利組強者我同學 qcl 大神」所寫的神專案QClean 移植到Firefox Mobile 上面。

這部分後來還在努力中,目標是把QClean 的Add-On 移植到Mobile上,研究時,Mozilla 的網頁都跑出一直跑出提醒:Add-on 寫的 plugin 很快就會被淘汰,請改用 Web extension 來寫,想說就順便,在寫mobile version 前,先把本來用 add-on 寫的QClean for Firefox 搬移到 Web-extension 上面。
總體來說是沒什麼難處,把原本用 Web-extension 寫的QClean for Google Chrome,複製一份,然後把裡面web extension 的API 從 chrome 取代為 Mozilla 所用的 browser ,結果就差不多會動了,真的是超級狂,整體來說沒什麼工作。

到時再送出Pull Request 之後,發現寫的Makefile 裡面有typo,雖然緊急在Pull Request 下面留言<先不要merge>,但在作者看到之前已經被merge 了。
接下來出現一個很有趣的狀況,直接幫我把 typo 修好並commit 之後,對上游的master branch送出新的 Pull request,github 卻顯示這個 Pull Request 有衝突,沒辦法像之前一樣直接合併。

後來想了一下才發現問題在哪裡,目前repository 的狀況大概像這個樣子。


問題在於送出新的 Pull Request 的時候,github 會比較兩個 branch 最近的共同祖先,從那個 commit 開始算pull request,在上圖中會是那個”Add features 2” 的commit。
這時我們從”fix some typo” 送了PR,github 會比較這個新的commit ,跟revert 過後的commit 作比較,因為revert 這個commit 修改的檔案(等同我們Pull Request的修改只是剛好反向),兩者因此有衝突;同時,之前的 commit “Add features 1, 2” 都不會算在這個Pull Request 裡面。

要修正這個問題,我們必須讓 upstream 跟現在這個 develop branch完全沒有共同祖先,我所知的有兩個解法:
一個是從開始的master branch開一個新的branch newDev,然後用cherry-pick 把develop 上面的 commit 都搬到這個branch 上,再重送 Pull Request;就如這篇文中所示:
http://stackoverflow.com/questions/25484945/pull-request-merged-closed-then-reverted-now-cant-pull-the-branch-again

$ git checkout master
$ git checkout -b newDev
For every commit in develop branch do
$ git cherry-pick commit-hash

另外一個方法比較方便,直接在 develop branch 上面切換到 newDev branch,然後使用 interactive rebase ,然後把所有的commit 都重新commit 一篇,newDev 就會變成一個全新的分枝了:
$ git checkout develop
$ git checkout -b newDev
$ git rebase -i master
Mark every commits as reword (r) in interactive setting

修正完之後的 repository 大概會長這樣


這時候就能由 newDev branch 對上游的 master 送出Pull Request,把這次的修改都收進去了。

題外話:
寫這篇時發現了這個工具,滿好用的,可以輕鬆畫各種git 的圖,雖然說試用一下也發現不少bugs XD,不過這篇文中的圖都是用這個工具畫的:
https://github.com/nicoespeon/gitgraph.js

2016年12月30日 星期五

使用git hook在commit 前進行unittest

使用 git 做為版本控制系統已經有一段時間了,最近在寫Facebook message viewer 的時候,就想到能不能在本地端建一個 CI,每次 git commit 的時候都會時自動執行寫好的測試?
查了一下就查到這一個網頁,裡面有相關的教學:
https://www.atlassian.com/continuous-delivery/git-hooks-continuous-integration
在這裡記錄一下相關的設定:

Git hook 可以想成git 的plugin system,在某些特定的指令像是commit, merge的時候觸發一個script。
Hook 可分為 client side 和server side,又有 pre-hook 跟 post-hook 的分別,pre-hook 在指令執行前觸發,並可取消行為;post-hook 得是在指令結束後執行,它無法取消行為只能做其他自動化的設定。

網頁中有給一些使用範例
Client-side + Pre-hook: 檢查coding style
Client-side + Post-hook: 檢查repository 的狀態
Server-side + Pre-hook: 保護master
Server-side + Post-hook: broadcast 訊息

所有的hook 會放在.git/hook 資料夾裡面,在開啟專案時就有預設的一些範例,只要拿掉檔名的 .sample就能執行,它們是從 /usr/share/git-core/templates/hooks/ 複製而來。
望檔名生義,這些檔名大多直接了當的指名它們的功用;有些script 如 pre-commit 在回傳非零值時,能阻止git 接受這次commit;詳細的使用方法跟觸發時機請見參考資料。

這裡我想做的是用 Client-side + Pre-hook ,在每次commit 前都先跑過一次測試,如果測試不過就拒絕此次commit,所以我們用到的是 pre-commit。
首先先在project 裡面加上一個test.py,用來統整所有的test script,這樣就可以用 python test.py 跑過所有測試,可以先在command line 測一下:
python test.py || echo $?

pre-commit script 就很簡單:
#!/bin/sh

python test.py || exit 1
這個|| exit 1 是要確保pre-commit script 一定以錯誤結束,如果這個測試之後就沒有其它測試就不需要這段,因為script 會回傳最後一行command 的回傳值。
另外,如果不想在打下commit 的時候出現 unittest 的輸出,可以改寫成:
#!/bin/sh

python test.py 2>/dev/null
if [[ $? -ne 0 ]]; then
  echo "> Unit tests DID NOT pass !"
  exit 1
fi
注意後面的判斷跟echo 是必要的,否則python 輸出導向null之後,使用者打git commit 會變成完全沒有反應,這顯然不是個好狀況;完成之後,試著下git commit,就會發現test 不過,而不像正常流程一樣跳出commit message編輯器:
Garbage@GarbageLaptop $ git commit
.F
======================================================================
FAIL: test_zh_tw (__main__.REdictParseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 17, in test
assert(ans == parse)
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.007s

FAILED (failures=1)

我的建議是在 test case 有一定規摸的時候,足以進行TDD 的時候才引入此流程,或者要先把test 裡都加上expected failure ,否則有test 不過就會讓git 根本commit 進不去,如果又因為過不了就每次都下git commit --no-verify,就失去TDD 的意義了
另外,請記得所有要跑的 script 一定要有執行權限,之前試了一段時間,每次commit 還是都commit 進去,後來發現是 pre-commit 沒有執行權限lol;hook 內的東西clone 的時候也不會複製到 client side,要把它們包到repository 裡面,再由使用者自己把script 加到.git/hook 裡。

這裡大致介紹 git hook 的簡單用法,網路上能輕易找到許多亂七八糟的script 來做各種事,例如用autopep8, cppcheck 檢查語法是否合標準,自動跑npm test 等等。

參考資料:
git hook in gitbook
https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
stackoverflow about pre-commit and unittest
http://stackoverflow.com/questions/2087216/commit-in-git-only-if-tests-pass

2016年12月13日 星期二

那些在工作上看到的各種東西

十二月小弟剛結束手邊的一個從七月延續到現在的案子,在這為期5個月的工作過程中,個人也有不少收穫,在這裡寫篇文章記錄一下。

溝通:

這次的案子是在不同的平台間移植,工作上沒遇到太多技術問題,在對方公司原有架構上,把平台相關的程式碼換成目標平台的程式即可,撰寫的程式碼不超過300行,只花大約一週半的時間,相關的程式碼就已經寫完了。
後來之所以會拖這麼久,一個原因是對方公司的程式碼還在不斷的更新,而我們必須將這些更新都整進我們的程式中,並且還要在機器上測試過。
這次真的是見證軟體工程,最重要的是團隊間的溝通,少了溝通再怎麼會寫也沒有用,整合上很多問題,諸如架構、測試、環境設定、測試硬體,都是由我的頭頂上司出面,多多溝通就解掉了,溝通的能力有時比技術能力還要重要。


架構:

整個過程中不斷撞牆的部分,在於整個Project 的開發過程中幾乎看不到架構設計的概念,感覺上就是東改一些西改一些,沒有一個系統性的規劃;甚至到現場準備產品發佈的前兩週,突然出現架構的大幅修正,我們整合的部分當然也要收進去,也因此在週末加了一天班(yay。
後來在言談中得知,對方公司的確沒有架構工程師這個職位,那個<架構的大幅修正>只是某個資深工程師這麼寫,其他人看看覺得不錯,其他專案也就跟著這麼做。
對方公司也算台灣有點大的硬體廠,軟體部門的狀況卻是如此,幾乎接近各程式設計師各自為政,知道了還是覺得有些心寒,不知道更上面的廠商狀況會不會好一點?

開發過程中,我自己也犯了類似的錯,應對方要求,我們這邊要整合兩個不同平台的程式碼,我竟然把所有的程式碼放在同一個檔案再用編譯macro 隔開,這當然不符合軟體工程的概念,只要加上更多平台,程式碼必定會增長到難以維護。
後來反省,的確是我的經驗不足才會這樣寫,真是幾年程式寫下來,軟體架構的概念還是沒練成,也難怪要成為架構工程師,都要先經過好幾年的軟體工程師的訓練。

測試:

這是專案中移植的目標平台,使用了一家中國廠商提供的SDK,無奈這家廠商有點兩光,通常會送有問題的程式碼過來,文件也寫得不清不……啊不對是通常沒什麼文件。
一開始在做整合的時候,因為sdk有問題的關係,我們的程式碼執行不定時間就會造成其他的process crash,從log 上來說就是系統上某process (而且每次的process 還不一定一樣)segmentation fault 了,跟這篇文章描述的有 87 % 像
當下無論是我們亦或對方公司覺得是自己的錯,但程式碼怎麼看都沒有問題,無奈它又是個不穩定的錯誤,在實際環境上測試非常花時間,在這個問題上面就花了近一個月的時間。
後來是利用一支之前用過的測試程式,可以用迴圈不斷執行核心功能,透過執行這條測試時也會crash,確認問題是出在核心功能而不是其他地方。

實際測試需要手動碰觸控螢幕,測試程式只需要用終端機執行,透過測試程式才能快速的註解掉可能有問題的部分,再測試會不會crash 來判定問題在哪,最後才定位出問題出在對方的sdk中,回報上去才拿到修正這個問題的新版sdk 。
覺得就算沒有要用到TDD,在專案中維持一些簡單的測試,光是簡單的回歸測試都能預防開發中可能的問題,就算真的出問題找不到在哪時,也能利用測試來定位錯誤。

能力:

這次發覺,自己的能力和對方公司的工程師比起來並沒有差太多,一些方面可能還超前對方。

很多工具例如git 的使用,如何透過Format patch同步兩個git repository 的狀態(當然對方公司內部是用svn ,所以這部分比較勝之不武),shell script 一些工具,我也比對方公司的工程師知道多一些些,工作的效率自然更高
我個人是認為,對方工程師雖然有較長的工作經驗,卻沒有-亦或沒有辦法-持續的和外圈技術圈交流,學習和知道一些最新的小工具:例如我在Code & Beer 聊天中知道的 autojump
一些無名的小工具能大幅提高生產力,卻未必能大紅大紫,是在小眾的技術圈中流傳,多多交流還是能有效擴展自己的見聞。

工具:

這次也學到,平時多多熟悉一些工具的使用,用到的時候可以發揮很大的助益
案子的結尾,有機會跟對方公司的工程師一起到客戶現場去準備產品上線,到現場是個很新奇的經驗,但工作環境實在不太好,程式碼不讓我們用版本控制就算了,連網路也不讓我們使用,遇到問題都只能問男人。
沒辦法查網路的時候真的是在比底力,平常有沒有熟悉shell 工具和程式語言這時立分高下。
我們又被現場公司雷:他們的環境裡有他們自己的設定,造成我們的code 放上去,clean 重build 的時候,把他們的設定移除,造成機器當掉,又是感覺起來是我們的程式有問題
其實只要把他們的設定再裝一次就解決掉了-但從沒人告訴我們,為了抓這個問題花了兩個工作天,大部分時間都在比較build log,在不同版本間進退,也因此用到不少 shell 工具。

我整理記憶上用得上的工具:
一套版本控制系統,至少要能做到在兩台機器上同步,我是用 git
至少一款diff 工具,我是用 vimdiff
檔案關鍵字的搜尋工具:shell grep
找檔案工具:shell find
簡單的 shell script,一旦發現到有重複性的工作,可以的話就盡快改寫成shell script,可以加速工作的效率。
上面這些除掉網路還要能熟練地使用。

總結上面五個,這五個月來接這案子的小小心得,記錄一下。


註:照片有不少篇幅都是用Google Doc, Voice Typing 的功能打的,覺得直接用講的雖然可以省下打字的時間,還有簡省手力,不過事後校稿也滿耗時的。
感覺用說的句子跟用手打出來的風格不太一樣,句子拉得比較長,贅字跟語助詞較多,如果是先寫在紙上再打到電腦又會是另一種風格,挺有趣的。