首页 / 资讯中心 / 文章详情

Git 第八章:fetch 只更新 origin/*,pull 还要再合并一次

Git 第八章:fetch 只更新 origin/*,pull 还要再合并一次 ★ FEATURED ARTICLE
上一章讲的都是从本地到远程的方向git push用本地的分支去更新 GitHub 上的分支。但是一个仓库往往不止一个人在推送。如果合作者在这期间往 GitHub 上推送了新的提交本地仓库并不会自动知道这时就需要反方向的操作把远程的新提交拿到本地来。这一章的git fetch和git pull就是做这件事的两条命令。这两条命令的区别在于拿到提交之后做到哪一步。fetch只把提交拿进本地仓库并且更新origin/main这样的记录而pull在 fetch 之后还要再做一次合并所以本地的分支和工作区里的文件都会跟着改变。要讲清楚这个区别先要弄明白origin/main到底是什么。一、远程跟踪分支本地对远程的一份记录和上一章一样下面用一个本地的裸仓库github/notes.git来代替 GitHub 上的仓库。这个仓库里已经有两个提交另外还有一条 draft 分支。先把它克隆下来gitclone github/notes.git notes# Cloning into notes...# done.cdnotesgitbranch# * maingitbranch-r# origin/HEAD - origin/main# origin/draft# origin/main代码 1git branch只列出本地分支-r列出远程跟踪分支-a则把两种都列出来。克隆之后本地分支只有 main 一条而git branch -r列出了三条以origin/开头的分支。上一章介绍过它们叫作远程跟踪分支remote-tracking branch每一条对应远程仓库里的一条分支记录的是那条分支在上一次通信时所处的位置。其中origin/HEAD - origin/main这一行表示远程仓库的默认分支是 main。这些分支的名字里虽然带着 origin但是它们并不保存在 GitHub 上而是保存在本地的.git里cat.git/packed-refs# # pack-refs with: peeled fully-peeled sorted# 73366ceae5a8141a103b7cd332345d8615854a56 refs/remotes/origin/draft# 8c565ad4ee538e8c0c580e5e44bfc8e6df0ce9da refs/remotes/origin/main代码 2克隆时Git 把远程跟踪分支打包写进了 packed-refs 这个文件每一行是一个提交号和一个名字。上一章推送之后origin/main是.git/refs/remotes/origin/目录下的一个单独的文件。克隆时 Git 把它们合在一个文件里保存两种保存方式记录的内容是一样的。既然远程跟踪分支只是本地的一条记录那么 GitHub 上发生变化时它并不会跟着变。只有在和远程仓库通信的时候也就是执行 fetch、pull 或者 push 的时候它才会被更新。前提是 Git 版本较新旧版本 origin/* 是散文件别以为两种存法一直共存。在 README.md 末尾加上第八章的小标题然后在 main 上提交一次就能看到本地分支和远程跟踪分支分开了gitcommit-am开始写第八章# [main 67f2ea7] 开始写第八章# 1 file changed, 4 insertions()gitstatus# On branch main# Your branch is ahead of origin/main by 1 commit.# (use git push to publish your local commits)## nothing to commit, working tree cleangitlog--oneline--graph--all# * 67f2ea7 (HEAD - main) 开始写第八章# | * 73366ce (origin/draft) 草稿fetch 和 pull# |/# * 8c565ad (origin/main, origin/HEAD) 补上第七章# * 8db4435 第一篇笔记代码 3提交只移动了 mainorigin/main 仍然停在 8c565ad。提交移动的是 HEAD 所在的本地分支 main。origin/main 记录的是远程的 main而远程仓库并没有收到这个提交所以它留在原地。git status所说的领先 1 个提交就是拿 main 和 origin/main 比较得出的。远程跟踪分支也不能直接切换过去gitswitch origin/main# fatal: a branch is expected, got remote branch origin/main代码 4退出码是 128。switch 只接受本地分支。二、在远程的另一条分支上工作本地没有 draft 分支但是 origin/draft 已经记下了远程的 draft 在哪里。如果只是想看看 draft 上的文件可以用 checkout 切换到 origin/draftgitcheckout origin/draft# Note: switching to origin/draft.## You are in detached HEAD state. You can look around, make experimental# changes and commit them, and you can discard any commits you make in this# state without impacting any branches by switching back to a branch.# 中间几行提示和第六章代码 2 相同这里省略# HEAD is now at 73366ce 草稿fetch 和 pull代码 5checkout 接受任何能找到提交的名字这时 HEAD 直接记录着 73366ce 这个提交。这就是第六章讲过的分离头指针状态。工作区里已经是 draft 上的文件了照样可以修改和提交但是新的提交不属于任何一条分支gitcommit-am在 origin/draft 上提交# [detached HEAD 1d2747b] 在 origin/draft 上提交# 1 file changed, 1 insertion()gitlog--oneline--graph--all# * 1d2747b (HEAD) 在 origin/draft 上提交# * 73366ce (origin/draft) 草稿fetch 和 pull# | * 67f2ea7 (main) 开始写第八章# |/# * 8c565ad (origin/main, origin/HEAD) 补上第七章# * 8db4435 第一篇笔记代码 6新提交 1d2747b 只有 HEAD 记着它origin/draft 没有跟着往前走。origin/draft 不会跟着提交移动原因和上一节一样它只在和远程通信时更新。所以切换回 main 的时候Git 会警告you are leaving 1 commit behind, not connected to any of your branches1d2747b 从此就没有分支能找到了。如果真的想在 draft 上工作应该切换到不带origin/的 draft。回到 main 之后执行gitswitch draft# Branch draft set up to track remote branch draft from origin.# Switched to a new branch draftgitstatus# On branch draft# Your branch is up to date with origin/draft.## nothing to commit, working tree clean代码 7本地新建了一条 draft它从 origin/draft 所在的 73366ce 开始并且把 origin/draft 设为上游。本地明明没有 draft 这条分支switch 却没有报错这是因为它会去远程跟踪分支里找同名的分支。手册里对--guess选项的说明是这样的Ifbranchis not found but there does exist a tracking branch in exactly one remote (call itremote) with a matching name, treat as equivalent togit switch -c branch --track remote/branch也就是说如果本地没有这个名字的分支而恰好有一个远程仓库里有同名的分支那么 switch 就会照着 origin/draft 新建一条本地的 draft同时设置好上游。这个行为是默认打开的git checkout draft的效果也完全一样。需要注意的是参数要写 draft如果写成 origin/draftswitch 会像代码 4 那样报错而 checkout 则会进入分离头指针状态。三、合作者推送了新的提交为了模拟合作者再从github/notes.git克隆一份到 partner 目录把它当作合作者的电脑。合作者在 main 上新建了 ssh.md 并推送接着又新建了一条 review 分支也推送了上去cd../partnergitaddssh.mdgitcommit-m补上 SSH 的笔记gitpushgitswitch-creviewgitaddreview.mdgitcommit-m审读第七章gitpush-uorigin review代码 8合作者那一边的操作输出省略。回到本地的 notes 仓库切换回 main再看看 status 怎么说gitstatus# On branch main# Your branch is ahead of origin/main by 1 commit.# (use git push to publish your local commits)## nothing to commit, working tree cleangitpush# To ……/github/notes.git# ! [rejected] main - main (fetch first)# error: failed to push some refs to ……/github/notes.git# hint: Updates were rejected because the remote contains work that you do# hint: not have locally. This is usually caused by another repository pushing# hint: to the same ref. You may want to first integrate the remote changes# hint: (e.g., git pull ...) before pushing again.# hint: See the Note about fast-forwards in git push --help for details.代码 9status 仍然说领先 1 个提交push 却被拒绝了退出码是 1。地址里的长路径用 …… 代替。status 说领先 1 个提交这个判断其实已经过时了。上一章说过git status 不会联网它比较的是本地的 main 和本地的 origin/main。远程的 main 已经被合作者推到了新的提交上但是本地的 origin/main 仍然停在 8c565ad所以 status 看不出远程有了新东西。push 则要真正连接远程仓库Git 这时才发现远程的 main 上有一个本地没有的提交于是拒绝了这次推送。括号里的fetch first已经说明了下一步该做什么也就是先把远程的提交拿下来。坑status 不联网看到「领先」别急着 push先 fetch 再判断。到这里可以把几个区域之间的命令放在一起看一看。前几章的 add 和 commit 把改动从工作区送进暂存区再送进本地仓库上一章的 push 把本地仓库里的提交送到远程仓库。反方向的命令有两条它们的区别就在于拿回来的提交送到哪里为止。图 1fetch 只走到本地仓库更新的是origin/*pull 一直走到工作区因为它在 fetch 之后还会做一次合并。四、git fetch只更新origin/*gitfetch# remote: Enumerating objects: 7, done.# remote: Counting objects: 100% (7/7), done.# remote: Compressing objects: 100% (5/5), done.# remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0# Unpacking objects: 100% (6/6), 653 bytes | 217.00 KiB/s, done.# From ……/github/notes# 8c565ad..7169997 main - origin/main# * [new branch] review - origin/review代码 10不带参数时fetch 会从当前分支的上游所在的远程仓库拉取没有上游就用 origin。前面几行是在接收对象6 个对象是合作者的两个提交、它们各自的目录树以及新建的 ssh.md 和 review.md。从 From 开始的几行才是结果。每一行箭头左边是远程仓库里的分支右边是本地记录它的远程跟踪分支。第一行说的是 origin/main 从 8c565ad 挪到了 7169997第二行的[new branch]表示远程多了一条 review 分支于是本地新建了一条 origin/review。可见 fetch 对已经存在的远程跟踪分支只是移动它的位置只有远程新出现的分支才会让本地多出一条远程跟踪分支。gitstatus# On branch main# Your branch and origin/main have diverged,# and have 1 and 1 different commits each, respectively.# (use git pull to merge the remote branch into yours)## nothing to commit, working tree cleanls# README.mdgitlog--oneline--graphmain origin/main# * 7169997 (origin/main, origin/HEAD) 补上 SSH 的笔记# | * 67f2ea7 (HEAD - main) 开始写第八章# |/# * 8c565ad 补上第七章# * 8db4435 第一篇笔记代码 11status 的说法变成了分叉两边各有 1 个对方没有的提交但是工作区里仍然没有 ssh.md。fetch 之后origin/main 指向了合作者的 7169997status 这才看出两边已经分叉了。main 仍然停在 67f2ea7工作区里也没有 ssh.md因为 fetch 不会碰本地分支和工作区。正因为如此任何时候执行 fetch 都是安全的它不会打乱手上正在做的修改。图 2前两行的区别只在 origin/main 和 7169997 上。第三行是下一节 pull 之后的样子合并提交 0480eef 让 main 同时包含了两边的提交。拿回来的提交在合并之前就可以查看gitlog--onelinemain..origin/main# 7169997 (origin/main, origin/HEAD) 补上 SSH 的笔记gitshow--statorigin/main# commit 7169997232dbdbfc89b7d094577cc97e539236b0 (origin/main, origin/HEAD)# Author: teammate teammateexample.com# Date: Fri Oct 9 10:06:00 2026 0800## 补上 SSH 的笔记## ssh.md | 3 # 1 file changed, 3 insertions()代码 12main..origin/main列出的是在 origin/main 上、但是不在 main 上的提交也就是这次 fetch 拿到、还没有合并进来的提交。如果想直接看文件也可以像上一节那样执行git checkout origin/main看完再git switch main回来。fetch 也可以只拿一条分支写法是git fetch 远程 分支。合作者接着又往 review 和 draft 上各推送了一个提交gitfetch origin review# 接收对象的几行这里省略# From ……/github/notes# * branch review - FETCH_HEAD# 40f0372..4e79d9d review - origin/review代码 13只更新了 origin/reviewdraft 上的新提交这一次没有拿下来。第一行里的 FETCH_HEAD 是.git里的一个文件它记录着这一次 fetch 拿到了什么下一节的 pull 就会用到它。第二行说明 origin/review 也跟着更新了。draft 上虽然也有新提交但是这一次只拿了 review所以本地的 origin/draft 没有变。五、git pullfetch 之后再合并手册对 pull 的说明是这样的More precisely, git pull runs git fetch with the given parameters and then depending on configuration options or command line flags, will call either git rebase or git merge to reconcile diverging branches.也就是说pull 分成两步先用同样的参数执行一次 fetch再根据配置调用 rebase 或者 merge把拿到的提交合并进当前分支。这一章只讲 merge 这一种。合并的时候会遇到下面三种情况。本地没有新提交快进先看 draft。代码 13 没有拿 draft 上的新提交所以切换过去时status 仍然说一致gitswitch draft# Switched to branch draft# Your branch is up to date with origin/draft.gitpull# 接收对象的几行这里省略# From ……/github/notes# 73366ce..b1e8a68 draft - origin/draft# Updating 73366ce..b1e8a68# Fast-forward# draft.md | 1 # 1 file changed, 1 insertion()代码 14From 开头的两行是 fetch 的输出Updating 开头的几行是合并的输出。pull 先执行 fetch把 origin/draft 从 73366ce 挪到了 b1e8a68然后把它合并进本地的 draft。本地的 draft 上没有自己的新提交所以合并只需要把 draft 往前挪这就是第三章讲的快进输出也和第三章一样。两边都有新提交先说明怎样合并main 就不一样了本地和远程各有一个对方没有的提交gitswitch maingitpull# hint: You have divergent branches and need to specify how to reconcile them.# hint: You can do so by running one of the following commands sometime before# hint: your next pull:# hint:# hint: git config pull.rebase false # merge (the default strategy)# hint: git config pull.rebase true # rebase# hint: git config pull.ff only # fast-forward only# hint:# hint: You can replace git config with git config --global to set a default# hint: preference for all repositories. You can also pass --rebase, --no-rebase,# hint: or --ff-only on the command line to override the configured default per# hint: invocation.# fatal: Need to specify how to reconcile divergent branches.代码 15退出码是 128。git switch main 的输出和代码 11 里 status 的前几行相同这里省略。在 git 2.34.1 上如果两边已经分叉而仓库里又没有配置过合并的方式pull 就会拒绝执行。手册的开头也写着这一条If the current branch and the remote have diverged, the user needs to specify how to reconcile the divergent branches with --rebase or --no-rebase (or the corresponding configuration option in pull.rebase).提示里列出了三种配置。pull.rebase false表示用合并也就是这一章说的 fetch 加 mergepull.rebase true表示用变基要等讲到 rebase 时再看pull.ff only表示只允许快进分叉时直接报错留给人自己处理。上一小节 draft 那样的快进不受这个限制没有配置也能直接完成。这里选择合并gitconfig pull.rebasefalsegitpull代码 16这样配置只对当前仓库生效。如果加上--global以后所有的仓库都会这样合并。和第三章的三方合并一样Git 会先打开编辑器默认的提交信息已经写好了Merge branch main of ……/github/notes # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with # will be ignored, and an empty message aborts # the commit.代码 17提交信息里写着合并的是哪个远程仓库的哪一条分支。如果远程是 GitHubof后面就是 GitHub 上的仓库地址。# 存盘退出之后# Merge made by the ort strategy.# ssh.md | 3 # 1 file changed, 3 insertions()# create mode 100644 ssh.mdls# README.md ssh.mdgitlog--oneline--graph# * 0480eef (HEAD - main) Merge branch main of ……/github/notes# |\# | * 7169997 (origin/main, origin/HEAD) 补上 SSH 的笔记# * | 67f2ea7 开始写第八章# |/# * 8c565ad 补上第七章# * 8db4435 第一篇笔记gitstatus# On branch main# Your branch is ahead of origin/main by 2 commits.# (use git push to publish your local commits)## nothing to commit, working tree clean代码 18ssh.md 出现在了工作区里。领先的 2 个提交是本地的 67f2ea7 和合并提交 0480eef。这一次 pull 没有打印 From 那几行因为代码 10 已经把 7169997 拿下来了fetch 这一步没有拿到新的东西。合并之后main 指向 0480eef它有两个父提交一个是本地的 67f2ea7另一个是合作者的 7169997图 2 的第三行画的就是这个时刻。origin/main 仍然停在 7169997因为远程仓库还不知道这次合并。现在再推送就不会被拒绝了gitpush# 打包和发送对象的几行这里省略# To ……/github/notes.git# 7169997..0480eef main - main代码 19远程的 main 从 7169997 快进到了 0480eef因为 0480eef 的父提交里就有 7169997。两边改了同一行冲突合作者先执行 git pull把 0480eef 快进下来然后把 README.md 的第 3 行改成「Git 第七章remote、push 和 SSH」并推送。与此同时本地把同一行改成了「Git 第七、八章remote、push、fetch 和 pull」并提交gitcommit-amREADME 写到第八章# [main 4379b48] README 写到第八章# 1 file changed, 1 insertion(), 1 deletion(-)gitpull# 接收对象的几行这里省略# From ……/github/notes# 0480eef..28cb0da main - origin/main# Auto-merging README.md# CONFLICT (content): Merge conflict in README.md# Automatic merge failed; fix conflicts and then commit the result.代码 20fetch 这一步成功了合并这一步发生了冲突退出码是 1。gitstatus# On branch main# Your branch and origin/main have diverged,# and have 1 and 1 different commits each, respectively.# (use git pull to merge the remote branch into yours)## You have unmerged paths.# (fix conflicts and run git commit)# (use git merge --abort to abort the merge)## Unmerged paths:# (use git add file... to mark resolution)# both modified: README.md## no changes added to commit (use git add and/or git commit -a)catREADME.md# # 学习笔记## HEAD# Git 第七、八章remote、push、fetch 和 pull# # Git 第七章remote、push 和 SSH# 28cb0da68ec21dc209038d8d5f6a919d8a5a80fe## 后面几行没有冲突这里省略代码 21冲突标记和第三章一样只是后面写的是提交号因为 pull 合并的是 fetch 拿到的那个提交而不是一条本地分支。解决冲突的办法和第三章完全一样。把这几行改成想要的样子比如合成「Git 第七、八章remote、push、SSH、fetch 和 pull」一行并且删掉冲突标记然后再 add 和 commitgitaddREADME.mdgitcommit# 编辑器里已经填好了 Merge branch main of ……/github/notes存盘退出# [main 0580ec3] Merge branch main of ……/github/notesgitpush# 打包和发送对象的几行这里省略# To ……/github/notes.git# 28cb0da..0580ec3 main - main代码 22add 表示冲突已经解决commit 生成合并提交最后再 push。和第三章不同的是最后多了一步 push。合并提交只在本地远程仓库并不知道这次合并所以要再推送一次合作者下一次 pull 的时候才能拿到。如果冲突解决到一半想放弃这次合并可以按照 status 的提示执行git merge --abort。这时 main 和工作区会回到 pull 之前的样子但是 fetch 拿下来的 28cb0da 仍然记在 origin/main 上因为撤销的只是合并这一步。六、不带参数的git pull依靠上游前面的 pull 都没有带参数Git 是怎样知道应该拉取哪个远程仓库的哪一条分支的呢手册里写的是Default values forrepositoryandbranchare read from the remote and merge configuration for the current branch as set by git-branch(1) --track.这里说的 remote 和 merge就是上一章-u写进.git/config的那两行也就是上游。clone 给 main 设置了上游代码 7 的 switch 给 draft 设置了上游所以前面的 pull 都不需要参数。如果一条分支没有上游会怎样呢下面新建一条 fetch-notes 分支提交之后推送但是推送时没有加-ugitswitch-cfetch-notesgitaddfetch.mdgitcommit-mfetch 的笔记# [fetch-notes 62632b8] fetch 的笔记# 1 file changed, 3 insertions()# create mode 100644 fetch.mdgitpush origin fetch-notes# 打包和发送对象的几行这里省略# To ……/github/notes.git# * [new branch] fetch-notes - fetch-notesgitpull# There is no tracking information for the current branch.# Please specify which branch you want to merge with.# See git-pull(1) for details.## git pull remote branch## If you wish to set tracking information for this branch you can do so with:## git branch --set-upstream-toorigin/branch fetch-notes#代码 23退出码是 1。远程明明有一条同名的 fetch-notespull 也不会自己去找。switch 和 pull 在这一点上的做法不同。代码 7 的 switch 会去找同名的远程分支但是 pull 不会它只看上游配置。fetch-notes 没有上游所以即使远程有同名的分支pull 也不知道该拉取哪一条。解决办法有两种一种是在命令里写明远程和分支另一种是用上一章的git branch -u设置上游gitpull origin fetch-notes# From ……/github/notes# * branch fetch-notes - FETCH_HEAD# Already up to date.gitbranch-uorigin/fetch-notes# Branch fetch-notes set up to track remote branch fetch-notes from origin.gitpull# Already up to date.gitbranch-vv# draft b1e8a68 [origin/draft] 草稿想清楚了# * fetch-notes 62632b8 [origin/fetch-notes] fetch 的笔记# main 0580ec3 [origin/main] Merge branch main of ……/github/notes代码 24git branch -vv在每条分支后面的方括号里列出它的上游。报错信息里的--set-upstream-to就是-u的完整写法。带参数的写法有一点需要注意。git pull origin main的意思是把远程的 main 合并进当前所在的分支而不是合并进本地的 main手册的第一句话就是Incorporates changes from a remote repository into the current branch。所以如果当时在 fetch-notes 上执行git pull origin main远程 main 上的提交就会被合并进 fetch-notes。带参数执行 pull 之前最好先确认一下当前在哪条分支上。易误解pull origin main 是并入当前分支不是并进本地 main。小结写法作用和要点git branch -r列出远程跟踪分支-a连同本地分支一起列出origin/main远程跟踪分支保存在本地的 .git 里记录上一次通信时远程 main 的位置只有 fetch、pull、push 会移动它git switch 分支本地没有这条分支、远程有同名分支时照着origin/分支新建并设置上游git checkout origin/分支进入分离头指针状态可以查看文件新的提交不属于任何分支git fetch把远程的新提交拿进本地仓库只更新origin/*本地分支和工作区不变git fetch 远程 分支只拿一条分支同时更新origin/分支和 FETCH_HEADgit log main..origin/main列出 fetch 拿到、还没有合并进 main 的提交git pull先 fetch再把上游合并进当前分支落后时快进分叉时要先配置 pull.rebasegit config pull.rebase false分叉时用合并加--global对所有仓库生效git merge --abort放弃 pull 时发生冲突的那次合并origin/*仍然保持 fetch 之后的位置git pull origin main把远程的 main 合并进当前所在的分支不一定是本地的 main也不需要上游git branch -vv列出每条本地分支的上游表中的远程通常是 origin。这一章的命令都可以用origin/*这组记录串起来。因为origin/*保存在本地所以 git status 不联网也能比较但是它说的永远是上一次通信时的情况。fetch 只负责更新这组记录不碰本地分支和工作区所以随时都可以放心执行。pull 在 fetch 之后再做一次合并所以它和第三章的合并一样可能快进可能生成合并提交也可能发生冲突。两边都有新提交时除了合并还可以用变基把本地的提交接到远程提交的后面pull.rebase true就是这种做法等讲到 rebase 的时候再回来看。
阅读完成 · 觉得有帮助?
咨询建站