跳至內容

從零開始學 AI 工程(二):
Phase 0-2 Git 協作

學習Git的常識,理解備份作業的基本邏輯

本文作者是 AI 工程的初學者,正透過《AI Engineering from Scratch》課程自學,並全程搭配 AI 輔助:一步步照著課程開發者的進度動手執行,卡關時就向 AI 提問、請它引導。這篇文章,是把作者與 AI 互動的完整過程與學習歷程,先交由 AI 記錄、統整成初稿,再由作者親自逐段審視、修改、優化而成。AI 負責整理,最終判斷與文字由作者把關。

寫下它有兩個用意:替後來的讀者鋪一條能照著走的路徑,也幫作者自己複習、把學過的東西沉澱下來。

這篇怎麼讀

這是我這個非技術背景的人,怎麼把一堂「學 Git」的課變成自己的理解。 每一步我都紀錄了三塊:

  • 作者要學習者做什麼 — 原始指令
  • 背後的道理 — 給零基礎的解釋
  • 跟著走會遇到的事 — 按著課程走、但作者沒明說的補充或遺漏的步驟

每一個指令,我在學習的過程,有疑惑我就問AI,問到理解為止,才往下走。 不懂就停,繼續深挖。

這一課導覽框

  • 課名:Phase 0 · Lesson 2 — Git 協作
  • 目標:把資料夾變成 Git 倉庫、學會 add / commit / push 的節奏,把第一個 repo 推上 GitHub
  • 時間:課程沒明標,我含理解時間走了大概兩個晚上
  • Mac 使用者最重要的一句:GitHub 從 2021 年起不接受用密碼 push。第一次 push 要走 OAuth 授權或 Personal Access Token,密碼填了會被拒絕
  • 完課驗證:能在 GitHub 上看到自己 push 的第一個 repo,README 顯示得出來

先看懂作者的地圖:Git 的四個地方

作者一開始丟了一張時序圖,畫出 Git 的五個動作在四個「地方」之間怎麼搬東西。這張圖是整堂課的地基,看懂它,後面所有指令都會變順。

Git 的四個地方地圖:工作目錄、暫存區、本地倉庫、遠端倉庫(GitHub),用 add / commit / push 往外推,用 fetch / pull 往回拿

四個地方

  • 工作目錄:電腦資料夾裡看得到的檔案,我們現在正在改的那些
  • 暫存區:一個「待提交的購物車」,挑好要 commit 哪些改動先丟進來
  • 本地倉庫:電腦上的一本存檔紀錄本,正式蓋章之後就進來這裡
  • 遠端倉庫(GitHub):雲端上的共用備份,讓別人也看得到

五個動作把它們串起來

  • git add:工作目錄 → 暫存區(丟進購物車)
  • git commit:暫存區 → 本地倉庫(結帳蓋章)
  • git push:本地倉庫 → GitHub(上傳雲端)
  • git fetch:GitHub → 本地倉庫(去看看有沒有新東西)
  • git pull:GitHub → 工作目錄(拉下來直接套用)

一句話記住:你的檔案在「改 → 選 → 存 → 傳」四關之間流動,前三個指令往外推,後兩個往回拿。

步驟 1:告訴 Git 我是誰

作者要你做什麼

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

每行在做什麼

  • git config:讀寫 Git 設定
  • --global:設定寫進 ~/.gitconfig,這台電腦全域生效,之後任何資料夾都用同一組身分
  • user.name:每次 commit 時掛的作者顯示名字
  • user.email:每次 commit 時掛的作者信箱
  • 兩個引號裡的 Your Name / you@example.com 是佔位符,換成自己的

這兩行只是替 commit 準備簽名

每次要 commit,Git 會把這對 name / email 蓋在 commit 上,像簽名一樣。 之後打 git log 看到的 Author: ... 就是它。

打完不會有任何回應。 這是正常的,Git 很多指令成功時是安靜的,沒特別反應就算成功。 想確認的話,把值再讀出來看:

git config --global user.name
git config --global user.email

會把剛設的值吐回來。

走到這裡才會知道的事:身分不等於地址

我一開始把「身分」跟「地址」混在一起,以為設了 email 就等於告訴 Git 我要 push 去哪。

  • user.name / user.email 只是寄件人簽名
  • push 需要的是收件地址(後面步驟 6 會設的 remote),兩件事完全獨立

Git 就算知道我的 email 是 gmail,也不會自動去 gmail 找我的 GitHub 帳號。它是離線工具,不會偷偷連網。

