6篇文章讲清楚Git:Git 远程协作:fetch、pull、push 到底有什么区别?(3/6)

前面两篇讲的都是本地的事,但代码最终是要和别人交换的,这就轮到远程仓库了。刚开始用的时候最容易被三个长得很像的名字绕晕:main、origin、origin/main,所以这一篇先把这三个东西掰开。

三个名字要分清

名字 是什么 在哪
origin 远程仓库的别名 只是本地的一个名字,指向一个 URL
main 本地分支,我们自己的 本地,.git/refs/heads/main
origin/main 远程跟踪分支,我们对远程状态的缓存 本地,.git/refs/remotes/origin/main

origin 是 git clone 的时候自动起的默认名字,想改或者想加别的远程都可以:

shell 复制代码
git remote -v                              # 看现在有哪些远程
git remote add upstream https://...        # 加一个叫 upstream 的远程
git remote rename origin old-origin        # 改名

origin/main 不会自己更新

这是这一篇里最需要理解的一点:origin/main 是一个本地文件,它记录的是"上次和远程通信时,远程的 main 长什么样"。

它不会自动跟着远程变。别人往远程推了新提交,我们本地的 origin/main 还是老样子,直到我们跑一次网络命令(fetch 或 pull)去更新它。这就引出了 fetch 的重要性:它是唯一一个专门用来同步这个本地缓存的命令。

上游分支

本地分支可以和某个远程分支建立一条"跟踪关系",被跟踪的那个就叫上游分支(upstream)。

shell 复制代码
git push -u origin main        # -u 是 --set-upstream 的简写

设好之后有两个好处:直接敲 git push 或者 git pull 就够了,不用每次都写 origin main;而且 git status 和 git branch -vv 会告诉我们"领先几个、落后几个":

shell 复制代码
git branch -vv
text 复制代码
* main ae04634 [origin/main] alice 的提交

方括号里的 [origin/main] 就是上游分支。如果本地和远程有差距,这里还会直接显示出来:

text 复制代码
* main ae04634 [origin/main: behind 1] alice 的提交

behind 1 是说本地落后远程 1 个提交,ahead 2 是说本地领先 2 个。每次 git status 的时候先扫一眼这行,能省掉很多"为什么推不上去"的困惑。

如果是已经存在的分支想补一个上游:

shell 复制代码
git branch -u origin/main        # 给当前分支设
git branch -u origin/main mybranch

fetch 和 pull

这两个是最容易混的一对,其实关系很简单:

text 复制代码
git pull = git fetch + git merge

fetch:只下载,不动我们的代码

shell 复制代码
git fetch origin

它做两件事:从远程下载本地没有的对象(提交、文件内容),然后更新 origin/main 这个远程跟踪分支的指向。它不会碰我们的工作区,也不会碰我们的 main 分支。

具体看一遍就清楚了。假设同事 Bob 往远程推了一个提交,我们本地还不知道:

shell 复制代码
git log --oneline origin/main
text 复制代码
ae04634 alice 的提交

还是旧的。跑一次 fetch:

shell 复制代码
git fetch origin
git log --oneline origin/main
text 复制代码
bb2f7d5 bob 的提交
ae04634 alice 的提交

origin/main 更新了。再看我们的本地分支:

shell 复制代码
git branch -vv
text 复制代码
* main ae04634 [origin/main: behind 1] alice 的提交

main 纹丝没动 ,还是 ae04634,只是现在 Git 明确告诉我们"你落后远程 1 个提交"。

这就是 fetch 的价值所在:下载和合并是两个独立的动作,fetch 只做前一半。所以可以用它来安全地看看远程有什么变化,看完再决定怎么办:

shell 复制代码
git fetch origin
git log --oneline main..origin/main      # 远程比我多了哪些提交
git diff main origin/main                # 具体差了什么

pull:下载并合并

shell 复制代码
git pull origin main

等价于先 fetch 再 merge origin/main。图省事,但它一步就合并进来了,如果这时候本地有一堆没提交的改动,或者有冲突,会被打断在合并的中间状态里。所以更稳的做法还是分开:

shell 复制代码
git fetch origin
git log --oneline main..origin/main       # 先看看来的是啥
git merge origin/main                     # 再决定合不合

pull 用 merge 还是 rebase

