程倉庫操作實(shí)戰(zhàn):從remote配置到?jīng)_突解決)
真正讓人頭疼的往往不是寫代碼而是 Git 遠(yuǎn)程倉庫操作。我在某團(tuán)隊(duì)帶新人時(shí)A同學(xué)一臉懵地跑過來明明本地 commit 都成功了git push 卻被拒絕而且報(bào)錯(cuò)信息里全是 detached HEAD、non-fast-forward 這種看似懂又不懂的詞。他問我的第一句話是“遠(yuǎn)程倉庫到底是個(gè)啥為什么我這一套跟單機(jī)版 Git 完全不是一個(gè)畫風(fēng)”。這個(gè)問題幾乎每個(gè)剛開始協(xié)作的人都會(huì)遇到。Git 遠(yuǎn)程倉庫操作說白了就是解決一件事讓不同電腦上的 Git 歷史保持同步。但同步這件事背后牽扯到 remote 配置、fetch、pull、push、分支跟蹤、沖突處理、權(quán)限管理任何一環(huán)沒搞明白協(xié)作就會(huì)變成災(zāi)難。這篇文章我打算把日常實(shí)戰(zhàn)中最常用的遠(yuǎn)程操作完整捋一遍包括每一條命令在底層到底做了什么、為什么有些操作要按特定順序來、以及我這些年踩過的真實(shí)坑。適合剛學(xué)會(huì) commit 還不太敢碰遠(yuǎn)程的新人也適合想讓自己的協(xié)作流程更規(guī)范的開發(fā)者。1. 別再背命令了先搞懂 remote 到底是什么1.1 遠(yuǎn)程倉庫不是網(wǎng)盤是另一份完整歷史很多人第一次 clone 完項(xiàng)目以為遠(yuǎn)程倉庫就是一個(gè)存儲(chǔ)代碼的網(wǎng)盤push 就是把文件傳上去pull 就是把文件拉下來。這個(gè)理解在簡單場景下勉強(qiáng)能用但一旦遇到分支、沖突、回滾就徹底失靈了。從本質(zhì)上說遠(yuǎn)程倉庫是獨(dú)立存在于另一臺(tái)機(jī)器上的一個(gè)完整 Git 倉庫。它和你本地倉庫一樣包含對象數(shù)據(jù)庫、引用分支、標(biāo)簽、HEAD 指針這些東西。你 clone 下來的不是“一份代碼快照”而是那個(gè)倉庫的全部提交歷史。這也是為什么 Git 遠(yuǎn)程倉庫操作的核心不是文件同步而是提交歷史的同步。我用一個(gè)生活類比解釋給 A同學(xué)聽本地倉庫像你的工作臺(tái)遠(yuǎn)程倉庫是另一個(gè)同事的工作臺(tái)。你們不是通過互相發(fā)文件來協(xié)作而是通過交換“記事本”——每一頁記錄一個(gè)變更。只要雙方記事本上的頁碼對得上協(xié)作就順暢對不上就需要先對齊頁碼fetch/pull再把自己新寫的頁面貼上去push。1.2 origin 這個(gè)名字是從哪冒出來的剛開始用 Git 的人總會(huì)疑惑為什么大家都把遠(yuǎn)程倉庫叫 origin是自己起的嗎其實(shí) origin 只是當(dāng)你執(zhí)行 git clone 時(shí)Git 默認(rèn)給唯一那個(gè)遠(yuǎn)程倉庫起的名字。它沒有任何魔法含義就是個(gè)默認(rèn)別名。remote 的真實(shí)身份是一串 URL加上一組遠(yuǎn)程跟蹤分支引用。你運(yùn)行 git remote -v 時(shí)看到的兩行地址fetch 和 push 地址才是遠(yuǎn)程倉庫的真實(shí)位置。如果 push 地址和 fetch 地址不同還可能出現(xiàn)“能拉不能推”的怪事。這里有個(gè)關(guān)鍵點(diǎn)需要理解remote 的名字和分支名沒有關(guān)系。origin 只是指代“那臺(tái)機(jī)器”至于那臺(tái)機(jī)器上叫 master、main、develop 還是 release/v1完全看對方倉庫自己的分支結(jié)構(gòu)。你甚至可以給同一個(gè) URL 起多個(gè)名字或者給同一個(gè)倉庫配置兩個(gè)不同名字的 remote這是合法的。1.3 配置多遠(yuǎn)程你需要先有拓?fù)湟庾R(shí)真實(shí)協(xié)作里單人單倉庫的“origin 一條路”遠(yuǎn)遠(yuǎn)不夠。開源項(xiàng)目的標(biāo)準(zhǔn)場景是 fork 后配置兩個(gè) remote一個(gè)是上游主倉庫一個(gè)是你自己的倉庫。這時(shí)候很多人就會(huì)犯迷糊哪個(gè)是 origin該往哪個(gè) push我的習(xí)慣是把 fork 出來的自己的倉庫保持叫 origin把官方上游倉庫叫 upstream。這樣語義清晰origin 是我能 push 的地方upstream 是我要跟蹤和同步的地方。添加 upstream 只需要一條命令git remote add upstream gitgit.example.com:team/project.git多 remote 的難點(diǎn)不在于添加而在于你要時(shí)刻清楚當(dāng)前分支在跟哪個(gè)遠(yuǎn)程的哪個(gè)分支對應(yīng)。否則就可能發(fā)生“我明明推的是自己的分支怎么跑到別人倉庫里了”的慘案。所以每次操作前先花十秒想清楚拓?fù)浔镜赜心膸讉€(gè)遠(yuǎn)程、當(dāng)前分支跟蹤的是誰、這次提交希望進(jìn)入哪個(gè)歷史。2. Clone 之后的第一件事盤一盤你的遠(yuǎn)程倉庫2.1 git remote -v這是你的第一張地圖我接手任何項(xiàng)目的第一件事永遠(yuǎn)是在項(xiàng)目根目錄執(zhí)行 git remote -v。這條命令會(huì)列出當(dāng)前倉庫配置的所有遠(yuǎn)程名稱與地址。如果輸出為空說明這個(gè)目錄雖然是個(gè) Git 倉庫但還沒有和任何遠(yuǎn)程倉庫建立關(guān)系。這時(shí)候 git push / git pull 都會(huì)報(bào)“No configured push destination”或類似錯(cuò)誤。輸出非空后我會(huì)再執(zhí)行 git branch -vv查看本地分支和遠(yuǎn)程跟蹤分支之間的對應(yīng)關(guān)系。兩行輸出合起來就能拼出項(xiàng)目協(xié)作的基本拓?fù)洹:芏嗄涿畹?push 失敗其實(shí)在第一張“地圖”上就能發(fā)現(xiàn)端倪比如本地分支根本沒有設(shè)置上游比如 remote 的 push 地址寫錯(cuò)了。養(yǎng)成先看 remote -v 再加 branch -vv 的習(xí)慣能幫你省掉后面大量的排查時(shí)間。2.2 添加、改名、換地址三個(gè)高頻操作遠(yuǎn)程地址不是一成不變的。公司搬家換 Git 服務(wù)器、項(xiàng)目從一套協(xié)作平臺(tái)遷到另一套都會(huì)導(dǎo)致遠(yuǎn)程 URL 失效。這時(shí)候不要?jiǎng)h掉倉庫重新 clone改地址就行g(shù)it remote set-url origin https://git.example.com/team/project.gitgit remote rename 用于給遠(yuǎn)程改名。比如一開始默認(rèn) origin后來團(tuán)隊(duì)想統(tǒng)一叫 central可以執(zhí)行g(shù)it remote rename origin central注意 rename 會(huì)自動(dòng)更新已配置的跟蹤分支引用這個(gè)動(dòng)作比手動(dòng)刪了再加更安全。添加遠(yuǎn)程是另一種高頻場景前面提到的 upstream 就是典型。git remote add 的完整語法是git remote add name url高頻率操作里最容易出錯(cuò)的不是命令本身而是“是否要添加 remote 還是直接 clone”。如果你本地已經(jīng)有一份完整代碼目錄但又想從某個(gè)遠(yuǎn)程倉庫拉取東西git remote add 是對的如果你是想在一臺(tái)新電腦上獲取項(xiàng)目副本git clone 遠(yuǎn)勝于先建空倉庫再 add remote。2.3 HTTPS、SSH以及一個(gè)容易被忽略的配置點(diǎn)選 HTTPS 還是 SSH 這個(gè)問題幾乎每周都有人問。我的答案很直接優(yōu)先 SSH。原因不是 HTTPS 不安全而是 SSH 密鑰可以避免你每一次 push/pull 都輸密碼。尤其是當(dāng)你使用 commit 簽名、頻繁推送時(shí)HTTPS 的憑證交互會(huì)打斷思路。和 SSH 相關(guān)的實(shí)操細(xì)節(jié)經(jīng)常被忽略ssh-keygen 生成密鑰后你配置的 remote URL 必須是 SSH 格式如 gitgit.example.com:team/project.git而不是 https:// 開頭??珊芏嗳嗣髅髋浜昧嗣荑€remote 地址還是 HTTPS結(jié)果 SSH 協(xié)議根本沒被用到。另外說一個(gè)容易踩坑的配置項(xiàng)url. .insteadOf。這個(gè)配置可以讓你在 clone 時(shí)寫 HTTPS但實(shí)際走 SSH或者反過來。比如git config --global url.gitgit.example.com:.insteadOf https://git.example.com/這個(gè)配置的妙處在于你從文檔里復(fù)制下來的 HTTPS 地址不用改Git 會(huì)自動(dòng)替換成 SSH。但它也會(huì)帶來困擾某些托管平臺(tái)對 SSH 和 HTTPS 的權(quán)限校驗(yàn)不同一旦全局配置了 insteadOf可能出現(xiàn)“明明賬號(hào)有權(quán)限卻 push 失敗”的詭異問題。排查遠(yuǎn)程問題時(shí)記得用 git config --list 看看是不是被這個(gè)配置影響了。3. fetch、pull、push遠(yuǎn)程協(xié)作的三駕馬車但方向千萬別搞反3.1 fetch把遠(yuǎn)程狀態(tài)“搬”到本地的隱藏區(qū)域git fetch 是我個(gè)人用得非常頻繁的命令但新手往往不太理解它到底干了什么。fetch 會(huì)把遠(yuǎn)程倉庫的分支狀態(tài)、提交對象、標(biāo)簽都下載到本地然后保存在“遠(yuǎn)程跟蹤分支”里。這些分支不會(huì)出現(xiàn)在你正常的工作區(qū)而是以 origin/main、origin/feature/x 這樣的形式存在于引用命名空間中。很多人以為 fetch 之后本地代碼就變成遠(yuǎn)程最新了這是錯(cuò)誤理解。fetch 只是更新了“遠(yuǎn)程在某一時(shí)刻長什么樣”的本地記錄你的工作區(qū)、當(dāng)前分支、暫存區(qū)完全不受影響。這是 fetch 最安全的地方你可以隨時(shí)查看遠(yuǎn)程別人提交了什么而不必?fù)?dān)心自己的半成品代碼被打斷。在我負(fù)責(zé)的流程里fetch 通常作為 pull 的前置步驟單獨(dú)執(zhí)行或者用 git fetch --prune 來同步清理遠(yuǎn)程已刪除分支的本地記錄。--prune 參數(shù)很實(shí)用下面講幽靈分支時(shí)會(huì)細(xì)說。3.2 pull一條命令背后的合并動(dòng)作git pull 是 fetch 加上合并兩個(gè)動(dòng)作的合體。它先執(zhí)行 fetch然后把遠(yuǎn)程跟蹤分支合并到當(dāng)前分支。這里的“合并”默認(rèn)是 merge不是 rebase。這個(gè)默認(rèn)行為導(dǎo)致了很多“git pull 之后突然多了一個(gè) Merge commit”的現(xiàn)象尤其在多人長期共用一個(gè)分支時(shí)。理解 pull 的兩個(gè)動(dòng)作很重要。假設(shè)你在 main 分支上git pull 實(shí)際做了git fetch origin git merge origin/main因?yàn)槟惝?dāng)前的 main 跟 origin/main 有共同祖先但也有各自的新提交merge 會(huì)把兩邊歷史縫合起來生成一個(gè)新的合并提交。如果你想讓本地歷史更線性可以配置git config --global pull.rebase true這樣 git pull 就成了 fetch rebase。關(guān)于 merge 和 rebase 的取舍我會(huì)在第 5 章展開這里先記住一個(gè)結(jié)論沒有永遠(yuǎn)正確的策略只有適不適合團(tuán)隊(duì)協(xié)作習(xí)慣的策略。3.3 push不是上傳是請求合并git push 是最直觀但也是風(fēng)險(xiǎn)最大的遠(yuǎn)程操作。它的語義是把本地分支的新提交推送到遠(yuǎn)程對應(yīng)分支讓遠(yuǎn)程分支指針前進(jìn)到本地分支的位置。但遠(yuǎn)程倉庫不是一定接受你的推進(jìn)它有一個(gè)校驗(yàn)規(guī)則非 fast-forward 的更新會(huì)被拒絕。所謂 non-fast-forward簡單說就是“你要推進(jìn)的遠(yuǎn)程分支基于的歷史已經(jīng)不再是遠(yuǎn)程當(dāng)前歷史”。這通常意味著遠(yuǎn)程分支上陸續(xù)有了別人的新提交而你的本地分支還停留在舊位置。Git 拒絕你是為了防止你覆蓋掉別人的提交。這是保護(hù)機(jī)制不是 Bug。理解了 push 的真實(shí)語義你就明白為什么 push 之前最好先 pull/rebase你的分支歷史如果和遠(yuǎn)程歷史完全對齊push 就是一次快速前進(jìn)如果沒對齊你需要先整合遠(yuǎn)程的新提交再推送自己這部分。3.4 跟蹤分支與 upstream讓命令少打一半每次 push 都要寫git push origin main太啰嗦跟蹤分支就是來解決這個(gè)問題的。當(dāng)你用git push -u origin main推送過一次后本地 main 分支就記錄了它的 upstream 是 origin/main。之后你再執(zhí)行 git pull 或 git pushGit 會(huì)自動(dòng)識(shí)別該往哪里推、從哪里拉。upstream 的信息存在本地分支配置里你可以用 git branch -vv 查看。每次新建分支時(shí)我建議第一時(shí)間明確它的 upstream尤其在多人協(xié)作項(xiàng)目中。有幾種情況容易讓 upstream 變得混亂本地分支名跟遠(yuǎn)程分支名不一樣、同一個(gè)本地分支跟蹤了另一個(gè)本地分支、遠(yuǎn)程分支被刪除但本地配置還指向它。遇到這些情況可以用 git branch --unset-upstream 清掉跟蹤關(guān)系再用 git push -u 設(shè)置新的。3.5 為什么我堅(jiān)持 pull 之前先看一眼 fetch 結(jié)果雖然 git pull 一條命令很方便但我自己在處理關(guān)鍵分支時(shí)還是習(xí)慣先 fetch 再手動(dòng)處理。原因很簡單pull 的合并是“自動(dòng)的”而自動(dòng)合并失敗時(shí)你已經(jīng)處在合并中間狀態(tài)有些人會(huì)驚慌失措。fetch 之后的信息非常明確。git fetch 會(huì)打印出哪些分支有新提交、提交作者是誰。這時(shí)候你可以冷靜判斷要不要馬上拉取會(huì)不會(huì)跟我的改動(dòng)沖突能不能先讓我本地提交整理完再說這種“先搞清楚情況再動(dòng)手”的節(jié)奏對多人同時(shí)改一個(gè)文件的高壓場景特別重要。它能讓你在沖突發(fā)生之前就有了心理準(zhǔn)備和應(yīng)對預(yù)案。4. 遠(yuǎn)程分支之間那些繞不開的規(guī)則4.1 遠(yuǎn)程分支的完整引用名本地分支的引用名形態(tài)是 refs/heads/main遠(yuǎn)程跟蹤分支則是 refs/remotes/origin/main。你在終端里能看到的 origin/main其實(shí)是后者的縮寫。這個(gè)完整名字解釋了為什么你在本地創(chuàng)建一個(gè)叫 origin/main 的分支時(shí)會(huì)報(bào)錯(cuò)它和遠(yuǎn)程跟蹤引用的命名空間沖突。理解引用名還有一個(gè)實(shí)際幫助你可以直接查看遠(yuǎn)程分支指向的提交比如 git log origin/feature/login 和 git log feature/login 對比就能看到“遠(yuǎn)程已有哪些提交”和“我本地還有哪些提交”之間的差異。這種對比在需要確認(rèn)“我的分支是不是落后了”時(shí)非常高效。4.2 為什么你 push 的分支在別人那里叫別的名字git push origin feature/login 這條命令的完整語義是把本地 feature/login 分支推送到遠(yuǎn)程的 feature/login 分支。如果你想推送到遠(yuǎn)程不同名字的分支可以寫成git push origin feature/login:feature/authentication左邊是本地分支右邊是遠(yuǎn)程分支。這個(gè)語法很冷門但在一些特殊情況比如遠(yuǎn)程分支命名規(guī)范臨時(shí)調(diào)整很有用。真正讓新人困惑的是另一種情況明明我 push 了但同事說在遠(yuǎn)程看不到。多半原因是你的 push 只是把分支上傳到了遠(yuǎn)程倉庫但遠(yuǎn)程倉庫的默認(rèn)合并請求頁面只會(huì)顯示那些跟默認(rèn)分支有差異的分支名稱。如果你推送后沒人提合并請求分支自然不會(huì)被別人注意到。這是協(xié)作流程問題不是 Git 命令問題。4.3 刪除遠(yuǎn)程分支和本地分支要分開刪除本地分支用 git branch -d feature/login刪除遠(yuǎn)程分支則是git push origin --delete feature/login這兩個(gè)動(dòng)作互不相關(guān)。我見過很多團(tuán)隊(duì)負(fù)責(zé)人只刪了遠(yuǎn)程分支沒提醒大家清理本地分支結(jié)果一段時(shí)間后每個(gè)人本地都堆了一大堆已經(jīng)合并過的舊分支。反過來也有只刪本地不刪遠(yuǎn)程的遠(yuǎn)程倉庫成了垃圾場。在多人協(xié)作項(xiàng)目中刪除分支前最好確認(rèn)這個(gè)分支的改動(dòng)是否已經(jīng)合并到主分支。強(qiáng)制刪除用 -D但請想清楚一旦別人已經(jīng)基于這個(gè)分支開發(fā)強(qiáng)刪會(huì)破壞他們的歷史。4.4 分支名撞車與大小寫帶來的坑Git 分支名是區(qū)分大小寫的但某些操作系統(tǒng)文件系統(tǒng)不區(qū)分。比如本地有 Feature/Login 和 feature/login 兩個(gè)分支在 macOS 默認(rèn)文件系統(tǒng)上就可能出現(xiàn) checkout 錯(cuò)亂。遠(yuǎn)程倉庫通常也區(qū)分大小寫但如果托管平臺(tái)對分支名規(guī)范有限制大小寫混用會(huì)在 push 時(shí)產(chǎn)生詭異報(bào)錯(cuò)。我的建議很簡單全團(tuán)隊(duì)統(tǒng)一分支命名規(guī)范全部小寫用斜杠做目錄層級(jí)。比如 feature/用戶端-登錄雖然不規(guī)范但至少統(tǒng)一。更通用的是 feature/login、bugfix/payment-timeout 這種。別小看命名遠(yuǎn)程協(xié)作一半的混亂都來自名字。5. 多人協(xié)作現(xiàn)場從提交沖突到代碼評審5.1 一個(gè)標(biāo)準(zhǔn)的提交流程長什么樣我推薦團(tuán)隊(duì)新人先按這個(gè)順序走完一輪再談優(yōu)化git fetch origin git checkout -b feature/xxx origin/main git add . git commit -m feat: ... git push -u origin feature/xxx這里的關(guān)鍵動(dòng)作是git checkout -b feature/xxx origin/main它從遠(yuǎn)程最新的 main 創(chuàng)建新分支而不是從你自己可能已經(jīng)落后的本地 main 創(chuàng)建。這一步能減少大量不必要的沖突。提交后用 git push -u 推送然后到遠(yuǎn)程平臺(tái)創(chuàng)建合并請求。代碼評審?fù)ㄟ^后再合并。這套流程里的每一步都有明確目標(biāo)fetch 是為了獲取最新狀態(tài)基于 origin/main 是為了確保起點(diǎn)干凈合并請求是為了讓代碼在進(jìn)入主分支前有人審閱。別省略任何一個(gè)環(huán)節(jié)。5.2 沖突產(chǎn)生的根本原因很多新人把沖突當(dāng)成“事故”其實(shí)沖突是 Git 正常工作的證明。沖突只發(fā)生在“兩個(gè)人基于同一個(gè)歷史分別修改了同一處內(nèi)容而 Git 無法自動(dòng)決定該保留哪份”的時(shí)候。舉個(gè)例子你和同事都是從 main 的提交 A 拉出的分支。你在自己分支上修改了登錄函數(shù)第 10 行同事在另一個(gè)分支上也修改了第 10 行。當(dāng)你們倆的分支先后合并回 main 時(shí)Git 發(fā)現(xiàn)同一行有兩個(gè)不同的新版本不知道聽誰的于是把決定權(quán)交給你。從遠(yuǎn)程操作的角度看沖突更多發(fā)生在 pull 和 merge/rebase 階段。比如本地 main 落后遠(yuǎn)程你執(zhí)行 git pull而遠(yuǎn)程的改動(dòng)恰好撞上你本地未提交的改動(dòng)。這就是為什么我強(qiáng)烈建議pull 之前先提交或者 stash不要帶著半成品去拉取。5.3 merge vs rebase我的選擇邏輯關(guān)于 merge 和 rebase 的爭論每個(gè)團(tuán)隊(duì)都能吵三天。我的原則是在對外共享分支上堅(jiān)決用 merge在還沒推送過的個(gè)人分支上可以用 rebase。merge 的優(yōu)點(diǎn)是不改寫歷史保留所有真實(shí)發(fā)生的分叉和合并適合多人協(xié)作的主分支。缺點(diǎn)是會(huì)產(chǎn)生大量合并提交歷史圖看起來很亂。rebase 的優(yōu)點(diǎn)是把我的提交“重放”到目標(biāo)分支頂端歷史是線性的、干凈的缺點(diǎn)是改寫提交對象如果這些提交已經(jīng)被別人使用就可能造成他們的歷史錯(cuò)亂。具體到遠(yuǎn)程協(xié)作我最常用的組合是個(gè)人功能分支上git pull --rebase origin main保持線性合并到主分支時(shí)用 merge生成一個(gè)明確的合并點(diǎn)。不要在已經(jīng)推送到遠(yuǎn)程的分支上隨意 rebase這是底線。5.4 解決沖突的實(shí)操步驟與心態(tài)真到?jīng)_突發(fā)生時(shí)不要慌。Git 會(huì)把你標(biāo)記為沖突的文件同時(shí)保留雙方內(nèi)容并在文件里用 、、 標(biāo)出沖突區(qū)域。你需要做的是打開文件、逐段決定保留誰、刪除標(biāo)記、git add 該文件然后繼續(xù)合并或完成 rebase。merge 沖突和 rebase 沖突的解決流程稍有不同。merge 中常用git add 沖突已解決的文件 git commitrebase 中每一步?jīng)_突解決后需要git add 沖突已解決的文件 git rebase --continue如果中途想放棄可以分別用 git merge --abort 或 git rebase --abort 回到操作前狀態(tài)。這個(gè)命令是很多人的救命稻草我建議每個(gè)拿到 Git 的人都先記住它出錯(cuò)時(shí)不是趕忙改文件而是先想清楚能不能回到安全狀態(tài)。5.5 何時(shí)才能用強(qiáng)推git push --force 是遠(yuǎn)程操作中風(fēng)險(xiǎn)最高的命令。它的本質(zhì)是讓遠(yuǎn)程分支指針直接指向你指定的提交無視遠(yuǎn)程當(dāng)前的歷史。一旦強(qiáng)推別人的提交可能被徹底抹掉。強(qiáng)推的唯一合理場景是你明確知道遠(yuǎn)程分支上那些多余的提交已經(jīng)沒有任何價(jià)值而且團(tuán)隊(duì)達(dá)成共識(shí)。比如某些自動(dòng)化流程把錯(cuò)誤提交推到臨時(shí)分支你想重置這個(gè)分支。即便如此也更推薦 --force-with-lease這個(gè)選項(xiàng)會(huì)在遠(yuǎn)端狀態(tài)和你上次看到的狀態(tài)一致時(shí)才允許強(qiáng)推能避免誤傷別人新推的提交。我在團(tuán)隊(duì)里定了一條規(guī)矩任何情況下不要把帶 -u 的強(qiáng)推用到共享主分支上。主分支的修復(fù)可以新開分支可以 revert就是不能 force push。這條規(guī)矩救過團(tuán)隊(duì)很多次。6. 遠(yuǎn)程倉庫的衛(wèi)生與安全tags、大文件、幽靈引用6.1 tag 不是普通分支推送方式也不同標(biāo)簽在遠(yuǎn)程協(xié)作里經(jīng)常被忽略但它承擔(dān)著版本發(fā)布的重要作用。tag 是某個(gè)提交的固定別名它不像分支那樣會(huì)移動(dòng)。本地打的標(biāo)簽不會(huì)自動(dòng)推送到遠(yuǎn)程需要顯式操作git push origin v1.0.0批量推送所有標(biāo)簽用 git push origin --tags。刪除遠(yuǎn)程標(biāo)簽git push origin --delete v1.0.0這里容易踩坑的是如果本地標(biāo)簽名和遠(yuǎn)程已有標(biāo)簽重名但指向的提交不同push 會(huì)被拒絕。發(fā)布流程里建議先拉取遠(yuǎn)程標(biāo)簽最新狀態(tài)再打新標(biāo)簽。打錯(cuò)標(biāo)簽后也要及時(shí)同步到遠(yuǎn)程否則后續(xù)部署系統(tǒng)會(huì)讀取到錯(cuò)誤版本。6.2 遠(yuǎn)程分支刪除后本地還能看到“幽靈分支”前文提過 git fetch --prune。當(dāng)你或同事刪除了遠(yuǎn)程分支后如果你只在本地執(zhí)行 git fetchGit 并不會(huì)自動(dòng)清理本地緩存的遠(yuǎn)程跟蹤分支。于是你執(zhí)行 git branch -r 時(shí)還能看到已經(jīng)不存在于遠(yuǎn)程的 origin/feature/old。清理方法是git fetch --prune origin這條命令會(huì)刪除本地對應(yīng)已消失的遠(yuǎn)程跟蹤分支引用并打印出刪除了哪些分支。我建議把它加入日常習(xí)慣尤其是團(tuán)隊(duì)分支生命周期很短、合并后立即刪除的場景。否則半個(gè)月后你本地的遠(yuǎn)程分支列表可能比遠(yuǎn)程倉庫還厚。6.3 大文件是怎么把遠(yuǎn)程倉庫堵死的Git 的設(shè)計(jì)初衷是文本代碼而不是二進(jìn)制大文件。當(dāng)有人不小心把幾百 MB 的視頻、壓縮包提交進(jìn)倉庫所有同事 clone、fetch 時(shí)都要把這份歷史下載下來倉庫體積很快膨脹到幾個(gè) GB。這種問題的麻煩在于即使后來刪除了大文件它仍然存在于歷史提交里每次 clone 還是要下載。解決思路是git filter-repo這類歷史重寫工具但歷史重寫意味著所有協(xié)作者的遠(yuǎn)程倉庫都要重新同步屬于大型手術(shù)。最有效的辦法是預(yù)防加入 .gitignore安裝大文件檢查工具在代碼評審時(shí)留意二進(jìn)制文件已經(jīng)需要存儲(chǔ)大文件的場景改用大文件存儲(chǔ)擴(kuò)展而不是直接進(jìn) Git 倉庫。遠(yuǎn)程倉庫操作不能只考慮“能不能提交”還要考慮“提交之后別人承受什么成本”。6.4 權(quán)限與密鑰最容易被忽視的協(xié)作風(fēng)險(xiǎn)遠(yuǎn)程操作的安全問題繞不開 SSH 密鑰和部署密鑰。很多團(tuán)隊(duì)習(xí)慣把所有開發(fā)者都加到同一個(gè)倉庫管理員組省事但風(fēng)險(xiǎn)極大。合理做法是給不同角色分配不同權(quán)限等級(jí)能直接 push 主分支的人越少越好普通開發(fā)者通過合并請求進(jìn)入代碼。另一個(gè)隱蔽風(fēng)險(xiǎn)是個(gè)人機(jī)器上配置了錯(cuò)誤的 remote 地址導(dǎo)致代碼被推到錯(cuò)誤的倉庫。我見過因?yàn)?URL 復(fù)制錯(cuò)把內(nèi)部項(xiàng)目推到一個(gè)公共倉庫的案例雖然及時(shí)刪除但已經(jīng)造成了不可控傳播。所以 git remote -v 不只是新手要看的命令老手同樣要在 push 敏感項(xiàng)目前會(huì)看一眼。7. 三個(gè)真實(shí)翻車現(xiàn)場和一張保命檢查清單7.1 案例手滑把改動(dòng)推到了錯(cuò)誤分支某次我從本地 feature/a 切到 main忘了切回來就把實(shí)驗(yàn)性改動(dòng) commit 了然后又順手 git push。結(jié)果實(shí)驗(yàn)代碼直接推到了遠(yuǎn)程 main?,F(xiàn)場同事跑過來指著倉庫大喊“main 上怎么多了一個(gè)亂七八糟的提交”。那次修復(fù)用 git revert 生成一個(gè)反向提交把 main 恢復(fù)到正常狀態(tài)。這里我選擇 revert 而不是 reset force push是因?yàn)?main 是共享分支reset 會(huì)改寫歷史影響所有同事revert 是向前增加一個(gè)提交遠(yuǎn)程歷史不會(huì)被重寫是最安全的修復(fù)路徑。這類事故給我的教訓(xùn)是push 真正屬于共享分支前必須確認(rèn)當(dāng)前分支名。7.2 案例同事的分支被“清理”了有次團(tuán)隊(duì)做一次大范圍分支清理用了git push origin --delete刪除了一批看起來已經(jīng)合并的分支。結(jié)果后來發(fā)現(xiàn)其中一條分支其實(shí)只合并了一部分另一位同事還在那條分支上繼續(xù)開發(fā)。因?yàn)檫h(yuǎn)程分支被刪他本地 push 時(shí)關(guān)聯(lián)失敗現(xiàn)場一片混亂。最后只能從同事本地找到那條分支的完整歷史重新推回遠(yuǎn)程。這件事讓我意識(shí)到刪除遠(yuǎn)程分支前要核對合并狀態(tài)不能只看列表。后來我們改用腳本列出所有已合并分支人工二次確認(rèn)后才刪除。7.3 案例clone 速度慢到懷疑人生團(tuán)隊(duì)加入一位新同學(xué)第一次 clone 項(xiàng)目花了快一個(gè)小時(shí)。排查時(shí)發(fā)現(xiàn)倉庫里面有幾份幾百 MB 的設(shè)計(jì)稿壓縮包這些文件躺在歷史提交里即使后來刪了每次 clone 都要重新下載整份 Git 歷史。我們用歷史重寫工具徹底清理了那些大文件并讓所有同事重新同步遠(yuǎn)程倉庫clone 時(shí)間才恢復(fù)正常。這個(gè)案例給我的經(jīng)驗(yàn)是遠(yuǎn)程倉庫操作不只是你自己機(jī)器上的命令它關(guān)乎團(tuán)隊(duì)每個(gè)成員的身心健康。提交大文件之前多想幾秒比事后清理容易一萬倍。7.4 我每次 push 前會(huì)過的檢查項(xiàng)最后分享一張我自己的保命檢查清單。不是每次 push 都逐條執(zhí)行但凡是推送到共享主分支、發(fā)布分支或者推送涉及敏感項(xiàng)目時(shí)我一定會(huì)快速過一遍git status看清楚當(dāng)前分支名和未提交改動(dòng)。git branch --show-current確認(rèn)自己在哪個(gè)分支。git remote -v確認(rèn)遠(yuǎn)程地址沒有串。git log --oneline -3確認(rèn)要推的東西確實(shí)是我想推的。git pull --rebase先把遠(yuǎn)程新提交整合進(jìn)本地再 push。如果是合并請求場景確認(rèn)目標(biāo)分支沒有選錯(cuò)。這套清單不復(fù)雜但能過濾掉九成以上的低級(jí)事故。協(xié)作這件事慢幾秒是為了后面不浪費(fèi)幾個(gè)小時(shí)。我最后再補(bǔ)充一句個(gè)人的體會(huì)遠(yuǎn)程倉庫操作練到后來拼的并不是命令記得多熟而是你對自己正在改哪份歷史、打算讓它進(jìn)入哪條主線有沒有清晰的判斷。每次操作前多想一步比任何花哨技巧都管用。