顯示具有 git 標籤的文章。 顯示所有文章
顯示具有 git 標籤的文章。 顯示所有文章

2020年5月16日 星期六

第一次跳槽 vscode 就上手

故事是這樣子的,小弟第一次學寫 code 的時候,是在大一修計算機程式(嚴格來說是高三下學期上了幾個小時的 C,不過那實在稱不上是"學")的時候,第一個使用編輯器是破舊破舊的 Dev C++ ,我打這篇的時候差點都忘了它叫 Dev C++ 了。
當然那時候的功力跟現在實在是天差地遠,淨寫一些垃圾,啊雖然現在也是淨寫一堆垃圾…。
總之後來應該是大二,被同學們拉去演算法課上當砲灰,第一次接觸了工作站 + vim,從那時候把 Dev C++ 給丟了跳槽到 vim,就一直用到現在,之中當然也會用一下其他的編輯器,像是改 windows 的 .NET 程式用到 Visual Studio,但大體還是以 vim 為主力,算算也是超過 10 年的 vimer 了。

不過這兩三年在工作上、日常 project 上面,多多少少都見識到 vim 的不足之處,例如新語言(主要是 Rust)支援不足、跟編譯除錯工具整合不佳、跟 GUI 整合不佳、跟 Git 整合不佳要另外開終端機跟 gitg、自動格式化/排版操作麻煩而且通常排不好;正好此時 Microsoft 回心轉意擁抱開源,推出了 vscode,隔壁棚的 emacs 有大神跳槽鬧得風風雨雨,台灣 CUDA 第一把交椅強者我同學 JJL 也跳槽 vscode 惹還來傳教。

正好最近寫 code 沒什麼靈感,而且最近正好武漢肺炎的關係時機歹歹,就來試著跳槽一下吧(?,到目前為止用 vscode 對最近碰的一個 ncollide package 做了一些除錯的工作,筆記一下到目前為止的設定還有使用方式的筆記。

vscode 基本上的優勢就是是它編輯/建構/除錯三位一體的編輯介面;還有它的擴充功能,用過的都說讚。
擴充方面主要參考的文件有兩個:VSCode 如何提高我的寫扣效率小克的 Visual Studio Code 必裝擴充套件,另外台灣 CUDA 第一把交椅強者我同學 JJL 大大也有推薦一些:

擴充的安裝方式是按快捷鍵 Ctrl + P,打入 ext install 後面接套件名,下面擴充的連結裡面也有顯示安裝的指令:

vim 擴充,讓 vscode 的編輯介面套用 vim 的操作方式,想要手跟鍵盤黏踢踢就一定要裝
語言相關:
C/C++ 擴充:還沒試用只是覺得起家的 C++ 必須裝一下:
Python 擴充:一樣還沒試用只是覺得有一天會寫到先裝一下:
Rust 擴充:這個是這次語言唯一試用過的,雖然結果不怎麼樣
codelldb 除錯擴充,可以用 LLVM 的 lldb 對程式除錯,裝了這個是為了要對 Rust 除錯

工具類:
Git Graph:整合 gitg 類似的圖形化顯示工具到介面,git 管理上當然可以靠打字,但看歷史還是看圖方便
GitLens:還沒試過,強者我同學 JJL 推薦的
TODO tree:統一管理 project 內部的 TODO, FIXME, XXX
Trailing Spaces:自動刪掉程式碼行尾的空白
Markdown Github Style:編輯 markdown 文件時可以直接預覽輸出的格式,解決每次編輯 Github README.md 都要一直 push -f 直到格式完全改對為止,這點很強烈的突顯出 vim 等純文字編輯器的弱項,無法和圖形整合,以致在 markdown、LaTex 這類文字和顯示有相互關係的文件編輯會很吃虧(好啦好啦我知道有人能人腦 render latex 的)。

怎麼建構專案?
在 vscode 裡面的建構叫 task,在選單 terminal 下面的 run Task 跟 run Build Task (Ctrl + Shift + B),沒有 cargo 預設的話就要自行編輯 tasks.json,以下是我這次 debug 時使用的 tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "cargo run",
      "type": "shell",
      "command": "cargo",
      "args": ["build"],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}
應該滿直覺的,就是呼叫 cargo build 幫我編譯整個專案;在寫完 code 之後使用快捷鍵 Ctrl + Shift + B 就能編譯專案了。

如何除錯:
除錯是 vscode 一項殺手級的功能,vscode 公開一個 API 讓安裝的語言擴充使用,需要什麼語言的除錯安裝擴充就好,像我上面就安裝了 C/C++, Python, Rust 的擴充。如果我記憶沒錯的話,跟 visual studio 一樣,vscode 快捷鍵是也執行 Ctrl + F5 跟除錯 F5:

用 Ctrl + Shift + D 展開 debug 介面。
理論上用滑鼠在原始碼旁邊點一下就能加上 breakpoint 不知道是 Rust 還是 lldb 的問題,我用滑鼠加上去的 breakpoint 都煞不住,至少一定要先在 debug console 裡下一個 b main 讓程式煞住之後,用滑鼠加的 breakpoint 才會有用,真的很奇怪。
另外就是 debug console 的指令跟習慣的 gdb 有點不同要重新習慣,最奇怪的大概是按了 enter 竟然不會重複上個指令,這樣要一直按 n + enter + n + enter 怪麻煩的,只能去習慣 vscode 的除錯指令:F10/next、F11/step、Shift+F11/finish 了。

這次最主要的目的是要對 Rust 程式除錯,我參考的是下面這篇文章…不過試用之後沒有成功
首先我們要加上一個 launch.json 告訴 vscode 要怎麼跑除錯的程式:
{
  "version": "0.2.0",
  "configurations": [
  {
    "name": "Debug example contact_query2d",
    "type": "lldb",
    "request": "launch",
    "program": "${workspaceRoot}/target/debug/examples/contact_query2d",
    "args": [],
    "cwd": "${workspaceRoot}",
  }]
}
再來用 F5 就能開始除錯了,但不知道為什麼我 step into 一個函式,瞬間都變成 assembly code,連 stack 資訊都爛掉了,根本無從 debug 起,感覺是 vscode 哪裡跟 lldb 沒弄好,不過我覺得這不是 vscode 的問題,畢竟我在終端機用 rust-gdb 一樣會有問題,正好反過來如果下 b main 停下來的話,rust-gdb 下一步會停不下來,一口氣跑到 main 的尾巴…。
這個問題一時之間好像無解,也許要等 rust 跟 codelldb/gdb 真的接好之後再來看看了。

下面就是個零碎操作:
Ctrl + KT 叫出 color theme 設定,我用的是 light 的 Solarized Light ,最近眼睛好像不太適合全黑的畫面了QQ。

回頭一看怎麼一堆快捷鍵,不過算啦,跟 vim 的快捷鍵比起來這還是算少的吧XD;話說大概是「把手留在核心區」這個哲學的關係,vim 大部分的按鍵都少有用 Ctrl/Alt 開頭的,剛好一般的圖形應用程式包括 vscode ,大部分的快捷鍵都是 Ctrl/Alt 開頭,也因此在操作上面,vim 很容易就能跟桌面應用程式整在一起,就像是瀏覽器的 vimium 跟 vscode vim 擴充。

2018年1月25日 星期四

使用 git submodule 管理 project 所需的其他模組

