细读 Git | 弄懂 origin、HEAD、FETCH_HEAD 相关内容

图片源自 Freepik

如果大家平常都在使用 Git 作为版本控制工具的话,那么一定每天都能见到 origin,诸如:

$ git push origin main
$ git fetch origin main
$ git pull origin main
# ...

这里的 origin,还有看似相同的 origin/masterorigin/main 又是什么呢?

一、远程名称(Remote Name)

在 Git 中,其实无论是 origin,还是 upstream 并没有特殊的含义,但由于被广泛使用,因此它们有了约定俗成、众所周知的含义。

就好比如说,在现实世界中小明、小红是再普通不过的名字,但由于在小学语文课本的对话中常被作为男一女一,用于表示对话的两个人而已,并没有特别的意义在里面。而在技术博文中,经常可以看到使用 foobar 作为变量标识符举例,它们就相当于语文课本中的小明、小红一样。那么本文接下来要讨论的 originupstream 等也是同样的道理。

先来个餐前菜...

如果我跟你说,以下两条命令是完全等效的,你是不是就差不多猜得出 origin 表示什么了?

$ git push origin main
$ git push git@github.com:toFrankie/repo-demo.git main

是的,跟你猜的一样...

1.1 Origin

我们用示例来讲...

先在本地随意创建一个 Git 仓库 repo-demo,然后新增一个 README.md 文件,接着 Commit 一下(如下图):

以上都没问题!接着,我们试着 Push 一下:

可以看到 git push 失败了,原因很容易理解:我们只是在本地创建一个仓库,并没有将本仓库与远程仓库进行关联,因此 Git 无法理解是将其推送至哪个代码托管平台,然后也不知道是平台上的哪个远程仓库,是 GitHub 平台的,还是 GitLab 平台的?是平台上的 React 仓库,还是 Vue 仓库,还是别的什么仓库?Git 统统都不知道,那么自然是无法替你办事了。

因此,我们需要做的就是把本地的 repo-demo 仓库与远程仓库关联一下(请注意,一个本地仓库是可以关联多个远程仓库的):

$ git remote add origin <repo_address>

这里用到了 origin,我们先不管为什么用 origin,用其他(比如 foo)行不行的问题?(答案是可以的)

关联之后,再进行 Push 就能成功了。

