[Git] 使用 Git SubTree 來共享函示庫原始碼

採用 GitSubtree 的方式來共用多個 project 會共用的 sourceCode,不過 subtree 的資料似乎不多,查到的資料都沒有順利的讓我完成我的 subtree 情境,所以自己參考資料並且嘗試成功後,就寫一篇 blog 來記錄一下,希望 subtree 以後會更方便用

寫在前面的前面 (2017/05/06更新)

經過了大概幾個月的使用,近日決定放棄用 Subtree 改回用 SubModule,因為遇到了一些問題,最嚴重的是兩個使用同一個 Subtree 會莫名的不同步,
不同步的意思就是一邊更新上去了,並確認在 Github 上已經是新的,另外一邊 subtree pull 回來的竟然不是最新的,這樣就算了,此時把 subtree push 回去,
要發 PR 還會發現竟然會把之前修改的蓋回去!!這也不是第一次遇到,第二次遇到,所以決定要放棄 Subtree 改回去 Submodule ,應該穩定運行是沒有問題,雖然麻煩了一點。

其他的問題例如:
1. 你在 Subtree 的 repo 會看到許多的 commit ,那些 commit 是使用 subtree repo 的人所造成的,並不一定真的對 subtree repo 裡面的檔案有影響
2. Main project 把修改 push 回 subtree repo 後,發 PR 竟然檔案沒有任何變更,這樣就算了,直接 clone subtree repo 下來,下 merge 竟然會說是 unrelated repo... 真的是 WTF

也許是我 git 指令及使用方式不熟悉,所以才會有此問題,但現階段來說研究這個浪費時間,所以決定改走回 submodule 的老路。

不過如果你還是有興趣試看看的話,歡迎繼續往下看


寫在前面

為什麼會用到 SubTree 呢?因為之前同事有用了 SubModule 覺得麻煩,雖然目前 Git 的 GUI 工具其實應該都有支援了,但如果是下指令的話,相較於 SubTree 來說是繁瑣了一些。
最近共用 Library Project 的需求再起(WPF & UWP)所以花了點時間找了替代 SubModule 的方案,看起來就只有 SubTree 囉~
看到一些地方也說建議大家可以用 SubTree 來替代 SubModule

SubTree vs SubModule

當初的出發點只是要找一個替代品,沒有仔細的想過這問題,也是被問到後才去查的,其實目的上當然是差不多,但使用的情境上,或者看指令的設計上還是有差距的
查到 這篇 覺得解釋的不錯,直接把原文節錄出來

  • submodule is a better fit for component-based development, where your main project depends on a fixed version of another component (repo). You keep only references in your parent repo (gitlinks, special entries in the index)
  • subtree is more like a system-based development, where your all repo contains everything at once, and you can modify any part.

我是這麼理解的

  • SubModule 適合像是說所謂的共用函示庫你是沒有主空權的狀況下,通常你用的都是一個固定版本,就是變動不那麼頻繁的函示庫,在指令的使用情境上 SubModuel 的確有比較繁瑣
  • SubTree 就是比較像我們需要的情境,會跟著 Main Project 一起成長的與改變的,在指令上使用相對簡單許多

關於 git subtree vs git submodule 指令的操作,可以看 這篇 ,在裡面就可以看到使用 git subModule 的狀況下,指令需要滿繁瑣的,而 subTree 就幾個簡單的指令就搞定了


But... 人生最想不到的就是這個But...不然我也不會想要寫這一篇 blog 了 XD

就讓我來說一下從 SubTree 從頭開始和我遇到的狀況吧~(指令詳細的參數使用方式就麻煩自行去查,謝謝~)

一、分割原來的原始碼

因為我們要共用的函示庫本來是我們 main repo 的一個資料夾,所以第一個步驟就是要把這個資料夾切割出來成為另外一個 repo,而 git 就有提供了一個 git subtree split 的指令,就可以指定一個目錄,把那個目錄的東西切割到另外一個 branch 去,
接著就可以把那個 branch 的東西 push 到你遠端的另外一個函示庫專用的 repo 了。

然後就遇到第一個問題,在 split 後我看 split 出來的資料夾也沒有任何東西 (ex: .git 的資料夾,沒有 .git 資料夾就不會有原來的 commit log 了) 阿我這樣到底要怎麼 push 啦?XD 從 GUI tool 是可以看到我 split 出來的另外一個 branch沒錯,結果搞半天後,
發現因為那個是個 branch 所以索性就切換到那個 branch,然後就可以 push 了 囧

OK~ 解決

二、加入遠端的 repo 為 subtree 並且 push 回去 main repo 後讓 git subtree 指令也都運作正常

這邊就是卡最久的地方,出乎意料的麻煩,不過也多學到了一些東西就是。

要加入一個另外一個 repo 當作自己的 Subtree 來源,首先當然就是要用 git remote add 來增加一個 remote 的來源,就讓新增的這個 remote 叫 shareLibraryRepo 吧!
然後就是要把它加入自己的 main repo 當作 subtree 啦~ 這時候我用第一時間查到的指令就是用 git subtree add -P ShareLibrary shareLibraryRepo master 來把 repo 放到我的 ShareLibrary 的資料夾當作 subtree ,
一切都看起來很簡單順利,然後我就把 main repo 給 push 回去,這時候當然要自己測試一下這樣測試一下別人拉回去後,如果要用 git subtree pull 可不可以正常運作囉!

所以我就弄了一個乾淨的 main repo ,一樣加了一個叫 shareLibraryRepo remote 後,下了 git subtree pull -P ShareLibrary shareLibraryRepo master 的指令,要來更新看看,結果就遇到了

refusing to merge unrelated histories

