Git 輕鬆上手

Git

今天老師要求期末前每個小組需要繳交 50 頁的書面報告,回到家裏每個同學就像發了瘋似的猛讀文獻,他們選了一個同學作為他們的組長。只要每個人想要更新他們所寫的內容,都需要知會組長,估計還沒到學期末組長還沒有瘋掉,組員也差不多要被送走了。這麼多不同的版本,作為組長要把每部份處理好,只要內容多起來想必是難如登天的事情。

很幸運地,這群人非常的堅強,他們成功熬過寫報告的日子了。要繳交報告前,組長的桌面有這些檔案:

  • report.docx
  • report1.docx
  • report2.docx
  • report_final.docx
  • report_real_final.docx
  • report_hand_in_ver.docx
  • report_hand_in_ver1.docx
  • report_hand_in_ver2.docx
  • report_hand_in_ver3.docx
  • report_0929_ver.docx
  • report_0930_revised.docx
  • reprot_revised_done.docx

很明顯,這裏所有的檔案全部都是一個即將要繳交的檔案,只是它經歷了非常多的修改版本。成功繳交是一回事,如果老師覺得同學們應該要寫的更好而選擇退回作業呢,我應該要怎麼在那麼多的檔案中找到差異,修改並且繳交最終的版本?

在這個情境中已經出現了一些問題:

  • 每個同學都必須與組長溝通,如果組長身體不適的那幾天,整理報告這件事就癱瘓了。
  • A 同學自己的桌面可能就一大堆 A_report1.docxA_report2.docxA_report111.docxA_report0909.docxA_report1001.docx… 這部分很明顯也是同一份文件的不同版本,命名無法完全看出版本先後順序。
  • 組長的桌面亦是如此,也不確定到底在「什麼時候」合併了「哪些同學」的「哪些內容」。
  • 每個版本迭代都會有差異,如果要比較不同版本的差異需要開兩個視窗人工核對,將不一樣的地方用不同顏色註記。
  • 同學沒辦法隨時看到處理完的完整版報告內容。

為什麽要使用 Git

我們先講個小故事:

創造 Linux 系統的上帝 Linus Torvalds 也創造了 Git。 在 2002 至 2005 年間 Linus 使用 BitKeeper 作為管理 Linux kernel 的程式碼版本控制工具,後來,BitKepper 收回了給 Linus 與其 Linux Kernel 開發團隊的免費軟體授權。Linus 一氣之下花費了 10 天的時間寫了另外一個程式碼版本控制的工具 aka Git。

Git 到底是什麼意思?Git 原始碼的 README.md

The name “git” was given by Linus Torvalds when he wrote the very first version. He described the tool as “the stupid content tracker” and the name as (depending on your mood):

  • random three-letter combination that is pronounceable, and not actually used by any common UNIX command. The fact that it is a mispronunciation of “get” may or may not be relevant.
  • stupid. contemptible and despicable. simple. Take your pick from the dictionary of slang.
  • “global information tracker”: you’re in a good mood, and it actually works for you. Angels sing, and a light suddenly fills the room.
  • “goddamn idiotic truckload of sh*t”: when it breaks

翻譯吐司:

Git 是 Linus 給定的第一個版本命名,他把這個工具描述成「愚蠢的內容追蹤器」,他的名字可以按照你的心情去解釋:

  • 沒有被 UNIX 使用到的指令並且是三個字母可發音的組合。可能是「get」的錯誤發音,這可能有關也可能無關。
  • 愚蠢。卑鄙無恥。簡單。從俚語字典裡挑一個吧。
  • 全球資訊追蹤器:如果你的心情好的話他就會為你工作。天使唱歌,燈光會充滿整個空間。
  • 裝了一卡車天殺的屎:當 Git 炸裂的時候

回到問題,為什麼要用 Git?答案就是 Git 可以解決上述的問題。別的功能我們不需要全部知道,但是作為使用者我們必須了解他的兩個最核心的功能:

  • 追蹤檔案的變更紀錄。
  • 將所有開發進度合併起來,讓專案的拼圖逐漸完整。

Git 基礎概念

版本控制與其必要性

什麼是版本控制?他可以幫助我們像是奇異博士一樣利用時間寶石查看這個檔案過去每個時間點的不同版本,甚至可以回到選定的過去版本,但是不能看未來(我是認真的)。既然同一個檔案都可以看過去的內容了,換句話說,每次更新都會是一個版本的迭代,也就是不會出現 report_hand_in_ver3_revised_really_final_ver.docx 這種檔案地獄了,任何一個人看到都都會非常痛。呈現的方式會變成這個檔案在「某個時間點時」「誰」做了「什麼修改」,如此一來,團隊成員追蹤程式碼的修改歷程就是輕而易舉的小事了。