步驟 2:把資料夾變成 Git 倉庫

作者要你做什麼

進到要管理的資料夾,打:

git init

每行在做什麼

  • git init:在當前資料夾建一個隱藏的 .git/ 子資料夾

.git/ 是 Git 的大腦

.git/ 裡面存著這個倉庫所有的 commit 歷史、所有快照、所有設定。這個資料夾在,這裡才算 Git 倉庫,git 指令才會動;沒有 .git/,任何 git 指令都會噴 fatal: not a git repository

打完會看到:

Initialized empty Git repository in /path/to/your/folder/.git/

這句話才是「成功」的訊號,不是靜默的那種。

走到這裡才會知道的事:.git/ 是一切歷史的家,別亂砍

千萬別亂 rm -rf .git。這條指令會把整本歷史砍掉,資料夾變回普通資料夾,之前存過的所有快照消失。

反過來,如果 clone 完(見步驟 7)之後你又多打一次 git init,Git 會回:

Reinitialized existing Git repository in ...

意思是「這裡本來就是 Git 倉庫,你叫我再 init 一次,我照做但沒新東西產生」。無害,但沒必要。看到 clone 完成訊息,直接 cd 進去開始工作。

步驟 3:日常工作流程的三個動作

作者要你做什麼

git status
git add file.py
git commit -m "Add perceptron implementation"

每行在做什麼

  • git status:查看現況。紅字 = 未追蹤或改動未加入暫存區;綠字 = 已加入暫存區、等著 commit
  • git add file.py:把 file.py 從工作目錄放進暫存區
  • git commit -m "...":把暫存區內容拍成快照,存進本地倉庫
  • -m = message,這次 commit 的說明,寫在引號裡
  • 省略 -m Git 會開編輯器讓你寫,對初學者不友善,先加 -m 就好

一個作者沒明說的前提:file.py 是佔位符

作者假設你已經有一個 file.py,但一開始的資料夾裡沒這個檔。照打終端會回應你:

fatal: pathspec 'file.py' did not match any files

我用一行 shell 指令臨時建一個來練:

echo "print('hello git')" > file.py

shell 補充:echo> 是怎麼建檔的

這行不是 Git 指令,是 shell 通用寫法,值得停下來搞懂:

  • echo "xxx":把引號裡的字印到螢幕
  • >重導向,把本來要印在螢幕的東西改導向到檔案。檔案不存在就建、已存在就整個蓋掉
  • >>:跟 > 一樣但追加在檔案後面,不會蓋掉

所以 echo "print('hello git')" > file.py 的完整故事是:本來 echo 要在螢幕印那句字,> 把水管接過來寫進 file.py,結果就是建了一個檔、內容是那句 python。

這個知識可以複用在任何指令:git log > history.txt 就是把 git log 的結果存成檔案。

暫存區為什麼是必要的一步

我一開始不懂為什麼要有暫存區——為什麼不 add 完直接 commit?

想像你一個下午改了 5 個檔:新功能兩個檔、學習筆記一個檔、測試用暫時 config 一個、還有一個不小心存了密碼的 secret.txt。

有暫存區,你可以挑:

git add feature.py test_feature.py
git commit -m "新功能"

git add notes.md
git commit -m "更新學習筆記"

# config 跟 secret 從頭到尾沒進購物車,不會被 commit

沒暫存區,你只能全 commit 或全不 commit,沒辦法精挑細選。暫存區是讓你組織 commit 顆粒度的地方,所以每筆 commit 都是「一件有意義的事」,未來找歷史才好找。

commit 存的是快照,不是差異

這是我覺得整堂 Git 最重要、也最反直覺的一件事。

我一開始以為 commit 只存「改了哪幾行」。 原來是每 commit 一次,Git 把當下暫存區裡所有檔案的完整內容拍一張照存下來。 像手機拍照,按下快門那一刻整個畫面完整封存。

commit 成功會吐出類似:

[main (root-commit) 6715f21] Add perceptron implementation
 1 file changed, 1 insertion(+)
 create mode 100644 file.py

root-commit 代表這是倉庫的第一筆 commit,一個倉庫只有一次。6715f21 是這筆 commit 的 hash,Git 用它唯一辨識每一筆快照。

走到這裡才會知道的事:git add 不會創建檔案

我這裡也以為 git add 會直接生檔案。 不會。 檔案是自己(或編輯器、或剛剛的 echo)先建好的,git add 只是「登記進暫存區清單」的動作。

驗證方法:故意打 git add abc.py 但 abc.py 不存在,會回饋:

