
1. 先搞清楚本地分支和远程分支到底是什么关系1.1 三个概念本地分支、远程跟踪分支、真正的远程分支很多人第一次推送失败不是命令敲错了而是脑子里对分支的模型是糊的。我刚开始用 git 的时候也栽过这个跟头以为git branch列出来的和远程仓库里的是一回事结果一推送就报错。所以先把这个模型理清楚后面所有命令都是水到渠成。你在本地仓库里其实同时存在着三类东西。第一类是本地分支比如main、dev、feature/login它们指向你自己的提交历史是你真正在改动的那条线。第二类是远程跟踪分支形如origin/main、origin/dev它本质上是你本地缓存的一份远程仓库上次同步时的快照注意关键词——快照它不是实时的。第三类才是最远端的、真正存在于服务器上的那个分支你在本地是看不到它的实时状态的只能通过网络请求去同步。理解这个分层特别重要。git push干的事情说穿了就是拿你的本地分支去更新远端的那个分支同时顺手把本地的origin/xxx这个快照也刷一遍。而git fetch干的事情相反它是把远端的最新状况拉下来更新origin/xxx快照但不动你的本地分支。这两个命令的方向完全不同很多人混着用导致线上和本地对不上号还找不到原因。记住一句话origin/main是你印象里远程的样子不是远程此刻真实的样子。远程真实的样子只有连上网那一刻才知道。1.2 为什么很多人第一次推送会失败最常见的第一次推送失败场景是这样的你在本地git checkout -b feature/pay建了一个新分支写了几次提交然后顺手敲了个git push控制台直接甩给你一段红字fatal: The current branch feature/pay has no upstream branch.这句话翻译过来就是——你这个本地分支还没有和任何远程分支建立配对关系git 不知道该往哪儿推。这个设计其实是刻意的。git 不会擅自帮你猜目标分支因为万一猜错了你可能把新分支的代码推到别人的分支上去那就尴尬了。所以它要求你第一次必须显式指定推送到哪里并且建议你顺手把这个配对关系记下来以后就能偷懒直接git push。另一种失败是权限问题。本地仓库是用 HTTPS 克隆的推送时要输账号密码或者 token很多人用的是旧密码而现在各大平台早就改成需要 access token 了于是报Authentication failed。这类问题不是分支的问题是认证的问题后面排查章节会专门讲怎么区分。还有一种纯属路径问题你终端所在的目录根本不是 git 仓库敲什么命令都白搭报fatal: not a git repository。这种时候先pwd看当前目录再确认.git文件夹是不是存在别急着怀疑人生。2. 第一次推送本地分支到远程完整命令拆解2.1 最基础的推送命令与 -u 参数的真实作用第一次把新分支推上去标准写法是这样git push -u origin feature/pay拆开看每一段是什么意思。git push是动作origin是远程仓库的名字默认克隆下来的仓库都叫origin你可以用git remote -v确认它指向哪个地址feature/pay是你本地要推的分支名-u是--set-upstream的简写。这个-u是精髓所在也是新手最容易忽略的一个参数。加了它之后git 会在本地记下一笔配置feature/pay这个本地分支对应远端的origin/feature/pay。有了这笔记录你之后在这个分支上只需要敲git push它就自动知道往哪推同理git pull也知道该拉哪个。不加-u的话每次推送你都得把origin feature/pay再打一遍纯属自找麻烦。实操心得只要是新建分支的第一次推送无脑加-u。这个习惯能帮你省掉后面无数次重复输入而且团队里别人克隆你的分支时追踪关系也清晰。如果你本地分支名和想推的远程分支名不一样可以用冒号语法格式是本地分支:远程分支git push -u origin local-name:remote-name这个写法在实际工作里挺有用。比如你本地分支叫tmp-fix但提交上去想叫规范点的hotfix/order-null就用这种方式映射过去。注意冒号前后别加空格加了就报错。2.2 推送前的自检清单我踩过的坑多了以后养成了一个推送前先看一眼的习惯能挡掉八成的低级错误。下面这几条建议你抄走检查项命令你要确认什么当前在哪个分支git branch --show-current别推错了分支工作区有没有没提交的改动git status有改动说明这份代码还没进版本库推了也白推本地提交和远端差了几笔git log --oneline origin/main..HEAD看清这次到底要推哪些提交上去远程地址对不对git remote -v别推到了别人的仓库或者测试仓库有没有推送权限git ls-remote origin能列出说明认证通了重点说说第三行那个命令。origin/main..HEAD这个双点语法意思是在 HEAD 里但不在 origin/main 里的提交也就是你这次推送真正会传上去的提交列表。如果你本以为只推一笔结果列出来五笔那说明你之前有些提交没注意这时候停下来看一眼是有必要的别稀里糊涂就推了。还有一种情况是误提交了不该提交的文件比如本地配置、密钥、测试数据。git status和git log -p结合起来看能在推送前抓住这些。一旦推上去清理成本会高很多后面第 5 章会讲怎么补救。3. 日常推送场景从单人开发到多人协作3.1 已建立追踪关系的分支怎么推第一次用了-u之后后续推送就简化成一句话git pushgit 会读取当前分支的追踪配置自动找到对应的远程分支。你可以用git branch -vv查看每个本地分支追踪的是谁输出里带[origin/xxx]的就是已经建立关系的。但这里有个容易被忽略的概念叫push.default也就是默认推送策略。它在 git 2.0 之后默认是simple规则是只推送当前分支并且要求本地分支名和远程分支名一致。这个默认值其实挺安全的它避免了那种你以为只推一个分支结果把本地一堆实验性分支全推上去的事故。想改成别的策略也有比如current表示不管名字一致不一致都推当前分支到同名远程分支上git config --global push.default current我个人是建议保持simple的除非你明确知道自己在做什么。因为一旦改成matching旧版本的默认值git push会把所有本地有、远程也存在的同名分支全部推一遍多人协作时这简直是灾难很容易把别人没准备推的分支内容带上去。补充一个实用命令git push origin HEAD。它的意思是把当前分支推到远程同名分支不用打分支名改分支名后也不用改命令在某些脚本里特别好用。3.2 删掉的分支、重命名的分支怎么处理本地分支删了远程分支还在这是很常见的遗留问题。删除远程分支的命令是git push origin --delete feature/pay老一点的写法是git push origin :feature/pay那个冒号前面留空表示推一个空的分支过去效果等同删除。两种写法都对但我更推荐--delete因为它语义明确别人一看就懂而且不会因为手滑少打冒号变成推动作。分支重命名的处理稍微绕一点。git 没有直接重命名远程分支的命令正确步骤是本地改好名字推新名字上去再删掉远程的旧名字。具体来说git branch -m old-name new-name git push origin new-name git push origin --delete old-name需要提醒的是如果团队里已经有人基于旧分支开了工作分支删掉远程旧分支会让他们后续 pull 出现问题。所以重命名这种事最好提前在群里说一声或者干脆等当前迭代结束再处理别在大家正忙的时候突然改。还有个小坑如果你删了本地分支但git branch -a里还能看到origin/xxx那是因为你的本地快照还没刷新。跑一下git fetch --prune它会把远程已经不存在的分支对应的本地快照清理掉。这个命令我建议设成自动的加一条配置git config --global fetch.prune true配好之后每次 fetch 或 pull 都会自动清理失效快照省得你看着一堆幽灵分支发愁。4. 推送报错排查实录4.1 常见错误速查表推送报错千奇百怪但九成的错误都能归到下面这几类里。我把它们整理成一张表遇到问题先对号入座报错信息关键词根本原因解决方向no upstream branch新分支没有追踪关系加-u origin 分支名推送rejected ... non-fast-forward远程有你本地没有的提交先 pull --rebase 再推Authentication failed账号密码或 token 不对更新凭证或换成密钥Permission denied (publickey)SSH 密钥没配好检查密钥是否加到平台账号not a git repository当前目录不是仓库进入项目目录或初始化仓库remote: Repository not found地址错或没权限git remote -v核对地址failed to push some refs泛化的失败提示看上文结合上面具体原因判断其中failed to push some refs这个是笼统说法真正的信息在它上一行或下一行别只盯着这一句干着急。认证问题再展开说两句。现在主流平台都要求用访问令牌而不是账号密码如果你本地缓存了旧密码会出现一直认证失败但也不提示你重新输入的情况。这时候清一下凭证缓存Windows 上在凭据管理器里找 git 相关条目删掉macOS 上可以git credential-osxkeychain erase。清完之后再推一次它会重新问你要账号密码。4.2 非快进拒绝的处理non-fast-forward是推送里最经典的一个错误值得单独讲。它的含义是远程分支上有一些提交是你本地没有的直接推会把那些提交覆盖掉git 出于安全拒绝了。处理方式二选一。第一种是合并式git pull origin main git push origin main这会生成一个合并提交。第二种是变基式我个人更推荐git pull --rebase origin main git push origin main--rebase会把你本地的提交摘下来先同步远程的最新提交再把你的提交一个个接在后面。好处是提交历史是一条直线没有那些Merge branch xxx into xxx的杂乱节点看起来清爽。坏处是如果你有多个提交解决冲突的过程可能更磨人每个提交都可能要处理。实操心得多人高频协作的分支上进仓库第一件事就是把git config --global pull.rebase true配上让 pull 默认走变基。这样能极大减少历史里的合并噪音。当然代价是遇到冲突时要认真处理每一笔。还有个更隐蔽的情况你自己一个人开发怎么也 non-fast-forward。这通常是因为你在网页上直接编辑过文件并提交了或者用另一个客户端推过。处理方式一样先拉再推但拉之前最好git fetch一下看看远程到底多了什么用git log --oneline HEAD..origin/main查看远端比你多的提交心里有数再动手。5. 推送之外几个容易被忽略的细节5.1 强制推送的正确姿势与风险先说结论能不用--force就别用。因为它会无条件用你本地的历史覆盖远程远程上别人推的提交会被直接抹掉而且很难恢复。团队协作的分支上一旦强推等于给所有人挖坑。但有些场景确实需要重写历史比如你刚推上去发现提交信息写错了、提交里带了不该带的文件。这种时候推荐用带安全检查的强推git push --force-with-lease origin feature/pay它和--force的区别在于它会先检查远程分支是不是还停在你上次看到的位置。如果在你准备强推的这段时间里别人正好推了新东西上去它就会拒绝防止你误删别人的提交。相当于给你加了一道保险。如果情况再极端一点你想让远程完全等于本地不管别人有没有推那就得用--force但那基本只发生在你自己的私有分支上。公共分支上强推属于团队协作里最需要谨慎对待的操作之一。还有个折中做法不要强推而是用git revert生成一笔反向提交来撤销错误的提交。虽然历史里会多一笔记录但安全、可追溯适合已经共享出去的分支。5.2 大文件、敏感信息推送后怎么补救最后聊个很多人会忽略的坑你已经把东西推上去了才发现里面有敏感内容或者超大文件。这时候光删文件再推是没用的因为历史记录里还留着。真正要清理得重写历史工具上可以用git filter-repo比老的filter-branch快很多也更好用。基本思路是用工具把目标文件从所有历史提交里剔除然后强推覆盖远程。这个过程会改变所有涉及到的提交的哈希值所以必须通知所有协作者重新克隆或者做特殊同步不然他们会把旧历史又推回去。这也是为什么我一直强调推送前自检——补救的成本是预防的几十倍。超大文件的处理类似除了重写历史更根本的是配置好忽略规则。把构建产物、依赖目录、本地配置都写进.gitignore从源头挡住# 常见的忽略项示例 node_modules/ dist/ build/ *.log .env .idea/ .vscode/另外提一嘴如果团队仓库确实需要管理较大文件可以考虑 git 的 LFS 方案它把大文件内容存在单独的地方仓库里只留指针clone 和推送都会轻快很多。不过这需要服务端也支持上之前先确认平台是否开启了这个能力。我个人在这块吃过最大的亏就是有次把一个几十兆的数据文件顺手提交推了上去后来清理历史折腾了大半天还惊动了同事重新拉代码。从那以后git status在git add之前看一遍已经成了肌肉记忆。