该git pull
命令与大多数其他 Git 命令完全不同。我想说,在很多方面,最接近的其他 Git 命令是git gc
,它——就像git pull
——是一个方便的包装器,以避免需要输入多个单独的 Git 命令。1
什么git pull
是:
- 运行
git fetch
;然后
- 运行第二个 Git 命令。
第一个命令git fetch
需要远程的名称。该名称origin
是第一个远程的标准名称,并且由于大多数 Git 存储库只有一个远程,origin
因此是该存储库中第一个、最后一个和唯一远程的名称。
你可以把它关掉——你可以在git pull
没有额外参数的情况下运行——Git 会找到一些合适的遥控器。但是如果你要提供额外的参数,第一个非选项参数是远程名称,所以git pull frabjous
使用这个词frabjous
作为远程的名称。
第二个命令是git merge
or git rebase
。2 第二个命令需要一个提交哈希 ID,例如4c53a8c20f8984adb226293a3ffd7b88c3f4ac1a
,或可以代替提交哈希 ID 的东西。3 但是,我们通常使用一个名称——一个分支名称,如master
or main
、 ordev
或其他任何名称——作为此处的“可行的东西”。 总的想法——思考的方式——是git pull
:从另一个人那里得到东西,然后合并它。 这里的“其他人”是遥控器,“要获取的东西”是“他在某个分支上的任何新提交”。所以你在这里输入的名字,当你输入一个名字时,是另一个人的分支名称。
请注意,与 一样git fetch
,您可以忽略所有这些,然后运行:
git pull
pull 命令将根据您为当前分支设置的上游确定要使用的遥控器(可能)origin
和要使用的名称,这一切都是独立的。“上游”只是您可以设置的东西:对于您名为 的分支,上游可能已经设置为.xyzzy
origin/xyzzy
请注意,此处的上游名称 ,origin/xyzzy
中有一个斜杠:它由远程名称, origin
、斜杠和远程分支名称,组成xyzzy
。因此,如果在遥控器上看到的分支名称是frab/jous
,您将在origin/frab/jous
此处使用两个斜杠:一个与origin
另一个人的分支名称分开,一个在另一个人的分支名称中。
如果你要输入一个名字,在你的git pull
命令中,你必须把它放在 remote 之后。完成此操作后,Git 假设您只需输入远程上看到的分支名称。所以你输入:
git pull origin frab/jous
或这里的任何内容,意思是:
- 运行
git fetch origin
;然后
- 解析
origin/frab/jous
为哈希 ID 并根据需要运行git merge
或运行git rebase
。
请注意,这两个步骤中的任何一个都可能完全失败,而第二个步骤可能会在中间停止。如果一个步骤失败,则任何剩余步骤都不会发生,并且您应该从失败点重新开始,无论是什么,如果您想从中断的地方继续 - 所以您需要知道哪一步失败,如果有一个失败。幸运的是,对于我们大多数人来说,运行额外的时间git fetch
是非常安全的,所以我们几乎可以忽略它的失败与成功。但是您仍然需要知道是完成中间停止的合并还是变基。由于这个和其他原因,我总是鼓励 Git 新手先学习单独的命令. 认识到他们何时工作、何时完全失败以及何时停止是很重要的。
不幸的是,这意味着您需要了解这种奇怪之处,即在哪里git pull
使用其他人的名字作为分支(省略origin/
),git merge
或者使用您的git rebase
名字(包括)。但是无论如何你都必须学习这个。记下它! 他们的分支名称是他们的;您的Git 存储库从它们中读取它们的 name-and-hash-ID 值(在该步骤期间),并将它们存储在您的Git 存储库中以这些-prefixed 名称。origin/
git fetch
origin/
这还是漏掉了很多。Git 的设置学习曲线非常陡峭。我现在休息一下脚注,然后讨论另一件事。
1git gc
运行git repack
, git prune-packed
, git reflog expire
, git worktree prune
, git prune
, git pack-refs
, 和/或git rerere gc
如果/酌情。这并不意味着是一个完全详尽的列表,因为该列表有时会发生变化(例如,git worktree
在 Git 2.5 之前不存在)而且我并没有真正跟踪。我通过浏览文档生成了这个列表git gc
。我认为这个特定的手册页可能是https://git-man-page-generator.lokaltog.net/的主要灵感来源
2有一些特殊情况例外,包括如果git fetch
步骤失败则什么也不做。
3这有点过于简单化了,因为git merge
并且git rebase
可以采用多个哈希 ID,并且对于从未使用过的情况git pull
,git rebase
也需要分支名称。但是,出于由 运行的目的, git pull
它们最终在此处使用哈希 ID。
origin
既是遥控器又是……嗯……
但是为什么会这样
git rebase -i origin
作品?
这是陡峭的学习曲线的另一部分让你大吃一惊的地方。
最后,Git 是关于提交的。存储库中的提交是使用 Git 的原因。单个提交是有编号的,但数字本身是大的、丑陋的、看起来随机的东西,完全不适合人类。例如,这些是散列 ID 或对象 ID git log
。它们只有通过剪切和粘贴才能真正使用,所以我们基本上不使用它们:我们使用names。
因此,Git 提供的不是一个而是两个 键值数据库。其中之一由哈希 ID 索引,这就是 Git 获得对其提交和其他内部数据的访问权限的方式。Git 放入一个哈希 ID,并获取其键是该特定哈希 ID 的提交或其他对象。当对象是提交对象时,它代表每个文件的完整快照,以您(或任何人)提交时的形式一直冻结。
不过,为了找到哈希 ID,Git 保留了第二个数据库,其中的键是名称:分支名称、标签名称和其他类型的名称。分支名称,如master
or main
、dev
or develop
、frab/jous
等等,由您决定:您可以选择任何您喜欢的名称(尽管在 [0-9a-f] 集合之外插入破折号或斜线或字母是明智的,因为“名称”cafebabe
和badf00d
和deadcab
可能是哈希 ID 的缩写)。为了防止分支名称和标签名称相互碰撞,Git 实际上粘贴refs/heads/
在每个分支名称refs/tags/
的前面,以及每个标签名称的前面。
为了记住其他 Git 存储库的分支名称,Git 存储在您的存储库中的名称是远程跟踪名称(Git 将这些远程跟踪分支名称称为),并且实际上以 为前缀refs/remotes/
,而不是origin/dev
,这些实际上是refs/remotes/origin/dev
.
所有这些名称,在这些不同的命名空间中,每个都拥有一个哈希 ID。这就是 Git 所需要的,因为提交本身也包含其他提交哈希 ID。从一个提交中,Git 可以找到另一个。从那里,Git 可以找到另一个提交——依此类推。Git 简单地将分支名称定义为“此名称包含提交的哈希 ID,该提交将在此分支上被称为最新的”。
因此,如果您在某个分支上main
,则该名称包含一些哈希 ID H
,这是某个提交的哈希 ID:
<-H <-- main
每个提交都包含一个先前提交的哈希 ID 列表,通常只有一个条目长,以及所有文件的快照。H
那是从, 这里出来的向后箭头。CommitH
持有一些较早提交的哈希 ID。让我们称之为G
并把它画进去:
<-G <-H <-- main
当然,G
是一个带有快照和另一个向后箭头的提交,所以它必须指向一些更早的提交,它一遍又一遍地重复:
... <-F <-G <-H <-- main
那是一个 Git 分支。要将提交添加到分支,我们按名称“签出”或“切换到”,使名称成为当前分支名称,对应的提交H
成为当前提交。
我们可以有多个名称指向此提交。让我们画几个名字:main
and dev
and Also origin/main
,它不是一个分支名称,但仍然指向一个提交。为了懒惰,我将停止在提交之间使用箭头,但请记住 Git 只能向后工作,从不向前:
...--F--G--H <-- dev, main, origin/main
我们选择一个分支——比如说dev
——切换到。为了记住我们使用的是 name dev
,我们将特殊名称附加HEAD
到它:
...--F--G--H <-- dev (HEAD), main, origin/main
现在我们调整使用 Git 的方式——我不会在这里介绍,但索引或暂存区域(同一事物的两个术语)至关重要——并最终做出一些新的提交。新的提交,我们称之为I
,有一个新的唯一哈希 ID 并向后指向现有的提交H
,如下所示:
...--F--G--H
\
I
棘手的一点是 Git 在完成新的提交后立即更新当前分支名称I
。其他名称均未更新,因此它们仍指向H
:
...--F--G--H <-- main, origin/main
\
I <-- dev (HEAD)
CommitI
现在是. _ dev
提交通过H
仍在进行中dev
,并且也将继续进行main
。特殊名称HEAD
仍然附加到dev
,我们当前的提交现在是 commit I
。提交H
仍然存在(而且,对于 Git 的散列方案而言至关重要的是,它完全没有受到影响:这就是为什么箭头都向后而不是向前)。
好吧,但是——那又怎样?好吧,正如我之前所说,Git 是关于提交的。当您给 Git 一个分支名称时,大多数情况下它会通过找出名称指向的位置来快速将该名称转换为哈希 ID。(git switch
andgit checkout
命令在这里很不寻常,因为它们也必须记住名称,以便在它们完成后您可以“打开”该分支。)有一个命令行 Git 命令可以为您执行此操作,即git rev-parse
. 如果我们给出git rev-parse
一些分支名称,我们可以看到它的实际作用:
$ git rev-parse master
5d01301f2b865aa8dba1654d3f447ce9d21db0b5
$ git rev-parse diff-merge-base
fa1c8acabf0d5649baf87f549d67426d14255e0f
它也可以解析标签名称和远程跟踪名称,并且--symbolic-full-name
可以告诉我们每个名称的完整拼写是什么:
$ git rev-parse --symbolic-full-name v2.35.1
refs/tags/v2.35.1
$ git rev-parse --symbolic-full-name origin/master
refs/remotes/origin/master
$ git rev-parse origin/master
5d01301f2b865aa8dba1654d3f447ce9d21db0b5
如果我们origin
单独给它会发生什么?
$ git rev-parse origin
5d01301f2b865aa8dba1654d3f447ce9d21db0b5
$ git rev-parse --symbolic-full-name origin
refs/remotes/origin/master
嗯,这有点奇怪,不是吗?让我们看一下gitrevisions 文档,它非常重要,并且巧妙地隐藏在一堆 1000 个基本上不可读的手册页中:
SPECIFYING REVISIONS
...
... a通过以下规则中的第一个匹配来消除歧义:
<refname> e.g., master, heads/master, refs/heads/master
<refname>
- 如果
$GIT_DIR/<refname>
存在,这就是你的意思(这通常只对HEAD
, FETCH_HEAD
, ORIG_HEAD
,MERGE_HEAD
和有用CHERRY_PICK_HEAD
);
- 否则,
refs/<refname>
如果存在;
- 否则,
refs/tags/<refname>
如果存在;
- 否则,
refs/heads/<refname>
如果存在;
- 否则,
refs/remotes/<refname>
如果存在;
- 否则,
refs/remotes/<refname>/HEAD
如果它存在。
正是这个六步规则使名称缩写起作用。我们写:
git rebase master
并且 Gitmaster
在(步骤 1)中尝试作为文件.git
,但它不存在,因此 Git 继续尝试refs/master
作为名称(步骤 2)。这也不存在,因此 Git 尝试refs/heads/master
作为名称(步骤 3)。那个确实存在,无论如何在这个存储库中,所以它解析为一个哈希 ID 并且修订指定是完整的。
如果我们使用origin/master
,步骤 5 会找到它,因为refs/remotes/origin/master
存在(用于git for-each-ref
转储 ref 表,并查看它确实存在)。如果我们使用origin
——这似乎根本不是一个引用名称——第6步会找到它,因为refs/remotes/origin/HEAD
存在。
现在,HEAD
——相应地,refs/remotes/origin/HEAD
——是一种特殊情况:它是一个符号引用,在 Git 中类似于 Unix/Linux 文件系统中的符号链接。(事实上,在早期的 Git 实现中,它只是一个符号链接。虽然在 Windows 上不能很好地工作,所以现在它是一个包含内容的文件。)该git for-each-ref
命令默认扩展链接,但git branch -r
没有,所以这是一个看到这个的方法。
底线
这一切的结论是:
origin/HEAD
是HEAD
in的任何分支的符号 ref origin
,通常是master
or main
;
origin
其本身是远程的(由 使用git fetch
),或可通过 gitrevisions 的第 6 步解析(与大多数其他 Git 命令一样);
git rebase -i origin
通过origin/HEAD
第 6 步解决;但
git pull origin master
根本不使用第 6 步:字符串origin
只是一个remote,并且字符串master
通过远程跟踪名称映射成为origin/master
(在这种特殊情况下git pull
实际上回避了所有这些,因为它使用了.git/FETCH_HEAD
文件机制,这早于所有这些东西并通过一些不同的代码路径)。
该git pull
命令将其大部分标志和参数传递给git fetch
,除了它传递给第二个命令的一些标志和它自己使用的一些标志。由于历史......错误,它非常复杂?想法?概念?无论如何,Git过去工作方式的历史,必须在接下来的 3 亿年或其他任何时间里保存在琥珀中。(不过说真的,Git 人非常重视兼容性本身,并尽量不破坏现有的使用和工作流程。)