fatal: pathspec 'abc.py' did not match any files

這就證明 add 只認得已存在的檔案。Git 是被動的紀錄員,不會替你變出東西。

步驟 4:看歷史、從歷史挖檔案

作者要你做什麼

git log

每行在做什麼

  • git log:從新到舊列出所有 commit,每筆顯示完整 hash、作者、時間、message
  • 想看精簡版:git log --oneline,每筆一行、只有 hash 前幾碼跟 message

HEADmain 是兩個獨立的位置指針

git log 的第一行你會看到 (HEAD -> main),這是兩個標籤黏在同一筆 commit 上:

  • HEAD你目前站在哪筆 commit
  • main:main 分支的最新一筆在哪

剛開始這兩個標籤指同一筆,你之後開分支後會分開走(步驟 5 會看到)。

git show 從歷史挖出舊快照

想驗證「commit 真的存了完整快照」,可以做個小實驗。假設你的第一筆 commit hash 是 6715f21,內容是 print('hello git')。現在把檔案改掉:

echo "print('second version')" > file.py
cat file.py                      # 印出:print('second version')
git show 6715f21:file.py         # 印出:print('hello git')

同一個檔名,兩個版本並存——一個活在工作目錄(會變),一個死在快照裡(永遠不變)。這個實驗做過一次,你才會真的相信「Git 是版本控制」不是口號。

走到這裡才會知道的事:hash 前 7 碼就夠當縮寫

完整 hash 是 40 個字元,但實務上 Git 到處用前 7 碼當縮寫(例如 6715f21)。你打指令引用 commit 也可以用短版:

git show 6715f21                 # 有效
git show 6715f218f3b0138d5a...   # 也有效但沒必要

7 碼在同一個 repo 內幾乎不會撞。撞了 Git 會叫你多打幾碼區分。

步驟 5:分支 — 平行時空

作者要你做什麼

git checkout -b experiment/new-optimizer
# ... 改東西、commit ...
git checkout main
git merge experiment/new-optimizer

每行在做什麼

  • git checkout -b NAME:建新分支叫 NAME + 跳過去站在上面(一步做兩件事)
  • -b = branch
  • NAME 裡的 / 是命名習慣(把分支分類,像資料夾一樣的視覺分組),Git 內部不會真的建資料夾
  • git checkout main:切回 main 分支
  • git merge NAME:把 NAME 分支的成果併回你目前站的分支
  • git branch:列出所有分支,星號標你當前在哪條
  • git branch -d NAME:刪除 NAME 分支(-d = delete,用完清掉)
  • 新版 Git 更推薦 git switch NAME 取代 git checkout NAMEgit switch -c NAME 等同 git checkout -b NAME。舊版指令兩種都能用

分支就是從主線岔出去做實驗

用一條時間軸比喻:

初始 ← c1 ← c2 ← c3 ← c4 ← c5    (main 主線)

                  ← c3' ← c4'      (experiment 分支)

在某一點做決定「我想岔出去試試看,不影響主線」,Git 就長出一條平行時間軸。兩條線各自進化、互不干擾。你可以隨時跳過去某條、玩完併回主線、或整條砍掉當沒發生過。

分支解決的實際問題:

  • 試新想法:怕改壞主線?開分支去玩,失敗砍掉,主線完全沒動
  • 多人協作:他寫 login、你寫 payment,各開一條分支,寫完再併回 main
  • 急件插隊:正在寫新功能寫到一半、code 還是壞的,老闆要你先修線上 bug,切回 main 開條 hotfix 分支修完併回,再切回原本那條半成品分支繼續

新分支建完當下,資料夾檔案沒有變

這一點初學很容易搞錯。我第一次打 git checkout -b 之後偷偷去資料夾看,以為檔案會消失或變空。沒事,全都在。

因為分支是從 main 的最新 commit 岔出去的,兩條線在岔點之前的內容一模一樣。你只是站到另一條時間軸,但目前為止那條時間軸長得跟 main 完全一樣。真正的差異要等你在新分支上 commit 之後才會出現。

合併的規則:要站在收方,把送方拉進來

merge 有一個直覺會搞反的規則:你要站在「收方」,把「送方」拉進來

想把 experiment 併回 main:

git switch main                        # 站到收方(main)
git merge experiment/new-optimizer     # 把送方(experiment)拉進來

想成「main 打開門,把 experiment 迎進家裡」,而不是站在 experiment 那邊硬塞給 main。

Fast-forward 是什麼

merge 成功會看到:

