Vinc3nt's Life

當 Deploy 按鈕摧毀了一切:Git 版本控制的修復之旅

2024-10-30
develop
git
最後更新:2025-01-26
8分鐘
1425字

前言

在部落格開發過程中,我使用了一個來自其他開發者的 Astro 主題,並透過該專案的 Vercel Deployment 按鈕進行部署。這意味著我的部落格與主題原始專案在程式碼和版本控制上其實沒有直接的關聯。

然而,由於我也會對這個 Astro 主題進行貢獻,因此需要進行同步:當我新增功能至主題時,也希望能在自己的部落格中使用這些更新。因此,隨著時間推移,我必須將我的部落格 repository 與主題的 repository 保持同步更新。

這個同步過程並不總是順利,我在實作時遇到了不少麻煩與挑戰。這篇文章的目的是紀錄我在這個過程中所面臨的問題,例如如何解決合併衝突、處理依賴更新,以及版本控制上的困難,並分享我嘗試的解決方式,希望可以幫助有類似需求的開發者。

Repository 的全貌

有三個 repository:

  • cirry/astro-yi:我所使用的 Astro 主題專案。
  • vincent97277/astro-yi:從 cirry/astro-yi 主題 fork 出來的專案,目的是避免在提交程式碼時影響到原始的 repository。
  • vincent97277/vinny987-blog:我部落格的 repository,使用 cirry/astro-yi 中的 Vercel Deployment 建立。實際上這是一個全新的專案,GitHub 認定其與 cirry/astro-yi 沒有任何關聯。

它們之間的關係如下圖所示:

default

如果我需要提交貢獻到這個 Astro 主題,流程如下圖所示:

default

平時推送部落格文章時,只涉及部落格專案與 Vercel,如下圖所示:

default

部落格更新的困境

為了方便解釋,我們給 vincent97277/astro-yi 別名為 Avincent97277/vinny987-blog 別名為 B

想要將我的部落格 B 更新到最新版本,直覺上來說,直接將 A 的程式碼合併到 B 應該輕輕鬆鬆。但實際上,由於兩個 repository 完全沒有關聯,merge 時沒有共同的基準 commit。

可以這樣理解:B 是從 A 分出來的,但失去了 Git history 的連接,這意味著我們無法輕鬆地進行合併操作,因為缺乏共同的提交基準點,導致在合併過程中容易出現衝突和版本管理上的挑戰。我們需要重新建立這個連接。

建立關聯的方案分析

這個方案的關鍵點:

  • 我們將 A 設置為 B 的 remote -> upstream,這樣可以方便地拉取 A 的更新。
  • 使用臨時分支來處理合併,這樣比較安全。
  • 可能會遇到衝突,原因包括:
    • B 可能修改了一些 A 的原始程式碼。
    • A 的新功能也可能修改了相同的部分(例如設定檔和文章)。
  • 建立關聯後,未來的更新就不會再發生大規模的衝突(因為已經有基準點)。

既然釐清了做法,接下來進行實際操作。

實際操作步驟

我們會從 B 的 main 分支開始,進行操作。

設置 upstream

Terminal window
1
git remote add upstream <A repo url>

拉取 A 的最新更新

Terminal window
1
git fetch upstream

創建一個臨時分支來處理合併

Terminal window
1
git checkout -b temp-merge-branch
2
# 現在在 temp-merge-branch 分支

嘗試合併 A 專案的新特性

Terminal window
1
# 假設 A 專案的新特性在 main 分支
2
git merge upstream/main

我們遇到了錯誤:Refusing to Merge Unrelated Histories

這是 Git 不允許直接將兩個沒有關聯的分支做合併。所以我們需要使用額外的 flag 跳過它:

Terminal window
1
git merge upstream/main --allow-unrelated-histories

接下來我們將面對這次操作最大的麻煩。由於 Git 沒辦法透過任何資訊判斷合併的合理性,Git 會將所有差異的地方都列為衝突。如果這個程式碼很龐大,兩個倉庫的程式碼差異又非常大,那我們連 3-way merges 的結果都不能相信。

處理所有的衝突,合併更改回主分支

Terminal window
1
git checkout main
2
# 現在在 main 分支
3
git merge temp-merge-branch

刪除臨時分支

Terminal window
1
git branch -d temp-merge-branch

推送主分支更新到 B 的遠端倉庫

Terminal window
1
git push origin main

之後要更新 A 的新特性時,就可以直接合併回主分支:

Terminal window
1
git fetch upstream
2
git merge upstream/main

延伸思考: Rebase 還是 Merge

或許有細心的朋友會發現:

  • 為什麼第四步使用 merge 而不是 rebase
  • 為什麼不選擇讓項目歷史看起來更為乾淨的 rebase 呢?

首先,讓我們來聊聊 Rebase

Rebase 操作,簡單來說,是通過將一系列的提交 移動 到另一個基點上,從而產生一系列全新的提交對象。但這個過程中,Git 需要重新計算每個提交的差異,這不僅耗時,有時還可能導致與原來提交不同的結果出現,甚至可能需要重新解決一些之前已經解決過的衝突。

另一方面,考慮到我們在 temp-merge-branch 分支中實際上是要合併一個外部分支,如果 upstream/main 的歷史已經相當複雜,那麼過度追求項目歷史的乾淨可能就失去了意義。其實,讓項目歷史一塵不染不太現實,不如少點麻煩,保留變動的蹤跡。

因此,儘管 Rebase 能夠提供一個乾淨的項目歷史視圖,但在這個情境下,選擇 merge,接受那些歷史的複雜性,可能是更加符合實際情況的選擇。這不僅能夠避免潛在的衝突重新出現的問題,還能保留那些珍貴的開發歷程和決策過程。

本文標題:當 Deploy 按鈕摧毀了一切:Git 版本控制的修復之旅
文章作者:Vincent Lin
發布時間:2024-10-30