本機端與遠端

本機端是你的電腦,遠端則是存放你的 Git 倉庫(Repository,我們常常簡稱為 repo)裡程式碼的伺服器。

  • 遠端可以讓你的團隊多人共享專案、同步進度
  • 本機端可以被當作 Git 倉庫,你可以離線的時候在你的電腦上做事情
  • 本機端可以與線上端互動,包括把本機端更新的內容推送到遠端、將遠端的更新抓下來…

在線上建立一個專案

接下來我們用 GitLab 示範:

  • 找到建立新專案的按鈕img

  • 建立空白專案img

  • 設定空白專案

    • 左上角的橘色框框:我們需要先決定好專案名稱
    • 中間的橘色框框:我們需要把這個專案放在哪裡,通常放在所屬專案的群組下,以我為例子我放在我自己的名字下
    • 右邊的橘色框框:通常這個不需要自己寫,會從專案名稱那裏複製過來,因爲這是 URL 的一部分,如果你有用到一些特殊符號在這裡會被改寫
    • 在這裡需要決定專案的權限需要多大
    • 最後但是也是最重要的:如果你已經有要推送到遠端的專案不要勾選紅色框框中的創建 README.md 文件,反之可勾選。這個文件是以 md 附檔名結尾,也就是他是 markdown 的檔案,許多開發者都會在專案第一層下建立 README.md 檔案對整個專案初步介紹。 如果建立這個檔案就會建立分支,不利於我們將本機上的檔案推送到遠端,因爲本機端並沒有從線上取得的 README.md 檔案。
    • 按下 create project 建立專案 img

首次將未被 Git 追蹤的內容推送到遠

現在筆者準備了一個 Java 視窗的小程式,我們現在要嘗試將這個專案加入 git 追蹤。 我們可以使用 VScode 開啟目錄,方便之後對檔案直接編輯。

初始化 Git 目錄

在開始所有動作前我們先初始化這個目錄,初始化之後這個目錄就可以開始進行版本的紀錄了,在終端機輸入 git init,他會跑出一大堆東西,輸入 ls -la 查看當前目錄還會多一個叫做 .git 的目錄,這是用來存放 git 相關紀錄的目錄。 img 命令列中有非常多黃色字體的內容,主要是想要提醒使用者當前使用 master 當作初始化的分支名稱。現在,新的專案主分支名稱都是 main,在舊的專案 GitHub 裡面你可能會看到 master,現在我們全部都使用 main 作為我們主分支名稱。我們不需要去修改預設分支名稱,只需要第一個 push 上去的分支是 main 就算是成功了。

忽略追蹤清單 .gitignore

當我們編譯完 Java 的程式就會出現,一個 .class 結尾的檔案,這個就像是 C 語言或是 C++ 語言的 執行檔 a.out 一樣,我們已經知道透過 java Main.java 編譯 Java 語言的檔案會自動產生 --.class 的檔案,這個檔案裡面是機器語言只有電腦看得懂。該檔案可能因爲架構的差異無法在其他平台上執行,我們會選擇忽略追蹤這個檔案。增加一個 .gitignore 的檔案,並且把你認為不需要追蹤的檔案名稱寫到裡面,之後他就不會被 Git 追蹤。圖中使用

