--- tags: 109-1 --- # Git Tutorial ## 0.大綱 1. 甚麼是 git 2. 為甚麼要用 git 3. 你一定要知道的指令 4. git 的原理 5. stage 6. commit 7. github 簡介 8. `.gitignore` 9. branch(分支) and merge(合併) 10. conflict 11. undoing 12. 結語 ## 1.甚麼是 git :::info distributed version control system(分散式版本控制系統) ::: 1. 紀錄檔案的變化 2. 可以在不同時間 commit 的版本之間切換(時光機) ## 2.為甚麼要用 git 1. 寫 project 時好分工合作(可以多人共用一個 github ) 2. 當程式碼寫壞了,能夠回到最近一次可以運行的版本 3. 知道 project 內某一部分的 code 是誰寫的 4. 可以輕鬆的測試實驗性的功能(branch) 5. ~~很炫~~(誤),全世界絕大部分專案都使用它做專案管理 ## 3.你一定要知道的指令 1. `cd` (change directory):轉移到不同的資料夾位置 2. `ls` (list):列出所有資料夾下的檔案 3. `rm` (remove):移除檔案 4. `touch` :建立空檔案 5. `mkdir` (make directory):建立資料夾 :::warning 以上介紹的指令都是在 git 上使用,windows 系統下命令提示字元中輸入的指令有一些是不同的,例如: 1. `ls` -> `dir` 2. `rm` -> `del` 3. `touch` -> `type nul >> filename` ::: ## 4.介紹 git 的原理 ### 0.`git init` 新建數據庫(repository) 會出現 `.git` 資料夾,裡面存放版本控管的資料(不用知道是甚麼也不會影響 git 的版本控管) ![](https://i.imgur.com/bZGZ5ub.png) ![](https://i.imgur.com/CZHA9mp.png) ### 1.`git config`(有預習的同學不用操作這個步驟) 1. 在 git bash 中,打入以下指令 * `git config --global user.name "你的名字"` * `git config --global user.email "你的信箱"` * `git config -l` 此時會跑出以下畫面![](https://i.imgur.com/K6NQ5Vp.png) 其中 `user.name` 和 `user.email` 會是你設定的名字和信箱 2. 此時游標會在分號:後一直閃動,只要打英文字母 `q` 進去,便會回到正常模式 :::success 這裡的 `user.name` 和 `user.email` 在多人合作專案時能夠顯示特定部分的程式碼是哪個人寫的,並非一種帳號密碼的關係 ::: ### 2.repository(儲存庫、數據庫) 有 git 追蹤的資料夾,裡面存放檔案 ### 3.staged(暫存檔案) 把檔案存放到暫存區,之後**一次性**將暫存區的檔案提交,儲存成新的版本 :::warning staged 的觀念像是在掃地時,我們會將地上所有垃圾先掃(add)到一個小區域(staged area),集合到一定體積後在一次性地掃到畚箕(repository)裡面(commit) 或是你可以想像你有一個倉庫(repository),在倉庫門口有個小廣場,你可能還會**移動,丟棄,更換**這些還沒進倉庫的物品,這個廣場的概念就像跟暫存區(staged area)一樣 ::: ### 4.commit(提交版本) 把現在的 repository 存檔紀錄,之後隨時都可以回到這個版本 :::info commit 的想法可以類比於設立遊戲的存檔點,玩遊戲時過了一個關卡後要設立存檔點(commit),之後如果死了可以回到之前的存檔點,從這個存檔點重新出發 ::: ### 5.檔案的生命週期 * untracked 檔案尚未被 git 控管 * staged 進入暫存區,準備變成新的版本 * unmodified 已經變成新的版本,還沒作任何更動 * modified 對新的版本做更動,且尚未進入暫存區 ![](https://i.imgur.com/aDfi18m.png) :::info 圖片說明 1. 將一個git尚未控管的檔案加入暫存區,~~讓git認識他(?~~ 2. 將一個已被git知道的檔案進行修改(**增減內容,刪除,重新命名,移動等**) 3. 將從2.來的檔案加入暫存區 4. 將一個已被git知道的檔案移除追蹤,讓git忘記他 > :::warning > 此處所提到的remove並不代表直接將檔案從檔案目錄刪除,而是指讓git**將該檔案從追蹤名單中去除**, > Ex: git rm --cached (下面會提到) > 不過普通的刪除當然也做得到,必須commit就是 > ::: 5. 將暫存區的檔案提交給數據庫,紀錄這次的提交各種細節 ::: (圖片來源:git 官網) ## 5.stage 先寫一個簡單的 `main.cpp` 檔案,可以直接從這裡複製,再將 `.cpp` 檔編譯產生 `.exe` 檔 ```cpp=1 #include <iostream> using namespace std; int main(){ cout << "hello world" << endl; system("pause"); return 0; } ``` ![](https://i.imgur.com/P9CxD5M.png) 此時點擊 `main.exe`檔會出現`hello world!` * `git add main.cpp` * 這個指令將 `main.cpp` 加入 git 的控管,此時我們在 git bash 上輸入`git status`,得知 `main.cpp` 在 staged area,`main.exe` 在 untracked area 裡面 ![](https://i.imgur.com/4mGSgBL.png) * `git add .` * 這個指令可以將資料夾中**所有**檔案加入 git 的控管 * `git rm --cached main.exe` * 如果此時我不想讓 `main.exe` 加入 git 的控管,輸入以上指令,再用 `git status` 確認 ![](https://i.imgur.com/rzzJ9yn.png) 此時 `main.exe` 在 untracked area 中 :::info 為甚麼要有 staging area ? 1. 安全性 如果我直接 `git add .` 就將所有檔案儲存成一個版本,那麼很可能裡面很多資料都是有問題的,因此 git 先將 `git add .` 中所有檔案加進緩衝的 staged area,一切確認 ok 後再利用 commit 儲存成新的版本 2. commit 要有 logical point 若每次 `git add` 都會新增一個版本,那麼便會有許多零散的版本,之後想做時光機回到之前的版本選擇上會十分困難,因此利用 commit 的 message 功能上使用者清楚知道每個版本的進度 回到倉庫的例子,在倉庫(repository)門口有個小廣場(staged area),你把要存放到倉庫的貨物**先放到這邊**(git add),然後等收集的差不多了就可以打開倉庫門,把在廣場上的貨物送進倉庫裡(git commit),並且**記錄下來這批貨是什麼用途的、誰送來的**。 示意圖: ![](https://i.imgur.com/d4Je1vG.png) ::: ## 6.commit * `git commit -m "say hello world!"` 我們將 staged area 的檔案**提交**成新的版本,這個版本我們要給一個 message,叫做 `say hello world`,此時我們輸入 `git status`,發現目前的 repository `On branch master` ,再輸入 `git log --oneline` 這個指令來看當前 repository 的所有版本 :::danger commit message 要精簡具有意義,冗長的 commit message 會使人不耐煩,沒有意義的 commit message 會使得之後要回到前面的版本時很難選擇對的版本 ::: ![](https://i.imgur.com/lYygnVy.png) ## 7.github 簡介 * 遠端存放 git repository 的地方 :::success github 有雲端硬碟的功能(存放檔案),同時擁有 git 版本控制的功能 ::: ### 1.在 github 上創建新的 repository 1. 在右上角有小小的加號,點選後會出現下拉式選單,點選 New repository ![](https://i.imgur.com/ICufYf6.png) 2. 在 Repository name 中打入`intro-to-git`,下面的全部選項不用更動,按下 Create Repository ![](https://i.imgur.com/qz9XGba.png) ### 2.介紹 push 指令 以下三個步驟,我們根據 github 提供的 quick setup 中 `...or push existing repository from the command line` ,一一做指令的介紹 ![](https://i.imgur.com/pNSZLmK.png) #### 1.在本地端設置遠端(remote)的資料 在 git bash 上輸入 `git remote add origin https://github.com/你的帳號名稱/intro-to-git.git` 以上指令用 `origin` 來代表`https://github.com/你的帳號名稱/intro-to-git.git` 這個遠端的數據庫位置。 輸入 `git remote -v` 檢查遠端目前的資料,會出現以下結果 ![](https://i.imgur.com/0r8mfE6.png) :::warning 電腦上如果有多個 git 控管的repository(資料夾),則每個 repository 可以對應不同的遠端數據庫(即可以有不同的 github repository) ::: #### 2.將本地端的分支(branch)重新命名成 `main` 在 git bash 上輸入 `git branch -M main` ,這個指令將原本 github 上 `master` 分支改名成 `main`,可以輸入指令`git log --oneline`,會發現分支名稱從 `master` 改成 `main` ![](https://i.imgur.com/RCbFbcL.png) :::success 本地端(電腦)的 branch 名稱和遠端(github)上的 branch 名稱需相同,但本地端 branch 預設名稱為 `master`,遠端 branch 預設名稱為 `main`,因此我們需要先將本地端的名稱改為從 `master` 改成 `main`,才能將程式 push 到 github ::: :::warning 小故事:[github 預設分支名稱從 `master` 改成 `main`](https://www.ithome.com.tw/news/140094) ::: #### 3.將本地端檔案資料 push 到 遠端 在 git bash 上輸入 `git push -u origin main` ,其中 `origin` 代表遠端數據庫名稱(即 `https://github.com/你的帳號名稱/intro-to-git.git`),而 `main` 代表你要推送哪個分支,其中引數 `-u` 代表 `origin` 這個遠端數據庫和 `main` 這個分支是預設分支,因此之後若在 `origin/main` 上推送資料和取得遠端資料,可以簡寫成 `git push` 或 `git pull` ![](https://i.imgur.com/FSi7vXc.png) 完成 push 後重整 github repository 頁面,本地端的 `main.cpp` 出現在 github 上 ![](https://i.imgur.com/fdupjea.png) :::info 關於-u參數的細節可以參考[這裡](https://zlargon.gitbooks.io/git-tutorial/content/remote/upstream.html) ::: :::info 本地端與遠端之流程圖: ![](https://i.imgur.com/KiiAr8C.png) * git push將本地端的更新推送到遠端,使兩端同步 * git pull將遠端的更新下載到本地端,使兩端同步 * 這兩個指令是**與遠端互動的關鍵** ::: ### 3.`git clone` 若今天組員創了一個 [github網址](https://github.com/yuanchiachang/clone-practice.git) 請我一起合作開發,以下為流程 1.在桌面上(任意路徑)打開 git bash,輸入以下指令 `git clone https://github.com/yuanchiachang/clone-practice.git` (其中網址部分可以在 git bash 上按右鍵選擇 paste) ![](https://i.imgur.com/FZ8AliE.png) 執行 `main.exe` 會出現 ![](https://i.imgur.com/iKw0v09.png) 但由於這個 github repository 是別人的,你沒有這個 github repository 的管理權限,因此無法 push 更改的程式到遠端,若想要擁有管理權限,必須 github repository 的**擁有者**進行以下操作(上課時兩兩一組,其中一人負責 invite,另一人負責 accept invitation) * 1.點選 Settings/Manage access/Invite Collaborators ![](https://i.imgur.com/RKb2IMZ.png) ![](https://i.imgur.com/tqcqACI.png) ![](https://i.imgur.com/bhdGS9M.png) ![](https://i.imgur.com/Kzikx9e.png) 此時受邀者的信箱(申請 github 填的信箱)會收到邀請信 ![](https://i.imgur.com/EQTqg0l.png) 按下 View invitation,網頁會跳到 github 上 ![](https://i.imgur.com/kAyRyhz.png) 按下 Accept Invitation ,之後便可以 push 程式碼到組員的 github repository 上了:sunflower: ![](https://i.imgur.com/njtFokF.png) ## 8. `.gitignore` 在C++中,`.cpp` 檔是我們想要給 git 控管的檔案格式,相反的,`.o` 檔、`.exe` 檔、 `.dll` 檔是我們不想讓 git 控管的檔案格式,但是我們又想要用`git add .`將所有 `.cpp` 檔加入暫存區,因此為了防止不想讓 git 控管的檔案加入暫存區,我們創建新的 `.gitignore` 檔,創建方法如下: ### 1.新增 `.gitignore` 檔 先在 `intro-to-git` 資料夾中新增 `.txt` 檔,把 `.txt` 檔重新命名成 `.gitignore` ![](https://i.imgur.com/ylcwV8D.png) ### 2.編輯 `.gitignore` 檔 打開 `.gitignore`,輸入 `*.exe` 並存檔 :::success `*` 代表所有同附檔名的檔案都不加入 git 控管 ::: :::danger 1. 若要忽略某個資料夾底下的所有檔案,可以在 `.gitignore` 中輸入`資料夾名稱/` 2. [此連結](https://github.com/github/gitignore/blob/master/C%2B%2B.gitignore)為 C++ 專用的 `.gitignore`,可以直接複製文字貼到自己專案的 `.gitignore` 裡面 ::: ![](https://i.imgur.com/IakoRsd.png) ### 3.將 `.gitignore` 加入控管 在 `intro-to-git` 上打開 git bash ,輸入 `git add .`,輸入完後輸入`git status` ![](https://i.imgur.com/ObtOv2O.png) 此時只有 `.gitignore` 檔被加入暫存區, `main.exe` 執行檔沒有被 git 控管 ### 4.推送到 github 在 git bash 中輸入 `git commit -m "add .gitignore"`,接著輸入`git pull`將 github 上的檔案抓下來,再輸入 `git push` 將新的檔案 push 到 github 上 ![](https://i.imgur.com/LSFevwe.png) ![](https://i.imgur.com/cTuEzI2.png) :::warning 和他人合作專案時,如果要 push 程式碼前要先 pull github 上的程式碼,將 github 上的程式碼和本地端的程式碼 merge(後面會講到),如果遇到 conflict(後面會講到)必須手動解除,如果沒有 conflict 再將程式碼 push 到 github 上 ::: ## 9.branch (分支) and merge(合併) ### 1.為甚麼要有 branch? 在 git 操作上, `main` 分支上的版本是穩定的版本,當我們要新增功能(feature)時,並不會直接更改 `main` 上面的版本,而是新開一條測試用的分支,當這條測試用的分支成功新增功能,我們再將這條分支和 `main` 分支合併,反之若這條測試用的分支被寫爛了,我們可以直接丟掉這條分支。 ### 2.實際操作 #### 1.創建並切換到新的分支 `greeting` 在 git bash 上輸入 `git checkout -b greeting` ![](https://i.imgur.com/3amK9NP.png) 其中 `checkout` 代表轉換到另一條 branch,`-b` 代表創建一條新的 branch(`greeting`),並同時轉換到這條新的 branch(`greeting`) 上 此時在 git bash 上輸入` git status `,顯示 `On branch greeting`,代表現在的分支已經在 `greeting` 上,再輸入 `git log --oneline`,此時 `HEAD` 指向 `greeting` 分支,代表現在的更改程式碼影響到的是 `greeting` ![](https://i.imgur.com/rdm2Yit.png) #### 2.在新的分支 `greeting` 上創建版本 在 `main.cpp` 中輸入以下程式碼並編譯 ```cpp=1 #include <iostream> #include <string> using namespace std; int main(){ string name = ""; cout << "please enter your name?" << endl; cin >> name ; cout << "hello " << name << endl; system("pause"); return 0; } ``` 在 git bash 上輸入 1. `git add .` 2. `git commit -m "finish greeting"` 3. `git log --oneline` ![](https://i.imgur.com/D8zhQT7.png) 此時我們要將 `greeting` 分支合併進 `main` 分支,這步之前我們要先切換回 `main` 分支,才能將 `greeting` 分支合併進 `main` 裡面 在 git bash 上輸入 1. `git checkout main` 2. `git status` 3. `git log --oneline` ![](https://i.imgur.com/8UPWLpO.png) `git status` 回應 `On branch main`,而 `git log --oneline` 回應 `HEAD` 指向 `main` 分支,此時的 `main.cpp` 回到之前版本 ```cpp=1 #include <iostream> using namespace std; int main(){ cout << "hello world" << endl; system("pause"); return 0; } ``` 在 git bash 上輸入 `git merge greeting`,我們在 `main` 上將 `greeting` 分支的程式碼和 `main` 合併 再輸入 `git log --oneline` 此時 `main` 分支版本的 commit message 為 `finish greeting`, `main.cpp` 版本為 `greeting` 的版本 ![](https://i.imgur.com/F5Hu9y0.png) ```cpp=1 #include <iostream> #include <string> using namespace std; int main(){ string name = ""; cout << "please enter your name?" << endl; cin >> name ; cout << "hello " << name << endl; system("pause"); return 0; } ``` 最後,`greeting` 分支的程式碼已經和 `main` 合併,因此可以將 `greeting` 分支刪除,在 git bash 上輸入 1. `git branch -d greeting` 2. `git log --oneline` ![](https://i.imgur.com/R1IUBCD.png) `greeting` 分支不見,剩下 `HEAD -> main` 以上為 git branch 開發新功能的簡單例子 ## 10.conflict ### 1.甚麼是 conflict? 當你與組員同時開發功能時,若更改到同一份程式碼,git 在合併時沒有辦法判斷如何取捨程式碼,這種情況稱為 conflict,當遇到 conflict 時需要手動解決,以下我們將在本地端示範 conflict 以及如何解決 ::: warning conflict 容易出現在從 github pull 下來程式碼時,當 github 上面的程式碼被組員 push 上去,你又在本地端寫了同檔案的 code,便會出現 conflict ::: ### 2.conflict 示範 在 git bash 上輸入 1. `git branch age` 2. `git branch gender` 創建兩個新分支叫做 `age` 和 `gender` ![](https://i.imgur.com/613jTyF.png) 輸入 `git checkout age` ,切換到 `age` 分支 在 `main.cpp` 中輸入以下程式碼,編譯並存檔 ```cpp=1 #include <iostream> #include <string> using namespace std; int main(){ string name = ""; unsigned int age = 0; cout << "please enter your name?" << endl; cin >> name ; cout << "hello " << name << endl; cout << "how old are you?" << endl; cin >> age; if(!age > 0) age = 0; cout << "You are " << age << " year(s) old." << endl; system("pause"); return 0; } ``` 輸入 1. `git add .` 2. `git commit -m "finish age"` ![](https://i.imgur.com/3gz6z3L.png) 輸入 `git checkout gender` ,切換到 `gender` 分支 在 `main.cpp` 中輸入以下程式碼,編譯並存檔 ```cpp=1 #include <iostream> #include <string> using namespace std; int main(){ string name = ""; char gender = '\0'; cout << "please enter your name?" << endl; cin >> name ; cout << "hello " << name << endl; cout << "please enter your gender?(M/F)" << endl; cin >> gender; if(gender == 'M' || gender == 'm') cout << "Hi Mr. " << name << "." << endl; else if(gender == 'F' || gender == 'f') cout << "Hi Ms. " << name << "." << endl; else cout << "Hi " << name << "." << endl; system("pause"); return 0; } ``` 輸入 1. `git add .` 2. `git commit -m "finish gender"` ![](https://i.imgur.com/6qGgJic.png) 接著,我們要合併 `age` 和 `gender` 到 `main` 分支 輸入 1. `git checkout main` 2. `git merge age` 3. `git merge gender` ![](https://i.imgur.com/FLvMMCv.png) 在合併 `age` 分支時可以順利合併,但是在合併 `gender` 分支時由於 `age` 和 `gender` 都有改到 `main.cpp` 檔,git 無法幫我們決定要怎麼改動檔案,因此我們必須到 `main.cpp` 裡面手動解決 conflict ![](https://i.imgur.com/tqQTxNF.png) 由上圖可知,`unsigned int age = 0;`是目前 `main` 分支的程式碼,`char gender = '\0';` 是 `gender` 分支的程式碼,由於我們兩個都想要,因此我們要將 git 生成的 `<<<<<<< HEAD` `=======` `>>>>>>> gender` 手動刪除,或是直接按 `HEAD` 上面 `Accept Both Changes` 同理後面的程式碼,我們也是手動刪除 `<<<<<<< HEAD` `=======` `>>>>>>> gender` ,或是直接按 `<<<<<<<HEAD` 上面 `Accept Both Changes `(VSCode特有,~~真棒,yyds~~) ![](https://i.imgur.com/azjrwad.png) 解決所有 conflict 並存檔後,在 git bash 上輸入 1. `git add .` 2. `git commit` ![](https://i.imgur.com/cfnjrJR.png) 按下 enter 鍵,在新的視窗中輸入 `:wq` ,在按 enter 鍵 此時回到正常 git bash 頁面,輸入 `git log --oneline`,確認成功合併 `gender` 分支 ![](https://i.imgur.com/3KfQWkW.png) 最後,將合併成功的 `age` 分支和 `gender` 分支刪除 1. `git branch -d age` 2. `git branch -d gender` 3. `git log --oneline` ![](https://i.imgur.com/zs0VQK9.png) 以上為解決 conflict 的簡單例子 ## 11.undoing ### 1.甚麼是 undoing? 目前 `intro-to-git` 共有 5 個版本 ![](https://i.imgur.com/unC204J.png) 若我想要回到之前 `say hello world` 這個版本,在 git 中有三種方法,分別為 1. `git checkout` 2. `git revert` 3. `git reset` 其中 `git checkout` 指令不會改變任何 git 的版本歷史,最為安全,`git reset` 指令一旦執行就無法回到原本的檔案,最為危險,以下我們將介紹 `git checkout`,另外兩種方法可以參考以下教學影片 [Git & GitHub Tutorial for Beginners #7 - Undoing Things](https://www.youtube.com/watch?v=RIYrfkZjWmA&list=PL4cUxeGkcC9goXbgTDQ0n_4TBzOO0ocPR&index=7) {%youtube RIYrfkZjWmA %} ### 2.`git checkout` 在 branch 中我們有用到 `git checkout main`,那時的 `checkout` 指的是切換分支,然而 `checkout` 也可以代表切換分支中的版本,若我今天想切換到 `say hello world`,在 git bash 中輸入指令 `git checkout XXXXXXX`,其中`XXXXXXX`為 `say hello world` 前的七位數字(綠色框框裡面,每個人都不一樣) ![](https://i.imgur.com/ZDwjR9s.png) ![](https://i.imgur.com/yjueXE4.png) 此時 `main.cpp` 中的程式變成一開始的程式碼 ```cpp=1 #include <iostream> using namespace std; int main(){ cout << "hello world" << endl; system("pause"); return 0; } ``` 在 git bash 上輸入 `git log --oneline` ![](https://i.imgur.com/SYDTCKC.png) 只剩下 `say hello world` 這個版本 若我們看完後想回到有 `age` 和 `gender`的程式碼,則輸入 `git checkout main`,並用 `git log --oneline` 確認目前 `HEAD` 指向的版本 ![](https://i.imgur.com/ghOTaK2.png) :::info 甚麼是 `HEAD`? `HEAD` 就是你目前指向的版本狀態,當你的 `HEAD` 指向哪個版本,在文字編輯器中開啟的程式碼就會是那個版本的程式碼。 [git checkout 移動 `HEAD` 指標](https://w3c.hexschool.com/git/9a164fbe) ::: 這時 `main.cpp` 中的程式碼回到 undoing 前的程式碼 ```cpp=1 #include <iostream> #include <string> using namespace std; int main(){ string name = ""; unsigned int age = 0; char gender = '\0'; cout << "please enter your name?" << endl; cin >> name ; cout << "hello " << name << endl; cout << "how old are you?" << endl; cin >> age; if(!age > 0) age = 0; cout << "You are " << age << " year(s) old." << endl; cout << "please enter your gender?(M/F)" << endl; cin >> gender; if(gender == 'M' || gender == 'm') cout << "Hi Mr. " << name << "." << endl; else if(gender == 'F' || gender == 'f') cout << "Hi Ms. " << name << "." << endl; else cout << "Hi " << name << "." << endl; system("pause"); return 0; } ``` ## 12.結語 希望大家在這堂課中對 git 有初步認識,並且願意用 git 來管理計程的期末 project,有許多深入的觀念我們沒辦法用 2 小時全部教完,遇到問題時在 google 上搜尋會有很多解法,各位有能力自行解決。 ~~電機系 project 還用 messenger/雲端硬碟存 code 也太丟臉了~~