# Git & GitHub 版本管理 ###### tags: `程式` `programming` `git` `版本管理` [toc] --- ## 隊伍智慧財產 為了確保團隊知識能有效傳承,並維持團隊資源的一致性,我們要求所有成員了解:所有為團隊活動所創建的文件,只要有與任何隊員分享,就自動視為團隊知識庫的一部分。這些透過 Google Drive、GitHub、電子郵件或其他平台所分享的資料,皆視為團隊智慧財產。受到智慧財產政策保護的內容不得擅自移除或刪除對團隊的存取權限。 ## 基礎概念 git是現在最多人使用的程式碼版本管理工具,它會把每個版本與前一版的差異用diff整理出來,不但可以用更少空間保留所有歷史版本,也方便使用者查閱每個版本的變化。 ### diff diff會以行為單位比較兩份程式碼的差異,盡量尋找相同內容的行對在一起,其餘有變化的部分則列為增加/減少/修改的部分,可以在[wikipedia:diff](https://en.wikipedia.org/wiki/Diff)看到一些範例。 ### blob 因為git運作的原理是一行一行去做比較,所以如果沒有"行"這個概念,也就是非文字檔,而且檔案大小又很大的話,很可能會讓git看不完然後就卡住了! 所以如果有大型的二進位檔(blob, binary large object)最好是把它從程式碼中拿出來另外管理,通常如果是UI必須要用的圖片檔還可以接受,如果圖片數量很大例如整本相簿之類的,或者很大的檔案例如打包後的映象檔/壓縮檔動輒幾百MB以上的,就很有可能會掛掉。 ### 資料夾 & repo git版本管理的範圍是以一個一個專案資料夾計算,它會在專案資料夾的第一層新增一個`.git`的隱藏資料夾,git的所有資料例如專案的設定檔以及各個版本歷史都是放在這裡面的。 而一套受git管理的版本歷史我們稱為一個repository,常常會簡稱叫repo。 ### .gitignore 在專案資料夾底下的所有檔案都會被git追蹤(包括新創的檔案也會被git找到並且列為有更動的部分),除了空目錄裡面沒有檔案的話會被git忽略。另外還有一些檔案我們不希望被git備份出去的,例如每個user習慣用不同的editor設定檔、編譯時期的中繼檔(e.g, `.tmp`, `.obj`)或編譯後的輸出檔(e.g., `.bin`, `.lib`, `.so`, `.dll`, `.exe`, `.class`)、甚至可能是包含DB密碼的參數檔,因為各種不同程式語言、框架產生檔案的規則各不相同,所以git特別用`.gitignore`這個檔案讓使用者填入想要忽略的檔案規則。 ### github git是一種版本管理的工具,而[github](https://github.com)是一個最多人使用的提供git服務的平台,意思是你也可以自己另外架一個跑git的伺服器用來取代github的功能,不過正常來講真的是沒有需要啦XD github夠好用了 順便提醒一下,github在創repo的時候預設是public的,也就是公開給大家都看得到,如果覺得有什麼超級厲害的發明需要保密的話(我猜是沒有啦XD)記得要設成private。像國外的強隊通常也習慣賽季中先設private,等賽季結束之後才公開。 --- ## 工具 ### source tree 我個人習慣使用的GUI工具。 [官網](https://www.sourcetreeapp.com/) ### cli 夠硬派的人都直接用command line下指令的! [官網下載頁](https://git-scm.com/downloads) ### 其他GUI工具 可以參考[官方推薦的列表](https://git-scm.com/downloads/guis) --- ## level 1: 當雲端硬碟用 最入門(也最笨)的一種用法,就是把github當作雲端硬碟在丟,要下載最新版的時候從github拉下來,改完之後要記得把程式推上去。 ### remote / origin git剛發明的時候大家會強調它是"分散式"的版本管理,因為在那個時候大家習慣用的是"集中式"的版本管理。所謂"分散式"意思是除了在server端(例如github)有一套紀錄起來的版本歷史之外,在local端也會有一整套的版本歷史,當我們修改過程式要建立一個新的版本的時候,就是先加在local這套歷史裡面,然後再將這些新建立的版本推上去server端去做同步。 這個遠端的server在git的概念裡面就叫做remote,而git的設計是可以有好幾個remote達到多份備份的效果,但通常我們也只需要一個remote就好了,也就是只用github就安心了。多個remote裡最主要的那一個會被叫做origin。 ### init & clone 建立一個新的專案時有兩種做法,如果你已經有一些程式了,事後才想到要用git來管理這套程式,可以在你的專案資料夾裡用`git init`讓git做初始化,也就是會建立`.git`資料夾裡面必要的東西,但是這時候git會把現有的這些檔案視為新增但未列管的檔案,還需要執行後面講到的commit才會把它們存進版本管理。`git init`只有在local端建立repo,這時候沒有人知道它對應的origin是github哪個帳號或者其他server,所以還需要先去github上面把repo開好拿到url,然後再下`git remote add origin https://github.com/FRC7589/那個repo.git`其中https開頭.git結尾的這段就是github的url。 另一個方法我個人比較推薦,就是反過來先在github上開好repo,再拉下來到local,拉下來的時候自動就會記住它的origin在哪裡了。具體作法就是在github開好之後,它上面教學寫的那行`git clone http://github.com/FRC7589/那個repo.git`一次解決~ ### status & diff `git status`可以列出哪些檔案有被更動過或新增出來。 `git diff`可以把所有更動的行數都印出來仔細檢查。 這兩個動作在GUI工具裡面會直接顯示在畫面最顯眼的地方,所以用GUI的話可以省略。 ### add & commit 不論是檔案有被修改過或者新增的檔案,git會先"發現"它們有更動,我們第一步要把它們`add`進stage狀態,這些stage狀態中的檔案才會在下一步`commit`中被記入新的版本裡,其餘有更動但是沒有進stage的檔案(或行數)則不會進入新版本中,繼續維持"被發現有更動"的狀態。 如果 `commit`命令會將stage裡更動做成一個新的版本記錄起來,同時你會需要寫一段話去說明這個版本做了什麼事,稱作commit log,寫commit log就跟寫註解一樣,雖然隨便寫不影響程式執行,但是好的commit log會對於程式後續維護有很大的幫助。 commit(動詞)之後就會產生一個新的版本,這樣一個一個串在一起的版本我們也叫它一個一個的commit(名詞)。每個commit我們還是需要一個代號去識別我們要查的是哪一個commit,在git裡面是使用這次diff的hash值來識別,會長得像`326fc9f70d0295bed31b0072dbbae003783d77ed`這樣難看,不過還好在下指令的時候只要下前面幾個字git就會自動去找符合開頭的commit,除非給的字太少讓git找到不只一組同樣的開頭,git就會請你再給長一點,通常是寫到前7字或更短就足夠了。 ### push `git commit`之後創建的新版本只是存在local端而已(`.git`資料夾裡面),要更新到github上需要再執行`git push`,如果遠端的版本比local舊的話就會把推上去的新版本接到遠端的舊版後面,這樣遠端的版本歷史就會跟local一樣了;但是萬一遠端同時也有一些local沒有的新版本(通常這是因為有別人也做了修改產生另一份新版本而且比你搶先推上origin),這時候push就會失敗,會需要後面才要講的`merge`把兩種新版本合併成一個更新的版本再推上去。 ### pull `git pull`顧名思義就是跟push相反,從遠端把那邊的新版本拉下來到local端。為了避免別人推的新版本你沒拿到然後就用舊的版本開始改,這樣會造成之後又要再多一個動作merge,所以最好是每次要開始寫code之前都pull一下,然後寫到一個段落或者每次收工休息之前都把最新的修改push上去,比較容易讓大家都接在同一條線上。 --- ## level 2: 多人協作 前面講過,git是分散式的版本管理,如果兩個人分別對同一個版本拉回去修改,就會造成世界線分裂(?),例如Alice和Bob都拉了***O***版本下來,各自做了不同的修改之後commit成新版本(但是還沒push回去),Alice的commit叫做***A***,Bob的commit叫做***B***,那麼在他們各自眼中的世界線長這樣 ```graphviz digraph { rankdir=LR; Oo [label=O]; Ob [label=O]; Oa [label=O]; subgraph cluster_BBA { color=black; label="Alice世界線"; Oa -> A; } subgraph cluster_B { color=black; label="Bob世界線"; Ob -> B; } subgraph cluster_O { color=black; label="origin"; Oo } } ``` 然後Alice搶先把他的世界線push上去了 ```graphviz digraph { rankdir=LR; Oo [label=O]; Ao [label=A]; Ob [label=O]; Oa [label=O]; subgraph cluster_BBA { color=black; label="Alice世界線"; Oa -> A; } subgraph cluster_B { color=black; label="Bob世界線"; Ob -> B; } subgraph cluster_O { color=black; label="origin"; Oo -> Ao; } } ``` 當Bob要push的話就會失敗,因為git的規則「不能造成別人的世界線分岔」,這時候Bob只能把origin的世界線都拉下來,「在自己的手上分岔」 ```graphviz digraph { rankdir=LR; Oo [label=O]; Ao [label=A]; Ob [label=O]; Ab [label=A]; Oa [label=O]; subgraph cluster_BBA { color=black; label="Alice世界線"; Oa -> A; } subgraph cluster_B { color=black; label="Bob世界線"; Ob -> B; Ob -> Ab } subgraph cluster_O { color=black; label="origin"; Oo -> Ao; } } ``` 然後Bob就要負責把他手上分岔的兩個版本merge成一個新的版本,叫它版本***C*** ```graphviz digraph { rankdir=LR; Oo [label=O]; Ao [label=A]; Ob [label=O]; Ab [label=A]; Oa [label=O]; subgraph cluster_BBA { color=black; label="Alice世界線"; Oa -> A; } subgraph cluster_B { color=black; label="Bob世界線"; Ob -> B; Ob -> Ab B -> C; Ab -> C; } subgraph cluster_O { color=black; label="origin"; Oo -> Ao; } } ``` 這樣就能推上origin了 ```graphviz digraph { rankdir=LR; Oo [label=O]; Ao [label=A]; Bo [label=B]; Co [label=C]; Ob [label=O]; Ab [label=A]; Oa [label=O]; subgraph cluster_BBA { color=black; label="Alice世界線"; Oa -> A; } subgraph cluster_B { color=black; label="Bob世界線"; Ob -> B -> C; Ob -> Ab -> C; } subgraph cluster_O { color=black; label="origin"; Oo -> Ao -> Co; Oo -> Bo -> Co; } } ``` 注意這時候Alice還是沒有***B***和***C***,要等到他再次從origin pull下來才會拿到,那萬一他一直沒有pull然後又做了新版本***A2***,那他也會遇到push上去時***A2***和***C***分岔的問題,他就必須要pull回到自己手上把***A2***和***C*** merge起來才能push出去 ```graphviz digraph { rankdir=LR; subgraph cluster_A { color=black; O -> A -> A2 -> D; A -> C; O -> B -> C -> D; } } ``` ### merge --- ## level 3: 分支管理 ### branch ### pull request