## 教學網站 :::info https://git-scm.com/book/zh-tw/v2/ ::: ## 甚麼是Git :::success Git是一種「分散式版本的版本控制系統」,是一種軟體 * 版本控制:不管是新增/刪除/修改檔案,都稱之為一個「版本」,而版本控制系統會幫忙記錄這之間的所有變化,隨時切換到過去某個「版本」的狀態,亦會記錄每個檔案是何時被修改,以及被誰修改 * 集中式 v.s. 分散式: * 集中式:需要有一台專門的伺服器,所有檔案存取都須跟這台伺服器溝通,所以若沒有網路則無法使用,缺點是若無法將已修改完提交出去,就無法修改其他項目,因為這樣會使得應該被分為多次提交的內容混在一起了 * 分散式:與集中式最大的差別是,分散式的版本控制每個開發者都可以在自己的一部或多部開發機器上建立檔案庫,所有版本管理有關的資訊(例如提交訊息、版本變化等),都會記錄在每一個檔案庫上,因此可以直接在本地端對版本控制系統進行操作,可以在離線狀態下持續進行修改(同時可獲得版本控制系統的支持eg.管理版本、查詢歷史修改等),也不需持續將修改送至集中檔案庫上 ### 特點 * 紀錄檔案快照(snapshot),而不是差異: * Git和其他系統最主要的差別是處理資料的方式,其他系統是紀錄每個版本間檔案的差異,而Git是紀錄目前所有檔案的樣子(快照),不會再次儲存沒有變更的檔案,而是參照前後的快照 ![image](https://hackmd.io/_uploads/HynWbQIMC.png) * 每次版本更新時,Git會更新並記錄整個目錄跟檔案的樹狀結構 * 所有git設定都存在`.git`目錄裡(所以若想給別人一個單純的資料夾,只要把`.git`目錄移除就可以) * git通常只增加資料,所以只要有定期push,資料基本上都不會不見 * git檔案 * 預存區/索引/暫存區(Staging Area):`.git`目錄下,儲存關於下次要提交的資訊的檔案 * 分成三種狀態 1. 已提交(committed)- 檔案己安全地存(commit)在你的本地端資料庫 2. 已修改(modified) - 檔案已被修改但尚未提交到本地端資料庫(未預存) 3. 已預存(staged)- 檔案先被修改,接著被增加(add)到預存區域,這個檔案將會被存到下次你提交的快照中 * 工作流程 1. 修改檔案 2. 預存檔案,將檔案的快照新增到預存區(staging area) 3. 提交,讓預存區的檔案永久存在本地git directory中 ![image](https://hackmd.io/_uploads/Hyo5iE8zC.png) ![image](https://hackmd.io/_uploads/BkJbTVUfA.png) ::: ## 指令 :::info ### 初次設定 * 設定識別資料(使用者名稱、email):git再提交時都會用這個資料(不可再修改) ``` $ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com ``` p.s. `--global`:使得在此系統,不論 Git 做任何事都會採用此資訊 ### 檢查使用者設定 ``` $ git config --list ``` --- ### 查看檔案狀態 偵測那些檔案處於甚麼狀態的語法:`$ git status` 1. 已追蹤:上次在快照中的檔案 1. 未修改 2. 已修改:自從上次提交後編輯的某些檔案,會被視為已修改 3. 已預存(staged):預存已修改的內容,並提交所有已預存的檔案 2. 未追蹤:在工作目錄中卻不包含在上次的快照中,也不在預存區的檔案 ![image](https://hackmd.io/_uploads/rkN4QH8GR.png) * `Changes to be committed:`:已追蹤,已經放入預存區 * `Changes not staged for commit`:已追蹤的檔案被修改且未預存 * `Untracked files:`:未追蹤 ### 檢視檔案中修改內容 1. 比對「工作目錄」與「尚未被存入預存區」全部檔案的差異 → 用來檢視「未預存」 ``` git diff ``` p.s.`git diff`部會顯示最後一次提交後的所有變更,只會顯示未預存的變更,所以如果預存了所有變更,`git diff`不會輸出任何內容 2. 比對「預存區」和「最後一次提交」的差異 → 用來檢視「已預存」 ``` git diff --staged ``` ``` git diff --cached ``` ### 忽略不需要的檔案 * 不想讓 Git 自動加入,也不希望它們被顯示為未追蹤的檔案可設成忽略(例如編譯過程中產生的.o/.a檔) * 步驟: step 1:建立名為`.gitignore`的檔案 step 2:編輯`.gitignore`檔,寫入想要的正規運算式 * 要忽略的檔名以Glob模式(shell的簡化版正規運算式)表示,規則如下: * 空白列或以`#`開頭的列會被忽略 * `!`代表將模式規則反向 * 一個星號`*`匹配零個或多個字元 * `[]`匹配中括弧內其中一個字元,可用`-`連結代表匹配該範圍的字元(eg.[0-9]) * `?`匹配單一個字元 * 兩個星號`**`用來匹配巢狀目錄(eg.`a/**/z`會匹配到`a/b/z`或`a/b/c/z`等) ``` $ cat .gitignore *.[oa] //忽略.o .aˇ檔 *~ // 忽略已~為結尾的檔 ``` --- ### 建立git repository 兩種方法取得一個Git倉儲(repository) 1. 將現有專案或資料夾加入git * 追蹤現有的專案,須進入該專案的資料夾 ``` $ git init //建立一個名為 .git 的子資料夾,但此時倉儲還未追蹤任何檔案 ``` 2. 從其他伺服器clone一份倉儲 * clone會將遠端倉儲的資料都抓(pull)下來,包括歷史紀錄,並非單純複製 * 語法:`git clone [url]` ``` $ git clone https://github.com/libgit2/libgit2 mylibgit //將clone下來名稱是libgit2資料夾的名字改成mylibgit,且會在此資料夾下初始化一個`.git`資料夾 ``` ### add `add`指令有多種用途 1. 開始追蹤:將資料夾中檔案加入暫存區,開始追蹤該檔案 2. 預存檔案:將修改後的檔案放入預存區 * 語法: ``` $ git add [檔名or資料夾or"."可以加入全部or"*.檔案型態"可加入所有特定型態的檔案] ``` ### commit * 提交修改到本地端資料庫,並說明這次的 commit 做了什麼事 * 只會處理「暫存區」裡的內容,所以commit前要先add 1. 說明只能一行 * 語法:git commit -m '修改的說明' 範例 ``` $ git commit -m "Story 182: Fix benchmarks for speed" [master 463dc4f] Story 182: Fix benchmarks for speed 2 files changed, 2 insertions(+) create mode 100644 README //master是提交的分支、463dc4f是提交的 SHA-1 校驗碼 ``` 2. 說明寫在commit log檔案中跟其他檔案一起commit上去 * 語法:git commit 3. 將add和commit合併成一個指令,自動預存所有已追蹤的檔案 * 此方法雖方便但有時候它會納入你並不想要的變更 * 語法:`git commit -a` ``` $ git commit -a -m 'added new benchmarks' ``` --- ### rm刪除檔案 * 要從 Git 中刪除一個檔案,你需要將它從已追蹤、已預存中移除,然後再提交,它同時也會將該檔案從工作目錄中移除,這樣之後也不會身為未追蹤檔案而被你看到 * 語法: ``` $ git rm 檔名 ``` * **如果僅僅是將檔案從工作目錄中移除`rm 檔名`,那麼它會被列在 git status 輸出內容的「Changed but not updated」(也就是「未預存」),還需要經過`git add`將其變更放入預存區** ### mv移動檔案、重新命名檔名 * git不會明確地追蹤檔案的移動,你移動以後他會自己自動再找到檔案 ``` //重新命名 $ git mv file_from file_to //相當於以下,所以Git能自動追蹤找到檔案 $ mv README.md README $ git rm README.md $ git add README ``` 所以`rm`相當於一次執行三個指令,他只是一個方便的功能 ::: ## 檢視提交的歷史紀錄 :::success 在產生數筆提交或者clone一個有歷史紀錄的repository後,可以用`git log`檢視之前發生甚麼事 * 預設情況下,`git log`會由新到舊列出版本庫的提交歷史,會列出每筆提交的SHA-1校驗碼(用來識別每個commit,不會重複)、作者名字及電子郵件、寫入日期及提交訊息 ![image](https://hackmd.io/_uploads/HJWbYoUGR.png) 有數種參數可加: * 加上`-p`,顯示每筆提交所做的交易內容,也可加上`-<n>`選項,限制只輸出最後n筆,除了顯示相同的資訊,還會在每筆提交資訊後面加上每個修改檔案的差異內容 ``` $ git log -p -2 ``` * 加上`--stat`,檢視每筆提交簡略的統計資訊,包含「被更動的檔案」、「總共有多少檔案被更動」、「這些檔案有多紹行被加入或移除」 * 加上`--oneline`,一行一行檢視提交資訊 * 加上`--pretty`,用來改變原本預設輸出的格式,有數種參數可加: * `--pretty=online`:將每一筆提交顯示成單獨一行 * `--pretty=format`:可以指定自訂的輸出格式 範例: ![image](https://hackmd.io/_uploads/r11EAjUMR.png) 常見格式選項: ![image](https://hackmd.io/_uploads/HyUxpo8MC.png) ### 限制時間範圍的輸出 `git log`提供只顯示一個子集合的提交,以下是常見參數: * `--science`、`--after`:可用特定日期格式(yyyy-mm-dd)、相對日期格式(eg.`2 years 1 day 3 minutes ago`)、近期(eg.`2.weeks`這兩周),選擇要看到指定日期後的提交 ``` $ git log --since=2.weeks $ git log --since=2024-05-01 $ git log --since=2 years ago ``` * `--grep`:允許以關鍵字或正則表達式過濾提交訊息 ``` $ git log --grep '要找的字串' $ git log --grep <regexp> ``` ::: 復原 :::info ### 重新提交 有時太早提交,才發現忘記加入某些檔案,或是寫錯提交訊息可以使用 1. `--amend`修改最新一次的提交 ``` //跳出Vim視窗編輯要修改的訊息 $ git commit --amend //直接加上要修改的訊息 $ git commit --amend -m "要修改的訊息" //不要編輯提交訊息,所以不會跳出vim視窗 $ git commit --amend --no-edit ``` 對 git 而言 commit 的內容不同,他就會認為這是一個新的東西,所以會重新產生一組 SHA-1 碼 2. `--reset`退回到指定的提交紀錄,用`git log --oneline`查詢提交紀錄的SHA-1編碼 * 相對: ``` //^有幾個就代表依據前面的SHA-1編碼的提交紀錄往退回幾次 $ git reset [SHA-1編碼]^ //多個^時可以使用~<n>代替次數 //也可以使用HEAD或master表是當前最新的提交 $ git reset master^ $ git reset HEAD^ ``` p.s. HEAD是一個指標,通常指向某個分支,可以把 HEAD 當做「目前所在分支」看待,`.git`目錄裡有`HEAD`檔,紀錄HEAD的內容 master是默認的主要分支名稱 * 絕對: 直接指明目前的狀態要退回到哪個提交紀錄(SHA-1編碼) ``` $ git reset [SHA-1編碼] ``` * `reset`常見參數 * `--mixed`:預設參數。會把暫存區的檔案丟掉,但不會動到工作目錄的檔案,Commit 拆出來的檔案會留在工作目錄 * `--soft`:當前工作目錄跟暫存區的檔案都不會被丟掉,Commit 拆出來的檔案會直接放在暫存區 * `--hard`:不管是工作目錄以及暫存區的檔案都會丟掉,完全不保留原始 commit 結點的任何資訊,直接將工作區、預存區及 git 目錄都重置成目標 Commit 拆出來的內容 ### 復原被修改的檔案 若誤刪檔案或想將檔案回到上次提交前的樣子,可以使用 ``` $ git checkout [檔名or資料夾or"."可以加入全部or"*.檔案型態"可加入所有特定型態的檔案] ``` ::: ## 創建分支 :::success 當使用 git commit 建立一個提交時,Git 會先計算每一個子目錄的雜湊值,然後在 Git 版本庫中將這些目錄記錄為樹物件(用來列出目錄的內容並紀錄各個檔案); 之後 Git 建立提交物件,它除了包含相關提交資訊以外,還包含著指向專案根目錄的樹物件指標。 > 雜湊值: > 檢測目錄內容變化的一種機制,當目錄的內容發生變化時,雜湊值也會隨之改變。通過比較新的雜湊值和先前的雜湊值,Git 可以快速確定目錄內容是否已經發生變化。 ![image](https://hackmd.io/_uploads/HkWGprvMC.png) 如果你做一些修改並再次提交,這次的提交會再包含一個指向上次提交的指標。 ![image](https://hackmd.io/_uploads/BJ3LaSDzA.png) ### 建立一個新的分支 建立一個新分支會建立一個新的、可移動的指標; 比如新建一個 testing 分支, 可以使用 git branch 命令: ``` $ git branch testing ``` 在目前提交上新建一個指標。 ![image](https://hackmd.io/_uploads/r1M9CBDMR.png) 但執行`git branch`命令,只是「建立」一個新的分支——它並不會切換到這個分支。 要切換到一個已經存在的分支,可以執行 `git checkout` 命令 ``` $ git checkout testing ``` 這會移動 HEAD 並指向 testing 分支。 > 可以利用`$ git log --oneline --decorate`或`$ git branch` > 查看HEAD目前指向哪 ![image](https://hackmd.io/_uploads/r1LZyUvGA.png) 切換完分支再做提交,只有被HEAD指到的分支才會移動 ![image](https://hackmd.io/_uploads/ry5JbIwzC.png) ### 分支合併 1. 首先要切回原本的分支 ``` $ git checkout master ``` 2. 然後執行 `git merge` 命令: ``` $ git merge testing ``` 3. 如果不需要分支了就將合併完的分支刪除 ``` $ git branch -d testing ``` 這樣就完成了合併 ### 合併衝突的基本解法 如果在不同的分支中都修改了同一個檔案的同一部分,Git 就無法乾淨地合併它們。 Git 沒有自動產生新的合併提交, 它會暫停下來等你解決衝突; 在合併衝突發生後的任何時候,如果你要看看哪些檔案還沒有合併,可以使用 `git status`: ![image](https://hackmd.io/_uploads/ByQ4VUwfR.png) 它會列出所有有合併衝突且仍未解決的檔案(列在 *Unmerged paths:* 下面), Git 會在有衝突的檔案裡加入標準的「衝突解決(conflict-resolution)」標記,因此你可以手動開啟它們以解決這些衝突。 衝突的檔案大概會包含這些區段: ![image](https://hackmd.io/_uploads/HJbIXUDfA.png) 可以看到=======隔開的上半部是HEAD(即在執行合併指令前所切換過去的分支)中的內容,下半部則在iss53分支中的內容; 解決衝突的辦法無非是二選一,或者自己合併內容。 或是可以用`git mergetool`圖形介面的工具來解決 解決衝突後,可以再次執行git status來確認所有衝突都已經解決。 ![image](https://hackmd.io/_uploads/S1KUEUwM0.png) 如果確認所有衝突都已經解決也預存了,就`git commit`完成這次合併提交。 ::: ## Git工作流程 :::info Git常見有三種工作流程:GitFlow、GitHub flow、GitLab flow ### GitFlow 主要分成五種不同功能的分支:master(main)、develop、hofix、release、feature * 長期分支:master、develop → 會一直存活在GitFlow不會被刪除 * 短期分支:hofix、release、feature → 當完成專案後,這些更新的版本都會被合併進 Master 或 Develop 分支 ,之後就會被刪除掉 * 流程圖: ![image](https://hackmd.io/_uploads/ryZA0gfN0.png) * **master**:真正部屬的版本,必須確保上面的每個版本都要可以執行且穩定,此分支來源只能是從其他分支合併過來,開發者不能直接commit到這個分支,通常會在 master 分支上的 commit 貼上「 版本標籤 」 #### 五種分支 * **develop**:用於功能開發階段,所有開發的基礎分支,當要開發新功能時,所有的 feature 分支都是從這個分支切出去的,而 feature 分支的功能完成後,也都會合併回來這個分支。 * **hofix**:當master有Bug時,會緊急產生hotfix的分支修復,修完後再合併回master,也同時會合併到develop分支(避免之後develop合併到master時同樣的問題又出現) * **release**:當認為develop分支夠成熟了,就可以合併到release,此分支是正是部屬前的最後測試,測試完後,release再合併到master(正式部屬)和develop(release有bug時還需藉由develop修正,所以版本要相同) * **feature**:用於要新增一個新功能時。先從develop分支切出來做修改,完成後再合併回develop (p.s. feature branch的功能切割越小越好,避免多個feature合併時容易遇到衝突) #### 優缺點 * 優: - 職責分明:透過不同明確職責的分支分隔出特定的環境,適合大型團隊以及跨多個團隊的協作並行開發 - 有效處理多個產品版本:通過釋出分和修補分支,Gitflow 使得團隊可以同時支持多個版本(開發新版本的同時,維護和更新已發布的舊版本,確保所有版本都能及時得到支持和修補) * 缺: - 分支眾多較為複雜 --- ### GitHub flow * 簡化流程,改良GitFlow太複雜的缺點 * GitHub flow下只有兩種分支,一個是master,其餘的都是branch,而branch命名應該要有敘述性 * GitHub比git多兩個服務,一個是fork,另一個是pull request(PR) * 流程: 1. 從遠端倉儲用fork複製一份倉儲放到自己的個人遠端倉儲(也可以創建分支) 2. 在個人遠端倉儲中進行修改並提交(commit) 3. 要將分支合併到master,需發起Pull request,此PR有以下幾個功能: - 通知團隊自己的分支要合併回master - 遇到問題可在PR裡談 - 請管理者來合併此分支 - 留下修改、討論的歷史紀錄 4. master可部署上線 ![image](https://hackmd.io/_uploads/BknUuffN0.png) #### 優缺點 * 優:流程簡單、適合要持續發布的專案 * 缺:無法滿足分作開發、驗證、測試等區域 --- ### GitLab flow * 結合GitFlow的分區開發、GitHub flow的簡潔流程及PR * 上游優先(upstream first)原則:除了master,其餘分支都是下游,當下游出問題時,需要先開個分支,修完bug後合併回Master,再從master往出問題的下游合併,避免開發者在下游修完bug忘了合併回master,造成後續的分支又遇到同樣的bug * 分成**持續發布**、**版本發布**兩種情況因應不同的開發流程 * 持續發布: * 通常會有production(用於生產環境)、pre-production(用於欲發環境)分支(可根據不同環境建立對應的分支eg. 測試環境、欲發環境) * 若production出錯時,則要建一個新分支修改完後合併到最上游的master(開發環境),且經過測試,再繼續往pre-production合併,經過測試沒問題後才能夠在往下合併到production(這就是上游優先原則) * 目的:master能持續的穩定前進,release的分支既能在當下版本保持穩定,也能跟著master修補bug ![image](https://hackmd.io/_uploads/S1EbCQGV0.png) * 版本發布: * 當一個穩定的版本要對外發布時才從master拉出來創建一個新release分支,如下圖的2-3-stable、2-4-stable(release分支),若出現問題則從對應版本號的release分支中創建一個修復分支,修復完成需先合併至master,確認沒問題過後,再合併到release分支,且要更新版本號 ![image](https://hackmd.io/_uploads/HJo0amGVC.png) * 同GitHub flow一樣,先拉分之至本地端,修改完後提出Merge Request,相當於GitHub flow的Pull Request,再由管理者合併 :::