前面两篇讲的都是本地的事,但代码最终是要和别人交换的,这就轮到远程仓库了。刚开始用的时候最容易被三个长得很像的名字绕晕: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)三者是互相独立的,删了远程分支不会自动清掉本地的缓存,反过来也一样