那么 git remote add 内部做了什么默默无闻的工作呢,它其实是往 .git/config 中写入了一个叫 [remote "origin"] 配置:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = git@github.com:toFrankie/repo-demo.git
    fetch = +refs/heads/*:refs/remotes/origin/*

如果你本地的仓库是通过 git clone 下来的,Git 会默认将远程仓库命名为 origin,自动帮你关联上远端仓库(可在 .git/config 文件中看到已有 [remote "origin"] 配置项了),因此 Commit 之后就能直接 Push 了。

When a repo is cloned, it has a default remote called origin that points to your fork on GitHub, not the original repo it was forked from.(引自 Github page

如果我们在 GitHub 新创建一个 Repository 的话,会看到以下指引:

我们来分析一下,这配置表示什么意思。

[remote "origin"]
    url = git@github.com:toFrankie/repo-demo.git
    fetch = +refs/heads/*:refs/remotes/origin/*

通过 git remote add 命令,添加了一个叫做 origin 的远程名称(Remote Name),

  • 其中 url 参数,表示该远程名称对应的远程仓库地址。
  • 其中 fetch 参数分为两部分,以冒号 : 进行分割,冒号左边表示本地仓库文件夹,冒号右边表示远程仓库在本地的副本文件夹。里面的加号 + 表示往里面添加数据的意思。

当使用 git fetch origin 时,Git 将远程仓库下的所有分支拉取到本地的 refs/remotes/origin/ 目录下,然后 git merge 时,它会把 refs/remotes/origin/ 目录下的对应分支合并到 refs/heads/ 目录下对应分支上。

那么 origin 究竟是什么呢?

请注意,origin 只是一个名称(别名),用于指向远程仓库。这个别名是可以自行修改的,比如命名为 foobar 等。使用别名好处是「方便」。

比起记住一个远程仓库地址,别名实在方便太多了。将 origin 作为远程仓库的别名是较为普遍的做法,况且所有代码托管平台默认就是 origin

回到文章开头的例子:

$ git push origin main

# 相当于(其中 origin 指向了 git@github.com:toFrankie/repo-demo.git 远程仓库)
$ git push git@github.com:toFrankie/repo-demo.git main

以上两种方式是完全等价的,这样就更能体现别名的优势了,简洁很多。

既然是别名,自然是可以修改的,主要有以下命令:

# 新增远程名称(一个本地仓库,可以关联多个远程仓库)
$ git remote add <remote-name> <repo-address>

# 删除已存在远程名称(只会移除本地仓库与远程仓库的管理,不会删除远程仓库的代码哈)
$ git remote rm <remote-name>

# 更新远程名称关联的远程仓库
$ git remote set-url <remote-name> <repo-address>

# 修改远程名称(也可以先删除再添加)
$ git remote rename <old-remote-name> <new-remote-name>

比如,像这样:

然后,我们修改下远程名称为 foo,也是可以的:

接着,我们随意修改个文件 Push 一下,是这样的 git push foo main

到这里,你应彻底明白 origin 是什么了吧。

前面提到过,一个本地仓库是可以关联多个远程仓库的,举个例子:

$ git remote add bar git@github.com:toFrankie/git-test-demo.git

我们可以查看下 .git/congfig 配置文件,如下(或者通过 git remote -v 查看)

从图中可以看到,别名 foobar 分别指向了两个不同的远程仓库,然后使用方法与 origin 是相同的,比如:

# 将本地的 main 分支推送至 foo 对应的远程仓库(repo-demo)
$ git push foo main

# 将本地的 main 分支推送至 bar 对应的远程仓库(git-test-demo)
$ git push bar main

1.2 upstream(一个特殊的 remote name)

upstream 的译为“上游”。当你 git clone 一个别人的 Repository 到本地,由于你不是该仓库的成员,因此你是无法向该仓库推送代码的。此时,相较于本地仓库,别人的这个 Repository 称为 upstream

我们可以 Fork 这个 Repository 到自己 GitHub 账号下,然后通过 git clone 将这个 Fork 出来的仓库克隆到本地电脑上。(下文将这个别人的仓库称为 Upstream-Repo,Fork 出来的仓库称为 Origin-Repo

大致关系如图所示(源自),其中 Upstream-Repo 对应图中的 Original,Origin-Repo 对应图中 Fork:

当我们将 Origin-Repo 克隆到本地,Git 会默认创建一个 origin 的别名指向 Origin-Repo 的仓库地址。

如果要跟踪 Upstream-Repo 仓库的变更,您需要添加另一个名为 upstream 的别名,使其指向 Upstream-Repo 仓库。

# 1. 添加上游仓库的别名
$ git remote add upstream <upstream-repo-address>

# 2. 获取上游仓库的变更
$ git fetch upstream

# 3. 有需要的话,可以通过 merge 或 rebase 方式合并到本地分支中,比如:
$ git merge upstream/main

尽管添加了 upstream,诸如 git push upstream main 等方式试图向 Upstream-Repo 提交代码仍然是不被允许的,因为你不是 Upstream-Repo 仓库的成员。想给 Upstream-Repo 仓库贡献代码的话,只能通过 Pull Request 的方式。

当然,这一节提到的 upstream 也是一个约定俗称的别名,也是可以自定义的。

1.3 小结

除了 originupstream 等有众所周知的含义的远程名称之外,我们还可以这样使用:

由于一个本地仓库是可以关联多个远程仓库的,因此,可以设置多个「别名」分别指向不同的远程仓库(比如一个 GitHub、一个 GitLab、一个 Gitee),然后通过别名的方式方便、快速地拉取某个远程仓库的代码或者将代码推送至某个远程仓库。

# 添加 github 别名
$ git remote add github git@github.com:toFrankie/repo-demo.git

# 添加 gitlab 别名
$ git remote add gitlab git@gitlab.com:toFrankie/repo-demo.git

# 添加 gitee 别名
$ git remote add gitee git@gitee.com:toFrankie/repo-demo.git

小结一下:

  • 常见的 originupstream 都只是通过 git remote add 命令创建的名称(Remote Name),用于指向某个远程仓库(Remote URL)。

  • 常用 origin 作为远程仓库的别名,是一个较为主流的做法。同时,也是各大代码托管平台的默认名称(即 git clone 一个远程仓库, Git 会默认将 origin 指向该仓库)。如果你觉得不爽,完全可以自定义(git remote set-url)为“阿猫”、“阿狗”等名称。

  • 查看本地仓库关联的远程仓库信息,可以在 .git/config 文件或通过 git remote -v 命令查看。

  • 使用别名的最大好处是,无需记住远程仓库的 URL,也是唯一的好处吧。不用也是完全 OK 的,完全可以直接使用远程仓库 URL,但我想不会有这种朋友吧。

(建议)若无特殊需求,不要为了个性,试图更改 originupstream 等被广泛使用的别名,其中所表示的约定俗成的、众所周知的含义。

二、远程分支(Remote Branch)

常说的「远程分支」是远程仓库对应分支在本地的一个副本。比如常见的 origin/masterorigin/mainorigin/develop 等都是远程分支,可以在 .git/refs/remotes/ 目录下看到。

# 查看所有本地分支
$ git branch

# 查看所有远程分支(-r 是 --remotes 的简写)
$ git branch -r

# 查看所有本地分支和远程分支(-a 是 --all 的简写)
$ git branch -a

可以通过 git branch -r 命令查看所有的远程分支:

frankie@iMac repo-demo %  git branch -r
  origin/HEAD -> origin/main
  origin/dev
  origin/main

如果对 origin/HEAD 不理解的话,先不管下文会介绍。

上一节,我们介绍了远程名称只是一个代号、别名,是可以修改的。那么我们将 Remote Name 由 origin 修改为 foo,那么远程分支,会不会由 origin/main 变为 foo/main 呢?

修改前:

frankie@iMac repo-demo %  git remote -v
origin  git@github.com:toFrankie/repo-demo.git (fetch)
origin  git@github.com:toFrankie/repo-demo.git (push)

frankie@iMac repo-demo %  git branch -r
  origin/HEAD -> origin/main
  origin/dev
  origin/main

修改后:

frankie@iMac repo-demo %  git remote rename origin foo

frankie@iMac repo-demo %  git remote -v
foo     git@github.com:toFrankie/repo-demo.git (fetch)
foo     git@github.com:toFrankie/repo-demo.git (push)

frankie@iMac repo-demo %  git branch -r
  foo/HEAD -> foo/main
  foo/dev
  foo/main

果然,将远程名称修改之后,远程分支名称也会跟着改变的。我们通过 tree 命令看下目录结构,如下:

frankie@iMac repo-demo %  tree .git/refs
.git/refs
├── heads
│   ├── dev
│   └── main
├── remotes
│   └── foo
│       ├── HEAD
│       ├── dev
│       └── main
└── tags

那么接下来,若无特殊说明,都将以 origin 作为远程名称进行说明或举例。

通常,拉取最新代码的过程是这样的:

  1. 通过 git fetch 拉取代码的过程:先读取 .git/config 文件里面的配置 [remote <remote-name>],将里面的所有(因为 fetch 并没有指定其中一个或多个远程仓库)远程名称对应仓库的分支下载到本地,并放在 .git/refs/remotes/<remote-name>/ 目录下。

    比如 git fetch origin main 会创建或更新 .git/refs/remotes/origin/main 的文件,此时通过 git branch -r 就能看到一个 origin/main 的分支。但注意,我们使用的时候还是用 origin/main 而不是 remotes/origin/main 哦。

  2. 有时候,我们可能会通过 git diff 命令来对比本地分支与远程分支的一些信息,才决定要不要合并。比如,git diff main origin/main

  3. 通过 git mergegit rebase 来进行分支合并。比如 git merge origin/main,表示将远程分支 origin/main 合并至本地分支 main 中。

也可以直接使用 git pull 命令,其实包括了 git fetchgit merge 两个过程。请注意 git fetch 并不会修改「本地分支」的代码。

细心的同学可能会发现,refs/remotes/origin/ 目录下,相应的分支文件记录的只是一个 Commit-ID(SHA-1),比较特殊的是 HEAD 文件(即 origin/HEAD 分支)记录的是 ref: refs/remotes/origin/main 的东西,它始终指向默认远程分支。

三、HEAD、Detached HEAD、origin/HEAD、FETCH_HEAD、ORIG_HEAD 区别

这个对于刚接触的同学,可能看起来有点懵。

其实 Git 中的「分支」是由一个或多个 Commit-ID 组成的集合。通过 git branch 命令创建的分支,只是对某个 Commit-ID 的「引用」。因此,使用 git branch -d 删除某个本地分支,也只是删除了这个「引用」而已,并不会删除任何的 Commit-ID。但是,如果一个 Commit-ID 没有被任何一个分支引用的话,在一定时间之后,将会被 Git 回收机制删除。

本节内容将会讲述以下相关内容:

  • HEAD 跟「本地分支」相关。
  • Detached HEAD 是一种特殊状态的 HEAD
  • origin/DEAD 跟「远程分支」相关。
  • FETCH_HEADgit fetch 操作相关
  • ORIG_HEADgit mergegit reset 等「危险操作」相关

3.1 HEAD

我们在哪能看到 HEAD 呢?

接着,我们从 main 分支切换至 dev 分支。

对比发现,HEAD 发生改变了。

前面提到,分支只是对 Commit-ID 的引用。每当在某个分支上提交代码,Git 都会产生一个全新的、唯一的 Commit-ID,此时我们的分支名称也随之移向最新的一个 Commit-ID。

关于 HEAD 存放于本地仓库的 .git/HEAD 文件里面,利用 cat 命令可以看到它的内容。

frankie@iMac repo-demo %  cat .git/HEAD
ref: refs/heads/dev

frankie@iMac repo-demo %  cat .git/refs/heads/dev
866bc9f1d8f4797c0e46e959cb0c9abdd47d8176

所以说到底,此时 HEAD 只是对 Commit-ID 为 866bc9f1d8f4797c0e46e959cb0c9abdd47d8176 的引用。如果切回 main 分支,那么 HEAD 相应的内容就会跟着改变。

HEAD 则是比较特殊的一个引用(有些文章称为「指针」,也问题不大)。除了 git commit 之外,git checkoutgit reset 等命令都会影响 HEAD 的指向。

一句话总结:HEAD 是对当前 Commit-ID 的「引用」。

  • 当使用 git commit 时,HEAD 会跟着移动,并指向最新的 Commit-ID。
  • 当使用 git checkout 时,HEAD 会移动并指向对应分支的最新一个 Commit-ID。
  • 当使用 git reset 时,HEAD 会移动至对应分支的某个 Commit-ID。请注意 git reset --hard 可以将 HEADBranch 移动至任何地方。

顺道提一下,git reset 的本质就是移动 HEAD 来达到撤销的目的。

观察以下示例,我使用 git reset --softHEAD 从 Commit-ID 为 42d46a2 移至 222a33c,变化如下:

frankie@iMac repo-demo %  git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main, origin/HEAD)
Author: Frankie <1426203851@qq.com>
Date:   Sat Feb 26 16:44:30 2022 +0800

    docs: update

commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:55:48 2022 +0800

    docs: update readme.md

commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:36:51 2022 +0800

    docs: update README.md

commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 15:45:15 2022 +0800

    docs: add README.md

frankie@iMac repo-demo %  git reset --soft 222a33cb3185457a5d726325aa7233e43f0b92d3

frankie@iMac repo-demo %  git log
commit 222a33cb3185457a5d726325aa7233e43f0b92d3 (HEAD -> main)
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:36:51 2022 +0800

    docs: update README.md

commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 15:45:15 2022 +0800

    docs: add README.md

此时,我们再看下 .git/HEAD 的内容:

frankie@iMac repo-demo %  cat .git/HEAD
ref: refs/heads/main

frankie@iMac repo-demo %  cat .git/refs/heads/main
222a33cb3185457a5d726325aa7233e43f0b92d3

关于 git reset 的三个参数 --mixed(默认)、--hard--soft 的区别,推荐看这篇文章,讲得很详细易懂。

3.2 Detached HEAD

detached HEAD 可以称为「游离 HEAD」,也有称为「分离 HEAD」的。

一般情况下,我们的 HEAD 会指向某个分支的某个 Commit-ID。但是 HEAD 偶尔会发生「没有指向某个本地分支」的情况,这种状态的 HEAD 称为 detached HEAD

以下情况,就可能会出现 detached HEAD

  1. 使用 git checkout 跳转至某个 Commit-ID,而这个 Commit-ID 刚好目前没有分支指向它。
  2. Rebase 的过程其实也是处于不断的 detached HEAD 状态。
  3. 切换至某个远程分支的时候。

我们先将 Git 的提示语言切换为英文,看得更加清晰。

frankie@iMac repo-demo %  git checkout e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Note: switching to 'e0c619ca3978a38f6eabe79c3dfc67d4296ccc36'.

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.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at e0c619c docs: update readme.md

此时 HEAD 指向 e0c619c,这个就是 detached HEAD

还有,前面提到「没有指向某个本地分支」,但其实我们使用 git branch 会发现有以下这样一个分支:

frankie@iMac repo-demo %  git branch
* (HEAD detached at e0c619c)
  dev
  main

但注意,当我们切换至其他分支时,这个 (HEAD detached at e0c619c) 分支是会被干掉的,因为它只是临时的。因此人家也提醒你可以使用 git switch -c <new-branch-name> 命令,以创建一个新分支来指向该 Commit-ID。

假设我们有这样一个场景:想要查看某个历史版本的源码,就可以利用此功能来解决。

# -t 即 --track
$ git branch <new-branch-name> <commit-id>
# 或者
$ git checkout -b <new-branch-name> <commit-id>

如果我们使用 git checkout origin/main 切换至 origin/main 远程分支时,也会产生一个 detached HEAD 的。如果我们想基于某个远程分支,新建一个同名本地分支,可以这样:

$ git checkout -t origin/dev
branch 'dev' set up to track 'origin/dev'.
Switched to a new branch 'dev'

# 相当于
$ git checkout -b dev origin/dev

如果要离开 detached HEAD 状态很简单,只要切换至其他分支即可。

3.3 origin/HEAD

从名字可以看出,origin/HEAD 也是一个「远程分支」,其中 origin 则对应远程名称。

一般情况下,origin/HEAD 总是指向远程仓库的「默认分支」。假设我们的远程默认分支为 main。那么在远程仓库在本地的副本,origin/HEAD 就是相当于 origin/main

Git 提供了以下命令让我们去修改 origin/HEAD 的指向(可通过 git remote set-head -h 查看):

# 将 origin/HEAD 设为远程仓库的默认分支(-a 即 --auto)
$ git remote set-head <remote-name> -a

# 将 origin/HEAD 设为某个远程分支,
# 比如 git remote set-head origin dev,将 origin/HEAD 指向远程的 dev 分支,相当于 origin/dev
$ git remote set-head <remote-name> <branch-name>

# 删除 origin/HEAD(-d 即 --delete)
$ git remote set-head <remote-name> -d

以上注释部分,假定了远程名称为 origin

我们修改下 origin/HEAD 为远程仓库的 dev 分支,origin/HEAD 文件里面存储的内容,同样表示的也是对某个远程分支的引用,仅此而已。

frankie@iMac repo-demo %  git remote set-head origin dev
frankie@iMac repo-demo %  cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/dev

3.4 ORIG_HEAD(拓展内容)

.git 目录下,有一个 ORIG_HEAD 的文件,你有没有好奇怪,它是什么呢?

frankie@iMac repo-demo %  cat .git/ORIG_HEAD
222a33cb3185457a5d726325aa7233e43f0b92d3

还记得,前面使用过 git reset --soft 222a33cb3185457a5d726325aa7233e43f0b92d3 指令来移动 HEAD 的指向吗?

当我们进行了一些「危险操作」时,比如 git resetgit mergegit rebase 等操作时,Git 会将当前 HEAD 指向的的 Commit-ID 原值保存至 ORIG_HEAD 文件内。需要注意的是,类似 git commit 等操作并不会更新 ORIG_HEAD 的内容。

这样的话,加入我们执行了一些「误操作」时,可以利用 git reset --hard ORIG_HEAD 回退至上一步。

举个例子,我们进行一次 git reset 操作:

frankie@iMac repo-demo %  git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main)
Author: Frankie <1426203851@qq.com>
Date:   Sat Feb 26 16:44:30 2022 +0800

    docs: update

commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:55:48 2022 +0800

    docs: update readme.md

commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:36:51 2022 +0800

    docs: update README.md

commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 15:45:15 2022 +0800

    docs: add README.md

reset 之前,我们可以看到 HEAD 指向的 Commit-ID 为 42d46a2356cfdde0ad80bfc042b6cb15eae04759,接着我们执行 git reset 指令:

frankie@iMac repo-demo %  git reset --soft e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
frankie@iMac repo-demo %  git log                                                                           [16:39:25]
commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36 (HEAD -> main)
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:55:48 2022 +0800

    docs: update readme.md

commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:36:51 2022 +0800

    docs: update README.md

commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 15:45:15 2022 +0800

    docs: add README.md

git reset 完成之后,HEAD 指向了 e0c619ca3978a38f6eabe79c3dfc67d4296ccc36

既然我们前面提到过,ORIG_HEAD 会记录高危操作前的 Commit-ID。如果没错的话,此时 ORIG_HEAD 记录的应该是 42d46a2356cfdde0ad80bfc042b6cb15eae04759

frankie@iMac repo-demo %  cat .git/ORIG_HEAD
42d46a2356cfdde0ad80bfc042b6cb15eae04759

如果我们想吃后悔药了,就可以通过 git reset --hard ORIG_HEAD 进行回退:

frankie@iMac repo-demo %  git reset --hard ORIG_HEAD
HEAD is now at 42d46a2 docs: update

frankie@iMac repo-demo %  git log
commit 42d46a2356cfdde0ad80bfc042b6cb15eae04759 (HEAD -> main, origin/main)
Author: Frankie <1426203851@qq.com>
Date:   Sat Feb 26 16:44:30 2022 +0800

    docs: update

commit e0c619ca3978a38f6eabe79c3dfc67d4296ccc36
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:55:48 2022 +0800

    docs: update readme.md

commit 222a33cb3185457a5d726325aa7233e43f0b92d3
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 18:36:51 2022 +0800

    docs: update README.md

commit 62733124c6485bc5d81123dd3eae95d4da22753f
Author: Frankie <1426203851@qq.com>
Date:   Sun Feb 20 15:45:15 2022 +0800

    docs: add README.md

当然,不同的场景下版本回退还有其他方式的,视乎实际场景,这里就不展开了。

3.5 FETCH_HEAD

其中 FETCH_HEADgit fetch 有关,也是关键部分。

FETCH_HEAD 指的是某个分支在远程仓库上最新的状态。每一个执行过 git fetch 操作的本地仓库都会存在一个 FETCH_HEAD 列表,这个列表保存在 .git/FETCH_HEAD 文件中。FETCH_HEAD 文件中的每一行对应着远程仓库的一个分支。当前本地分支指向的 FETCH_HEAD 就是该文件中的「第一行」对应的分支(这段表述源于此处)。

我们知道 git fetch 用以下几种用法:

# 1 
$ git fetch

# 2
$ git fetch <remote-name>

# 3
$ git fetch <remote-name> <remote-branch-name>

# 4 
$ git fetch <remote-name> <remote-branch-name>:<local-branch-name>
  • git fetch

拉取「所有远程仓库」所包含的分支到本地,并在本地创建或更新远程分支。所有分支最新的 Commit-ID 都会记录在 .git/FETCH_HEAD 文件中,若有多个分支,FETCH_HEAD 内会多行数据。

  • git fetch origin

拉取 origin 对应的远程仓库的所包含的分支到本地,FETCH_HEAD 设定同上。

  • git fetch origin main

拉取 origin 对应远程仓库的 main 分支到本地,且 FETCH_HEAD 只记录了一条数据,那就是远程仓库 main 分支最新的 Commit-ID。

  • git fetch origin main:temp

拉取 origin 对应远程仓库的 main 分支到本地,其中 FETCH_HEAD 记录了远程仓库 main 分支最新的 Commit-ID,并且基于远程仓库的 main 分支创建一个名为 temp 的新本地分支(但不会切换至新分支)。

因此,FETCH_HEAD 记录的是从远程仓库拉取到本地,「对应分支」的最新一个 Commit-ID。当通过 git fetch 拉取代码时,

  • 若有具体指定了某个远程仓库的某个分支,那么 FETCH_HEAD 就对应此分支。
  • 若没有具体指定远程仓库的某个分支,
    a. FETCH_HEAD 总是指向 .git/FETCH_DEAD 首行对应的分支。
    b. 文件 .git/FETCH_DEAD 可能会记录着多个分支,且该文件首行对应的是 git fetch 时所在分支的同名远程分支。

接着,与 FETCH_HEAD 相关的是 git pull 操作。

git pull 等价于 git fetch + git merge FETCH_HEAD 两个步骤的结合。

git pull 不添加其他参数时,等价于 git pull <remote-name> <当前分支名>,如果远程仓库无与之对应的同名分支,执行该命令就会抛出错误。举个例子:

frankie@iMac repo-demo %  git branch -a
* temp
  remotes/origin/HEAD -> origin/dev
  remotes/origin/dev
  remotes/origin/main

frankie@iMac repo-demo %  git pull
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-to=origin/<branch> temp

好,我们切换到本地的 main 分支,远程仓库有与之对应的同名分支。

# 相当于 git pull origin main
$ git pull

拆分为以下步骤:

  • git fetch origin main
    将远程仓库的 main 分支最新 Commit-ID 记录到 .git/FETCH_HEAD 中,此时 FETCH_HEAD 指向该 Commit-ID。

  • git merge FETCH_HEAD
    FETCH_HEAD 对应的 Commit-ID 合并至本地 main 分支中。如果合并过程不存在冲突(即只是 Fast-Forward),那么可以顺利完成 git pull 最后一个步骤,否则的话,需要手动解决冲突。

四、More...

其实上面介绍了很多,细心的同学可能会发现,其实无论是「本地分支」,还是「远程分支」,它们记录的只是一个 Commit-ID 或者是对某个分支的引用(形如 ref: refs/heads/main)。

我们观察本地仓库的 .git 目录可以发现,我们的本地分支、远程分支、标签都是存在于 .git/refs/ 目录下:

frankie@iMac repo-demo %  tree .git/refs
.git/refs
├── heads
│   ├── dev
│   └── main
├── remotes
│   └── origin
│       ├── HEAD
│       ├── dev
│       └── main
└── tags

前面介绍过,「分支」是由一个或多个 Commit-ID 组成的集合。但我们合并的确是实实在在的代码啊,那么这些代码被存放到哪呢?

具体数据都被放在 .git/objects/ 目录下。

然后,现在回头再看 .git/config 的配置,看起来是不是很容易理解了。

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = git@github.com:toFrankie/repo-demo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
    remote = origin
    merge = refs/heads/main
[branch "dev"]
    remote = origin
    merge = refs/heads/dev

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容