本节摘要:
git push把本地提交上传到远程,是"让队友看到你的工作"的唯一通道。本节讲清 push 的五个内部步骤与快进式/非快进式推送的区别,重点处理最经典的报错——non-fast-forward:为什么会发生、怎么用 pull 解决。最后覆盖 -u 设置跟踪、--tags、--delete 删除远程分支,以及 --force-with-lease 的安全强推。
阅读完本节,你应当能够:
你在本地辛辛苦苦提交了五条,觉得"这些改动应该让团队知道了"。但现实是:只要不 push,远程仓库就对你的工作一无所知。提交只存在你的本地仓库里,像一篇只写在自己日记本上的文章——队友看不到,CI 不触发,发布不包含。
git push 就是"把日记本上的文章投稿给编辑部的过程"。它把本地分支的提交上传到远程分支,让远程仓库跟上你的进度。它是 Git 协作里"输出"的半边天——pull 负责"输入",push 负责"输出",两者构成远程协作的循环。
但 push 不是无脑上传。远程仓库有自己的历史,你的本地历史要"接"上去,必须满足一个条件:你的历史必须包含远程当前的最新提交(或者远程没有你缺的东西)。这个条件不满足时,Git 会拒绝推送——它宁可报错,也不愿让任何人的工作悄悄消失。理解这个"拒绝机制",就是理解 push 的关键。
这个"拒绝机制"初看像阻碍,细想是保护。设想一个没有它的世界:你和同事都在 main 上工作,你推完,他直接推,他的本地没有你的提交,一推就把你的改动覆盖了——无声无息,连报错都没有。Git 的 non-fast-forward 拒绝正是为了防止这种"静默覆盖"。它逼你"先同步、再推送",让所有改动都经过合并或整合,而不是粗暴替换。协作工具的安全,往往就体现在这些"烦人"的拒绝里——它们是对每个人的工作成果的尊重。
执行 git push origin main 时,Git 依次:
第 5 步的"快进式"是关键——它决定 push 是成功还是被拒。
push 成功的前提:你的本地历史是远程历史的"延伸"(包含远程的最新提交)。否则被拒,需要先同步。
当远程分支当前指向的提交是你本地历史的祖先时,push 是快进式——远程只需把指针向前移到你的最新提交,没有冲突,直接成功。这是最理想的推送,也是默认预期。
如果远程分支有你的本地没有的提交(比如同事在你上次 pull 之后又推了新东西),你的本地历史不再是远程历史的延伸,push 被拒。报错信息很典型:
! [rejected] main -> main (non-fast-forward) error: failed to push some refs to ... hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git 已经告诉你该怎么办:先整合远程变化再推。标准流程:
git pull origin main # 拉取远程改动并合并 # 如有冲突,解决后提交 git push origin main # 再推,此时通常是快进式
这个"被拒→先拉→再推"的循环,是远程协作里最常见也最重要的一课。它背后的原则是:绝不覆盖别人的提交。Git 用拒绝机制强制你尊重他人的工作,这正是协作安全的核心。
用两个目录模拟两个人协作,亲手把被拒的循环跑一遍:
# 目录一:模拟同事,先推送 git clone 远程地址 repoA && cd repoA echo a > a.txt && git add . && git commit -m "A 的改动" git push origin main # 目录二:模拟你,基于旧状态开发 git clone 远程地址 repoB && cd repoB echo b > b.txt && git add . && git commit -m "B 的改动" git push origin main # 被拒!remote 已有 A 的提交 # 解决:先拉再推 git pull origin main # 拉取 A 的改动并合并 git push origin main # 成功
这个循环完整再现了"我明明 push 了怎么被拒"的困惑及其解法。亲手跑一遍,你以后再遇到 non-fast-forward 会条件反射地"先 pull"。值得注意 pull 后可能发生合并冲突(3.4 节的内容),那也顺便复习了。
推送一条远程还没有的分支时,用 -u(--set-upstream)同时设置跟踪关系:
git push -u origin feature/login
设置后,本地 feature/login 记住"我的上游是 origin/feature/login",以后 git push、git pull 都可以省略参数。没有 -u 的话,Git 会提示你"没有上游分支",需要每次写全参数。
推送标签(默认 push 不推标签):
git push origin --tags
删除远程分支:
git push origin --delete feature/old-branch # 等价旧写法 git push origin :feature/old-branch
删除远程分支后,本地对应的远程跟踪分支也要清理(4.6 节 --prune)。
普通 push 被拒时,git push --force 可以强行覆盖远程历史。但它极其危险——它会无条件地把远程分支变成你的本地状态,可能抹掉别人推送的提交。
更安全的替代是 --force-with-lease:
git push --force-with-lease origin main
它只在"远程分支的状态与你上次拉取时一致"时才强推。如果你拉取之后远程被别人推了新东西,它会拒绝,从而保护他人的工作。凡是需要强推的场景,默认用 --force-with-lease,裸 --force 留给"你完全确定远程没别人动过"的极少数情况。
git status:确认当前分支和要推送的内容。git log --oneline:确认要推的是预期的提交。git pull(若不确定):先同步远程,避免推送被拒。| 场景 | 正确做法 |
|---|---|
| 推错分支 | 用 push 远程名 正确分支,或删除远程错误分支 |
| 推到不存在的远程 | 先 remote add 或检查 remote -v |
| 被拒 non-fast-forward | pull 合并再推,别强推 |
| 需要撤回已推送提交 | 用 revert 出反向提交再推(第 5 章) |
push 报错五花八门,但按信息归个类,绝大多数都能对号入座:
| 报错关键词 | 含义 | 处理 |
|---|---|---|
| non-fast-forward | 本地落后远程 | pull 合并再推,或 --force-with-lease(慎用) |
| no upstream branch | 本地分支没设跟踪 | git push -u origin 分支名 |
| Authentication failed | 凭据错误 | 更新令牌/密码,检查是否过期 |
| Permission denied | 无写权限 | 确认是否有推送权限,或改用 PR |
| Repository not found | 地址错误或无访问权 | 检查远程地址与账号可见性 |
| cannot lock ref | 引用锁冲突 | 等一会重试,或检查是否有并发 push |
记住:报错的第一行是"发生了什么",hint 行是"该怎么办"。Git 的报错信息是排错手册而不是天书,按提示逐条走,绝大多数 push 问题都能自己解决。
推送到共享远程前,花十秒过一遍清单,能挡掉大部分"推了又后悔"的事故:
git status:确认当前分支、确认没有把不该推的文件(密钥、日志)留在暂存区。git log --oneline -n 3:确认要推的提交确实是要推的那些,消息别写错。git pull(或先 fetch 对比):确认本地已包含远程最新提交,避免被 non-fast-forward 拒。这份清单的每一条都指向一个真实事故:密钥误推、提交内容错、推错分支、强推覆盖同事提交。push 是"出门",出门前检查一遍总比在路上返工便宜。
⚠️ 常见坑:
git push --force覆盖了同事的提交,事后才发现。强推永远是最后手段,优先 --force-with-lease,且与团队沟通后再做。
💡 关键直觉:把 push 想成"往共享工作台放文件"。快进式是"台子上没别人东西,直接放上去";非快进式是"台子上已经有别人的文件,你硬放会把它们盖住"——Git 拦住你,让你先看看别人的文件(pull),再决定怎么摆。
"为什么我提交了但远程没变化?" 因为 commit 只写本地,必须 push 才上传。三步链:改 → commit → push,缺了 push,远程永远不知道你的工作。
"push 被拒是不是我做错了什么?" 不是。被拒是远程保护机制在起作用——防止覆盖别人的提交。按提示 pull 合并再推,是标准且安全的行为。
"--tags 和单独推标签有什么区别?" --tags 一次性推所有本地标签;git push origin 标签名 只推指定标签。想精确控制用后者,图省事用前者。
"push 很慢怎么办?" 大仓库推送慢,可能是有大二进制文件或历史积压。检查是否有大文件误入仓库(git 社区用 LFS 管理大文件),或考虑压缩对象。
"push 能推多个分支吗?" 能,git push origin 分支A 分支B 一次推多个;git push --all origin 推所有本地分支(慎用,协作环境一般不推荐,容易把不想公开的分支也推上去)。
"push 之前一定要 pull 吗?" 不一定。如果你的本地分支领先远程且远程没有新提交,直接 push 就是快进式,无需 pull。只有当你怀疑远程有别人推的新提交时才需要先 pull——这也是"push 前先同步"这个习惯的由来。
"为什么我 pull 之后 push 还是被拒?" 可能 pull 产生了合并,但之后又有别人推送;或者你 pull 的是别的分支。排查:git status 看 ahead/behind 状态,git log origin/main 看远程最新,确认本地确实包含了远程的最新提交再推。
理解了 push,还要知道它和平台协作功能(Pull Request / Merge Request)的分工,否则会困惑"我明明推了,为什么代码没进 main"。
在大多数团队的实践中:
git push origin feature/login 把分支推到远程。关键认知:push 只负责"把分支送到远程",不负责"合并进 main"。合并动作由 PR 流程(或直接 merge)完成。所以"推了却没进 main"不是 bug,而是流程设计——把"共享代码"和"合入主线"两个动作分开,让审查有机会介入。这是团队工程实践里 push 最常见的真实用法。
下一节逆向操作:git pull 与 git fetch——把远程的更新拿到本地来。