Updating 0c2de54..22ca244
Fast-forward
 README.md | 3 +++
 1 file changed, 3 insertions(+)

Fast-forward 這個字值得停下來理解。它的意思是:Git 發現「main 從你岔出去之後,主線一步都沒動」,所以合併這件事超簡單——根本不用真的合併,只要把 main 這個標籤往前推到 experiment 的位置就好

像賽跑,main 跑者沒動、experiment 跑者跑到前面,你直接把 main 的號碼牌拿去掛到 experiment 那個位置。兩個名字同一個位置,就完成了。

合併後 git log --oneline 會看到 HEAD -> main, experiment/new-optimizer 三個標籤擠在同一筆 commit 上,這就是 fast-forward 的痕跡。

走到這裡才會知道的事:分支不是取代,是並存

最能讓人「懂」分支的實驗:在分支上改完 commit 完之後,切回 main,你會親眼看到剛才改的東西「不見了」

不是消失,是躲回它住的那條分支去了。切回實驗分支,東西又回來。

同一個資料夾能裝多個版本的檔案,切分支就是切頻道,切分支的當下 Git 會把資料夾裡的檔案改寫成該分支最新 commit 的樣子。這是為什麼工程師敢在分支上做很冒險的改動:主線永遠安全,分支再怎麼亂搞都不影響它。

步驟 6:推到 GitHub

作者要你做什麼

git push origin main

但這一行要 work,前面得先做兩件事

課程截圖只寫這一行,但直接照打會噴:

fatal: 'origin' does not appear to be a git repository

因為你還沒告訴這台 Git 你的雲端家在哪。完整流程是三步:

# 1. 去 GitHub 建一個空 repo(瀏覽器操作,不是終端機)
#    網址:https://github.com/new
#    三個「不要勾」:Add README / Add .gitignore / Choose license

# 2. 拿到網址後,註冊給本地
git remote add origin https://github.com/你的帳號/repo名字.git
git remote -v          # 驗證有存好

# 3. 第一次推
git push -u origin main

每行在做什麼

  • git remote add origin URL:註冊遠端網址到本地,取名叫 origin
  • git remote -v:列出所有已註冊的遠端,-v = verbose 詳細(不加只會列名字)
  • git push -u origin main:把本地 main 分支推到 origin 上的 main
  • -u = --set-upstream第一次推時才用,順便綁定本地 main <-> origin/main
  • 綁定過之後,以後打 git push 不用寫 origin main,Git 自己知道

origin 是通訊錄暱稱,不是網址

origin 不是關鍵字,是你自己取的暱稱,慣例大家都取這個名字。就像手機通訊錄,你不會每次打電話都輸入 11 位號碼,你存「媽媽」,以後直接找「媽媽」。

Git 一樣。你可以取任何名字,但沒必要標新立異,用 origin 就對了。

建 GitHub 空 repo 的三個「不要勾」

網址 https://github.com/new,注意三個不要勾:

  • 不要 Add a README file
  • 不要 Add .gitignore
  • 不要 Choose a license

因為我們要做的是「把本地已有內容推上一個空 repo」。如果 GitHub 那邊自動幫你建了 README/gitignore/license,那個 repo 就已經有內容了,跟你本地的內容會衝突,第一次 push 會噴錯,你就要學一堆合併衝突的東西,完全沒必要。

空的 repo = 一張白紙 = 本地直接推上去 = 順利。

GitHub 從 2021 起不接受密碼 push

Mac 上第一次 push,可能會發生下面幾種情況:

  • 情況 A(最順):Mac 有 Git Credential Manager,自動跳瀏覽器 → GitHub 網頁點 Authorize → 回終端機自動繼續 → push 成功
  • 情況 B:跳出視窗要 Username / Password。Username 填你 GitHub 帳號名。Password 千萬別填 GitHub 網頁密碼,會被拒絕。要填 Personal Access Token(PAT),那是另一個要去 GitHub 網站產生的東西
  • 情況 C:之前有裝過 GitHub CLI(gh)或設過 SSH,直接 push 成功

我這邊是情況 A 過關。

push 成功會看到:

