)
前面两篇讲的都是本地的事但代码最终是要和别人交换的这就轮到远程仓库了。刚开始用的时候最容易被三个长得很像的名字绕晕main、origin、origin/main所以这一篇先把这三个东西掰开。三个名字要分清名字是什么在哪origin远程仓库的别名只是本地的一个名字指向一个 URLmain本地分支我们自己的本地.git/refs/heads/mainorigin/main远程跟踪分支我们对远程状态的缓存本地.git/refs/remotes/origin/mainorigin是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 的提交还是旧的。跑一次 fetchgitfetch 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 还是 rebasegit pull默认用 merge会在历史里留下一个合并提交。如果本地只是有几个小提交、不希望历史变乱可以改成 rebasegitpull--rebaseorigin main想一次性设置好gitconfig--globalpull.rebasetrue# 全局默认用 rebasegitconfig--globalpull.rebase merges# 保留已有的合并提交其余 rebasegitconfig--globalrebase.autoStashtrue# rebase 前自动 stash 未提交的改动方式结果git pull默认 merge产生一个合并提交历史有分叉git pull --rebase把本地提交搬到远程最新之后历史是直线选择的依据和 上一篇里 merge 与 rebase 的原则是一样的如果这些本地提交还没推送给别人用 rebase 把历史捋直是安全的如果已经推送上去了那就老实用 merge。pushgitpush 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三者是互相独立的删了远程分支不会自动清掉本地的缓存反过来也一样