我在这里是一个单独的用户。
我认为这意味着您只在自己的存储库上工作。然而,你接着说:
git remote set-url path/to/remote/repo
这表明您想与他人协调,这与“单独用户”的说法相矛盾。
同时,让我们从基础开始。
版本控制、存储库和工作树
当您使用任何版本控制系统 (VCS) 时,您就是在声明对控制版本感兴趣。也就是说,您希望保留并能够访问各种文件的旧版本。为此,我们需要将每个保存文件的每个保存版本存储在某处。保存这些版本的位置是 repository。
某些版本控制系统对单个文件进行操作。 Git 不会:Git 存储 commits,它们是一次完整的 sets 文件。修订或版本控制的单位是提交。如果提交只是按顺序编号(尽管它们不在 Git 中),那么第一次提交,提交 #0 或 #1 取决于我们如何计数,其中可能有十几个文件。每个后续提交也包含所有这些文件(加上您添加的任何文件,减去您删除的任何文件)。告诉 VCSget me version 3 意味着“回到我保存第 3 版时的时间,并获取所有这些文件。”
要实现这项工作,面向提交的 VCS 需要一个工作树(或连字符,或“工作树”,或此主题的任何数量的类似变体)。在这个工作树中,你有你的文件。如果您提取旧版本,您将获得与该版本相同的所有文件。如果你跳转到最新的(分支的“头”)版本,你会得到所有最新的文件。同时,您还可以更改工作树中的文件,对它们进行处理。最终,您将告诉 VCS 将新的工作树保存为新的提交。 (Git 在这里增加了几处皱纹。)
Git 的提交和 Git 风格的分支
不同的 VCS 有不同的处理分支的方式。 Git的很不寻常。 Git 的分支由 Git 的提交形成。 Git 中的每个提交(实际上,Git 存储在存储库中的每个 对象,尽管您大多只会看到这些提交)都有一个唯一的 ID,Git 通过一些深奥的魔法分配, 1 那是一串难以理解(通常是无法发音)的数字和字母:1f93ca2395be0f98... 或类似的。
我们已经提到,提交存储工作树的快照,就像提交时一样。 (Git 存储了 Git index 的快照,但我们将把它完全留到另一篇文章中。)
在 Git 中,每个提交不仅有这个工作树快照,而且还有:
-
提交者:提交者的姓名和电子邮件地址,带有时间戳
-
作者: 文件作者的姓名和电子邮件地址(通常与提交者相同,但可以通过电子邮件发送补丁,从而将它们分开),有时间-邮票
-
日志消息,这是提交者编写的内容,用于向自己和其他人稍后回顾提交描述提交
-
父提交的身份
parent 提交是该新提交之前 的提交的哈希 ID。也就是说,如果我们从一个完全空的存储库开始并进行第一次提交,我们可能会这样绘制它:
A
(使用单个大写字母而不是难以理解的哈希 ID——我们将在 26 次提交后用完!)。
现在,当我们进行 new 提交时,它看起来像这样:
A <-B
我们说新的提交B“指向”第一个提交A。因为A 是第一次提交,所以它没有指向任何地方:它根本没有父级。它不能;这是 first 提交。技术术语是 A 是一个 root 提交。
当我们第三次提交C时,它又指向B:
A <-B <-C
等等。
在文本中绘制这些箭头是一件痛苦的事,而且并不是很有用,因为这些箭头显然总是指向后面。您不能将提交点 forwards 指向尚不存在的子级,您只能将 backwards 指向存在的父级。而且,这些箭头永远不会改变:任何提交都不会改变。 (如果您尝试更改某些内容,则哈希 ID 会更改,因为哈希 ID 是内容的加密校验和!)所以我们只需制作一条连接线:
A--B--C--D
要找到 最新 提交,Git 需要一些帮助。这是分支名称进入图片的地方:分支名称只是一个带有指向某个提交的箭头的名称。
与提交的out 箭头不同,从分支名称出来的箭头not 是固定的。随着我们添加新的提交,它一直在变化。所以我们把它们画进去:
A--B--C--D <-- master
分支名称 master 指向分支上的tip(最近的)提交。
要创建一个新的分支,在 Git 中,我们只需选择我们已经拥有的任何开始提交(通常是一些现有的分支提示,如 D),并将其设为当前提交,同时也将一个指向它的新分支 name:
A--B--C--D <-- br1 (HEAD), master
我们现在有两个名称指向提交D,所以我们需要知道哪个是“我们的”。这就是我们在这里添加HEAD 的原因,以便我们知道我们“打开”的分支被命名为br1。现在让我们进行一个新的提交E。 Git 将移动当前分支名称br1 以指向新的提交。新的提交将指向我们曾经的提交,即D。我们需要在新行上绘制:
A--B--C--D <-- master
\
E <-- br1 (HEAD)
让我们回到master 并在那里添加一个新的提交,通过执行git checkout master,对一些文件进行一些更改,然后git adding 和git commiting 他们使F:
A--B--C--D--F <-- master (HEAD)
\
E <-- br1
我们在这里画的这个东西是提交图。该图在技术上是有向无环图或 DAG,因此也称为“DAG”。了解这些 Git DAG 是有效使用 Git 的关键之一。
1这个 ID 目前实际上是一个以十六进制表示的 160 位数字。 ID 是通过计算对象内容的加密散列来找到的。这保证了每一个都是独一无二的,尽管随着时间的推移,失败的可能性很小。为了保持可接受的机会,最好不要将超过 1.7 万亿个对象放入任何一个 Git 存储库中。请参阅How does SHA generate unique codes for big files in git 了解更多信息。
远程和分布式存储库:git fetch 和 git push
使 Git 特别有趣和现代的原因在于我们可以分发存储库的想法。也就是说,我们可以拥有其中一个存储库,以及它的提交和分支,然后放入一个 second Git 存储库,具有它自己的 提交和分支。 Git 在内部进行这项工作的方式首先是产生那些奇怪的哈希 ID 的原因:这些 ID 不仅对于 您的 存储库是唯一的,而且实际上在 所有共享的中都是唯一的> 存储库。
这意味着如果你交叉连接两个不同的 Git 并告诉它们共享一些提交,每个 Git 可以判断另一个 Git 是否已经提交。如果你从他们那里得到提交,但你已经有了那个特定的,你不必再次得到它。如果您还没有拥有它,那么你得到它,现在你拥有它。如果您向它们提交到,则相同的方法以相同的方式工作:如果你们都有哈希ID,那么你们都有对象;如果没有,一个 Git 将对象的副本提供给另一个,现在两者都拥有它。
因为每个提交父链接都是一个哈希 ID,所以提供或获取他们或您还没有的所有提交就足够了。谁没有提交,现在有。获得提交(和其他相关对象)的人中的新 DAG 现在已满。
这个传输提交的过程是Git的fetch和push操作。运行 git fetch 意味着“调用其他 Git,并从该 Git 获取 (fetch) 提交和其他对象,进入我的存储库。”运行 git push 意味着“调用其他 Git,并为他们提供提交和其他对象来自(从推送)我的存储库。”
远程跟踪分支
这里有一个问题,尤其是在git fetch 方面。我们在上面提到 Git 通过分支名称找到 latest 提交。当我们从一些 other Git 获得一些新的提交时,会发生一些有趣的事情。考虑我们上面绘制的图表:
A--B--C--D--F <-- master (HEAD)
\
E <-- br1
假设我们 git fetch 并引入了两个新的提交 G 和 H,我们这样绘制:
G--H
/
A--B--C--D--F <-- master (HEAD)
\
E <-- br1
我们如何让 Git 找到提交 H?如果它们只是像这样的连续字母,我们的 Git 可以,比如说,记住有八个提交,然后找到 H。但它们不是——它们是难以理解的哈希 ID。我们使用自己的分支名称,如master 和br1,分别记住F 和E 的哈希ID。
这是远程跟踪分支名称输入图片的地方。 (这个术语,remote-tracking branch,在我看来并不是一个很好的名字,但它是我们所拥有的,它就足够了。)
他们的 Git 要提交H,他们必须有一些分支名称——可能是他们的master——指向到提交H。如果我们有 我们的 Git 记住 他们的 Git 的分支名称,但使用其他名称,我们可以让 我们的 Git 以这种方式定位 H。所以这就是我们得到的:
G--H <-- origin/master
/
A--B--C--D--F <-- master (HEAD)
\
E <-- br1
名称origin/master,我们在其分支名称前加上origin/,跟踪“master 在另一个 Git 上的位置”。
origin 这个名字来自 Git 所称的 remote。任何其他 Git 的标准单个远程名称是 origin,因为我们通常通过执行 git clone 来设置这一切。我们克隆其他一些现有 Git 存储库,获取所有 的提交和所有 的分支。然后我们重命名它的所有分支,使其master 是我们的origin/master,它的br1 是我们的origin/br1。
(顺便说一下,这个 remote 主要是 URL 的简称。但它也是每个远程跟踪分支的前缀。)
虽然您可以 git checkout 远程跟踪分支名称(例如尝试 git checkout origin/master),但这会立即导致 Git 称为 分离的 HEAD。在这种情况下,名称 HEAD 不再指代 any 分支。我们得到的结果如下所示:
G--H <-- HEAD, origin/master
/
A--B--C--D--F <-- master
\
E <-- br1
名称HEAD 现在直接指向提交H,而不是包含指向提交H 的分支 的名称。我们的master 指向提交F,我们的br1 指向E;我们没有任何分支名称指向H。我们只有一个 remote-tracking 分支 名称,而 remote-tracking 分支 不是分支:它只是一个名称。2
2更糟糕的是,Git 有一个动词,tracking,它的意思与 all 不同。您现在可能会明白为什么我认为“远程跟踪分支”不是一个好名字。我们可以用不同的方式使用“远程”、“跟踪”和“分支”这几个词来表示不同的东西,在我们都感到困惑之前,我们可以使用多少次? :-)
git checkout 做了什么
我们已经提到我们可以使用git checkout 来检查提交或分支。这是它主要做的两件事:签出提交,或签出分支。
它是做什么的?好吧,它“更喜欢”分支。如果你:
git checkout master
然后,由于master 是分支名称,它检查分支名称master,将HEAD附加到master。 br1 也是如此:这是一个分支名称,因此可以将其作为分支检出。
但是,如果您 git checkout <hash-id>,它会检查特定的提交,并进入这种“分离的 HEAD”模式。如果您尝试签出标签名称或远程跟踪分支名称,也会发生同样的情况。这两个都不是分支名称,所以你不能将它们作为分支“打开”,所以它只是检查提交。
当git checkout 签出一个提交时,它会重新排列工作树(以及Git 的索引,我们之前提到过,但这里仍不解释)以匹配该提交。检查分支实际上也是如此。当我们git checkout master 时,我们得到on branch master,正如git status 所说;但这具有从该分支的 tip 提交中填充索引和工作树的效果。
在一个分支上 意味着当我们进行 new 提交时,Git 将使 分支名称 指向新的提交。我们在上面看到 master 和 br1 如何增加新的提交 E 和 F:这是因为当我们 git commit 时我们在这些分支上。
合并:git merge
只要我们在某个分支上,并且有一个干净的索引和工作树(使用git status 来检查——经常使用git status!),我们可以让 Git 合并我们的提交与其他一些提交一起进行新的合并提交。3
要执行此合并操作,Git 必须找到 合并基础。如果您使用过其他 VCS,其中一些需要您手动查找合并库。 Git 使用提交图 - DAG - 为您找到合并基础。
让我们继续我们的示例,我们在存储库中通过origin/master 引入了两个我们命名的提交。让我们继续我们的master,这是我们的提交F。我将重新绘制图表并在此处完全省略 br1,因为我们现在不需要它:
A--B--C--D--F <-- master (HEAD)
\
G--H <-- origin/master
现在我们在分支 master 上,git status 表示 nothing to commit, working tree clean,我们将运行:
git merge origin/master
这告诉我们的 Git 找到提交 H 并将其与我们当前的提交 F 合并(我们的 Git 通过我们的 HEAD 找到)。 Git 搜索提交图以找到第一个 可从H 和 F 的提交,沿着父箭头向后工作。我们可以通过查看看到这是commit D。
然后,Git 实际上运行:
git diff D F # to find out what we changed since D
git diff D H # to find out what they changed
然后,合并代码会尽力合并这些更改。如果一切顺利,它将合并的更改写入索引和工作树,然后运行git commit 以进行新的合并提交。
与往常一样,此合并提交在我们的master 上进行,但有点奇怪,它有两个 父级。 first 父级是我们之前的分支提示提交F。 second 父级是我们刚刚合并的提交,即提交H。结果如下图所示:
A--B--C--D--F---I <-- master (HEAD)
\ /
G--H <-- origin/master
我们的master 现在指向这个新的合并提交I,I 又指向F 和H。
3这是另一个 Git 重载单词的例子:我们 merge(作为动词)两次提交,然后我们进行 merge commit em>(作为形容词合并),我们称之为a merge(作为名词合并)。重要的是要记住,merge-as-a-verb 是一个 action,而 merge-as-a-noun(或形容词)指的是 merge commit。如果我们愿意,我们可以让 Git 在不创建合并提交的情况下执行合并作为动词。但这也是以后的话题。
请注意,git merge 并不总是合并
有时git merge 没有 来合并事物。例如,假设我们根本没有提交F?假设我们从这个开始:
A--B--C--D <-- master (HEAD)
\
G--H <-- origin/master
如果我们现在运行git merge origin/master,Git 可以看到合并基础提交D 是 当前 提交。这意味着 Git 不需要做任何工作——它根本不需要合并动词。相反,Git 可以只git checkout 提交H,也可以让我们的名字master 指向提交H:
A--B--C--D
\
G--H <-- master (HEAD), origin/master
现在我们不需要图表中的扭结了:
A--B--C--D--G--H <-- master (HEAD), origin/master
在另一种愚蠢的命名综合症中,Git 称之为 快进合并,即使不涉及合并(也没有任何可以高速旋转的录音设备,虽然现在我们都习惯于“快进”通过 Netflix 上的数字电影或其他)。
关于git pull(不要用)
git pull 命令是一种方便快捷的方式。而且它有时很方便,但它也是一个陷阱。
很长一段时间以来,在旧版本的 Git 中,git pull 中存在许多错误,这些错误有时会破坏您的工作。我相信这些都是固定的,所以如果你有一个现代的 Git,这不是一个真正的问题。但它还有其他一些缺点,例如隐藏它所做的只是运行git fetch 后跟第二个命令,通常是git merge。
如果您使用git pull,您不会了解git fetch 的作用,也不会意识到您正在运行git merge。一切似乎都太神奇了。此外,如果git merge 步骤失败——最终会失败——你可能会非常无助:你不会知道自己正处于冲突合并中,更不用说如何阅读了关于如何处理。
最后,虽然它很小,但git pull 的语法很奇怪。这是因为它实际上早于遥控器和远程跟踪分支名称的发明。 (事实上,这就是为什么看起来pull,而不是fetch,应该与push相反:原来是这样!)
代替:
git fetch origin
git merge origin/master
(这是有道理的),你运行:
git pull origin master
为什么这是origin master 而不是origin/master?或者,如果您隐约知道涉及到git fetch 步骤,那为什么不是git pull origin origin/master?为什么我们git merge origin/master 而git pull origin master?答案都与 Git 的古老历史有关,而且它们都不是真正有用的——除了它们解释了为什么 git fetch origin master br1 是一个非常糟糕的主意(不要这样做! )。
如果您完全避免使用git pull(请记住,它只是在git fetch 后跟第二个 Git 命令),您将学习git fetch 和其他 Git 命令。一旦你真正理解了它们,你就可以开始使用git pull,如果你觉得它更方便的话:你就会知道,当它出错时,该怎么办。但在那之前,我建议避免它。