的錯誤訊息... WTF... 那時候以為中間那邊弄錯了,重試了一次結果還是遇到一樣的問題... 囧

只好再去找資料,又花了點時間結果找到 Github 其實就有 這篇 在介紹 subtree merge 的方式,照著上面的步驟做後,成功了~ 用了乾淨的 main repo 後,可以正常的下 git subtree pull 的指令更新 shareLibraryRepo 的 repo 了!
不過... 事情當然沒有這麼簡單... =.=

當我開心的在 main repo 做了一些修改,包含有修改到 ShareLibrary 的東西,所以 commit 完 main repo 後,想要把對於 ShareLibrary 修改的東西一樣 push 回去,然後就開心的下了 git subtree push -P ShareLibrary shareLibrary master,然後就遇到

! [rejected]        72a6157733c4e0bf22f72b443e4ad3be0bc555ce -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:***'
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.

的錯誤訊息... 被 reject !!! WTF... 然後又查半天找不到什麼好方式,然後我就轉方式想說一不做二不休,想說用 force push 的方式,但很不幸的 git subtree push 並不支援 --force 的參數,那....有辦法嗎?答案是有的!請參考下列指令的方式

git push shareLibraryRepo `git subtree split -P ShareLibrary --ignore-joins`:master --force

利用的是 git push 的 chain command 的方式來達到,嗯~總算鬆了一口起,起碼剛剛改的一堆東西還可以回去 shareLibrary 的 repo!

但是這不是個解決問題的方式,總不能每次都要 force push 啊!!所以又花了時間嘗試了整合 subtree 的指令步驟,最後終於發現了關鍵的一個地方...直接講結論...

1. 用 git subtree add 是對的!
2. 記得下完 git subtree add 後,一定要多下一個下面的指令!記得下完 git subtree add 後,一定要多下一個下面的指令!記得下完 git subtree add 後,一定要多下一個下面的指令!因為很重要所以說三次
git merge -s ours --no-commit --allow-unrelated-histories shareLibraryRepo/master

這行指令就是建立好 subtree 關係的關鍵步驟啊!!!(抱頭)就差了這一個步驟,所以讓我一開始會遇到 refusing to merge unrelated histories 的錯誤訊息 >"<

真的是雷!我其實不確定這到底是 bug 還是 feature (攤手)

總之在這之後,git subtree pull/push 都運作正常啦!(灑花)

王子和公主從此過著幸福快樂的日子


寫在後面

嘗試了 SubTree 的東西,大概有點心得

  • main repo 有沒有加入 subtree 對 main repo 的操作沒有任何影響,就把它視為 main repo 的一步份即可,無須特別理會
    • 換句話說修改到 subtree 裡面的 sourcecode commit & push 回去 main repo 後,其他在同一個 main repo 的人不需要用 subtree 指令去 pull,就直接 pull main repo 即可,因為基本上它就是 main repo 的一部分
  • 有影響的狀況只有
    • (1) subtree 的 repo 有更新,並且你想要 pull 回來的時候
    • (2) 你有修改到 subtree 的東西並且想 push 回去的時候
  • 要 pull / push subtree 的 repo 幾個步驟
    • (1) 加入 shareLibrary 的 repo 為 remote 的來源(這個只要加過一次即可)
    • (2) 使用 git subtree 的指令來 pull / pull
  • git subtree 僅有 add / pull / push / split / merge 幾個 command 而已,其實沒有太複雜
    • split 比較特別,用途是將你目前 repo 的一個目錄可以切成另外一個 repo,目的就是切割你原來 repo 的一個目錄然後可以當作 subtree
  • subtree 最好不要加到與原來 shareLibrary 相同的路徑,不然原來的 commit 歷史紀錄會被一併推回去遠端的 repo (這點我不確定是不是因為這樣的緣故,不過我是這樣解決的)
  • 建議可以看看的連結:
    • mastering subtree
      • 裡面有提到 git subtree 的指令,其實是包裝好的一些指令的只是先幫你包裝好而已
      • 例如 git subtree pull/push 其實就跟上面看到的 force push 的是一樣的,其實都是透過 chaine command 的方式做到,都會幫你串 git subtree split 的指令
      • 例如 git subtree add 也是幫你做好了在那篇 Github 文章的幾個步驟

最後也是要來說一下目前用 subtree 所覺得的缺點

  1. git subtree push 會花很久的時間,就像心得裡面因為他會先 split 然後再 push,split 的時候就會計算那個資料夾的 commit 哪些是有關係的,通常都會花一段時間,目前這個還沒找到解法
    1. 所以如果要比較快的 workaround 的方式,就是直接去修改 shareLibrary 的那個 repo,然後再用 git subtree pull 的方式拉回來,不過這樣其實應該比較麻煩,所以就不要每次修改到 subtree 就都要 push 就好
  2. GUI 工具支援度不足,目前只知道 SourceTree 有支援,不過還好,就下下指令而已

以上~ 感謝各位的觀賞~ 我們下次再見~


2017/01/18 更新:

上面有說道 git subtree push 會花很久的時間,後來發現原來是有一個地方做的方式改變一下就不會了,如果你原本就照上面的方式使用 git subtree add 的指令,沒有沒有加其他的參數的話,應該就不會遇到我說的 git subtree push 的時候會很久的問題;
我會說會花很久的原因是因為我再下 git subtree add  的時候多加了一個 --squash 的參數,這個參數可以讓 subtree 加入的時候只會產生一個 commit ,但是看樣子這樣的缺點就是因為他沒有完整的 subtree 的 history,所以會導致在 subtree push 的時候必須"重新"
檢視一次紀錄所導致,所以... 如果沒有特別理由,還是不要用 --squash 嚕~