*/*.class

用來表示「不追蹤所有路徑下的 .class 檔案」,左側的檔案總管顯示綠色的部分是未追蹤(untracked)的檔案以綠色提示使用者。Main.class 因為不被追蹤而變成灰色。通常我們只追蹤程式碼,哪些編譯過後就可以拿到的檔案或是一些可能有敏感資訊的環境檔(.env)我們都會將他們的路徑放到 .gitignore 下,避免被追蹤。 img

加入遠端倉庫 git remote add

透過 git remote add <remote_name> <remote_url> 指令,我們可以讓這個這個目錄知道我們之後只要與遠端通訊的時候都是透過這個 URL。origin 是遠端 URL 的代號,你也可以使用自己的代號,在這裏以 origin 作為示範。 img

建立分支 git checkout/switch

根據上一部分,我們可以建立一個「main」的分支。當前我們還沒有任何分支,當我們執行以下兩行指令都可以幫我們完成建立一個建立一個新的名為「main」的本機端分支

git checkout -b main # b for branch
# 或是
git switch -c main # c for create

img

查看狀態 git status

目前為止,我們已經認識了「Untracked」這個狀態,他會在左側以明顯的綠色提示使用者這是尚未追蹤的檔案,下表是一個完整的 Git 文件狀態表。

狀態 (英文) 中文解釋 如何出現/轉換 常用指令
Untracked 未追蹤檔案 新增檔案但還沒 git add git status 後看到 “Untracked files”
Tracked 已追蹤檔案 Git 已經記住的檔案(可能是 staged、unmodified、modified) -
Unmodified 已追蹤且未修改 Commit 後什麼都沒動 -
Modified 已修改但未暫存 編輯過檔案但還沒 git add git status 後看到 “Changes not staged”
Staged 已暫存 git add 後,等待 commit git add file
Committed 已提交 git commit 後,內容寫入 Git 版本庫 git log
Deleted 已刪除 檔案在工作目錄被刪掉,但 Git 還記得它 git rm file → staged 刪除

git status 可以被當作 Git 的「即時總覽」。 img

在這個圖片中,我們可以發現 Untracked 的檔案有 「.gitignore」以及「Main.java」。我們將在下個部分將這些內容加入暫存的名單(staged)。

加入成暫存檔案 git add

git add <file_name> 會將所輸入名稱的檔案的狀態改為 staged;我們回到這個 git 目錄的根目錄下,git add . 指令會將所有未被追蹤(Untracked)的檔案改為 staged。這樣就會把所有的新檔案全部都加到追蹤名單了。

我們可以想像現在有一所監獄,git add <file_name> 將檔案一個一個加入 stage 的過程就像嫌犯來到相機前面一個一個拍照;git add . 就像全體嫌犯站在一起拍了一張大合照。

img

提交訊息 git commit

git commit 是將已經在 staging area(暫存區)的檔案變更正式記錄到版本控制歷史中。每次 commit 都會建立一個版本的快照,讓你可以隨時回到這個時間點。

你可以搭配 git add 為每一個檔案賦予自己的 commit message,如下所示:

git add file1.txt # 將 file1.txt 加入到 staged 狀態
# 給 1 號嫌犯拍照
git commit -m "file1.txt" # 為 file1.txt 添加提交訊息,m for message
# 給 1 號嫌犯的犯罪紀錄歸檔

git add file2.txt
git commit -m "file2.txt"

git add file3.txt
git commit -m "file3.txt"

也可以為所有的檔案都賦予相同的 commit message,如下所示:

git add . # 將所有未追蹤檔案加入到 staged 狀態
git commit -am "commit message for all untracked and modified file" 
# 為未追蹤檔案及已修改檔案添加提交訊息,a for all

img

筆者反白的地方表示我們已經成功為這些檔案夾上了提交訊息,但是上面一堆訊息主要是想要表達 git 還不認識你,你必須先表達身分

git config --local user.name "your_name"
git config --local user.email "example@mail.com"

img

取回遠端狀態 git fetch

在我們要把東西推送到遠端之前,我們需要先確認遠端的狀態,如果取回來的狀態沒有問題肯定才能將手裡的程式碼推到遠端對吧?

git fetch

img 嗯,看起來沒有問題。

推送到遠端 git push

終於,我們可以把程式碼推到遠端了,如果前面沒有任何問題的話直接執行

git push

img

這一串訊息的意思是你沒有成功 push 到遠端,還記得遠端的狀態沒有分支對嗎?我們要把本機端的 main 分支 push 到遠端也要在遠端建立一個 main 分支,複製反白的地方即可。 img 這樣就成功了,接下來再去我們的 GitLab 上看看: img

分支策略

想像一下你經歷了一場船難但倖存下來了,你的主線(main)任務是活下去,於是你漂到一座島上開始建立自己的庇護所、搜尋食物充飢、尋找物資。你每天的進度肯定都要被保存下來(commit),應該不會有人千辛萬苦找到的資源一覺醒來就全部回到原來的位置吧,還要重新去收集,那也太痛苦了。

今天你發現了有一個洞穴,裡面可能藏有一些珍貴資源,雖然不是活下去的主要目標,但是可能可以提供更高的生活品質,於是你開始了你的支線任務(git checkout -b feature/explore-cave,開一個搜索洞穴的分支),當然在這個分支上你也必須保存(commit)取得的物資。

經過了一整個下午的洞穴搜索你終於離開了洞穴將取得的物資拿到庇護所妥善收好,你成功回到主線了(git merge feature/explore-cave)。現在你的主要目標是繼續活下去,但是有了來自支線任務的補給,你會過得比以前更舒服,並繼續按照主線的方向持續進行。

開發時常用指令

  • 建立新的分支/切換分支
    git checkout branch_name
    git switch branch_name
    git switch -c new_branch
    
    就像你在遊戲中選擇走「主線」還是「支線」,或新開一條探險小路

  • 查看狀態
    git status
    
    看看哪些資源(檔案)有修改、哪些還沒收集、哪些已經準備好存檔

  • 加入成暫存狀態(staged)
    git add .
    # 或是
    git add <filename>
    
    把收集到的物資放進背包,準備存檔

  • 提交訊息
    git commit -m "完成收集燧石"
    
    在日誌上記錄今天的探險成果,方便日後回顧或回到這個時間點

  • 取回遠端狀態
    git fetch
    
    隊友那邊拿最新探險地圖,但還沒把地圖整合到你自己的行程表

  • 推送到遠端
    git push
    
    把你的日誌或資源更新傳給隊友,大家的地圖保持一致

  • 合併分支
    git switch <合併到...(目標分支 target)>
    git merge <要合併的來源分支(source)>
    
    支線任務的成果整合回主線,可能會發生衝突(merge conflict),需要手動整理

接下來,你每天要做的事情不斷的尋找支線任務再合併到主線,讓你在孤島的日子過的越來越像一個人。

如果是小型專案,分支名稱其實可以不需要太正式,可以參考以下內容:

  • 主要分支
    • main:穩定的生產分支,僅包含已發布的版本。
    • beta:開發分支,包含即將發布的功能。
  • 輔助分支
    • feature/*:開發新功能。
    • release/*:準備發布的版本。
    • hotfix/*:修復生產環境的緊急問題。

壞習慣

  1. 提交過多無關的變更

    • 每次提交應只包含一個功能或修正,避免提交過於雜亂。
  2. 使用模糊的提交訊息

    • 修正問題更新,這樣的訊息無法幫助團隊理解變更內容。
  3. 頻繁使用強制推送

    • git push -f 會覆蓋掉其他人開發的內容,要更加小心地使用。
  4. 直接在主分支上開發

    • 這會增加衝突風險,並使回滾(roll-back)變更更加困難。

在專案裡面,你每開一個新分支代表你要開發一個新功能,當這些新功能開發完畢的時候都必須儘快合併(merge)回到主分支上,一旦時間久了,開發分支的內容就會越來越落後主分支的內容,最終必須捨棄,否則合併上去會覆蓋新內容導致毀滅性的災難發生。

解決衝突

今天,你在孤島上收集了 5 顆燧石,準備生火做飯。在這之前,你有一個舊分支裡只收集了 2 顆燧石,後來你把那個舊分支合併(merge)到現在的主線分支,新增了燧石紀錄。

現在,你卻意外地試圖用舊分支覆蓋新的狀態——把「只有 2 顆燧石的舊版本」合併到「已經有 5 顆燧石的新版本」上。 這就像在現實中,你想用舊地圖替換掉最新的資源紀錄,結果兩個版本衝突了(merge conflict)。

  1. 切換到目標分支(你要合併進去的分支)
    git checkout main
    # 或者
    git switch main
    
    就像你決定先回到「主線生活」的營地,準備整合支線任務的成果。

  1. 合併舊分支到目標分支
    git merge old-branch
    
    Git 嘗試把「舊分支的燧石紀錄」合併到主線,但發現兩邊的燧石數量不同所以發生衝突。

  1. 處理衝突 Git 會標記衝突檔案,你可以用 VSCode 或其他編輯器查看: 衝突部分會被標記成

    <<<<<<< HEAD
    這是目標分支的新狀態
    =======
    這是被合併分支的舊狀態
    >>>>>>> old-branch
    

    你需要手動決定:

    • 保留新的燧石數量(5 顆)
    • 或合併兩邊的紀錄(可能是累計 7 顆)

    編輯完成後,標記為解決:

    git add <衝突檔案>
    git commit
    

就像你整理營地,把舊的資料和新的資料合併,最後決定一個最終的燧石紀錄。

退回版本

我們再一次回到活下去的故事,今天你不小心在攀爬岩石的時候不小心腳滑從 8 公尺的高空失足重重的摔在地面上導致你的左腳斷了,你肯定會想「痛死了,如果我一開始不要來這裡我不就不會摔爛了,回到我的腳甚至還是完好的!」。接著你馬上想到你在 13 分鐘前有 commit 過一次,便嘗試回到之前的狀態(git reset HEAD --hard)。

git reset <commit 編號> --hard 
# 這個編號可以從 GitHub 或是 GitLab 上的歷史紀錄查找

git reset HEAD --hard
# HEAD 可以當作是一個指標,指向最後一次 commit

git clean -fd # 'f' for file 'd' directory

瞬間,你回到了那個「腳完好無傷」的狀態,摔斷的左腳神奇地復原了!

最後,讓我們回到現實,在開發的過程中,我們不需要知道每一道指令有什麼功用,我們不像神農氏或是李時珍,哪些草藥對應哪些症狀,我們只需要知道哪裡有稱手的工具,就足夠了,不清楚的一定要尋找真理。