故事是這樣子的,最近在寫一些分析資料的程式,用的是 python 跟 numpy。
一開始改寫的時候,發現一開始 load 資料的地方,Python 實在太慢了,載入 20000 筆資料耗時超過 150 秒,後來決定用 C++ 改寫載入數據的部分,同時利用別人寫好的 cnpy 這個 project,寫出 numpy 檔案作分析,結果載入速度竟然一口氣降到 4.5s,加速 15 dB,太可怕了(yay。
為了要使用 cnpy 這個 project,順手研究了一下要如何給使用 git submodule,這篇文章就做個介紹:

基本上submodule 利用的場合,就如我上面說,我的 project 要用到其他 project 的程式碼,我希望讓我的使用者能直接拿到其他 project 的程式碼,這樣他們不用自己再去載,麻煩之外可能還會載到錯誤的版本。
另一方面,我們又不想把對方的程式全部加到我的 repository 裡面,這樣會帶來不好的後果,上游的程式碼修改,要自己手動更新,沒辦法用 git 的方式更新,增加錯誤的機會。
git 針對這個使用情景的解決方式就是 submodule:

遇到 submodule 的時候通常有兩種狀況,第一種比較常見的,是載了一個別人的project,發現裡面有用到 submodule,例如知名的補齊工具 YouCompleteMe ,裡面針對各種語言的剖析工具都是 submodule,這些專案載下來的時候, submodule 裡面都還是空的,要先用下列指令把 submodule 載下來:
git submodule init
git submodule update
或者可以用一行指令解決:
git submodule update --init --recursive
--recursive 會在 submodule 裡面還有 submodule 的時候,一口氣都設定好。
又或者可以在 clone 專案的時候就指定要一齊複製 submodule(不過通常在 clone project 的時候還不知道裡面有 submodule,所以…通常不會這樣下):
git clone --recurse-submodules url

第二種狀況如我上面所述,我們自己新增一個 submodule,我要做的就是新增 cnpy 為我的 submodule:
git submodule add git@github.com:rogersce/cnpy.git cnpy
後面的 cnpy 是指定 submodule 要放在哪個資料夾裡面,通常名稱都跟 project 本來的名稱相同,才不會搞混;這時候記得會開始把這個 project 下載下來,接著檢視 status 的話會看到下面的內容:
new file: .gitmodules
new file: cnpy
.gitmodules 檔案裡面記錄了 submodule 的名字,現在的路徑以及遠端 url,這是可以用 add 及 commit 將這個 submodule 保存下來。

在一個有 submodule 的專案裡面工作,會和一般的工作內容稍有不同,當進到 submodule 內部的時候;submodule 從外面來看只是一個參照,如果真的進到這個資料夾,用起來就會像另外一個 git repository 一樣,一樣會有 master 等等 branch,可以用 remote 拉別人的修改下來,或者自己送 commit 出去。
比較讓人疑惑的通常是在外面的 project,當內部的內容有修改的時候,外面會出現一些讓人很疑惑的訊息,例如當我們對 cnpy 這個 project 新增一個 commit,從外面會看到這樣的訊息:
git status
modified: cnpy (new commits)
這則訊息的意思是,cnpy 這個 submodule 有了修改,修改的內容是新增了 commits;可以把git submodule 想成一個快照,現在 submodule 的狀態已經脫離這個快照,從 git diff 就會看出差別,最下面是 commit 的修改訊息:
git diff
diff --git a/cnpy b/cnpy
index f19917f..8f997be 160000
--- a/cnpy
+++ b/cnpy
@@ -1 +1 @@
-Subproject commit f19917f6c442885dcf171de485ba8b17bd178da6
+Subproject commit 8f997be1f87279f09054acbdb896162b1e9d3963

這時對這個 submodule 做 add, commit,就會更新這個 submodule 的快照值;另外如果我們想要 submodule 維持在之前的快照上,用 git submodule update ,git 即會將 submodule 簽回到當初記錄的版本:
git submodule update
Submodule path 'cnpy': checked out 'f19917f6c442885dcf171de485ba8b17bd178da6'

不過 update 之後會有一些不好的效果,因為這時 submodule 被強制簽出 f19917 這個 commit ,裡面就出現了一些沒有 commit 的修改,在這裡有內容未被 commit,所以它會顯示:
git status modified: cnpy (untracked content)
從外面會看到 submodule 有修改,但要消掉這個 untracked content 的訊息,就要進到 submodule 資料夾裡面,用 checkout 或 clean 的方式,讓 submodule 的狀態回到 clean 才行。
但同時也要注意的,這時候 submodule 就處在 detach HEAD 的狀態(在上面的例子,submodule 落後 master 一個 commit ),這時進到 submodule 做些 commit,這些 commit 並沒有 branch 參照,下一次再下 submodule update 的時候,所做的修改就會消失,如果有要修改的話,建議要先在 submodule 裡面生成一個 branch 來保留修改。

另外一個比較需要注意的,大概就是在移動 submodule 的參照的時候,儘量可以用 git mv 的方式來移動,用 os 本身的 mv 似乎比較容易出問題。

我想 submodule 我們就講到這裡,下面這篇官方的參考資料:
https://git-scm.com/book/en/v2/Git-Tools-Submodules
裡面還有很多 git submodule 神奇的用法,例如從外面用 git submodule 指令一口氣更新所有 submodule 的狀態到最新,把所有 submodule 現下的狀態推送到遠端,等等。
但我個人認為 submodule 相對來說是比較偏門的指令,自己也是用了這麼久,最近才第一次用到 submodule,大家還是用到再來查文件比較實在;另外話又說回來, git submodule 能管理的相關 project 數量也是有個限度,數量多到一定程度,submodule 也會顯得捉襟見肘,因而 google android 才會另外推 repo 這樣大量 git repository 的管理工具吧。

2017年12月31日 星期日

Git 教學影片系列

故事是這樣子的,自己大概三四年前開始使用git,一路用到現在,對 git 相關的功能算是相當熟悉,有時也會負責教其他人使用 git,自己的 blog 上其實也留了不少 git 相關的文章:
https://yodalee.blogspot.tw/search/label/git
大約兩個禮拜前突發奇想,反正都要教,乾脆就錄個影片,以後只要貼影片給別人看,不只教認識的,還能教虛擬世界中「沉默的多數」(誤),想著想著稍微規劃了一下,好像還真的有點模樣,於是就開始了我變身網(ㄈㄟˊ)紅(ㄓㄞˊ)的第一步。

最後確定的版本有以下幾集的影片,一部是相關的功能,順便還會介紹一些社群習慣或是我自己的習慣,講得滿雜的,也有些出錯的地方:

Ep. 1 安裝與設定
Ep. 2 Add, commit:50/72 rule, gitignore, commit hash, DAG
Ep. 3 如何指定一個 commit:hash, HEAD, ^ ~, reference, show, log, diff, diff --staged
Ep. 4 patch add and amend
Ep. 5 branch, checkout, merge:解衝突
Ep. 6 Rebase:rebase -i,解衝突
Ep. 7 遠端開發,使用 github:用 gitgraph.js 的開發經驗,來說明如何使用 github
Ep. 8 stash
Ep. 9 format-patch, apply, am, cherry-pick 各種搬移工作的方法
Ep. 10 bisect, blame
Ep. End ending:講一下一些沒提的東西

自己做起來就發現,想當 youtuber 要費的功夫真的超級大,除了初期收集資料跟準備材料,投影片跟 demo 用的 project,還要確認要講的內容沒有錯誤;真正錄的時候可能還要多錄幾遍,確定哪裡說不好要改進,如果不小心說錯了,要重頭再錄一次,事後還要上傳 Youtube 修改影片資訊等等。
像後來,就發現我在安裝的那章其實有個錯誤,Windows系統不能只安裝 Tortoise git,還要安裝 git 才行…不過暫時還沒想到怎麼去修正它(yay;如果像那些網紅一樣還要加後製,那成本真的超級大,我猜不組個小團隊其實是很難撐起來的

總之這些是最後的成品,收到一個 youtube 播放清單中:
https://www.youtube.com/playlist?list=PLlyOkSAh6TwcvJQ1UtvkSwhZWCaM_S07d
或者下面是嵌入式的影片:

自己回想起來其實花了非常多時間在錄這些教學影片,還去買了新的麥克風,錄製跟剪輯影片分別使用 obs 跟 ffmpeg 剪輯指令,因為加特效太麻煩,所以就沒加特效 =w=,希望這些影片能對大家有所幫助。
雖然本人比較喜歡低調路線,不過想想,既然都花了這麼多時間,這些影片要是沒有人看就太可惜了,因此來學一下農場的做法,希望大家覺得影片有你幫助的話,就幫小弟分享一下,無論是這篇文章或是上面 youtube 播放清單的連結都可以。

ps. 也能透過blog 旁邊的連結,用 Paypal 給點賞,鼓勵一下小弟,不過這不強求啦,畢竟 Paypal 手續費抽滿貴的。

2017年7月3日 星期一

使用 git-svn 和 svn 遠端協同開發

最近因為跟人協作,共同開了一個版本控制資料夾。
對方使用的版本控制是 svn ,而我則是用 git,重新學 svn 實在太麻煩了,有沒有一個好的解決方案呢?經過強者我同學 AZ 大神跟 qcl 大神的推薦,決定使用 git-svn 來解決。

git-svn 是 git 提供的一個…橋接工具?可以在遠端保持 svn 的狀態,本地則用 git 的管理,享受 git 那些branch 開很大開不用錢,git stash之類,種種我們再熟悉不過的使用方式;另外用了 git 也不用每次都跟遠端目錄同步,可以在自己家裡亂搞,最後有網路時再一次同步。
畢竟在 git 出世前,svn 才是世界上版本控制的霸主,有不少早期知名的 project ,例如LLVM, apache software;用了 git svn ,不需重新熟習 svn 也能用 git 參與這些 project 的開發。

第一步,在拉下 svn repository 的時候,直接使用 git svn 的指令,所有跟 svn 相關的指令都是 git svn xxx:
git svn clone http://SERVER/svn/trunk/ TARGET_DIR
這樣就會把整個svn給抓下來,它同等於執行 git svn init 跟 git svn fetch。
要注意的是因為 git 設計的邏輯就是「所有的機器裡面都有完全一樣的內容」,所以 git svn clone 的時候,它會把遠端的內容逐個載下來,如果遠端 svn 很大的話,這個動作可能會花上非常久的時間。
抓下來的 repository會產生一個叫 git-svn 的 remote ,這個 remote 只有用 git svn 的時候會動到;要注意一點,因為 svn 只能維持一條線性的歷史,同時也沒辦法修改歷史,所以在使用 git-svn 的repository 裡面,不要和其他的 git 遠端同時使用,保持所有使用者都用一個 svn 遠端,git svn 設計上也假定你只有一個遠端。

再來我們就能做些修改,一樣就是git add, git commit,這時提交的內容只會在本地中,可以用:
git svn dcommit
把內容送到 svn 遠端去。
git 在推送到遠端 svn 的時候,會把一個一個 commit 取出,並提交到 svn,然後最重要的,它會依照svn 的提交結果,重新在 git repository 裡面 commit 這些結果,整個 dcommit 的結果,最終效果更像是 git rebase,這跟一般的 git push 完全不同。
commit aea3964417e62759dadf9e1769d927623e0f5a1b
Author: yodalee <garbage@mail.com>
Date:   Fri Jun 30 16:31:30 2017 +0800

    add debug message to every function call

commit 2531676d1d1b81f898e9965c0e46f28e92e02c82
Author: yoda <yoda@59464745-af19-4556-b8ec-ef3a2794439b>
Date:   Fri Jun 30 08:00:23 2017 +0000

    fix description in sensor function

    git-svn-id: http://SERVER/svn/trunk@2345 59464745-af19-4556-b8ec-ef3a2794439b
上面的 git log ,包含一個已經推到遠端的 commit 跟一個還未推送的 commit,推送到 svn 上的 commit 會出現 git-svn-id 的遠端資訊,同時它的作者資訊跟雜湊值也會變化,這也是為何不建議同一個 repository 中同時使用 git跟svn的遠端,git-svn 修改雜湊值會讓 git 遠端天下大亂。
就算要有 SVN 跟 git 兩個遠端也要先向svn dcommit ,得到最終雜湊值後,再推送到 git 遠端上。

svn 身為版本控制,也是允許其他人共同協作,只要有協作就會有衝突要解決,如果發生衝突,svn dcommit 會無法推送到遠端。
為了解決該問題,可以執行 git svn rebase ,它和 git pull 很像,首先它會用 git svn fetch ,把 svn 遠端上的內容拉下來,沿著 git-svn 往前長,之後再用 git rebase ,把現在 git HEAD 指向的目標,rebase 到 git-svn 上。
如果沒有更新的內容,在 git svn rebase 時會看到:
Current branch master is up to date.
此時就能放心進行 git svn dcommit

這裡會牽涉到一些 git 跟 svn 設計不同的地方,在 git 裡面,假設 remote/master 跟本地的 master 有所不同,在 push master 的時候即會發生衝突,git 會要求你解決衝突後才能 push。
svn 在這點上,只有檔案有所衝突的時候才會要求,所以當遠端修改 A 檔案,本地修改 B 檔案,在 dcommit 的時候是完全沒有問題--只是遠端專案會進到一個 A, B 檔案都修改過,而本地檔案卻沒看到 A 檔被修改的狀態。
直接引用 git 文件的話:「如果做出的修改無法相容但沒有產生衝突,則可能造成一些很難確診的難題。」所以,誠心建議還是在 dcommit 前都 svn rebase 一下,確保跟遠端保持隨時同步。

其實有了 dcommit 跟 rebase,大概也就差不多了,有關 svn branch 的部分我就不太想看了,畢竟 git branch 比較強大;唯一要注意的,大概就是要送到 svn 伺服器之前,儘量用 rebase ,把 git 的各 branch 收整成一條線性,再進行 dcommit 。另外有個小技巧是,git svn dcommit/rebase 在操作的時候不允許任何 uncommit 的內容,所以在 svn 操作的前後,可以利用 git stash push/pop ,把未commit 的內容塞進stash,svn 操作結束後再取出來。

參考資料:
Git 與 Subversion

2017年3月9日 星期四

使用 git patch 來搬移工作內容

前幾天在改一個專案,因為筆電的設備不夠強大,只能到桌電上開發,兩邊都是開發機,也就沒有用remote 的方式來同步專案。今天,要把那時候的commit 搬回筆電,只好使用git 的patch 移動工作內容,這裡記錄一下整體工作流程和解決衝突的方法。

首先是生成 patch,git 本身有兩種 patch 的功能,一種是使用常見的diff,一種則是 git 專屬的patch system。
常見的 diff 其實也就是git diff 生成的 patch,內容就是:這幾行刪掉,這幾行加上去,用 git diff > commit1.patch 就能輕鬆生成。git patch system 則是用 git format-patch 來產生,它提供比 diff 更豐富的資訊,他的使用有幾種方式:
git format-patch <commit>  從某一個 commit 開始往後生成patch
git format-patch -n <commit> 從某一個 commit 開始從前先成 n 個patch

使用format-patch的好處是,它可以一次對大量的commit 各產生一個patch,之後就能用git 把這些commit 訊息原封不重放到另一個 repository 裡面;我自己的習慣,是開發一個 features 就開一個新的branch,這樣在生成 patch的時候只要使用format-patch master,就會把這條分枝的 commit 都做成patch,git 會自動在檔案前綴 0001, 0002,這樣用 wildcard patch 時就會自動排序好。
你說如果 patch 的數量超過10000 個怎麼辦……好問題,我從來沒試過這麼多patch,搞不好我到目前為止的 commit 都沒這麼多呢,一般project 超過 10000 個commit ,結果要用patch system 來搬動工作內容也是滿悲劇的啦
細看 patch 的內容,開頭是這個commit 的概略訊息,修改的檔案小結,後面就是同樣的diff 內容。
From commit-hash Mon Sep 17 00:00:00 2001
From: author <email>
Date: Mon, 6 Mar 2017 14:07:42 +0800
Subject: [PATCH] fix getDates function

---
database.py  | 9 +++++----
viewer.py      | 5 +++++
2 files changed, 10 insertions(+), 4 deletions(-)

現在我們來使用 patch ,把這些patch檔拿到要apply 的repository裡
針對常見的diff,其實用shell 的patch 指令就能apply 上去,但它出問題的風險比較高,也不能把 commit 訊息帶上,所以我都不用這招。對format-patch產生的 patch,我們就能用 git apply 的指令來用這個 patch,首先先用
git apply --check patch
來檢查 patch 能不能無縫補上,只要打下去沒噴訊息就是正常。

接著就是用 git apply patch 把patch 補上,然後記得自己commit 檔案。
這樣還是太慢,我們可以用
git am *.patch
把所有的 patch 一口氣送上,運氣好的話,會看到一整排的 Applying: xxxxx,所有的commit 立即無縫接軌。

如果運氣不好,patch 有衝突的話,apply 就會什麼都不做。
衝突其實很常見,只要你產生patch 跟apply patch 的地方有些許不同就會發生,因為patch 裡只記載了刪掉哪些內容、加上哪些內容,一旦要刪掉的內容不同,apply就會判斷為衝突。這跟merge 的狀況不同,merge 的時候雙方有一個 base作為基準點,可以顯示 <<<<<< ====== >>>>>> 的差異比較,apply 的資訊就少很多。
在 apply patch 的時候,也可以使用 -3 來使用3方衍合,但若你的repository 中沒有patch 的祖先,這個apply 一樣會失效。
衝突時am 會跳出類似這樣的訊息:
Applying: <commit message>
error: xxxxxxxxxxxxxxxxxxx
Patch failed at 0001 <commit message>
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

這裡我們只能手動解決,在apply 的指令加上:
git apply --reject patch
這樣會補上那些沒有問題的patch, 然後把無法補上的地方寫入 .rej 檔案中。
下一步我們要用編輯器,打開原始檔跟 .rej 檔,把檔案編輯成應該變成的樣子,把該加的檔案變化都git add add 之後,使用 git am --continue 完成commit。
後悔了,直接用 git am --abort 停掉 am 即可。

以上是git patch system 的簡介,我的心得是,能用git remote 就用git remote,merge 起來資訊豐富很多,解決衝突也有git-mergetool 如vimdiff 可以用,省得在那邊 format-patch 然後apply 起來機機歪歪,每個commit 都要手動解衝突是會死人的。

參考資料:
https://git-scm.com/book/en/v2/Distributed-Git-Maintaining-a-Project
http://aknow-work.blogspot.tw/2013/08/patch-conflict.html

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

2014年11月24日 星期一

使用git rebase 進行Pull Request 檢測

故事是這樣子的,自從我被加到Qucs project的專案小組,原本的管理員又因為博班進到最忙的時間開始比較少管事,變成我在管專案的Pull Request(PR,拉取要求)。
其實管就管,反正這個project 沒人鳥,平常也沒什麼PR進來;不過有PR進來的時候,還是要做適當的檢查,以下提一下幾個檢查PR的流程:

首先是把PR拉到本地資料夾,這部分參考github 的支援文件:
https://help.github.com/articles/checking-out-pull-requests-locally/

$ git fetch origin pull/ID/head:BRANCHNAME
其中ID 是github上Pull request 的編號,branchname則是你隨便取;這樣就會把網路上的提交全部拉下來,並創建一個新的分枝。

接著就要做點檢測工作,最重要的是所謂的阿蹦大神規則:每個提交都必須能編譯成功
這裡我會選用git rebase 搭配execute:
git rebase -i master –exec “make -C path”
git 就會在每個提交間插入一個執行make 的command,要注意git 在rebase 時的目錄位置是在專案最頂層的目錄(.git資料夾的所在),所以make 的路徑必須設好,不然rebae 會找不到make檔案,直接出錯;git會輸出下列的文件,在提交間插入執行命令,如果有提交不想要執行,可以手動移除掉。
pick 92c96a8 Add Xcode support to gitignore
exec make -C qucs/build
pick 0cdd379 Bugfix: LANGUAGEDIR
exec make -C qucs/build
pick 9a695b0 Skip Qt3 support for qucs-help
exec make -C qucs/build
execute的規則如下:
執行的指令如果回傳值非零,表示執行出錯,rebase 即會暫停在當前的提交,讓你有機會修正錯誤,可以使用git rev-parse HEAD來抓到當前的提交雜湊。

這樣就能放著電腦一直跑,反正出錯了就會停下來,表示這個Pull Request是有問題的,還不能合併到主線內。

2014年8月27日 星期三

使用git bisect 搜尋災難發生點

之前因為強者我同學阿蹦大神的關係,接觸了neovim這個大型專案,光星星數就有9300多顆,是我星星最多的project的9300多倍lol。
雖然說看了幾個issue,大部分都插不上話--討論的層次太高了,偶爾有個好像比較看得懂的,trace下去之後提出解法,沒想到是個不徹底的解法,pull request就被拒絕了TAT,要參加這個超過9000顆星星的project,像我這種花盆果然還是「垃圾請再加油」

--

雖然說是這樣,但我還是趁這個機會,研究一下如何使用git bisect 在project裡面找到洞洞。
基本上project無論用了多少test,多少還是跟我的腦袋一樣有一些洞,要如何找到洞洞就是一門學問了,git 提供了git bisect這個指令幫助開發者找到出錯的地方

我們用一個比較小的project: pyquery來實驗這個功能,這是強者我同學JJL大神參與的專案

https://github.com/gawel/pyquery

我們trace 一下issue 74: Behavior of PyQuery...
因為這個issue 發生在v1.2.4,但到了v1.2.9已經消失了,那我想知道這從哪裡發生的(這種狀況比較少見,一般都是有錯要找哪裡出錯了),就用bisect 來找吧。

首先是bisect 的基本設定,我們要先啟動bisect,然後指定一個good commit 跟一個bad commit,而bad commit 在歷史上要比good commit 來得晚,bisect 會從bad commit 一路回溯到good commit 為止。通常可以透過checkout tag的方式作大範圍的搜尋,以免bisect檢查太多commit,在這個例子中,我們發現v1.2.8->v1.2.9的過程中這個bug 被修掉了。

因此我們設定:
$ git bisect start
$ git bisect bad 1.2.9
$ git bisect good 1.2.8
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[bc1b16509cec70de7a32354026443fca777f4d7d] created a .gitignore file(which is almost a copy of .hgignore with some minor changes and comments)

這時候我們已經進入bisect 狀態,用git branch的話會看到現在是(no branch)狀態。
要說明一下這裡的good, bad只是bisect上的一個概念,它會從bad 開始找到good,至於裡面是不是真的 good/bad,這由開發者決定。
這時bisect會checkout 處在good/bad 中間位置的版本,我們執行事先寫好的一個測試檔test.py,它會自動測試這個issue的狀態

from pyquery import PyQuery as pq
x = pq("<div></div>")
y = pq("<div><table></table></div>")
print(x.is_("table"))
print(y.is_("table"))

執行發現它還是回傳False/False,因此我們輸入
$ git bisect bad
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[b81a9e8a2b0d48ec0c64d6de14293dd4a680a20b] fixed issue #9

bisect 會以binary search的方式checkout 一個更舊的版本,然後你再測試一次。
經過五次的bad/good的測試結果,bisect回傳:
300cd0822505a4bd308acd1520ff3ef0f20f8635 is the first bad commit
commit 300cd0822505a4bd308acd1520ff3ef0f20f8635
Author: Gael Pasgrimaud <gael@gawel.org>
Date: Fri Jan 3 10:35:30 2014 +0100

fixed issue #19

:040000 040000 1d9cb3b170a8fdb2846e3c0e0fb6d2be9a9538d5 07d3a40ff73dda078d7543be2fab2f9f927b0c1f M pyquery

這樣就抓到這個 fixed issue #19 的commit 就是修好這個issue 的commit 了,最後要用
$ git bisect reset
結束bisect狀態。

--

上面這個方法好像還是不夠方便,理論上bisect 支援git bisect run這個方法,可以送一個script 給它,它會自動執行,並以回傳值0表示這個commit 是good,回傳值1表示這個commit 是bad,回傳值125表示這個commit 要skip掉。
所以我改了上面這個script 為:
import sys
from pyquery import PyQuery as pq
x = pq("<div></div>")
y = pq("<div><table></table></div>")
if x.is_("table") == False and y.is_("table") == False:
    sys.exit(1)
else:
    sys.exit(0)
可是不知道為啥,bisect run ./test.py的結果,每個commit 都會是bad的輸出…這真的是太奇怪了,我猜有可能會是git bisect的問題也說不定,有空再來詳加研究。

--

8/28增補:

後來經過阿蹦大神的指出,問題可能是出在*.pyc上面,因為python要是看到現在的pyc跟現在的時間相同,就不會更新pyc而是直接跑pyc。
git bisect run會極速的checkout 舊分枝,跑python script,看結果跑下一步;而pyc的檢定大概是用秒在算的,就變成python並沒有更新pyc檔,反而是用pyc跑出同樣的結果,bisect 自然出錯了。
解決方法有兩個,第一個是寫一個shell script test.sh,先刪掉所有pyc檔之後,再執行python script:
find . -name “*.pyc” -exec rm {} \;
./test.py
然後用git bisect run ./test.sh
第二個是讓python script 跑慢一點,讓python能察覺到python 的版本變化:
import time
time.sleep(1)
第三個應該才是根本的解法:
在python 的shebang上面加上 -B的參數就好了
執行結果就跟手動的一樣了,去你的pyc。

結論:

bisect很好用?不是,我認為從這個案例中最重要的概念,其實是自動測試的重要性,如果程式能保持一個自動測試的script,在除錯上可以透過script 自動找到錯誤點,不需要人工手動介入,試想若每個commit都需要手動10步驟的測試,兩個版本間有10個commit ,測試步驟立刻變成100步,但用script只要一個指令就能知道是好是壞,搭配bisect才能事半功倍。

參考資料:


Git-bisect doc: http://git-scm.com/docs/git-bisect

2014年8月22日 星期五

Git-flow 簡介

最近因為工作的關係接觸了git-flow相關的內容,在這裡就介紹一下git-flow的相關概念。基本上git-flow就是一個git 的擴展,把一群git 指令集合在一起,更方便管理人的操控,如果去看它的執行檔,其實就是一個shell script,所以使用git-flow時也可以用git 指令,同時只要熟練git的話,就算不用git-flow 也能操作自如。

我認為git-flow最重要的還是背後那個分枝的規則,我覺得學起這個規則就好,我們就先討論它設計的理由,同時搭配相關的git 指令:

git-flow會有五個主要的branch: master, develop, feature, release, hotfix,以下一一介紹:

有master branch是當然的,master branch是要讓使用者使用的,使用者checkout project時就要看到的內容,master branch上的commit 應該加上tag表示軟體的版本。
$ git tag -a v0.1 -m 'message here'

因為是要讓使用者用的,所以平時自然不能隨便在master 上面修修改改,開發者要先換個 develop branch上面進行開發。
$ git checkout -b develop
or
$ git branch develop && git checkout develop

git 允許許多開發者一同開發這個project ,如果大家都一股腦往develop 上放東西,每個開發者都會被commit 時的衝突問題煩到死,因此要開發一個新的feature,就要新建一個feature 分枝。
開發完時,這個分枝再併回develop 上:
$ git checkout -b feature-issue42
git commits
$ git checkout develop && git merge –no-ff feature-issue42
這個部分應該是最花時間的,通常會遇到共同開發的pull/push也是在這個部分,這裡不贅述,就請參考之前的文章:

軟體開發到一個程度,就進入release 流程,從develop 切換到release中,停止新增feature,開始專注在修bug 跟改文件等等工作上,準備就緒後,就外送到master 跟送回develop 中。
$ git checkout develop && git checkout -b release
git commits (only bugs)
$ git checkout master && git merge --no-ff release
$ git tag add -v0.2 -m 'message here'
$ git checkout develop && git merge --no-ff release

最後,沒有軟體是沒問題的,如果master上被檢出需要立即修補的重大問題,這時就要動用hotfix,把master 分枝出hotfix branch,修完後再併入master 和develop中。
$ git checkout master
$ git checkout -b hotfix
fix bugs, commit
$ git checkout master && git merge --no-ff hotfix
$ git tag -a 0.2.1 -m 'message here'
$ git checkout develop && git merge --no-ff hotfix
$ git branch -d hotfix

在文中的merge 指令都有使用 no fast-forwarding的--no-ff 指令,這是為了保留一個分枝跟合併的 記錄,以免fast-forwarding造成所有branch全混成一條。
當然這個流程只是參考,事實上接觸的幾個project幾乎都沒有照這個流程,大部分都是把master當成文中的develop在用,一樣分出feature來開發但沒有release,然後hotfix branch就是用master一路往前修。

所以說,感謝大家花了幾分鐘看了這篇廢文XDD

2014年6月28日 星期六

自己犯的錯自己刪,使用git-gc

最近我修改了我的ADS2Origin,因為使用者回饋表示,有時候資料區塊長度不同是無可避免的,比如說遇到loadpull的圖形,因此我把輸出改為csv格式,程式只會提醒使用者資料區塊長度不同,而不會自動切掉長度過長的部分;這個東西其實也不難改,反正資料就在那裡,只是改一下寫出的方式。
倒是寫這個讓我想起data mining的名言:「做data mining的,用了70%的時間在處理資料,30%的時間在靠北處理資料」

與此同時,很高興6/25號晚上我又推了一位同學當使用者,使用者人數++。
為了方便使用者,把下載連結換到dropbox的公開連結上:
https://dl.dropboxusercontent.com/u/3192346/ADS2Origin.zip

--

另外,最近我發現到一個問題,因為我的git project把windows下的執行檔都包進去,git又是每記錄一個版本就把檔案都複製一份,看一下我的git repository已經14 MB(唔…跟某些project用幾十G在算的比起來其實還是很小),不過恁爸保留exe的commit好像也沒啥用,就趁這個機會研究一下怎麼刪掉舊記錄裡面的執行檔。
不能用git rm ,這是不夠的,git歷史資料還是會保留著,因此需要一些特別的方法,主要的參考資料是參考資料一的:「Git 內部原理 - 維護及資料復原」,其實大部分的內容它都講完了,我只是照做而已。

其實理論上是不能這麼做的啦,這樣的行為會改變整個有大檔案存在的歷史,在有協作時需要你的團隊重新拉你的commit,會如此麻煩是因為git的設計理念就是不讓你輕易的丟失資料。
不過不管啦,我有幾個理由:
一、這個project根本沒人理
二、恁北我看windows不爽,就是要把windows的執行檔幹掉
三、跳脫舒適圈

首先看一下project的狀況,在執行完git gc後,再用git count-objects -v看看project有多大:
得到size-pack為: 3350 KB
我們的目標是一個名為dist的資料夾,裡面放滿了py2exe產生的巨量檔案。
用git log找出所有有加入該檔的歷史資料:
git log --pretty=oneline --branches --abbrev-commit -- dist
這個的會列出所有head下,match到dist 這個路徑裡的log,使用縮短的sha來顯示,結果如下:
5f9f3dc output in csv format, allow various length data
23f8633 windows executable file of 0.3.1 version
bb1107f Merge branch 'master' of github.com:lc85301/ADSToOrigin
e891091 update ADSToOrigin.exe version
acbeb1d fix duplicate tital bug
5343e5c add data length different detect, multifile sup
9ff12b9 multi variable parser support add
c989cc4 allow drag in windows
b21d5c1 windows distribution
的確,第一次把dist這個資料夾加進來就是從這個windows distribution開始的,這個commit的SHA值為b21d5c1。

好的,讓我們動手,這個要使用git filter-branch的命令,基本上這是一個…破壞性有點高的指令,建議是把manpage好好讀過一遍之後再來使用,不然GG了我不負責。
Filter-branch可以傳入一個filter,然後基於這個filter上,把所有的commit重寫一篇
這裡要用的是--index-filter或--tree-filter,這個會改變commit的內容,另外還有像--env-filter改寫每個commit的環境設定,msg-filter用來改寫commit message等等。

每一個filter後面要接一個command,用來操作你的git倉庫,拿最簡單的來說,msg-filter會把目前的commit message送到你的command的stdin,然後以command stdout的內容當作新的commit message,例如我有一個倉庫依序加入abc三個檔案,現在長這樣:
b0b79d5 add c
f09a9d6 add b
2eee4d4 add a
如果我下:git filter-branch --msg-filter 'echo XDD' -- master
結果就會變成這樣:
075cdd6 XDD
44fe70f XDD
ec1e435 XDD
所有的commit message都被改寫成XDD,這簡直比改歷史教科書還要簡單

現在我們要移除掉dist資料夾,就使用--index-filter
git filter-branch --index-filter 'git rm -r --cached dist' -- b21d5c1^..
最後這個-- b21d5c1^..,是送到git rev-list的內容
git rev-list會把從某個branch往前回溯(嚴格來說是reachable)的每個branch寫出來,然後可以加上各種條件對這個群集進行操作,這裡我用例子來說明:
git rev-list master //master往前的所有commit
git rev-list master ^branch //master 往前,但不是branch往前的commit
git rev-list branch..master //與上一行等價
另外還有...的用法,只是那個我還沒搞懂,搞懂了再另外寫一篇XDD
前面提到,windows的東西第一次是出現在 b21d5c1這個commit
因此我們做的,就是從master往前所有commit裡面,把 b21d5c1前一個commit再往前的排除掉,因此只會更改到 b21d5c1這個commit之後的東西,不然filter-branch就會重寫這個歷史,會花比較多的時間(這個垃圾project算了,請想像你有幾千個commit要修正的話?)。
指令是移除掉dist下的東西,就是個rm。

output大概會像這樣:
Rewrite b21d5c1abbaf39e02b817b0ccb7efdc54dbf6090 (1/14)rm 'dist/ADSToOrigin_win.exe'
rm 'dist/_hashlib.pyd'
rm 'dist/bz2.pyd'
rm 'dist/library.zip'
rm 'dist/python27.dll'
rm 'dist/select.pyd'
rm 'dist/unicodedata.pyd'
rm 'dist/w9xpopen.exe'

以下類似的東西重複14次,無非就是刪掉exe, pyd 巴啦巴啦,可以看見我到底存了多少exe垃圾在我的project裡面lol。

另外,我有些commit是針對windows distribution去commit的,這個指令一下這個commit就變成empty commits,因此我們再來:
git filter-branch --prune-empty -- b21d5c1^.. 註:這裡的SHA應該要換掉,只是我忘了是多少了。
這個參數可以在上面就下,一次做完比較痛快,這時候像windows distribution這個commit就不見了。

這樣我們已經讓記錄中不再記錄這個檔案,最後把它從.git裡面記錄刪掉,因為git reflog跟branch-filter本身都有對它的引用(你看看從git裡面刪東西是有多機車……)。
rm -Rf .git/refs/original //刪除branch-filter的引用
rm -Rf .git/logs/ //刪除reflog
git gc

最後,強制把你修改的分枝樹,把遠端的東西蓋掉。
git push origin master -f

這樣應該就差不多了,雖然說我弄完之後好像沒變小很多,不過我不是很清楚問題在哪...

參考資料:

1. Git 內部原理 - 維護及資料復原
2. ignoring doesn't remove a file

2014年5月9日 星期五

以pull request參與github專案開發

本魯最近看到一個有趣的專案: qucs
http://qucs.sourceforge.net/
目的是要打造一個類似ADS, AWR一樣的開源電磁模擬軟體,基本上是個野心勃勃,不過實際上沒多少人參與的專案,老實說還真令人擔心,我覺得還沒到一般人可用的階段應該就會腰斬了吧lol
不過難得有個專案跟本科相關,就進去玩一下好了,看看bug report,爬爬程式碼還真的修掉幾個bugs

好啦都修掉了那就來送個Pull Request吧…這玩意要怎麼送來著?
這時就只好自力救濟,問問旁邊的AZ大神跟QCL大神,QCL大神還很給力的直接殺到我房間教我;另外自己查了一點資料:
http://stackoverflow.com/questions/14680711/how-to-do-a-github-pull-request

對github的pull request 整理如下。
註:我其實不是很確定這個步驟是不是完全正確,如果有錯請大神們不吝指正 m(_ _)m

Step 1: get project

首先先在github網頁上,喜歡的project上按fork,把它複製到你的github上。
接著用終端機clone你的github:
git clone git@github.com:username/project
這樣會生成一個本地的git repository,它的Origin remote 設在你的github上。
為了方便參與開發,建議連接上原project的github,才能隨時保持你跟project無時無刻處在同步狀態:
git remote add upstream git@github.com:mainuser/project
這樣你的本地git就會連上原始project的github

Step 2: Open branch

接著就來修改吧。

通常新手使用git都會在來個「master大蜈蚣法」,一個master一路往前衝;不過project一大的時候還是建議開branch,改用「master大開花法」,每個branch只做一點點feature,然後再merge/rebase回master裡,反正git 就是branch怎麼開都無所謂。
參與pull request的時候更是如此,通常這時候project已經相當大了;儘量讓project manager看到少量的檔案變化,而不是整個project的檔案都變過;每個branch取個簡單易懂的名字:bug編號,feature編號都是不錯的選擇。
git branch -b branchname
git checkout branchname
or
git checkout -b branchname

Step 3: Make modification

修修補補。

Step 4: Send pull request

pull request會比較原始的github(也就是upstream)的某個branch,跟你的github裡的某個branch,把相關的commit 列出來,讓管理人決定要不要把你的commit接到它的branch上。另外,如果你在master上面加點東西,然後原開發者接受pull request,也在他的master上面加一些東西,兩個master間就產生衝突,所以請務必確保你的master是乾淨的。
Git push -u origin branchname
在你的github上面產生一個遠端branch。
接著到對方的github,按pull request,比較對方的master branch跟自己剛產生的這個branch。
第一個pull request就送出啦。

同時,在對方表態之前,這個branch就不要再動了,如上所說,github會比較兩個branch的差異,所以如果送出pull request之後又有變化,這些內容只會算到一個pull request裡,而不是每個feature一個pull request出去。當然如果你被拒絕了,你也可以繼續在這個branch上做修改,改好了再pull request一次。

Step 5: Merge, clean up

等待對方merge你的branch,merge之後,github會自動問你這個feature的branch要不要刪掉。
或者也可以用
git push origin :branchname
來刪掉遠端的branch,另外要用
git fetch origin -p
讓本地端更新遠端刪除掉的branch 的資訊。

最後要保持你的project跟遠端是同步的:
git fetch upstream
git checkout master
git rebase upstream/master

完成的畫面大概會像這樣:

整體的流程就是:我在origin/master的地方fork對方的project,接著在本地產生branch bug147,修正bug,推到我的github(origin)上。
對方merge我的pull request,更新了他的github(upstream)的master,我再用rebase把我的master移到最新的狀態。
如果你是高手,一次修10個bugs之類的,這時可以用rebase把其他應該修改的branch rebase到現在的master上。

以上,祝大家pull request愉快,大家快點來開發各種open source project

致謝:

本文感謝AZ大神及QCL大神的指導。

2014年4月14日 星期一

使用git squash 合併commit

小弟之前一直有個習慣,每次寫程式都要寫到結果正確了,才把該commit的commit;這樣造成的結果是,常常累積了數百行的差異才commit,要是中途不小心手滑了一下,辛苦就全化作流水了。

阿蹦大神曰:不用結果正確,編譯可過就commit
這樣…不是會跑出一堆亂七八槽的commit嗎?

這就要用到git squash功能了
比如說現在我隨便commit一些版本,log顯示為:
commit 7549a19b591f1c802addf9b2344be2f607beff42
Date: Mon Apr 14 16:05:38 2014 +0000

    more line num

commit a15d1dde304646d542dc9cb596afbcd900a609c7
Date: Mon Apr 14 16:05:22 2014 +0000

    line num

commit 265e09db81b1c6aff81a6bedcd3a0e2f22e55acc
Date: Mon Apr 14 16:04:49 2014 +0000

    initial commit

如果要合併line num, more line num兩個版本:
$ git rebase -i 265e09db81b1c6aff81a6bedcd3a0e2f22e55acc

然後編輯將
pick a15d1dd line num
pick 7549a19 more line num
要squash起來的commit編輯為squash, 或fixup,前者會保留squash的commit message,後者只用最新的commit message,先改成:
pick a15d1dd line num
squash 7549a19 more line num

再來它會要求你修改commit message,這就隨便你改,預設是把兩個訊息寫在一起。
commit 0a947a06fd258e07615fb236696b8f31f04f4043
Date: Mon Apr 14 16:05:22 2014 +0000
    line num
    more line num

commit 265e09db81b1c6aff81a6bedcd3a0e2f22e55acc
Date: Mon Apr 14 16:04:49 2014 +0000

    initial commit

以後就寫個段落就commit一下,等到功能都寫完了,再全部squash起來即可。
參考資料: man git rebase
致謝: 本文感謝傳說中的阿蹦大神指導

2013年12月19日 星期四

應用git stash 於多分枝之版本控制

本文是要說明git中stash指令的應用,基本需要先知道git 基本的add, commit,以及branch的功能。

使用git進行版本控制,branch是相當重要的功能,一般會建議要開發一個新的功能,就要先分出一個新的branch,開發好新的功能後再merge到master的版本內。
編按:雖然話是這麼說啦,可是筆者在實作時通常還是一個master branch一直commit下去XD。
當分枝一多的時候,就會出現一些問題,例如在一個分枝中修改到一半的內容,例如:「雷射彈幕」,這時有些點子想要切到另一個分枝中修改其他內容,但這個「雷射彈幕」的功能還沒達到可以commit的等級,就需要stash來暫存目前修改的內容。
--

下面是git stash的相關操作環境範例: 我們有個master branch跟開發中的feature branch,現在在feature branch中開發一個新的feature,加入並commit "featurefile"這個檔案
$ git checkout feature
$ git commit featurefile  <-注意一般是不這麼寫的,這裡是為了說明方便。
some modification on featurefile
$ git checkout master
這時候我們會得到一個:
error:
Your local changes to the following files would be overwritten by checkout: featurefile Please, commit your changes or stash them before you can switch branches.
Aborting
因為切到master branch會清掉已修改的內容,而git不會輕易讓你這麼做。

這時候就要先stash(暫存)它,stash有點像commit,不過沒有commit這麼正式,使用:
git stash (save)
git stash list
git stash pop
git stash drop
save: 存入一個暫存,可以不打,git stash預設
list: 列出目前有的stash
pop: 取出暫存
drop: 刪掉暫存

在這裡我們就直接git stash,這時候會看到所有還沒commit的修改都已經消失,用git stash list會看到:
stash@{0}: WIP on feature: ef9d050 feature initial commit
這裡重要的是“0”這個數字,這是stash的index;另外”feature initial commit”則是這個stash是這在哪個commit中分支出來的。
Git stash時還可以加上message來取代上面的ef9d050 feature initial commit這段
git stash save “this is temporary stash of master commit”
不過這在stash量很少的時候大概不太需要用到。

現在我們已經可以切回master的branch了。在master進行修改後,同樣不想commit的內容也可以用stash進行暫存,這時候git stash list會變成
stash@{0}: WIP on master: e194f69 master initial commit
 stash@{1}: WIP on feature: ef9d050 feature initial commit
分別標示了從master和featurecommit中暫存的內容。

--
要取出stash的內容,我們用git stash pop,這預設會取出stash index 0的內容,如果要取出其他stash的內容,就要用例如:
git stash pop stash@{1}
在後面打上stash完整名稱。
取出其他index的內容相當重要,像在例子中,如果我們先切回feature branch,再pop出在master branch中記錄的stash,這個效果和pull是類似的,若是有衝突的檔案就會要求merge,會很麻煩,個人是不建議在這種狀況下進行merge,畢竟當初就是不想記錄下來才用stash,現在要是merge就記錄進去了。

 如果要刪掉已經存入的暫存就用
git stash drop
刪去就行。

 祝大家git stash愉快

2013年10月29日 星期二

git partial add

Git 是一款極為強大的工具,雖然說過於強大的功能有時會讓人感到卻步,我也是一項功能一項功能慢慢學,不會的時候就抓著會的同學問,好不容易才變得比較會一些。
最近新學會的功能是git的partial add功能,在這裡做個簡單的介紹:

使用partial add 的功能,可以讓我們每次只commit一部分的東西上去,假設我在某程式開發了讀取、寫入兩樣功能,比較好的做法是讀取部分和寫入部分可以分開commit。有一部分原因應該是這樣比較分得清楚,如果之後要從這個commit中取出修改內容,或是再修改時都更清楚一點,雖然說筆者到現在也沒有做過這樣的事,不過養成一個好習慣也是好事。

 --
要partial commit,第一步是先partial add:
假設今天我對一個main.c的檔案,新加上兩個function foo1, foo2,這時候使用git add -p main.c,git顯示出main.c裡面有差異的部分,這時候可選選項:

 y,n,q,a,d,/,s,e,?
 y/n: 加入/不加入這個區段
 q/a: 不加入/加入整個檔案
d: 不加入這個區段,跟之後所有的區段;我得承認不確定它跟q的差別...
/: 尋找regular expression;啊老實說我也沒用過OAO
 j/k: 跳到後/前一個undecided 區段
 J/K: 跳到後/前一個區段
 s: 分開這個區段
 e: 手爆編輯這個區段
 一般來說,git會把相近的修改區間放到同一個區段裡面,下(s)plit指令,git會用原檔案中「未修改的部分」把修改區間切開,例如圖中fun1-fun3是同一個區間,下s的話fun1, fun2會被切成一個區間,fun3切成另一個區間,但s並不會切開fun1和fun2。
如果fun1, fun2要分開commit的話,就要用(e)dit,跳出編輯器介面。

新加入的修改會有'+'在行首,不加入的話刪掉即可,vim: dddddddddd。
刪除的則是'-'在行首,不加入的話把'-'替換成' ',這個用vim的ctrl+v很容易就可以做到。
筆者試過一次在這個狀態下去修改原始碼,git會叫說patch產生錯誤,大概是不能這麼做,只能把你加上去的內。容刪掉,或是刪掉的內容mark成' '來復原刪除。

 --

commit時,直接git commit 就可以把add過(正式名字應該是staged)的內容commit上去。
這時不要使用git commit main.c,這會將檔案內容全部staged然後commit 上去,有一次筆者先partial add後下了commit file,再下git status就發現這個檔案已經全部stage進去了,沒什麼修改可stage…。
另外一個要注意的是用partial add的話,在回復的時候一定要非常小心,例如在上例中我們add了fun1 這時候原始碼的狀況是:
main() → 上一個commit
fun1() → staged
fun2() → unstaged
注意到fun2是unstaged,如果你亂改到什麼想要回復,而下:
checkout -- main.c
回復到上一個狀態,fun2的原始碼(該說它從來沒被記錄下來)也會整個被消掉。

2013年3月29日 星期五

使用vimdiff來解決git merge conflict

使用vimdiff來解決git merge conflict 最近同時家裡用筆電跟辦公室用桌電,在兩個地方使用git/github來管理程式作業,這兩個東西加起來根本神物,本來要用隨身碟同步的東西,現在可以用git直接完成。
關於git的基本介紹我就不解釋了,網路上隨便一找就有一堆資源,例如右邊友站連結,作者比我強30dB的JJL blog,裡面有git的基本使用方式;作者當初則是看參考資料一的progit來學git。

作者遇到的問題是:有時候檔案在github上的檔案已經更新,本地的檔案也有修改過,這時候若想要git pull的話會產生conflict,這時候就需要把本地的檔案刪掉重新clone使用merge來解決衝突,偏偏作者手殘常常把檔案merge成連<<<, >>>都保留下來的檔案,相當麻煩;這次好好的研究一下怎麼用vimdiff作為merge的工具,在這裡記錄一下。

首先呢,我們可以先設定vimdiff為git default的mergetool
git config --global merge.tool vimdiff
為什麼?沒辦法,用vim就是潮(誤)

那麼來merge吧,輸入
git mergetool
這時候應該會打開vimdiff,然後產生左上角、中上、右上、下四個視窗,說明如下:
左上:local:顯示本機的檔案內容,現在這個git資料夾的版本
右上:remote:顯示遠端,你要merge的分枝
中上:base:顯示上面兩個分枝的基部的內容
下:merged:顯示merge的內容,也是是包含了那堆<<<<<<, >>>>>> 的版本,我們的目標就是把下面這個修到我們希望的版本。
截圖:













到了這裡就開始解衝突啦,事實上不單是git,平時如果有兩個很像的檔案要合併,也可以用vimdiff開啟來解,使用的指令是這些:
[c:跳到上一個衝突點
]c:跳到下一個衝突點
:diffget,從某個視窗取得內容
:diffput,把內容丟去某個視窗
可以用:help do, :help dp查怎麼用,不過兩個指令的基本格式是:
:[range]dp|do bufspec

如果是雙方比較,那就沒什麼好說的,do/dp的對象就是另一個視窗的內容,這時候只要在衝突點在一般模式下用dp,do即可。但如果是現在這種3方比較時,就沒辦法這麼方便,而是要直接輸入:diffget/put bufspec來操作(可以打diffg, diffpu來少打幾個字,不過有差嗎=w=)
以上圖為例,我們游標停在下面的衝突點上,要使用remote的視窗內容。
這裡bufspec(用哪個視窗的內容)有兩種指定方式:
1. 先用:buffes,確認remote那個視窗編號,我是4號,因此用 :diffget 4
2. 用關鍵字,這個超強,用 :diffget REMOTE (因為git自動命名暫時檔名為 XXXX.REMOTE.yyy, XXXX.BASE.yyy, XXXX.LOCAL.yyy)即可。
如此就會套用remote的內容了,經過幾次套用之後,畫面可能會變得有點亂,這時候可以用 :diffupdate來重新產生diff的格式。













用上面的步驟,就可以快速的完成解衝突的工作,做完之後,在下面的合併檔存個檔離開吧。

參考資料:
1. progit download:
2. vim wiki about git vimdiff:
3. vim help:
Type in vim :help diff