版本管理VCSversion control亦作 版本控制
記錄檔案隨時間的每一次變化,讓人可以回到過去、知道誰改了甚麼和為甚麼、多人同時修改而不互相覆蓋。
- 出處
- M0 為甚麼需要版本管理 · 「最終版_v3_真的最終」 →
- 參考
- Pro Git 1.1 About Version Control ↗
記錄檔案隨時間的每一次變化,讓人可以回到過去、知道誰改了甚麼和為甚麼、多人同時修改而不互相覆蓋。
整個專案在某一刻的完整樣貌。Git 的每個 commit 都是一張快照;沒有改動的檔案不重複保存,只記一個指向舊內容的參照。
只保存第一版的全文,之後每一版只記和上一版的差異;取回舊版本要從頭把差異逐個套上。Git 在概念上不採用這種方式。
Git 為節省空間,把多個舊物件壓縮存成一個檔,其中會用差異壓縮。這只是儲存細節,使用時看到的永遠是完整快照。
完整歷史只存放在一部中央伺服器上(例如 SVN),查歷史和提交都要連上伺服器。
每個人都有整個倉庫連同全部歷史的複本(例如 Git),查歷史和提交都在本機完成,只有同步時才需要連線。
團隊約定作為正式版本的那一份,通常是託管服務上倉庫的 main 分支。其他複本可以自由實驗,改動要經 PR 才能進入正本。
網上的 Git 倉庫託管服務,存放正本並提供 PR、審查、議題和權限管理。GitLab 是同類服務。它不等於 Git:Git 是安裝在電腦上的程式。
你在編輯器裏看到、直接修改的檔案。它和暫存區、倉庫並列為 Git 的三個區域。不要和 worktree(工作樹)混淆。
下一個 commit 的完整草稿,包含所有已追蹤檔案的內容,而不只是剛 add 的改動。git add 把工作目錄的內容放進來,git commit 把整份草稿存成快照。
存放所有 commit 和設定的地方,也就是專案裏那個 .git 資料夾。git init 建立它,git clone 複製一個。
檔案在工作目錄裏,但暫存區和倉庫都沒有它。Git 不會管它,直到你第一次 git add。
暫存區裏有這個檔案的內容,Git 會留意它的每次改動。已追蹤的檔案不受 .gitignore 影響。
工作目錄裏的內容和暫存區不同:改了,但還沒 git add。
暫存區的內容和 HEAD 不同:這些改動會進入下一個 commit。同一個檔案可以同時是已暫存和已修改。
逐行列出兩個版本的分別。git diff 比較工作目錄和暫存區;git diff --staged 比較暫存區和 HEAD。
由檔案內容計算出的一串編號:內容相同,指紋就相同。沙盒卡片上的
一張專案快照,加上作者、時間、說明和指向上一個 commit 的連結。動詞「提交」指用 git commit 建立它。
每個 commit 附帶的文字。第一行是簡短具體的標題(約 50 字元內,動詞開頭);需要解釋原因時空一行寫正文。
倉庫根目錄的純文字檔,每行一條規則,列出 Git 應當作不存在的檔案(系統雜物、個人設定、可重新產生的檔案、密鑰)。它本身要提交,而且管不到已追蹤的檔案。
在目前的資料夾建立 .git 資料夾,使它成為 Git 倉庫。檔案不會被改動,也還沒有任何檔案被追蹤。
指向「目前所在位置」的指標,存在 .git/HEAD。通常內容是 ref: refs/heads/main,即指向目前的分支,因而間接指向最新的 commit。提交時移動的是分支,HEAD 本身不變。
Git 資料庫裏的基本單位,共四種:blob、tree、commit、tag。物件以內容的雜湊值命名,一旦寫入就永不改變,只會增加。
存放一個檔案內容的物件,不含檔名和日期。內容相同的檔案共用同一個 blob;檔名寫在指向它的 tree 裏。
代表一個資料夾的物件:每一行列出模式、類型、編號和名字,指向 blob(檔案)或下一層 tree(子資料夾)。commit 指向的是根目錄的 tree。
用內容計算出的雜湊值作為物件的名字和地址。內容相同就是同一個物件,改一個字就是另一個物件,所以已寫入的歷史無法被偷偷修改。
給 commit 取的名字:.git/refs/ 裏的一個純文字檔,內容只有一個編號(41 bytes)。分支和標籤都是 ref;分支會隨提交前進,標籤不動。
Git 內部使用的低層命令,例如 hash-object、cat-file,適合用來查看內部結構;平常使用的 add、commit 等稱為高層命令(porcelain)。
Git 用內容計算出的 40 位十六進位編號,用來命名每個物件。內容一樣,編號就一樣;改一個字,編號就完全不同。平常寫頭 7 位就夠。
固定貼在某個 commit 上的名字,例如 v1.0。和分支不同,它不會隨新的提交移動。
HEAD 直接指向一個 commit,而不是指向分支的狀態。可以查看舊版本或做實驗;但在這狀態下提交的 commit 沒有分支記住,離開前要用 git switch -c 或 git branch 保住。
仍在物件資料庫裏、卻沒有任何分支或標籤指向的 commit,git log 看不到它。常見於離開 detached HEAD 或強制刪除分支之後;可用 git branch 名字 編號 救回,或從 git reflog 找回編號。
切換到另一條分支:把 HEAD 改為指向它,並把工作目錄和暫存區換成那個 commit 的快照。-c 同時建立分支,--detach 進入分離 HEAD。取代舊命令 git checkout 的切換用途。
指向某個 commit 的可移動標籤。在分支上提交時,標籤會自動移到新 commit。建立分支幾乎不花成本。
把另一條分支的工作收進目前所在的分支。能 fast-forward 時只移動標籤;歷史分岔時做三方合併,產生合併 commit。
目前分支是被合併分支的祖先(沒有分岔)時,Git 只把目前分支的標籤往前推,不產生新 commit。加 --no-ff 可強制產生合併 commit。
用 ours、theirs 和兩者的合併基準三個版本,逐處判斷是誰改的:只有一邊改就採用那一邊,兩邊在同一處(或相鄰處)改得不同就是衝突。
兩條分支最近的共同祖先,即歷史分岔的那個 commit。三方合併用它判斷每處改動屬於哪一邊。可用 git merge-base 查看。
有兩個(或以上)parent 的 commit,把分岔的歷史接回一起。它和普通 commit 一樣是一張完整快照。
兩條分支在同一處(或相鄰的行)改得不同,Git 無法自動決定。Git 在檔案裏寫入衝突標記並暫停合併,等你解決後 git add、git commit,或用 git merge --abort 放棄。
Git 寫進檔案的三行記號:<<<<<<< 之後是目前分支的版本,======= 分隔,>>>>>>> 之前是被合併分支的版本。解決衝突時要全部刪除。
把目前的分支(連同 HEAD)搬到指定的 commit。--soft 只搬分支;--mixed(預設)再把暫存區改成目標內容;--hard 連工作目錄也改,未提交的修改會永久丟失。
計算出抵銷某個 commit 改動的修改,提交成一個新 commit,不改寫任何舊歷史,所以可以安全地用在已分享的分支上。它撤銷的是那一個 commit,而不是把專案回到那個版本。
用暫存區的內容和(新的)說明建立一個新 commit,取代最後一個 commit 的位置;舊 commit 沒有被修改,只是不再有分支指向它。已分享的 commit 不要 amend。
HEAD(和各分支)每次移動的本機記錄,用 git reflog 查看;HEAD@{1} 表示上一步的位置。可找回 reset、amend、刪除分支後「消失」的 commit,但找不回從未提交的修改。不會 push。
把目前分支上、不在上游歷史裏的 commit,逐個重新套用到新的起點,產生新 commit,再把分支搬過去。歷史變成一直線;舊 commit 只在 reflog。不要用在已分享的分支上。
git rebase -i:先列出待辦清單,讓你為每個 commit 選擇 pick、reword、squash、fixup 或 drop,也可調換次序,用來在分享前整理自己的 commit。
把某一個 commit 的改動套用到目前分支,產生一個新 commit(沿用原說明和作者時間)。常用於把單一修正帶到另一條維護分支。
把整條分支的改動壓成一個普通的新 commit 放到目標分支(git merge --squash 再 commit;GitHub 的「Squash and merge」)。新 commit 與原分支沒有 parent 關係,所以原分支會被判為「未合併」,刪除前應比較內容。
rebase、amend、reset 都會用新 commit 取代舊 commit;若舊 commit 已被別人取得,雙方歷史會分岔,強制推送更可能覆蓋別人的工作。只改寫只有自己用過的 commit。
git push --force:讓遠端分支直接改成你的版本,即使會丟掉遠端上別人的 commit。改寫個人分支後如需推送,應改用 --force-with-lease(遠端有你未見過的新 commit 時會拒絕)。受保護的分支通常禁止。
把未提交的修改暫時收起,讓工作目錄和暫存區回到 HEAD 的樣子;之後用 git stash pop 或 apply 取回。stash 是一個堆疊,本身由特殊的 commit 組成(refs/stash),所有 worktree 共用。
同一個倉庫的另一個工作資料夾。每個 worktree 各自 check out 一個分支、各有自己的檔案和暫存區,但共用同一份歷史。同一分支同一時間只能在一個 worktree。
git clone:下載遠端的全部物件(整段歷史),把遠端記為 origin,為每條遠端分支建立 origin/<分支>,並建立追蹤 origin/main 的本機 main。
本機上對遠端分支位置的記錄,例如 origin/main,代表「上一次聯絡遠端時,它的 main 在哪裏」。只在 fetch、pull、push 時更新,不能直接在上面提交。
本機分支所追蹤的遠端分支(如 main 追蹤 origin/main)。設定後,git push、git pull 不用寫參數,git status 會顯示 ahead/behind。第一次推送新分支時用 git push -u 設定。
從遠端下載你還沒有的 commit,並更新 origin/* 遠端追蹤分支;不會改動你的本機分支和檔案,所以永遠安全。
fetch 之後,把上游分支合併(--no-rebase)或 rebase(--rebase)進目前分支;兩邊分岔而沒有指定方式時,Git 會要求你選擇。
把遠端沒有的 commit 上傳,並移動遠端的分支。只在遠端分支是你分支的祖先時成功;否則被拒絕,以免蓋掉別人的工作。
另一處的同一個倉庫(通常在 GitHub 或 GitLab 上),本機用一個名字記住它,預設叫 origin。fetch/pull 從它取,push 往它送。
為一件工作開的短期分支(例如 background、fix-sample-size)。完成後經 PR 審查合併進 main,然後刪除;main 只放已審查的工作。
在 PR 上由別人檢查改動:批准(approve)、要求修改(request changes)或留言。知識工作同樣適用:審查論點、數據來源和用詞。
在託管服務上,把別人的倉庫複製一份到自己的帳號下(伺服器端的 clone)。沒有原倉庫寫入權時,在 fork 上工作,再對原倉庫開 PR;原倉庫通常加為名叫 upstream 的第二個遠端。
只有 main 一條長期分支;每件事開短期功能分支,經 PR 審查後合併,然後部署。是大部分小團隊最簡單合用的流程。
所有人非常頻繁地把小改動合併進 main(主幹),分支壽命以小時計,未完成的功能用開關隱藏;需要完善的自動測試。
以 main(已發佈)和 develop(開發中)兩條長期分支為主,另有 feature、release、hotfix 分支並按規則合併;適合要同時維護多個發佈版本的產品。
在託管服務上提出「請把我這條分支合併進正本」的請求,附帶改動、討論和審查記錄。GitLab 稱為 merge request(MR)。
保護規則中可以不受規則約束的人或角色(常預設包括管理員)。名單上的人違反規則時,伺服器只提示 Bypassed rule violations 並放行。與 AI 共用管理員帳號時,應清空名單。
每次推送或開 PR 時自動執行的檢查(測試、格式、連結、是否留下 TODO),結果顯示在 PR 上;配合「必須通過狀態檢查」規則,失敗的 PR 不能合併。GitHub 用 Actions 執行。
倉庫裏的設定檔(.github/CODEOWNERS),按路徑指定負責人;開啟「需要 CODEOWNERS 審查」後,改動了某路徑的 PR 必須有該負責人批准。
託管服務建在標籤之上的正式版本,附發佈說明和可下載檔案(gh release create v1.0)。版本號常用語意化版本:主版本.次版本.修訂。
用 GPG 或 SSH 私密金鑰在 commit 裏加上數碼簽名,證明確實由你建立;GitHub 驗證後顯示 Verified。簽名是 commit 內容的一部分,所以會改變編號。
託管服務上的規則,限制怎樣修改某條分支(通常是 main):必須經 PR、禁止強制推送、禁止刪除等。GitHub 用 ruleset 設定。
寫純文字文件時每一句(或每個子句)各佔一行。改一句只影響一行,diff 精確到句子;兩人改的句子之間隔着未改動的行就能自動合併,改相鄰兩句仍會衝突。渲染時通常仍是同一段,但中文之間會否多出空格、會否顯示成真換行,視乎編輯器和輸出工具的設定。
Git 的擴充:大型檔案本身存在另一個伺服器,倉庫裏只提交一個細小的指標檔,避免每個版本的大檔都塞進歷史。
能自己讀檔、執行命令、修改倉庫的 AI 程式。寫得快、不會累,但會照字面執行;在 Git 上要給它獨立的 worktree 和分支,經 PR 合併。
commit 說明最後幾行「鍵:值」格式的附註,例如 Co-Authored-By、Refs。可用 git log --format 或 --grep 搜尋,用來記錄共同作者或 AI 參與。
逐行列出最後改動該行的 commit、作者和日期,用來找到那個 commit、讀它的說明。
git log -S "字串" 列出令該字串出現次數改變的 commit;-G 則用正規表示式比對改動的行。用來找某段內容何時加入或刪除。
用二分法在好版本和壞版本之間找出第一個壞 commit:Git 每次切換到中間,你回答 good 或 bad,約 log₂N 步完成;可用 git bisect run 自動化。
Git 的設定分系統、全域(~/.gitconfig)、倉庫(.git/config)三層,另可加 worktree 層,越接近工作的地方越優先。git config --list --show-origin 顯示每個值的來源檔案。
一個設定值來自哪一層:system、global、local(倉庫)或 worktree。同一設定在幾層都有時,越接近工作的地方越優先;git config --show-scope 顯示每個值屬於哪一層。
config 裏 alias.名字 的設定,把常用的 Git 命令縮短,例如 git lg。值以 ! 開頭時交給 shell 執行,而且一律在倉庫最上層執行。
放在 .git/hooks/ 的腳本(要有執行權限、沒有副檔名),Git 在特定時刻執行。動作之前的 hook(pre-commit、commit-msg、pre-push…)以非零狀態結束就中止該動作;post-commit、post-merge 等在動作完成後才執行,不能中止。客戶端 hook 只在本機、可用 --no-verify 略過,所以不能代替伺服器規則。
找不到相符的術語。