* [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

最後那行 set up to track 就是 -u 綁定完成的證據。

走到這裡才會知道的事:身分跟地址是兩件事

我一度以為設好 user.email = Git 知道我要 push 去 gmail 綁定的那個 GitHub。這是誤解

  • 步驟 1 設的 user.name / user.email寄件人簽名
  • 這裡設的 remote 是收件地址
  • 兩件事完全獨立

為什麼要拆兩件事?因為它們的性質不同:身分一輩子不太會變,一台電腦全域設一次;地址每個專案不同,你可能有 10 個 repo,地址各自不同,所以每個資料夾各自設定。這樣分工才有彈性。

步驟 7:從別人那拿一份 — clone、fork、Watch

作者要你做什麼

git clone https://github.com/rohitg00/ai-engineering-from-scratch.git
cd ai-engineering-from-scratch

git checkout -b my-progress
# work through lessons, commit your code
git push origin my-progress

每行在做什麼

  • git clone URL:一句話等於「建資料夾 + cd 進去 + git init + 加 remote + 從遠端下載所有歷史跟檔案 + 切到預設分支」,六件事一次做完
  • cd ai-engineering-from-scratch:進到 clone 出來的資料夾(clone 會自動用 repo 名字建資料夾)
  • git checkout -b my-progress:開一條你自己的工作分支
  • git push origin my-progress:把 my-progress 分支推到 origin

git clone 是開箱即用套餐

對照你之前手動建 repo 的四步(mkdir → cd → git init → git remote add),clone 一句話全包了。因為「拿別人的 repo」這個動作幾乎每次都要做同一組事,Git 就把它包成一句指令。

clone 完後,你會處於一個「裝滿內容、跟遠端綁定好、可以馬上工作」的狀態。想驗證:

ls -la          # 看得到 .git/(自動 init 的證據)
git remote -v   # origin 已經設好(自動加 remote 的證據)
git branch      # 你在 main 分支上(自動切分支的證據)
git log --oneline | head -5   # 有作者的歷史(自動下載的證據)

fork 是 GitHub 按鈕,不是 git 指令

課程指令有個實務上會失敗的地方:作者最後那行 git push origin my-progress——你沒有作者 repo 的 push 權限,GitHub 會拒絕你。

正確的專業流程是「fork → clone → push」:

  1. 到 GitHub 網站,作者 repo 頁面右上角Fork 按鈕
  2. Fork 會複製一份到你自己的帳號名下(例如 你的帳號/ai-engineering-from-scratch
  3. clone 你 fork 那份(不是作者那份)
  4. 這時 push 就有權限了(推去自己家)

Fork 是 GitHub 網站上的按鈕,不是 Git 指令。GitHub 會永遠標記這個血緣關係(頁面會顯示 forked from ...),方便你之後同步作者更新。

不是每個人都需要 fork:Watch 就能收更新通知

我一度以為要拿到「作者更新通知」就一定要 fork + pull。不用

GitHub 每個 repo 右上角有三個按鈕:

  • Star:純書籤,收藏用,無通知
  • Fork:複製一份到你名下
  • Watch訂閱通知,作者更新就寄 email 給你

如果你只是想「知道作者有更新」,Watch 就夠。點 Watch → Custom → 只勾 Releases,作者發新版本才通知,吵度最低。

如果你想「實際拉更新到本地看 diff」或者「在裡面 commit 自己寫的 code」,才需要 fork + clone。

我最後的選擇是:Watch 作者原版收通知;fork 一份 clone 到本機當「隨時能拉最新的教科書」,不主動在裡面寫 code;code 練習寫在另一個自己的獨立 repo。分工清楚。

走到這裡才會知道的事:clone 完不要再打 git init

我當時 clone 完習慣性地打了一次 git init,Git 回我:

Reinitialized existing Git repository in /path/to/repo/.git/

「重新初始化一個已經存在的」。因為 clone 已經幫我做完 init 了,我又叫它 init,它就再 init 一次。無害,但沒必要。看到 clone 完成訊息,直接 cd 進去開始工作。

幾個我學習到的重點

幾組原則可以帶著走到未來所有 Git 使用場景:

  • Git 是紀錄員:檔案的產生、修改、刪除都是你做的(或編輯器、或 shell),Git 只在你叫它 add / commit 的時候動作。搞清這個分工,你就知道每個指令的責任範圍在哪
  • commit 是快照,不是差異:每次 commit Git 拍下整組暫存區內容,不是記錄「改了哪幾行」。這是為什麼你可以隨時挖出任何一個過去版本,git show COMMIT:FILE 就是挖法
  • 身分跟地址是兩件事user.name/email 是簽名,remote 是地址。分開處理是為了彈性——身分一輩子不變,地址每個專案不同
  • 分支是並存支線:同一個資料夾能裝多個版本的檔案,切分支就是切頻道。這是為什麼分支上敢做冒險實驗——主線永遠安全,實驗失敗砍分支就好