前面两篇讲的都是本地的事,但代码最终是要和别人交换的,这就轮到远程仓库了。刚开始用的时候最容易被三个长得很像的名字绕晕:main、origin、origin/main,所以这一篇先把这三个东西掰开。
三个名字要分清
| 名字 | 是什么 | 在哪 |
|---|---|---|
origin | 远程仓库的别名 | 只是本地的一个名字,指向一个 URL |
main | 本地分支,我们自己的 | 本地,.git/refs/heads/main |
origin/main | 远程跟踪分支,我们对远程状态的缓存 | 本地,.git/refs/remotes/origin/main |
origin是git clone的时候自动起的默认名字,想改或者想加别的远程都可以:
gitremote-v# 看现在有哪些远程gitremoteaddupstream https://...# 加一个叫 upstream 的远程gitremoterenameorigin old-origin# 改名origin/main 不会自己更新
这是这一篇里最需要理解的一点:origin/main是一个本地文件,它记录的是"上次和远程通信时,远程的 main 长什么样"。
它不会自动跟着远程变。别人往远程推了新提交,我们本地的origin/main还是老样子,直到我们跑一次网络命令(fetch或pull)去更新它。这就引出了fetch的重要性:它是唯一一个专门用来同步这个本地缓存的命令。
上游分支
本地分支可以和某个远程分支建立一条"跟踪关系",被跟踪的那个就叫上游分支(upstream)。
gitpush-uorigin main# -u 是 --set-upstream 的简写设好之后有两个好处:直接敲git push或者git pull就够了,不用每次都写origin main;而且git status和git branch -vv会告诉我们"领先几个、落后几个":
gitbranch-vv* main ae04634 [origin/main] alice 的提交方括号里的[origin/main]就是上游分支。如果本地和远程有差距,这里还会直接显示出来:
* main ae04634 [origin/main: behind 1] alice 的提交behind 1是说本地落后远程 1 个提交,ahead 2是说本地领先 2 个。每次git status的时候先扫一眼这行,能省掉很多"为什么推不上去"的困惑。
如果是已经存在的分支想补一个上游:
gitbranch-uorigin/main# 给当前分支设gitbranch-uorigin/main mybranchfetch 和 pull
这两个是最容易混的一对,其实关系很简单:
git pull = git fetch + git mergefetch:只下载,不动我们的代码
gitfetch origin它做两件事:从远程下载本地没有的对象(提交、文件内容),然后更新origin/main这个远程跟踪分支的指向。它不会碰我们的工作区,也不会碰我们的main分支。
具体看一遍就清楚了。假设同事 Bob 往远程推了一个提交,我们本地还不知道:
gitlog--onelineorigin/mainae04634 alice 的提交还是旧的。跑一次 fetch:
gitfetch origingitlog--onelineorigin/mainbb2f7d5 bob 的提交 ae04634 alice 的提交origin/main更新了。再看我们的本地分支:
gitbranch-vv* main ae04634 [origin/main: behind 1] alice 的提交main纹丝没动,还是ae04634,只是现在 Git 明确告诉我们"你落后远程 1 个提交"。
这就是 fetch 的价值所在:下载和合并是两个独立的动作,fetch 只做前一半。所以可以用它来安全地看看远程有什么变化,看完再决定怎么办:
gitfetch origingitlog--onelinemain..origin/main# 远程比我多了哪些提交gitdiffmain origin/main# 具体差了什么pull:下载并合并
gitpull origin main等价于先fetch再merge origin/main。图省事,但它一步就合并进来了,如果这时候本地有一堆没提交的改动,或者有冲突,会被打断在合并的中间状态里。所以更稳的做法还是分开:
gitfetch origingitlog--onelinemain..origin/main# 先看看来的是啥gitmerge origin/main# 再决定合不合pull 用 merge 还是 rebase
git pull默认用 merge,会在历史里留下一个合并提交。如果本地只是有几个小提交、不希望历史变乱,可以改成 rebase:
gitpull--rebaseorigin main想一次性设置好:
gitconfig--globalpull.rebasetrue# 全局默认用 rebasegitconfig--globalpull.rebase merges# 保留已有的合并提交,其余 rebasegitconfig--globalrebase.autoStashtrue# rebase 前自动 stash 未提交的改动| 方式 | 结果 |
|---|---|
git pull(默认 merge) | 产生一个合并提交,历史有分叉 |
git pull --rebase | 把本地提交搬到远程最新之后,历史是直线 |
选择的依据和 上一篇里 merge 与 rebase 的原则是一样的:如果这些本地提交还没推送给别人,用 rebase 把历史捋直是安全的;如果已经推送上去了,那就老实用 merge。
push
gitpush origin main# 推到远程的 maingitpush# 有上游分支时可以省略参数gitpush-uorigin main# 第一次推,顺便设好上游gitpush origin HEAD# 推到远程的同名分支push的本意是把本地的提交送到远程,并让远程分支指向新位置。但它有一个很硬的限制:只接受快进。
推送被拒绝
最常见的报错长这样:
gitpush origin mainTo /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 拒绝这个操作,是在保护别人的工作。
处理办法就按提示来:
gitfetch origingitlog--onelinemain..origin/main# 看看别人提交了什么gitrebase origin/main# 或者 git merge origin/maingitpush先把自己的工作接在别人的提交后面,然后就能正常推了。
只有确认远程那些提交真的该被丢弃(比如自己误推了一个不该推的东西),才能考虑强推。而且强推要用
--force-with-lease而不是--force,这两个的区别在下一篇会阐述
push 的完整语法
gitpush<远程名><本地分支>:<远程分支>冒号两边的形式可以有以下几种形式:
gitpush origin main# 省略冒号:推同名分支gitpush origin main:dev# 把本地 main 推到远程的 dev 分支gitpush origin :dev# 本地留空 = 删除远程的 dev 分支gitpush origin--deletedev# 同上,更易读的写法远程分支的删除和清理
删除远程分支
gitpush origin--deletefeature这个命令删的是远程的feature分支。注意删完之后我们本地可能还留着一个origin/feature,因为它也是本地文件。
清理已经不存在的远程跟踪分支
同事把远程分支删了,我们本地跑git branch -a还是能看到它,因为这些origin/xxx是本地缓存,不会自己消失。清理有两种方式:
gitfetch--prune# 拉取的同时,把远程已删除的分支从本地缓存里清掉gitremote prune origin# 单独做这件事想让它成为默认行为:
gitconfig--globalfetch.prunetrue这里有三个名字需要额外解释一下,分开看是:
git branch -d feature删的是本地分支git push origin --delete feature删的是远程分支git fetch --prune清理的是本地缓存的远程跟踪分支(origin/feature)三者是互相独立的,删了远程分支不会自动清掉本地的缓存,反过来也一样