# Git 版本控制核心與 GitHub 協作實務 :::info **Author: Tai Ming Chen  ==[Contact me](mailto:3526ming@gmail.com)==** **Lead, Google Developer Group On Campus NCUE** ::: > [!Important] **摘要:** > 本文檔涵蓋 Git 版本控制核心概念與 GitHub 協作實務,並於各章節後附上情境式實務演練,可由此練習並確保自己已充分理解。此文內容涵蓋 Git 環境建置與三區 (Working Directory / Staging Area / Local Repository) 運作原理、分支管理與衝突解決、SSH 遠端連線配置、標準 GitHub Flow (Issue / PR) 團隊協作流程、版本回溯救援機制(Stash / Reset / Revert)及 GitHub Actions 自動化整合,適合追求系統化學習與實戰接軌的開發者。 > [!Note] **前言** > 本文之實務演練與指令示範,均以 **Linux / Unix-like 終端機環境** 為基準。如尚未熟悉基礎系統操作,建議 (**但非必要**) 可先閱讀我寫的另一篇文章 **[Linux 系統核心與操作實務](https://hackmd.io/@mingchen/linux)**,以建立基礎觀念。 **目錄** [TOC] --- ## 一、Git 與 GitHub 生態系與環境建置 (Ecosystem and Environment Setup) ### 1.1 背景介紹:它們解決了什麼問題? ![image](https://hackmd.io/_uploads/r1BSgsTIZl.png) 在學習指令之前,我們必須先理解為什麼全世界的工程師都離不開這套系統。試想在沒有版本控制系統的年代,我們是如何管理檔案的? #### 1.1.1 傳統作業的痛點 (The Pain Points) 1. **備份地獄**:你一定看過這種資料夾結構 — `專案_final`, `專案_final_v2`, `專案_最終確認版`。這種人工備份方式不僅佔用空間,而且根本記不得各版本之間到底改了哪些內容。 2. **協作災難**:當多個人同時要修改同一個檔案,卻**缺乏即時同步機制**時,一旦兩個人剛好都改了第 50 行,**後存檔的人**就會**無情覆蓋**掉先存檔的人的心血,造成無法挽回的資料遺失。 3. **無法後悔**:寫程式經常需要實驗新功能。若沒有版本控制,一旦改壞了程式碼想回到三天前的狀態,往往只能憑記憶手動復原,或者看著損壞的檔案欲哭無淚。 #### 1.1.2 Git 的解決方案 (The Git Solution) **Git** 是由 Linux 之父 Linus Torvalds 於 2005 年為了管理龐大的 Linux 核心原始碼而開發的 **分散式版本控制系統 (Distributed Version Control System)**。它完美解決了上述問題: * **版本回朔**:Git 不只儲存檔案,它儲存的是檔案在每一個時間點的**快照 (Snapshot)**。你可以隨時輸入指令,將整個專案瞬間「回溯」到昨天、上個月、甚至一年前的狀態。 * **分支 (Branching)**:Git 允許你開啟多個「分支」。你可以在 `testing` 分支隨意測試實驗性功能,而不會影響到正在運作的 `main` 主分支。確認沒問題後,再將兩個分支「合併 (**Merge**)」。 * **分散式架構**:這意味著不需要伺服器,**每一台電腦**(開發者的本機)都擁有一份**完整**的歷史紀錄與程式碼。即使在沒有網路的環境,你依然能進行提交、檢視歷史、切換版本。 #### 1.1.3 GitHub 的角色 (The GitHub Platform) 若將 Git 定義為 **「本地端的版本控制工具」**(負責在個人裝置上追蹤檔案歷史),那麼 GitHub 就是基於 Git 技術構建的 **「雲端程式碼託管服務」** (Code Hosting Service) 與 **「團隊協作平台」**。 Git 專注於處理檔案版本的演算與紀錄,而 GitHub 則將這些紀錄同步至雲端,並延伸出以下四大核心價值: * **異地備份與資產保全 (Remote Hosting & Backup)**: * 可將它視作 **「程式碼數位保險箱」**。在專業開發流程中,程式碼被視為最重要的資產。透過將本地端的 Git 儲存庫推送 (Push) 至 GitHub 的遠端伺服器 (Remote Repository),即便是發生本機硬體故障或資料損毀等不可預期的災難,您的專案原始碼依然能安全地保存在雲端資料中心,並可隨時同步 (Clone/Pull) 至任何一台電腦繼續作業。 * **協作審查機制 (Pull Requests & Code Review)**: * 這是 GitHub 建構現代軟體開發流程的基石。它引入了 **Pull Request (PR)** 機制:當開發者欲將新的程式碼合併至主分支時,並非直接寫入,而是發起一個「合併請求」。這讓團隊成員能在合併發生前,進行嚴格的 **程式碼審查 (Code Review)**,針對邏輯漏洞、語法風格進行討論與修正,確保進入主分支的程式碼品質穩定可靠。 * **專案管理中樞 (Project Management)**: * GitHub 除了用於儲存程式碼,更整合了完整的專案管理工具。透過 **Issues** (議題追蹤系統),團隊可標準化地回報 Bug 或規劃新功能;透過 **Projects** (看板管理),可視覺化追蹤開發進度;搭配 **Wiki** (技術文件),讓需求定義、開發進度與技術文件能在同一個平台上無縫整合。 * **開源生態系 (Open Source Ecosystem)**: * GitHub 是全球最大的開源軟體集散地。它建立了標準化的貢獻流程(Forking Workflow),允許開發者複製 (Fork) 他人的專案進行改良,並貢獻 (Contribute) 回原專案。現今主流的技術框架(如 Linux Kernel, React, TensorFlow)皆託管於此,形成了龐大的技術共享生態。 ### 1.2 安裝與基礎指令 在開始之前,請根據您的作業系統安裝 Git。這是我們與電腦溝通的橋樑。 * **Windows**:請下載 [Git for Windows](https://git-scm.com/download/win)。安裝時建議一路點擊 "Next" 使用預設值即可。安裝後請習慣使用 **Git Bash** 這款終端機軟體,它能讓你在 Windows 上使用類 Linux 的指令。 * **macOS**:開啟 Terminal 輸入 `git --version`,若未安裝,系統會自動跳出提示引導安裝,或使用 Homebrew:`brew install git`。 * **Linux**:Debian/Ubuntu 系列使用 `sudo apt install git`;CentOS/RHEL 系列使用 `sudo yum install git`。 安裝完成後,我們需要使用以下指令進行環境確認與「身分」設定: * **`git --version`**:檢查 Git 是否安裝成功以及目前版本。 * **`git config`** (Configure Git):設定 Git 的環境變數。這是最重要的初始步驟。 * **`git config --global user.name "Your Name"`**:設定全域使用者名稱。 * **`git config --global user.email "mail@example.com"`**:設定全域使用者信箱。 * **為什麼要設定這個?** Git 的核心精神在於「明確的責任歸屬」。 因此每一筆提交 (Commit) 都會記錄作者身分,除了確保程式碼的**可追溯性**(當發生問題時能快速定位變更來源),更能讓平台正確識別並計算開發貢獻。 * **`git config --list`**:列出目前所有的設定值,檢查是否設定成功。 ### 1.3 初始分支命名設定 在舊版的 Git 中,初始化專案的預設分支名稱為 `master`。但近年來為了響應多元包容性(移除 Master/Slave 主從制度的隱喻),國際軟體社群與 GitHub 平台已將預設主分支名稱全面改為 **`main`**。 為了避免將來推送到 GitHub 時產生 `master` 與 `main` 的分支混亂,強烈建議在安裝後直接執行以下指令: * **`git config --global init.defaultBranch main`**:設定未來初始化新專案時,預設分支名稱為 `main`。 >[!Note] **實務演練#1: 在專案開始前...** > > :::warning > **情境說明**: 你是剛報到的菜鳥工程師。你的電腦是一台剛重灌好的全新機器。技術主管交派給你的第一個任務非常簡單:**配置好標準的開發環境**。請注意,公司非常重視程式碼的可追溯性,如果你的提交紀錄顯示為 "Unknown User",你的程式碼將會被 CI 系統自動退回。 > ::: >請依序完成以下 4 個關卡: > > #### 關卡 1:檢查工具 > 1. 開啟你的終端機 (Terminal 或 Git Bash)。 > 2. 我們不希望使用太過老舊的工具。請輸入指令確認你的 Git 版號,確保安裝過程無誤。 > > #### 關卡 2:設定身分 > 1. 請告訴 Git 你的英文名字。 > 2. 請告訴 Git 你的信箱。 > 3. **關鍵要求**:請使用 `--global` 參數,確保這台電腦未來所有的專案都能自動套用這個身分,不需要每次重複設定。 > > #### 關卡 3:設定分支規範 > 為了配合公司的 GitHub 儲存庫設定,請將你本機 Git 的預設分支名稱從 `master` 修改為 `main`。這能避免未來合併代碼時發生名稱衝突的尷尬。 > > #### 關卡 4:驗收設定 > 請使用一個指令列出電腦中所有的 Git 設定清單,並自行核對:Name、Email、DefaultBranch 這三項資訊是否都已正確寫入且無拼字錯誤。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:檢查工具 ```bash # 檢查 Git 版本 git --version # 預期輸出範例: git version 2.39.0 (版號因系統而異,只要有顯示版本即成功) ``` #### 關卡 2 解答:設定身分 ```bash # 1. 設定使用者名稱 (請替換引號內的文字) git config --global user.name "DevNewbie" # 2. 設定使用者信箱 (這將是 GitHub 辨識你的依據) git config --global user.email "newbie@legacy-code.com" ``` #### 關卡 3 解答:設定分支規範 ```bash # 設定初始分支名稱為 main,這是一勞永逸的設定 git config --global init.defaultBranch main ``` #### 關卡 4 解答:驗收設定 ```bash # 列出詳細設定資訊 (按 q 可離開列表檢視) git config --list ``` **預期輸出結果** (應包含以下關鍵字): ```text user.name=[YOUR_NAME] user.email=[YOUR_EMAIL] init.defaultbranch=main ... (以及其他系統預設值) ``` ::: ### 核心觀念複習 1. **為什麼 `git config` 很重要?** * Git 的提交 (Commit) 紀錄一旦寫入,就是**唯讀且永久**的歷史。如果你一開始使用了錯誤的 Email (例如打錯字),即便後來修正了設定,舊有的那些提交紀錄上的 Email 依然會是錯的,這會導致你的 GitHub 貢獻牆 (Contribution Graph) 上出現斷層,無法統計到你早期的工作成果。 2. **`--global` 的作用範圍**: * 設定檔其實是純文字檔。加上 `--global` 時,Git 會去修改你家目錄下的 `.gitconfig` 檔案 (`~/.gitconfig`),這對當前使用者登入的所有專案都有效。 * 若不加 `--global`,Git 會去修改「當前專案資料夾」內的 `.git/config`,僅對該專案有效(這通常用於特殊情況,例如在公司電腦上想要用私人 Email 開發 Side Project 時)。 3. **Git 不等於 GitHub**: * 這點必須再次強調。Git 是工具,GitHub 是平台。你可以在本機盡情地使用 Git 寫作(版本控制),而不發表到 GitHub 上;但若要與他人分享,GitHub 就是最好的發佈平台。 --- ## 二、核心運作機制與單機操作 (Core Mechanics and Local Operations) ### 2.1 核心理論:Git 的三個區域   ==**Important!**== 要掌握 Git,必須先理解檔案在 Git 系統中流動的三個狀態。這與我們習慣的「編輯 -> 存檔」邏輯截然不同。我們可以將其想像成「**購物流程**」: 1. **工作目錄 (Working Directory)**: * **狀態**:**「貨架上的商品」**。 * **說明**:這就是你目前打開資料夾看到的樣子,也是你平常編輯程式碼的地方。這裡的檔案更動尚未被 Git 追蹤或記錄。 2. **暫存區 (Staging Area)**: * **狀態**:**「購物車」**。 * **說明**:這是一個過渡區域。你必須明確地將你想保留的變更撿到這裡。這讓你可以選擇性地提交檔案(例如:只想提交修改過的程式碼,但不想提交測試用的暫存檔)。 3. **儲存庫 (Local Repository)**: * **狀態**:**「結帳後的發票與明細」**。 * **說明**:只有進入這裡的檔案,才算真正被「永久記錄」下來。Git 會為這一次的提交產生一個獨一無二的 ID (SHA-1 Checksum),作為未來的查找憑證。 ### 2.2 基礎操作指令 ![image](https://hackmd.io/_uploads/B11XUJJPWl.png) * **`git init`** (Initialize): * **用途**:將目前的資料夾初始化為 Git 專案。 * **效果**:會在目錄下產生一個隱藏的 `.git` 資料夾,Git 所有的資訊(歷史紀錄、設定檔)都記錄在這裡。**切勿手動刪除此資料夾**,否則歷史紀錄將全數消失。 * **`git status`** (Check Status): * **用途**:查詢目前檔案的狀態。這是**最重要、最常用**的指令之一。 * **<span style="color: red;">Modified/Untracked</span>**:檔案在工作目錄,尚未進入暫存區。 * **<span style="color: green;">Staged</span>**:檔案已在暫存區,準備好被提交。 * **`git add filename`**: * **用途**:將檔案從「工作目錄」加入「暫存區」 (放入購物車)。 * `git add .`:將當前目錄下**所有**變更一次加入暫存區(最常用的懶人指令)。 * **`git commit -m "message"`**: * **用途**:將「暫存區」的內容提交到「儲存庫」 (結帳買單)。 * **參數**:`-m` 後面接的字串是 **提交訊息 (Commit Message)**,用來描述這一次改了什麼。這對未來回顧或交接時十分重要,因此切勿留空或填入無意義資訊,要確保透過此訊息能清楚知道該版本做了甚麼修改。 * **`git log`**: * **用途**:檢視歷史提交紀錄。 * `git log --oneline`:以單行精簡模式顯示,適合快速瀏覽。 >[!Note] **實務演練#2: 專案創建與 Git 檔案狀態體驗** > > :::warning > **情境說明**: 你的開發環境已設定完成,現在正式開始開發專案。你需要經歷從建立檔案、初次提交,到**修改既有檔案**的完整週期,親身體驗檔案在 Git 中的狀態變化。 > ::: >請依序完成以下 6 個關卡: > > #### 關卡 1:初始化 (Init) > 1. 回到家目錄 (`~`) 。 > 2. 建立一個名為 `mygit` 的資料夾,並進入該目錄。 > 3. 告訴 Git:**「請開始追蹤這個資料夾」** (初始化專案)。 > 4. **觀察**:使用 `ls -la` 指令,確認目錄下是否自動產生了一個名為 `.git` 的隱藏資料夾(這是用於紀錄 Git 資訊的隱藏資料夾)。 > > #### 關卡 2:建立檔案 (Create) > 1. 建立一個空的 `README.md` 檔案。 > 2. 建立一個 `main.py` 檔案,並寫入一行程式碼 `print("Hello Git")`。 > 3. **觀察**:使用 `git status` 檢查狀態。 > * 你會看到這兩個檔案列在 `Untracked files` 底下(紅色)。 > * 這代表 Git **完全不認識**這兩個新檔案。 > > #### 關卡 3:加入暫存區 (Add) > 1. 我們決定先只追蹤 `README.md`。請使用指令將 `README.md` 加入暫存區。 > 2. 再次使用 `git status` 檢查。 > * `README.md` 變為 **綠色 (Changes to be committed)**。 > * `main.py` 仍為 **紅色 (Untracked)**。 > 3. 覺得太麻煩了,請使用指令將剩餘的所有檔案 (`main.py`) ==**一次全部**== 加入暫存區。 > > #### 關卡 4:初次提交 (Commit) > 1. 確保所有檔案都變成綠色後,進行提交。 > 2. 提交訊息請依照規範寫:`"feat: initial commit with readme and main script"`。 > 3. 提交後,再次執行 `git status`,系統應顯示 `working tree clean` (購物車已清空)。 > > #### 關卡 5:需求變更 (Modify) > 1. 專案經理覺得歡迎詞太單調。請修改 `main.py`,將內容改為 `print("Glad to have you here in this Git project.")`。 > 2. **觀察**:再次使用 `git status` 檢查。 > 你會發現 `main.py` 再次變成了 **紅色**。注意看標題,這次它不是在 Untracked 底下,而是在 **`Changes not staged for commit` (Modified)** 底下,這代表 Git **認識這個檔案**,但發現它的內容變了,且尚未存入暫存區。 > > #### 關卡 6:完成循環週期 > 1. 請將剛剛的變更加入暫存區 (`git add`)。 > 2. 提交這個變更,訊息寫:`"feat: update welcome message"`。 > 3. 使用 `git log --oneline` 確認現在是否有 **兩個** 版本紀錄。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:初始化 ```bash # 1. 建立並進入目錄 cd ~ mkdir mygit cd mygit # 2. 初始化 git init # 3. 觀察隱藏檔 ls -la # 預期輸出: # '.' (當前目錄), '..' (上層目錄) 及 '.git' (紀錄 Git 資訊的隱藏資料夾) ``` #### 關卡 2 解答:建立檔案 ```bash touch README.md # 使用 echo 將字串寫入檔案 echo 'print("Hello Git")' > main.py git status # 觀察: 兩個檔案都在 Untracked files (紅色) ``` #### 關卡 3 解答:加入暫存區 ```bash # 加入單一檔案 git add README.md git status # 觀察: README 變綠,main.py 仍紅 # 加入全部 (. 代表當前目錄下的所有變更) git add . ``` #### 關卡 4 解答:初次提交 ```bash # 提交並附上符合規範的訊息 git commit -m "feat: initial commit with readme and main script" # 輸出: 2 files changed... ``` #### 關卡 5 解答:需求變更 ```bash # 1. 修改檔案 (覆蓋寫入新內容) echo 'print("Glad to have you here in this Git project.")' > main.py # 2. 檢查狀態 git status ``` **預期輸出 (關卡 5)**: 由輸出可見 `main.py` 的位置是在 **Changes not staged for commit** 之下。 ```text On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: main.py no changes added to commit (use "git add" and/or "git commit -a") ``` #### 關卡 6 解答:完成循環 ```bash # 1. 再次加入暫存區 (更新購物車內容) git add main.py # 2. 提交變更 git commit -m "feat: update welcome message" # 3. 檢查歷史紀錄 git log --oneline ``` **預期輸出 (關卡 6)**: ```text b2c3d4e (HEAD -> main) feat: update welcome message a1b2c3d feat: initial commit with readme and main script ``` ::: ### 核心觀念複習 1. **為什麼要有「暫存區」(Staging Area)?** * **過濾機制**:為什麼不直接 Commit 就好?幹嘛多一個 `add` 步驟? * **理由**:暫存區是 Git 給你的「**緩衝區**」與「**篩選器**」。想像你在寫作,工作目錄是你雜亂的桌面,暫存區則是準備要在期末繳交的「定稿夾」。 * **情境**:假設你同時修改了 `A.py` (修復登入 Bug) 與 `B.py` (調整首頁顏色)。若沒有暫存區,你只能一次把這兩件無關的事情綁在一起提交。但有了暫存區,你可以先 `git add A.py` 並提交,再 `git add B.py` 並提交,這能讓歷史紀錄井然有序,此概念也就是下一點所述的 **原子提交**。 2. **原子提交 (Atomic Commits)** * **定義**:**「一個 Commit,只做一件事情」**。這就像原子是構成物質的最小不可分割單位一樣,你的 Commit 也應該是不可分割的最小邏輯單位。 * **錯誤範例**:一個 Commit 訊息寫著 `Fix login bug AND change header color AND refactor database`。 * **正確做法**:拆分成三個 Commit。 1. `Fix login session timeout` 2. `Update header background color to blue` 3. `Refactor database connection module` * **好處**: * **容易除錯**:如果發現「資料庫重構」導致系統崩潰,你可以精準地只「復原 (Revert)」第 3 個 Commit,而不會影響到前面修好的登入功能。 * **容易審查**:同事在 Review 程式碼時,一次只專注審查一個邏輯,效率更高。 3. **Commit Message 的藝術** * **溝通而非紀錄**:Commit Message 不是給電腦看的,是給「**三個月後的你**」與「**接手你工作的同事**」看的。 * **撰寫要點**: * **不要寫**:`update`, `fix`, `modify` (這些是無意義的內容)。 * **要寫**:**Why (為什麼要改)** 與 **What (改了什麼)**。程式碼本身已經解釋了 **How (如何實作)**,所以訊息要解釋 **Context (脈絡)**。 * **業界標準 (Conventional Commits)**: 為了讓歷史紀錄一目了然,主流團隊通常會遵循 `動作: 主題` 的格式: * **`feat:`** (Feature):新增功能。例如 `feat: add login page`。 * **`fix:`**:修復 Bug。例如 `fix: resolve division by zero error`。 * **`docs:`** (Documentation):只修改文件 (如 README)。例如 `docs: update installation guide`。 * **`style:`**:不影響程式邏輯的格式修改 (如空白、縮排)。 * **`refactor:`**:重構 (不修 Bug 也不加功能,純粹優化程式碼結構)。 * **範例對照**: * **Bad example:** `Update main.py` * **Good example:** `fix: handle empty input in main.py to prevent crash` 4. **檔案生命週期的迷思** * 很多人會以為 `git add` 只是用來「新增檔案」,但其實 `git add` 的真正意思是 **「將目前的 ==變更內容== 更新到暫存區」**。 * **情境**: 1. 你修改了 `main.py`。 2. 執行 `git add main.py` (此時檔案在暫存區)。 3. 在你 Commit **之前**,你又再次修改了 `main.py`。 4. 注意此時 Git 會顯示 `main.py` **同時** 出現在「暫存區 (綠色,第一次改的)」與「工作目錄 (紅色,第二次改的)」。 5. 你必須**再次**執行 `git add main.py`,第二次的修改才會被納入即將提交的版本中。 --- ## 三、分支管理與平行宇宙 (Branching and Parallel Universes) ### 3.1 核心概念:指標與平行時空 在傳統的「複製貼上備份法」中,若想嘗試一個可能把程式弄壞的新功能,通常會複製整個專案資料夾變成 `Project_Copy`。但在 Git 中,我們不需要複製檔案,只需要建立一個新的「指標」。 * **分支 (Branch)**: 在物理上,它只是一個只有 41 bytes 的文字檔,裡面記錄著某個 Commit ID。想像它是一張「**貼紙**」。預設的貼紙叫做 `main`,貼在最新的進度上。當你建立新分支 `feature`,只是多貼了一張貼紙在同一個位置。 * **HEAD**: * 這是 Git 中最重要的指標,代表「**你現在在哪裡**」。 * `HEAD` 通常指向目前的 **Branch**,而 **Branch** 再指向最新的 **Commit**。 * 當你切換分支時,其實只是把 `HEAD` 這個指標移到另一張貼紙上,Git 會瞬間把工作目錄的檔案替換成該分支的樣子。 ### 3.2 分支操作指令 早期的 Git 指令權責較為混亂(例如 `checkout` 同時負責「切換分支」與「還原檔案」)。自 Git 2.23 版本起,官方推出了語意更明確的新指令 `switch`,本章將以新指令為主。 ![image](https://hackmd.io/_uploads/rJN5LJJDWg.png) * **`git branch`**: * `git branch`:列出本機所有分支。前方有 `*` 號的代表目前所在分支。 * `git branch feature-a`:建立一個名為 `feature-a` 的新分支(但**不會**自動切換過去)。 * `git branch -d feature-a`:刪除指定分支(需先合併完成才能刪除)。 * `git branch -D feature-a`:強制刪除分支(不管有無合併)。 * **`git switch branchname`** (Switch):切換分支。 * `git switch feature-a`:切換到該分支。 * `git switch -c feature-a`:**建立並切換** 到新分支 (Create)。 * (舊版指令對照:`git checkout feature-a` / `git checkout -b feature-a`) * **`git merge branchname`** (Merge):合併分支。 * **情境**:你在 `main` 分支,想把 `feature-a` 的成果整併進來。 * **指令**:`git merge feature-a`。 ### 3.3 衝突解決 當兩個不同的分支同時修改了**同一個檔案的同一行**,Git 無法判斷該聽誰的,這時就會發生**衝突 (Conflict)**。Git 會暫停合併動作,並在檔案中標示出衝突點,要求人類介入處理。 衝突標記格式如下: ```text <<<<<<< HEAD (目前分支) print("這是 main 寫的內容") ======= print("這是 feature 寫的內容") >>>>>>> feature (欲合併分支) ``` >[!Note] **實務演練#3: 平行開發與衝突對決** > > :::warning > **情境說明**: 接續上一章的 `mygit` 專案。你現在要開發一個「除法功能」。但在你開發的同時,原本的主線 (`main`) 也經歷了修改。當你試圖將功能合併回來時,將會遭遇經典的「程式碼衝突」。 > ::: >請依序完成以下 6 個關卡: > > #### 關卡 1:開啟平行宇宙 (Branching) > 1. 確保你位於 `mygit` 目錄且在 `main` 分支上。 > 2. 建立並切換到一個名為 `feature/division` 的新分支。 > 3. 使用 `git branch` 確認目前 `*` 號是否指在新分支上。 > > #### 關卡 2:開發新功能 (Commit in Branch) > 1. 修改 `main.py`,在檔案**最下方**新增除法函式: > ```python > def divide(a, b): > return a / b > ``` > 2. 將變更加入暫存區並提交,訊息:`"feat: add division function"`。 > > #### 關卡 3:主線的變動 (Simulate Divergence) > 1. 切換回 `main` 分支 (`git switch main`)。 > * **觀察**:透過 `cat` 指令你會發現剛剛寫的 `divide` 函式消失了,變回原本的樣子。這是正常的,因為那段程式碼只存在於平行時空中。 > 2. 修改 `main.py`,在檔案的**最下方**(也就是剛剛寫函式的同樣位置),新增一個乘法函式: > ```python > def multiply(a, b): > return a * b > ``` > 3. 將變更加入暫存區並提交,訊息:`"feat: add multiply function"`。 > * **現狀**:兩個分支都在同一個檔案的同一行寫了不同的 code。 > > #### 關卡 4:發生衝突 (Merge & Conflict) > 1. 確保你現在位於 `main` 分支(我們要把別人的東西併進來)。 > 2. 執行合併指令:`git merge feature/division`。 > 3. **觀察**:系統應該會噴出 `CONFLICT (content): Merge conflict in main.py` 的錯誤訊息,且自動合併失敗。 > > #### 關卡 5:解決衝突 (Resolve Conflict) > 1. 使用文字編輯器 (`vim` 或 `nano` 或 VS Code) 開啟 `main.py`。 > 2. 你會看到 `<<<<<<<`, `=======`, `>>>>>>>` 的標記。 > 3. **決策**:我們決定**兩個都要保留**。請刪除那些亂碼標記,並調整程式碼順序,讓 `multiply` 和 `divide` 兩個函式都整齊地留在檔案中。 > 4. 存檔離開。 > > #### 關卡 6:完成合併 (Commit Merge) > 1. 雖然修好了檔案,但 Git 還在等待你確認。請使用 `git status` 查看,它會告訴你 `You have unmerged paths`。 > 2. 將修好的 `main.py` 加入暫存區 (`git add`)。 > 3. 執行 `git commit` (不需要加 `-m`,Git 會自動跳出預設的 Merge 訊息介面,直接存檔離開即可)。 > 4. 使用 `git log --oneline --graph` 觀察剛剛的合併線圖。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:開啟平行宇宙 ```bash # 建立並切換分支 (相當於 git branch ... + git switch ...) git switch -c feature/division # 確認目前所在分支 git branch # 預期輸出: # main # * feature/division ``` #### 關卡 2 解答:開發新功能 ```bash # 使用 echo 追加內容 (或是用編輯器開啟編輯) echo -e "\ndef divide(a, b):\n return a / b" >> main.py git add . git commit -m "feat: add division function" ``` #### 關卡 3 解答:主線的變動 ```bash # 1. 切回主線 git switch main # 此時 main.py 內容會變回舊版 # 2. 在同一位置寫入不同內容 (製造衝突的關鍵) echo -e "\ndef multiply(a, b):\n return a * b" >> main.py # 3. 提交主線變更 git add . git commit -m "feat: add multiply function" ``` #### 關卡 4 解答:發生衝突 ```bash # 嘗試合併 git merge feature/division # 預期輸出: # Auto-merging main.py # CONFLICT (content): Merge conflict in main.py # Automatic merge failed; fix conflicts and then commit the result. ``` #### 關卡 5 解答:解決衝突 ```bash # 開啟檔案 (以 cat 示意內容,實際請用編輯器修改) cat main.py ``` **檔案內的衝突狀況:** ```python print("Glad to have you here in this Git project.") <<<<<<< HEAD def multiply(a, b): return a * b ======= def divide(a, b): return a / b >>>>>>> feature/division ``` **修正後的內容 (移除標記並保留兩者):** ```python print("Glad to have you here in this Git project.") def multiply(a, b): return a * b def divide(a, b): return a / b ``` #### 關卡 6 解答:完成合併 ```bash # 1. 告訴 Git 衝突已解決 (透過 add) git add main.py # 2. 完成合併提交 git commit # (此時會進入 Vim 或預設編輯器,直接輸入 :wq 存檔離開即可使用預設訊息) # 3. 觀賞線圖 git log --oneline --graph ``` **預期輸出 (線圖)**: ```text * e4f5g6h (HEAD -> main) Merge branch 'feature/division' |\ | * c3d4e5f (feature/division) feat: add division function * | b2c3d4e feat: add multiply function |/ * a1b2c3d feat: update welcome message ``` ::: ### 核心觀念複習 1. **Fast-forward** vs **Merge Commit**: * **Fast-forward**:如果 `main` 在你切出去開發的這段期間**完全沒有前進**,當你合併回來時,Git 只會單純把 `main` 的貼紙撕下來,往前貼到你最新的進度上。這時線圖是一直線,不會產生合併節點。 * **Merge Commit**:以本實務演練為例,`main` 和 `feature` 都有各自的新進度(分岔了)。合併時 Git 必須建立一個新的節點(Merge Commit)來將兩條分支合在一起。 2. **為什麼要用 `git switch`?** * 舊指令 `git checkout` 既可以用來切換分支 (`checkout main`),又可以用來捨棄檔案變更 (`checkout main.py`)。對新手極不友善,容易誤刪檔案。 * 新版 Git 將其拆分為 `git switch` (專門切換分支) 與 `git restore` (專門還原檔案),職責分離更安全。 3. **衝突並不可怕**: * 當兩個人改檔案中的同一行,衝突是**必然會發生的**(電腦本來就不該自行幫你選擇),"CONFLICT" 它只是一個提醒,要人類介入做最後確認而已。 --- ## 四、雲端同步與遠端協作 (Cloud Synchronization and Remote Collaboration) ### 4.1 傳輸協定:HTTPS 與 SSH 的選擇 要讓本地端電腦(Local)與 GitHub 伺服器(Remote)溝通,必須先建立安全連線。 Git 主要支援兩種傳輸協定: 1. **HTTPS (Hypertext Transfer Protocol Secure)** * **機制**:類似傳統的帳號密碼登入。 * **優點**:設定最簡單,只要有網址就能連線,適合防火牆限制較嚴格的公司網路。 * **缺點**:GitHub 已於 2021 年廢除密碼驗證,改用 **Personal Access Token (PAT)**。這意味著必須定期去後台申請一組像亂碼一樣的 Token,且 Token 會有過期問題,管理較為繁瑣。 2. **SSH (Secure Shell) — 推薦使用** * **機制**:採用 **公鑰與私鑰配對 (Public/Private Key Pair)** 的加密技術。 你會在本機產生一對鑰匙(公鑰+私鑰)。將 **公鑰 (Public Key)** 交給 GitHub 保管,而 **私鑰 (Private Key)** 則嚴密保存在自己電腦裡。當你要推送程式碼時,GitHub 會確認你的私鑰是否能打開那個鎖頭。 * **優點**:**安全性高且便利**。設定好一次後,未來所有與 GitHub 的溝通都自動完成認證,不需要再輸入任何密碼(Set and Forget)。 #### 4.1.1 實戰教學:配置 SSH 金鑰 (SSH Key Generation) 由於 SSH 是業界標準配置,以下將逐步驟說明如何在 Linux/Mac/Git Bash 環境下完成設定: **步驟 1:檢查現有金鑰** 在終端機輸入 `ls -al ~/.ssh`。 * 如果看到 `id_rsa` 或 `id_ed25519` 等檔案,代表你已經有鑰匙了。 * 如果出現 `No such file or directory` 或目錄是空的,代表需要重新產生。 **步驟 2:產生新金鑰** 我們使用目前最安全的 `ed25519` 演算法。請輸入以下指令(將 Email 換成你的 GitHub 註冊信箱): ```bash ssh-keygen -t ed25519 -C "[YOUR_EMAIL]" ``` * 系統會詢問存放路徑:**直接按 Enter** 使用預設路徑。 * 系統會詢問 Passphrase (密碼短語):**直接按 Enter** 兩次,設定為空即可。 **步驟 3:讀取並複製公鑰** 我們要複製的是 **`.pub` (Public)** 結尾的檔案內容。 ```bash cat ~/.ssh/id_ed25519.pub ``` * **動作**:請將終端機印出來的那串以 `ssh-ed25519` 開頭的亂碼**完整複製**起來。 **步驟 4:於 GitHub 設置公鑰** ![image](https://hackmd.io/_uploads/HJZ1nTaLWl.png) 1. 登入 GitHub 網頁,點擊右上角頭像 $\to$ **Settings**。 2. 左側選單點擊 **SSH and GPG keys**。 3. 點擊綠色按鈕 **New SSH key**。 4. **Title**:幫這把鑰匙取名。 5. **Key**:貼上剛剛複製的那串亂碼。 6. 按下 **Add SSH key**。 **步驟 5:連線測試** 回到終端機,輸入以下指令來測試是否成功: ```bash ssh -T git@github.com ``` * 若出現 `Are you sure you want to continue connecting?`,請輸入 `yes`。 * 若看到 `Hi [User]! You've successfully authenticated...`,即為配置成功。 ### 4.2 遠端操作指令 ![image](https://hackmd.io/_uploads/BJ4lvkkvbe.png) * **`git remote`**:管理遠端伺服器的列表。 * `git remote add origin [SSH_URL]`:新增一個遠端節點。 * **`origin`**:這只是個**變數名稱**(慣例上主要伺服器都叫 origin ,但亦可自行命名),目的是讓你不用每次都打那串很長的網址。 * `git remote -v`:查看目前設定的遠端網址 (Verbose)。 * **`git push`**:將本地端的 Commit **上傳** 到遠端。 * `git push -u origin main`:第一次推送時使用。`-u` 代表 **Upstream (上游)**,意思是告訴 Git:「以後我只要打 `git push`,你就自動把本地的 `main` 推給 `origin` 的 `main`,不要再問我了」。 * **`git clone [URL]`**:**下載** 整個儲存庫。 * 通常用於「在新電腦開始工作」或「下載別人的專案」。它會自動做三件事:`init` (初始化)、`remote add` (加入遠端)、`pull` (下載所有檔案)。 * **`git pull`**:將遠端的最新變更 **拉取** 下來並 **合併**。 * 等同於 `git fetch` (抓取資料) + `git merge` (進行合併)。 >[!Note] **實務演練#4: 雲端部署與異地備份** > > :::warning > **情境說明**: 你的 `mygit` 專案在本機已經開發了一段時間。現在,為了防止資料遺失,你需要將其備份到 GitHub。接著,模擬你**買了一台新電腦(或是同事加入了專案)**,需要將程式碼下載下來繼續開發,最後再同步回原電腦,完成一次完整的協作循環。 > ::: >請依序完成以下 6 個關卡: > > #### 關卡 1:GitHub 新增存儲庫 (Create Repo) > 1. 開啟瀏覽器,登入 GitHub。 > 2. 點擊右上角 `+` $\to$ `New repository`。 > 3. Repository name 輸入 `mygit`。 > 4. **重要**:請選擇 **Public**,且 **不要** 勾選 "Add a README"、".gitignore" 或 "License"(因為我們本機已經有檔案了,我們要建立的是一個全空的倉庫,避免產生衝突)。 > 5. 按下 **Create repository**。 > > #### 關卡 2:建立連結 (Remote Add) > 1. 回到終端機,確保你在 `mygit` 資料夾內。 > 2. **傳輸協定選擇**:在 GitHub 頁面上,請確認你選擇的是 **SSH** 按鈕(而非 HTTPS),並複製那串 `git@github.com:...` 的網址。 > 3. 下指令告訴 Git:這裡有一個遠端節點叫做 `origin`,網址是...。 > 4. 使用 `git remote -v` 確認設定是否成功。 > > #### 關卡 3:首次推送 (First Push) > 1. 將本地的 `main` 分支推送到遠端。 > 2. **注意**:第一次推送必須設定上游 (`-u`)。 > 3. 指令執行後,回到瀏覽器重新整理頁面,你應該會看到你的程式碼已經出現在網頁上了! > > #### 關卡 4:模擬新電腦 (Clone) > 1. 現在模擬你到了另一台電腦。請先離開目前的資料夾 (`cd ..`) 回到家目錄。 > 2. 我們要下載一份副本。使用 `git clone` 指令,並為了區別,將下載的資料夾命名為 `mygit_work`。 > * 指令格式:`git clone [SSH網址] mygit_work` > 3. 進入 `mygit_work` 資料夾,並用 `ls -la` 確認檔案完整性。 > > #### 關卡 5:異地開發 (Collaborate) > 1. 在這台「新電腦」(`mygit_work`) 上,修改 `README.md`,新增一行文字:`## Updatee from work laptop` (`Update` 有錯字,但我們這裡先不處理)。 > 2. 執行標準的提交流程:`add` $\to$ `commit` (`commit` 訊息設為 `docs: update readme from work laptop`)。 > 3. 將變更推送到 GitHub (`git push`)。 > * 註:Clone 下來的專案已經自動設定好上游,所以這次不需要加 `-u`。 > > #### 關卡 6:同步回原點 (Pull) > 1. 模擬你下班回到家,用原本那台電腦。請離開 `mygit_work`,回到原本的 `mygit` 資料夾 (`cd ../mygit`)。 > 2. 此時原本的資料夾還不知道雲端有更新。請檢查 `README.md`,內容應該還是舊的。 > 3. 下指令將雲端的最新進度 **拉取 (Pull)** 下來。 > 4. 再次檢查 `README.md`,確認內容已同步更新。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:GitHub 新增存儲庫 * **(網頁操作,無指令)** #### 關卡 2 解答:建立連結 (SSH 版) ```bash # 請將網址替換為你 GitHub 上的 SSH 網址 # 格式通常為 git@github.com:你的帳號/mygit.git git remote add origin git@github.com:[YOUR_ACCOUNT]/mygit.git # 驗收連結 git remote -v # 預期輸出: # origin git@github.com:... (fetch) # origin git@github.com:... (push) ``` #### 關卡 3 解答:首次推送 ```bash # 推送並設定上游 (Upstream) git push -u origin main # 因為已設定 SSH Key,系統不會再問你密碼 # 預期輸出: # Branch 'main' set up to track remote branch 'main' from 'origin'. ``` #### 關卡 4 解答:模擬新電腦 ```bash # 回到上一層 cd .. # Clone 下來並重新命名資料夾,模擬這是同事的電腦 # 使用 SSH 網址進行 Clone git clone git@github.com:DevNewbie/mygit.git mygit_work # 進入新資料夾 cd mygit_work ls -la ``` #### 關卡 5 解答:異地開發 ```bash # 1. 修改檔案 (append 內容) echo "## Updatee from work laptop" >> README.md # 2. 提交 git add README.md git commit -m "docs: update readme from work laptop" # 3. 推送 (Clone 下來的專案預設已有上游,直接 push 即可) git push ``` #### 關卡 6 解答:同步回原點 ```bash # 1. 切換回原本的資料夾 cd ../mygit # 2. 檢查內容 (此時應尚未更新) cat README.md # 3. 拉取更新 git pull # 4. 再次檢查 (應已看到新內容) cat README.md ``` ::: ### 核心觀念複習 1. **`origin` 是甚麼?** * `origin` 並非是一個特殊的指令。其實它就只是一個 **「變數名稱」**, Git 為了方便,預設將你複製 (Clone) 來源的網址取名為 `origin`。 你完全可以自行改名,但在溝通上使用 `origin` 是全球通用的默契。 2. **`git push -u` 的懶人機制** * `-u` (或是 `--set-upstream`) 只需要在**第一次**推送新分支時執行。 * 它的作用是建立「本地分支 `main`」與「遠端分支 `origin/main`」的 **追蹤關係 (Tracking Relationship)**, 一旦連結建立,未來你只需要輸入 `git push` 或 `git pull`,Git 就會自動知道要找誰同步,不用每次都打 `git push origin main` 這麼長串。 3. **`git pull` 的真面目** * 這是一個「複合指令」, **`git pull` = `git fetch` (去拿資料) + `git merge` (合併資料)**。 * 它會先去 GitHub 把最新的 Commit 下載到你的背景資料庫(這動作叫 Fetch,不會動到你的工作檔案),然後立刻嘗試把它合併(Merge)到你目前正在工作的分支。如果雲端和本機有衝突,`git pull` 就會像 `git merge` 一樣停下來請你解衝突。 --- ## 五、團隊協作流程:Issue 追蹤與 Code Review (Team Workflow) ### 5.1 標準開發流程:GitHub Flow 在多人協作的環境下,直接將程式碼推送到 `main` 分支是被嚴格禁止的(甚至會設定權限鎖住)。標準的 **GitHub Flow** 包含以下五個步驟: 1. **開票 (Open Issue)**:先定義問題。在寫任何一行程式碼之前,先在 GitHub 上建立一個 Issue 描述要修什麼 Bug 或加什麼功能。 2. **開分支 (Create Branch)**:在本地端建立一個專門處理該 Issue 的分支。 3. **提交 (Commit)**:進行開發,並在 Commit Message 中關聯 Issue 編號。 4. **發起請求 (Open Pull Request)**:將分支推送到 GitHub,並發起 **Pull Request (PR)**,請求將你的變更合併回主線。 5. **審查與合併 (Review & Merge)**:團隊成員檢查程式碼,確認無誤後按下 Merge 按鈕。 ### 5.2 議題追蹤 (Issue Tracking) ![image](https://hackmd.io/_uploads/r1_rvk1wZl.png) **Issue** 除了用於回報 Bug ,亦是專案的「待辦事項清單」。 * **功能**:你可以為 Issue 加上 **Labels** (如 `bug`, `enhancement`)、指派負責人 (**Assignees**) ,甚至設定里程碑 (**Milestones**) 。 * **關鍵字魔法**:GitHub 支援一種自動化語法。若你在 Commit 訊息或 PR 描述中寫上 **`Fixes #12`** 或 **`Closes #5`**,當該程式碼被合併到主分支時,GitHub 會自動幫你把對應的 Issue 關閉。這能省下大量手動更新狀態的時間。 ### 5.3 合併請求 (Pull Request) **Pull Request (PR)** 的意義是:**「我寫好了,請你檢查一下,沒問題的話就拉 (Pull) 進去你的主線吧。」** * **Code Review (程式碼審查)**:在 PR 介面中,審查者可以對「每一行程式碼」進行留言。例如:「這裡邏輯有漏洞」、「變數命名不清楚」。 * **Diff 檢視**:GitHub 會用紅色(刪除)與綠色(新增)清楚標示出你改了什麼,讓變更一目了然。 --- >[!Note] **實務演練#5: 完美的修復流程** > > :::warning > **情境說明**: 你的 `mygit` 專案收到使用者的回報,指出說明文件中有錯字。這次你不能直接改了就推,你需要模擬一個「標準的團隊流程」:從收到回報、分配任務、修復、審查到最後合併的完整閉環。 > ::: >請依序完成以下 6 個關卡: > > #### 關卡 1:開票 (Open Issue) > 1. 開啟你 GitHub 上的 `mygit` 儲存庫頁面。 > 2. 點擊上方的 **Issues** 分頁 $\to$ **New issue**。 > 3. 標題輸入:`Fix typo in README`。 > 4. 內容輸入:`The word "Update" is misspelled.`。 > 5. 右側邊欄的 **Assignees** 點選你自己。 > 6. 按下 **Submit new issue**。 > 7. **記下標題旁邊的編號**(通常是 `#1`)。 > > #### 關卡 2:切換分支 (Local Branch) > 1. 回到本機終端機。確保你在 `main` 分支且是最新的 (`git pull`)。 > 2. 建立並切換到新分支,命名為 `fix/readme-typo`。 > > #### 關卡 3:開始修復 (Commit with Magic) > 1. 修改 `README.md`,將原本的 `Updatee from work laptop` 修改為 `Update from work laptop`。 > 2. 將變更加入暫存區 (`git add`)。 > 3. **關鍵步驟**:提交時,訊息請寫:`"docs: refine wording in readme (Fixes #1)"`。 > * 請確保 `#1` 對應到你剛剛建立的 Issue 編號。 > > #### 關卡 4:發起審查 (Push & PR) > ![image](https://hackmd.io/_uploads/H1wZM1kDbg.png) > 1. 將這個分支推送到遠端:`git push -u origin fix/readme-typo`。 > 2. 回到 GitHub 頁面,你會看到黃色的提示框 **"fix/readme-typo had recent pushes..."**,點擊綠色的 **Compare & pull request** 按鈕。 > 3. 檢查標題與內容,確認無誤後,按下 **Create pull request**。 > > #### 關卡 5:主管驗收 (Merge) > ![image](https://hackmd.io/_uploads/H1DuMkyP-x.png) > 1. 現在你是專案主管。在 PR 頁面上,你會看到 **Merge pull request** 的綠色按鈕。 > 2. 點擊它,並選擇 **Confirm merge**。 > 3. 合併成功後,你會看到紫色標示 **Merged**。 > 4. **觀察**:請回到 **Issues** 分頁,你會發現剛剛那個 `#1 Issue` 已經自動變成 **Closed** 狀態了! > > #### 關卡 6:更新狀態 > ![image](https://hackmd.io/_uploads/BJ5azJyP-x.png) > 1. 遠端的分支已經合併,我們可以刪除它。在 PR 頁面上點擊 **Delete branch** 按鈕。 > 2. 回到本機終端機,切換回 `main` 分支。 > 3. 拉取最新的主線進度:`git pull` (這時你才會在本機的 main 看到剛剛修改的成果)。 > 4. 刪除本機的舊分支:`git branch -d fix/readme-typo`。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:開票 * **(GitHub 網頁操作)** * 重點確認:Issue 建立成功,並記住編號 (例如 #1)。 #### 關卡 2 解答:建立分支 ```bash # 確保主線最新 git switch main git pull # 建立修復分支 git switch -c fix/readme-typo ``` #### 關卡 3 解答:開始修復 ```bash # 1. 修改檔案 echo "## Update from work laptop" >> README.md # 或者使用編輯器修改 # 2. 提交 (注意關鍵字 Fixes #1) git add README.md git commit -m "docs: refine wording in readme (Fixes #1)" ``` #### 關卡 4 解答:發起審查 ```bash # 推送分支 git push -u origin fix/readme-typo ``` * **(後續為 GitHub 網頁操作:點擊 Create Pull Request)** #### 關卡 5 解答:主管驗收 * **(GitHub 網頁操作)** * 觀察:合併後,關聯的 Issue 應自動關閉。 #### 關卡 6 解答:清理戰場 ```bash # 1. 回到主線 git switch main # 2. 同步雲端 (把剛剛在網頁上 Merge 的結果抓下來) git pull # 3. 刪除本機已經沒用的分支 git branch -d fix/readme-typo # 預期輸出: Deleted branch fix/readme-typo... ``` ::: ### 核心觀念複習 1. **標準化開發流程的必要性 (The Necessity of Standard Workflow)** * **疑問**:個人開發時,為何不直接推送至 `main` 分支? * **專業解析**:GitHub Flow 的核心價值在於建立嚴謹的 **CI/CD 前置作業**與**風險控管**。 * **Issue**:確立「需求邊界」,確保開發目標明確,避免範疇蔓延 (Scope Creep)。 * **Branch**:提供「環境隔離」,確保開發中的不穩定程式碼不會影響生產環境 (Production)。 * **PR & Review**:實施程式碼品質控管,透過同儕審查機制提前攔截邏輯錯誤與潛在 Bug。 2. **關鍵字自動化關聯 (Keyword Automation)** * **原理**:這是 GitHub 平台提供的 **自動化狀態管理機制**。 * **機制**:當 Commit Message 或 PR 描述中包含 `Fixes`, `Closes`, `Resolves` 接上 `#Issue編號` 時,系統會建立「程式碼變更」與「需求單」的資料庫關聯。 * **觸發時機**:當該程式碼被成功合併 (Merge) 至預設主分支(通常是 `main`)的瞬間,系統會自動觸發 Hook 將對應的 Issue 狀態更新為 **Closed**。確保了「需求解決」與「程式碼上線」狀態的一致性。 3. **分支生命週期管理 (Branch Lifecycle Management)** * **觀念**:Feature Branch 的目的僅是為了乘載特定功能的開發過程。 * **最佳實踐**:一旦 Pull Request 完成合併,該分支的歷史紀錄已被整合至主線,原分支指標即成為**冗餘資訊 (Redundant Information)**。 * **維護建議**:應立即刪除遠端與本地的已合併分支,以維持儲存庫的整潔(Repository Hygiene),避免累積大量無效的無用分支,干擾後續的分支查找與管理。 --- ## 六、版本回溯與狀態救援 (Version Rollback and State Recovery) ### 6.1 臨時場景切換:Stash 在多工開發環境中,經常發生以下情境:您正在 `feature` 分支開發到一半(檔案處於 Modified 狀態,尚未 Commit),突然接到緊急任務需切換到 `hotfix` 分支修 Bug。 此時若直接切換分支,Git 雖然不會報錯(若無衝突),但會將您未完成的「半成品」直接帶到分支,造成 **分支汙染**。為了避免此風險,SOP 是一律使用 `stash` 先將環境清理乾淨。 ![image](https://hackmd.io/_uploads/SJipqJyD-x.png) * **`git stash`** (儲藏): * **機制**:將目前工作目錄與暫存區的所有變更「打包」並存入一個臨時堆疊 (Stack) 中,使工作目錄回到乾淨狀態 (Clean State)。 * **適用時機**:尚未完成當前工作,但必須暫時切換分支時。 * **指令集**: * `git stash`:執行儲藏。 * `git stash list`:查看目前堆疊中所有的儲藏紀錄。 * `git stash pop`:取出最新的一筆儲藏套用回工作目錄,並從堆疊中移除該紀錄。 ### 6.2 本地端時光倒流:Reset 當您想撤銷「尚未推送到遠端」的 Commit 時,`reset` 是最強大的工具。它透過移動 `HEAD` 指標來達成「回到過去」的效果。參考下圖,假設目前的歷史紀錄為 `A -> B -> C` (HEAD 指向 C),而我們想透過 `git reset B` 回到版本 B: ![git reset modes](https://hackmd.io/_uploads/r1aW6k1P-g.png) * **`git reset [Mode] [Commit]`**: * **`--soft` (溫和模式)** — **左圖** * **行為**:`HEAD` 指標移回 B,但**保留** C 的內容在 **暫存區 (Index)**。 * **狀態**:Commit 紀錄消失,但檔案內容完全保留,且狀態為 **<span style="color: green;">Staged</span>**。 * **適用情境**:當你覺得「Commit 訊息寫爛了」或「想把剛寫好的 C 跟接下來的 D 合併成一個 Commit」時使用。 * **`--mixed` (預設模式)** — **中圖** * **行為**:`HEAD` 指標移回 B,**清空** 暫存區 (Index) 的 C,但**保留** C 的內容在 **工作目錄 (Working Directory)**。 * **狀態**:Commit 紀錄消失,檔案內容保留,但狀態退回為 **<span style="color: red;">Modified/Untracked</span>**。 * **適用情境**:這是 Git 的預設行為。適合「後悔把這些檔案加入暫存區」,想重新挑選 (`git add`) 哪些檔案要提交時使用。 * **`--hard` (強制模式)** — **右圖** * **行為**:`HEAD` 指標移回 B,並**強制重置** 暫存區與工作目錄,使其與版本 B 完全一致。 * **狀態**:C 的所有修改(包含檔案實體)都會**被永久刪除**,回到最乾淨的狀態。 * **適用情境**:當你覺得「剛剛寫的程式碼完全是垃圾,想徹底重來」時使用。**此為破壞性指令,使用前請務必確認。** ### 6.3 遠端修正與反向提交:Revert 若錯誤的 Commit **已經推送到 GitHub (遠端)**,嚴禁使用 `reset` 修改歷史,因為這會導致團隊成員的歷史紀錄錯亂。此時應使用 `revert`。 ![image](https://hackmd.io/_uploads/B1xZA1yvWe.png) * **`git revert [Commit ID]`**: * **機制**:它**不會刪除**舊的 Commit,而是建立一個**新的 Commit**,其內容剛好是將目標 Commit 的修改「反向操作」回去(新增變刪除,修改變還原)。 * **優點**:保持歷史紀錄的線性前進,符合協作規範。 --- >[!Note] **實務演練#6: 狀態救援** > > :::warning > **情境說明**: 延續 `mygit` 專案。你將面臨三種常見的緊急狀況:開發中途被迫切換任務、誤將錯誤代碼提交、以及需要撤銷遠端的錯誤版本。 > ::: >請依序完成以下 5 個關卡: > > #### 關卡 1:臨時場景切換 (Stash) > 1. 確保位於 `main` 分支。 > 2. 修改 `main.py`,在最後面加上一行半成品代碼:`# TODO: Implement advanced AI features...`。 > 3. 此時主管要求你去修一個 Bug。你準備切換到 `hotfix` 分支。 > :::danger > 若此時直接切換分支,這行 TODO 代碼會跟著你一起過去(因為 Git 沒衝突不會擋)。為了避免汙染新分支,請**先進行暫存**。 > ::: > 5. 使用 `git stash` 將手邊工作暫存。 > 6. 使用 `git status` 確認工作目錄乾淨後,建立並切換至 `hotfix` 分支,並檢視 `main.py` 是否含有該行註解。 > > #### 關卡 2:恢復環境 (Switch Back & Pop) > 1. 假設 Bug 修完了(模擬)。現在要回來繼續原本的工作。 > 2. **重要步驟**:請先**切換回原本的分支** `main` (因為那行 TODO 是屬於 main 的)。 > 3. 使用 `git stash list` 確認剛剛的暫存還在。 > 4. 使用 `git stash pop` 將工作進度取回。 > 5. 再次檢查 `main.py`,確認那行註解回來了。 > > #### 關卡 3:本地端時光倒流 (Reset --soft) > 1. 將剛剛的變更提交:`git add .` $\to$ `git commit -m "feat: unfinished AI"`。 > 2. 提交後發現:「啊!我不該把這個半成品提交進去的」,請使用 `git reset --soft HEAD~1` (代表退回上一個版本)。 > 4. **觀察**:使用 `git status`,你會發現 Commit 不見了,但檔案變更還在,且處於 **<span style="color: green;">Staged</span>** 狀態。 > > #### 關卡 4:徹底毀滅 (Reset --hard) > 1. 你決定這段程式碼完全寫爛了,想直接重來。 > 2. 使用 `git reset --hard HEAD` (重置到當前最新 commit 狀態)。 > 3. **觀察**:檢查 `main.py`,那行 `TODO` 註解應該已經徹底消失,檔案回復到未修改前的狀態。 > > #### 關卡 5:遠端反向提交 (Revert) > 1. 為了模擬遠端錯誤,請先故意製造一個錯誤提交並推送。 > * 在 `main.py` 加入一行 `print("This is a bug")`。 > * 提交並推送:`git commit -am "feat: add bug" && git push`。 > 2. 假設同事已經拉取了這個更新,你不能用 Reset。 > 3. 請找出這個錯誤 Commit 的 ID (或是直接針對最新的這筆):`git revert HEAD`。 > 4. Git 會跳出編輯器讓你確認 Revert 訊息,直接存檔離開即可。 > 5. **觀察**:使用 `git log --oneline`,你會看到多了一筆 `Revert "feat: add bug"` 的紀錄,且檔案內容已自動復原。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:臨時場景切換 ```bash # 1. 製造半成品 echo "# TODO: Implement advanced AI features...\n" >> main.py # 2. 暫存變更 (這是標準流程: 先 Stash 再 Switch) git stash # 輸出: Saved working directory and index state WIP on main... # 3. 檢查狀態 (確認乾淨) git status # 4. 安全切換分支 git switch -c hotfix ``` #### 關卡 2 解答:恢復環境 ```bash # 1. 切換回原本分支 git switch main # 2. 查看清單 git stash list # 輸出: stash@{0}: WIP on main... # 3. 取出並套用 git stash pop # 輸出: Dropped refs/stash@{0}... (並顯示檔案修改狀態) # 4. 驗證 tail main.py ``` #### 關卡 3 解答:本地端時光倒流 ```bash # 1. 建立錯誤提交 git add . git commit -m "feat: unfinished AI" # 2. 軟重置 (退回上一步,保留變更在暫存區) git reset --soft HEAD~1 # 3. 檢查 git status # 預期: main.py 為綠色 (Changes to be committed) ``` #### 關卡 4 解答:徹底毀滅 ```bash # 強制重置 (捨棄所有變更) git reset --hard HEAD # HEAD is now at ... (回到上一版) # 驗證 git status # working tree clean ``` #### 關卡 5 解答:遠端反向提交 ```bash # 1. 建立並推送錯誤 echo 'print("This is a bug")' >> main.py git add . git commit -m "feat: add bug" git push # 2. 反向復原 (Revert) git revert HEAD # (進入編輯器,輸入 :wq 存檔離開) # 3. 推送修復 git push # 4. 檢查歷史 git log --oneline # 預期看到: # [Hash] Revert "feat: add bug" # [Hash] feat: add bug ``` ::: ### 核心觀念複習 1. **Reset vs. Revert 的選擇** * **本機端 (Local)**:只要 Commit **還沒 Push 出去**,你可以隨意使用 `git reset` 來整理歷史、合併 Commit 或捨棄變更。這是你的個人自由。 * **遠端 (Remote)**:一旦 Commit **已經 Push 出去**,嚴禁使用 `git reset`。因為這會改變歷史的軸線,導致其他協作者在 `git pull` 時發生嚴重衝突。此時必須使用 `git revert`,用「新增一筆反向 Commit」的方式來抵銷錯誤。 2. **`HEAD~1`** * 在指令中常看到的 `HEAD~1` 代表「HEAD 的前一個版本」。 * `HEAD~2` 代表前兩個版本,以此類推。這在 Reset 操作中常用。 3. **終極救援:Reflog** * 如果不小心 `git reset --hard` 刪錯了東西,可透過 Git 的「黑盒子」(`git reflog`) 復原。它記錄了 **HEAD 指標每一次的移動軌跡**(包含切換分支、Reset、Revert)。 * 即使 Commit 已經在 `git log` 中看不到了,你通常還是能在 `git reflog` 中找到它,並用 `git reset --hard [SHA]` 救回來。這是 Git 最後的防線。 --- ## 七、自動化整合與交付:GitHub Actions CI/CD (Continuous Integration and Delivery) ### 7.1 什麼是 CI/CD? * **CI (Continuous Integration,持續整合)**: * **要解決的問題:** 過去團隊開發時,往往等到最後一刻才把所有人的程式碼合併,結果發現衝突連連、Bug 滿天飛(這被稱為「整合地獄 Integration Hell」)。 * **解決方法:** CI 強調**頻繁地**(每天甚至每小時)將程式碼合併回主線。且每次合併前,必須通過**自動化測試**。如果測試失敗,合併就會被拒絕。 * **CD (Continuous Delivery/Deployment,持續交付/部署)**: * **要解決的問題:** 程式寫好後,要手動 FTP 上傳伺服器、手動重啟服務,容易出錯又浪費時間。 * **解決方法:** 一旦 CI 測試通過,系統自動將程式碼部署到測試環境或生產環境,讓產品隨時處於可發佈的狀態。 ### 7.2 GitHub Actions 核心架構 GitHub Actions 是 GitHub 內建的自動化平台,它透過 **YAML** 格式的設定檔來運作。核心元件包含: 1. **Workflows (流程)**: * 定義一次完整的自動化過程(例如:「每次 Push 時跑測試」)。 * 檔案必須放在專案的 `.github/workflows/` 資料夾內。 2. **Events (觸發事件)**: * 決定「什麼時候」要執行流程。 * 常見事件:`push` (有人推送代碼)、`pull_request` (有人開 PR)、`schedule` (定時執行)。 3. **Runners (執行器)**: * 這是 GitHub 免費提供的一台**虛擬電腦** (通常是 Ubuntu Linux)。 * 所有指令(如 `npm install` 或 `python main.py`)就是跑在這台雲端電腦上。 4. **Steps (步驟)**: * 流程中具體要執行的動作。可以是執行 Shell 指令 (`run`),也可以是使用別人寫好的套件 (`uses`)。 --- >[!Note] **實務演練#7: 自動化整合與交付** > > :::warning > **情境說明**: 你的 `mygit` 專案越來越多人使用,主管擔心有人不小心提交了壞掉的程式碼導致專案崩潰。你的任務是設定一個 GitHub Actions 流程,**每當有人推送 (Push) 程式碼時,GitHub 必須自動執行該程式,確認它能正常運作。** > ::: >請依序完成以下 5 個關卡: > > #### 關卡 1:自動化設定 (Setup Directory) > 1. 確保位於專案根目錄。 > 2. GitHub Actions 的設定檔有嚴格的路徑規定。請使用指令建立多層目錄: >  `mkdir -p .github/workflows` > #### 關卡 2:定義流程 (Write YAML) > 1. 在 `.github/workflows` 目錄下,建立一個名為 `ci.yml` 的檔案。 > 2. 使用編輯器填入以下內容(這是告訴機器人要做什麼): > ```yaml > name: Python CI System > > # 觸發時機:當 main 分支有 push 事件時 > on: > push: > branches: [ "main" ] > > jobs: > check-code: > runs-on: ubuntu-latest # 使用最新的 Ubuntu 虛擬機 > steps: > - name: Checkout code > uses: actions/checkout@v4 # 步驟1: 把程式碼下載到虛擬機 > > - name: Set up Python > uses: actions/setup-python@v4 # 步驟2: 安裝 Python 環境 > with: > python-version: '3.10' > > - name: Run Application > run: python main.py # 步驟3: 嘗試執行主程式 > ``` > 3. 存檔並離開。 > > #### 關卡 3:啟用機器人 (Push to Trigger) > 1. 將這個設定檔提交並推送到 GitHub。 > * `git add .` > * `git commit -m "ci: setup github actions workflow"` > * `git push` > 2. **觀察**:打開 GitHub 網頁,點選上方的 **Actions** 分頁。你應該會看到有一個 workflow 正在轉圈圈(Running)或已經打勾(Success)。點進去可以看到詳細的 Log。 > > #### 關卡 4:模擬破壞 (Test Failure) > 1. 我們要確認機器人真的會抓錯。請修改 `main.py`,故意寫入錯誤的語法(例如拿掉字串的引號): > `print(This is a syntax error)` > 2. 提交並推送這個錯誤版本。 > 3. **觀察**:回到 GitHub Actions 頁面。這一次,你應該會看到紅色的 **Failure**。點進去查看 Log,會發現它在 `Run Application` 那一步報錯了。 > * 這就是 CI 的價值:它在第一時間告訴你「你的程式壞了」。 > > #### 關卡 5:修復與通過 (Fix & Pass) > 1. 修改 `main.py`,修復語法錯誤。 > 2. 再次提交並推送。 > 3. **觀察**:確認 GitHub Actions 變回綠色的 **Success**。 --- :::success ### 💡 實務演練解答 #### 關卡 1 解答:自動化設定 ```bash # -p 參數允許建立多層目錄 mkdir -p .github/workflows ``` #### 關卡 2 解答:定義流程 * **(建立檔案並貼上 YAML 內容)** * **注意 YAML 的縮排**:YAML 對縮排非常嚴格(通常使用 2 個空白鍵),縮排錯誤會導致流程無法執行。 #### 關卡 3 解答:啟用機器人 ```bash git add . git commit -m "ci: setup github actions workflow" git push ``` #### 關卡 4 解答:模擬破壞 ```bash # 1. 寫入錯誤語法 echo 'print(This is a syntax error)' > main.py # 2. 推送錯誤 git add main.py git commit -m "test: introduce syntax error" git push # 3. 前往 GitHub 網頁 Actions 分頁觀察錯誤 ``` #### 關卡 5 解答:修復與綠燈 ```bash # 1. 修復語法 echo 'print("Hello GitHub Actions")' > main.py # 2. 推送修復 git add main.py git commit -m "fix: resolve syntax error" git push # 3. 前往 GitHub 網頁確認變回綠色的 **Success** ``` ::: ### 核心觀念複習 1. **YAML 縮排地獄** GitHub Actions 的設定檔使用 **YAML** 格式。它跟 Python 一樣,是透過**縮排 (Indentation)** 來區分層級的。 `YAML` 只能使用**空白鍵 (Space)**,**絕對不能使用 Tab 鍵**。只要有一個 Tab 混進去,GitHub 就會報錯 "Invalid YAML"。 2. **虛擬環境的生命週期** 每次 Workflow 被觸發時,GitHub 會開一台**全新、乾淨**的虛擬機給你,這意味著: 上一次執行產生的檔案(例如編譯好的執行檔),在這次執行時**不會存在**。如果你需要在不同步驟間保留檔案(例如測試報告),需要使用 `upload-artifact` 等功能。 3. **CI 作為自動化品質檢測的閘門 (Automated Quality Gate)** * **核心價值**:CI (持續整合) 可視為團隊協作的 **「品質守門員」**。它透過自動化流程,防止 **回歸錯誤 (Regression Bugs)** 被合併至主分支。 * **完整流水線**:一個成熟的 CI Pipeline 通常包含三個層次: * **Linting**:靜態程式碼分析,檢查語法錯誤與排版風格 (如 Flake8, ESLint)。 * **Testing**:執行單元測試 (Unit Test) 與整合測試,確保邏輯正確。 * **Security**:掃描相依套件漏洞 (Dependency Vulnerabilities) 與敏感資料外洩 (Secret Scanning)。 --- ## 八、現代化工具輔助與指令列核心價值 (Modern Tooling and the Core Value of CLI) ### 8.1 VS Code 整合環境與擴充套件 Visual Studio Code 因其強大的擴充性與輕量化特性,已成為當前主流的程式碼編輯器。透過安裝特定的 Git 擴充套件,我們可以將複雜的版本資訊視覺化,從而更直觀地管理專案。 #### 1. Git Graph (歷史線圖視覺化) * **應用場景**:當專案分支眾多、合併頻繁時,單純使用 `git log --graph` 難以快速釐清分支間的關聯。 * **核心功能**: * **視覺化圖表**:提供清晰的彩色線圖,展示 Commit 的演進與分支的合併路徑。 * **介面操作**:允許使用者直接在 Commit 節點上右鍵執行 `Checkout`、`Merge`、`Revert` 或 `Cherry-pick` 等操作,簡化指令輸入流程。 * **安裝方式**:在擴充商店搜尋 "Git Graph" (作者: *mhutchie*)。 #### 2. GitLens (程式碼歷史追溯) * **應用場景**:在維護專案時,經常需要查詢特定程式碼片段的修改者、修改時間以及當初的修改原因(Commit Message)。 * **核心功能**: * **行內提交資訊顯示 (Inline Blame)**:當游標停留於某行程式碼時,會在行尾以淡色文字顯示該行的最後修改者、時間距今多久,以及提交訊息。 * **檔案歷史 (File History)**:提供側邊欄介面,可快速瀏覽單一檔案在不同版本間的變更差異 (Diff)。 * **安裝方式**:在擴充商店搜尋 "GitLens" (作者: *GitKraken*)。 ### 8.2 內建原始碼控制 (Source Control) VS Code 左側功能列中的「原始碼控制」(分岔圖示),即為內建的 Git GUI 面板。熟練使用此面板可大幅加速日常的標準化作業: * **暫存 (Staging)**:點擊檔案名稱旁的 `+` 號,其作用等同於 `git add` 指令。 * **提交 (Commit)**:在輸入框填寫訊息後點擊提交按鈕,等同於 `git commit`。 * **同步 (Sync)**:點擊同步圖示,系統會自動依序執行 `git pull` 與 `git push`,完成遠端同步。 * **衝突解決 (Merge Conflict)**:當發生合併衝突時,編輯器會以顏色區塊標示衝突段落,並提供 `Accept Current Change` (保留本地) 或 `Accept Incoming Change` (保留傳入) 的快捷選項,取代手動編輯衝突標記 (`<<<<<<<`) 的繁瑣過程。 --- ### 8.3 結語:為何仍需掌握指令列操作? (The Philosophy of CLI) 既然現代化的 GUI 工具已如此完善,為何專業工程師仍須具備熟練的指令列 (CLI) 操作能力?這並非為了形式上的專業感,而是基於以下三點的考量: 1. **GUI 僅是封裝,CLI 才是本質**: 所有 Git 圖形化工具的底層,最終都是呼叫 Git 指令來執行任務。GUI 工具雖然方便,但也遮蔽了底層邏輯。當工具發生非預期錯誤時,唯有透過終端機查看詳細的錯誤輸出,並使用對應的修復指令,才能解決複雜的版本庫損毀或索引錯誤。 2. **伺服器環境的限制**: 在進行自動化部署 (CI/CD) 或遠端伺服器維運時,操作環境通常是純文字介面的 Linux Shell(如 SSH 連線),在這些環境中沒有圖形介面可用,指令列操作是唯一可行的方式。 3. **精確控制與原理理解**: GUI 工具通常將多個步驟簡化為單一按鈕(例如 Sync 包含 Pull 與 Push),這在特定情境下可能導致非預期的結果。掌握指令代表理解 Git 的運作原理(如暫存區與儲存庫的區別)。唯有理解原理,才能在不同的開發環境與工具間靈活切換,而不受限於特定的軟體介面。