git pull 默认用 merge,会在历史里留下一个合并提交。如果本地只是有几个小提交、不希望历史变乱,可以改成 rebase:

shell 复制代码
git pull --rebase origin main

想一次性设置好:

shell 复制代码
git config --global pull.rebase true            # 全局默认用 rebase
git config --global pull.rebase merges          # 保留已有的合并提交,其余 rebase
git config --global rebase.autoStash true       # rebase 前自动 stash 未提交的改动
方式 结果
git pull(默认 merge) 产生一个合并提交,历史有分叉
git pull --rebase 把本地提交搬到远程最新之后,历史是直线

选择的依据和 上一篇里 merge 与 rebase 的原则是一样的:如果这些本地提交还没推送给别人,用 rebase 把历史捋直是安全的;如果已经推送上去了,那就老实用 merge。

push

shell 复制代码
git push origin main        # 推到远程的 main
git push                    # 有上游分支时可以省略参数
git push -u origin main     # 第一次推,顺便设好上游
git push origin HEAD        # 推到远程的同名分支

push 的本意是把本地的提交送到远程,并让远程分支指向新位置。但它有一个很硬的限制:只接受快进。

推送被拒绝

最常见的报错长这样:

shell 复制代码
git push origin main
text 复制代码
To /tmp/gitlab/remote.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '/tmp/gitlab/remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

关键信息是 non-fast-forward,它的意思是:在我们上次同步之后,远程已经有了新的提交,而我们本地的分支没有包含它们。

为什么不允许推?因为如果我们硬推上去,远程上别人那些提交就会被覆盖掉(在远程的视角看就是"这些提交突然没了")。Git 拒绝这个操作,是在保护别人的工作。

处理办法就按提示来:

shell 复制代码
git fetch origin
git log --oneline main..origin/main     # 看看别人提交了什么
git rebase origin/main                  # 或者 git merge origin/main
git push

先把自己的工作接在别人的提交后面,然后就能正常推了。

只有确认远程那些提交真的该被丢弃(比如自己误推了一个不该推的东西),才能考虑强推。而且强推要用 --force-with-lease 而不是 --force,这两个的区别在下一篇会阐述

push 的完整语法

shell 复制代码
git push <远程名> <本地分支>:<远程分支>

冒号两边的形式可以有以下几种形式:

shell 复制代码
git push origin main              # 省略冒号:推同名分支
git push origin main:dev          # 把本地 main 推到远程的 dev 分支
git push origin :dev              # 本地留空 = 删除远程的 dev 分支
git push origin --delete dev      # 同上,更易读的写法

远程分支的删除和清理

删除远程分支

shell 复制代码
git push origin --delete feature

这个命令删的是远程 的 feature 分支。注意删完之后我们本地可能还留着一个 origin/feature,因为它也是本地文件。

清理已经不存在的远程跟踪分支

同事把远程分支删了,我们本地跑 git branch -a 还是能看到它,因为这些 origin/xxx 是本地缓存,不会自己消失。清理有两种方式:

shell 复制代码
git fetch --prune                # 拉取的同时,把远程已删除的分支从本地缓存里清掉
git remote prune origin          # 单独做这件事

想让它成为默认行为:

shell 复制代码
git config --global fetch.prune true

这里有三个名字需要额外解释一下,分开看是:

  • git branch -d feature 删的是本地分支
  • git push origin --delete feature 删的是远程分支
  • git fetch --prune 清理的是本地缓存的远程跟踪分支 (origin/feature)

三者是互相独立的,删了远程分支不会自动清掉本地的缓存,反过来也一样

相关推荐
小小张说故事1 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python
明月_清风1 小时前
数据进入平台后怎么处理?一文搞懂 ETL
大数据·后端·数据分析
初学AI的小高1 小时前
LangGraph断点恢复与幂等执行实战
后端·架构
欧西1 小时前
AI全栈安全Agent平台(九):自动化测试用例生成引擎实战
后端
独泪了无痕1 小时前
Hutool之RandomUtil:随机数生成的终极利器
java·后端
金銀銅鐵1 小时前
[Java] 通过行为参数化传递代码
后端·函数式编程
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
明月_清风1 小时前
数据处理完放在哪里?一文搞懂数据仓库与数据湖
大数据·后端·数据分析
据说幸运很容易1 小时前
TestNG 分组接入现有框架:实现、踩坑与解法
后端·架构