跳转至

7.分支

Git最初设计的驱动因素之一是支持混乱的、非线性的开发方式,这种方式源自于大规模、快速发展的项目。我们需要将开发从主线上分离出来,独立于主线上的其他改动,轻松地将这些改动合并回去,并以轻量级的方式完成这一切,这就是促使Git的创造者们建立一个非常轻量级的、优雅的模型来支持这种工作流程的原因。

在本章中,你将探索这一范式的前半部分:branching。在第一章"Git速成班"中,你已经简单地接触了分支,但你可能并不完全理解你或Git在那一刻在做什么。

虽然你可以在你的开发生涯中蹒跚而行,从未真正理解过Git的分支是如何工作的,但分支对许多大小开发团队的开发工作流程来说是重要的,所以知道引擎盖下发生了什么,并对仓库的分支结构有一个坚实的心理模型,将对你的项目规模和复杂性的增长有极大的帮助。

什么是提交?

这个问题在几章前已经浅显地问过并回答过了,但现在是重温这个问题并更详细地探讨提交的好时机。

回顾一下,一个提交代表了你的项目树--你的目录--在某个特定时间点的状态:

├── LICENSE
├── README.md
├── articles
│   ├── clickbait_ideas.md
│   ├── ios_article_ideas.md
│   └── live_streaming_ideas.md
├── books
│   └── book_ideas.md
└── videos
    ├── content_ideas.md
    └── platform_ideas.md

你可能主要从文件的内容、它们在目录层次结构中的位置以及它们的名字来考虑文件。因此,当你想到提交时,你可能会想到文件的状态,它们在某一特定时间点的内容和名称。这在一定程度上是正确的。Git还以元数据的形式为"文件的状态 "这一概念增加了一些信息。

Git元数据包括诸如"这是什么时候提交的?"和"谁提交的?",但最重要的是,它包括"这个提交是从哪里来的?"的概念--这部分信息被称为提交的parent。一个提交可以有一个或两个父节点,这取决于它是如何被分支和合并回来的,但你会在后面提到这一点。

Git接收所有的元数据,包括对这个提交的父代的引用,并将其与你的文件的状态打包成提交。然后,GitSHA1对这些东西进行hashes,以创建一个ID,或key,在你的仓库中对该提交是唯一的。这使得通过哈希值来引用一个提交变得非常容易,或者就像你在前一章看到的,它的短哈希值。

什么是分支?

分支的概念在Git中非常简单。它只是一个参考,或者说是一个标签,指向你的仓库中的一个提交。就是这样。真的。因为你可以通过哈希值来引用一个提交,所以你可以看到创建分支是一个非常便宜的操作。没有复制,没有额外的克隆,只是Git说"好的,你的新分支是提交477e542的标签"。嘭,搞定。

当你在你的分支上进行提交时,该分支的标签会向前移动,并随着每次新提交的哈希值而更新。同样,Git所做的只是更新这个标签,它作为一个简单的文件存储在隐藏的.git仓库中,是一个非常便宜的操作。

你一直都在一个分支上工作--你意识到了吗?是的,main或任何你选择的作为你的版本库的起始分支的东西,只不过是一个普通的分支。只是根据惯例,以及Git在创建新仓库时对这个默认分支使用的默认名称,我们才会说:"哦,main分支是原始分支。"

Note

2.28.0版本开始,Git现在提供了一个设置,你可以通过它来控制在新仓库中创建第一个分支时使用的标签。默认是master,但你可以选择将其设置为main或任何你喜欢的。 设置是init.defaultBranch,你可以用下面的命令改变它。$ git config --global init.defaultBranch main。 这将默认的分支名称设置为main。 这只影响到你创建的新仓库;它不会改变任何现有仓库的默认分支名。

main没有什么特别之处;同样,Git只是知道main分支是你仓库中的一个修订版,由磁盘上的一个文件中的简单标签指向。很抱歉打破了任何认为main有魔力或其他的想法。

创建一个分支

之前在速成章节中你已经创建了一个分支,但现在你要创建一个分支,并准确观察Git在做什么。

Git中,创建分支的命令是git branch,后面是你的分支名称。

执行下面的命令来创建一个新的分支:

git branch testBranch

Git在完成这一动作时并不大张旗鼓,因为对Git来说,一个新的分支并不是什么大事。

Git如何跟踪分支

要想知道Git到底做了什么,可以执行以下命令,看看Git在后台做了什么:

ls .git/refs/heads/

这个目录包含指向你所有分支的文件。我得到该目录中的两个文件的如下结果:

main        testBranch

哦,这很有趣--一个名为testBranch的文件,与你的分支名称相同。用下面的命令看一下testBranch,看看里面有什么:

cat .git/refs/heads/testBranch

哇,Git是对分支真的很简单的。所有这些都是一个单一的哈希值。为了把这个问题提高到一个新的水平,你可以证明testBranch的标签是指向你的仓库中真正的最新提交。

执行下面的命令可以看到最新的提交:

git log -1

你会看到如下内容(你的哈希值会与我的不同):

commit 477e542bfa35942ddf069d85fbe3fb0923cfab47 (HEAD -> main, testBranch)
Author: Chris Belanger <chris@razeware.com>
Date:   Wed Jan 23 16:49:56 2019 -0400

    Adding .gitignore files and HTML

让我们把它拆开一点。这里引用的提交确实是与testBranch中的哈希值相同。接下来的(HEAD -> main, testBranch)意味着这个提交被maintestBranch两个分支所指向。这个提交之所以被两个标签所指向,是因为你只创建了一个新的分支,而没有在这个分支上创建任何其他的提交。所以在你再次提交之前,这个标签不能向前移动。

检查你的当前分支

如果你需要知道,Git可以很容易地告诉你你在哪个分支上。执行下面的命令来验证你是否在testbranch上工作:

git branch

在没有任何参数或选项的情况下,git branch会简单地显示你的仓库中的本地分支列表。你应该有以下两个分支被列出:

* main
  testBranch

星号表示你还在main分支上,尽管你刚刚创建了一个新的分支。这是因为除非你明确告诉它,否则Git不会切换到一个新创建的分支。

切换到另一个分支

要切换到testBranch,可以像这样执行checkout命令:

git checkout testBranch

Git的回应如下:

Switched to branch 'testBranch'

这就是创建和切换分支的全部内容。

Note

无可否认,checkout这个词有点名不副实,因为如果你曾经拥有过一张借书证,你就会知道,借出一本书,在你归还之前,其他人是无法进入的。 这个术语是一些老的版本控制系统的运作方式的遗留问题,因为它们使用的是锁-修改-解锁的模式,它阻止其他人在同一时间修改该文件。这对于防止合并冲突非常有效,但几乎扼杀了任何形式的分布式、并发式开发。 说到老的版本控制系统,如果你们中有人在过去使用过PVCS版本管理器(大约2000年左右),请给我留言,我们可以交换一下关于那个令人吃惊的稀少的文档的恐怖故事,以及与semaphores的无休止的斗争,和所有其他与那个软件一起出现的有趣的部分。

testBranch的探索就到此为止,所以用下面的命令切换回main

git checkout main

你真的不需要testBranch了,因为还有其他真正的分支需要探索。用下面的命令删除testBranch

git branch -d testBranch

是时候看看一些真正的分支了。你的仓库里已经有一个,正等着你进去开始工作......那是什么?哦,你不记得上次执行 git branch 时看到过这个分支?那是因为git branch本身只显示你的版本库中的本地分支。

当你第一次克隆这个仓库时(它是从最初的ideas仓库分叉而来的),Git开始跟踪本地仓库和remote仓库--即你在GitHub上创建的分叉仓库。Git知道远程和本地系统上的分支。

因此,由于本地仓库和远程仓库之间的同步,Git知道你在本地所做的任何提交--并可能推回给远程--都属于一个特定的、匹配的、远程的分支。同样地,Git也知道任何在远程分支上所做的修改--也许是由世界上某个地方的开发者所做的--都属于你本地系统中一个特定的、匹配的目录。

查看本地和远程分支

要查看Git知道的这个仓库的所有分支,无论是本地还是远程,请执行以下命令:

git branch --all

Git会做出类似于以下的回应:

* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/clickbait
  remotes/origin/main
  remotes/origin/master

Git会显示本地和远程仓库中的所有分支。在这个例子中,远程只有两个分支。clickbait和被废弃的master分支。所有列出的其他分支实际上都是main或指向main的指针。

你在clickbait分支上有一些工作要做。如果其他人都在做,你也应该做,对吗?要把这个分支下放到你的机器上,告诉Git开始跟踪它,并一次性切换到这个分支,执行以下命令:

git checkout --track origin/clickbait

Git的回应如下:

Branch 'clickbait' set up to track remote branch 'clickbait' from 'origin'.
Switched to a new branch 'clickbait'

解释起源

好吧,你一直看到的origin是什么东西?

originGit使用的另一个便利约定。就像main是仓库中第一个分支的默认名称,origin是远程仓库的默认别名,你从那里克隆了本地仓库。

要看这个,执行下面的命令,看看Git认为origin在哪里:

git remote -v

你应该看到与下面类似的东西:

origin  https://www.github.com/belangerc/ideas (fetch)
origin  https://www.github.com/belangerc/ideas (push)

你的URL中会有不同的东西,而不是belangerc。但你可以在这里看到,origin只是远程仓库的URL的一个别名。这就是全部。

要查看Git现在对所有本地和远程分支的视图,请执行以下命令:

git branch --all -v

Git会用它对本地和远程分支当前状态的理解来回应,并通过-vverbose)选项提供一些额外信息:

* clickbait                e69a76a Adding suggestions from Mic
  main                     477e542 [ahead 8] Adding .gitignore files and HTML
  remotes/origin/HEAD      -> origin/main
  remotes/origin/clickbait e69a76a Adding suggestions from Mic
  remotes/origin/main      f65a790 Updated README.md to reflect current working book title.
  remotes/origin/master    c470849 Going to try this livestreaming thing

Git告诉你,你在clickbait分支上,你也可以看到本地clickbait分支的哈希值与远程分支相同,正如你所期望的。

值得注意的是main分支。Git正在跟踪本地main分支和远程分支,它知道本地main分支比远程分支早8个提交。Git也会让你知道你是否落后于远程分支;也就是说,如果远程分支上有任何你还没有拉到本地分支的提交。

以图形方式查看分支

要查看本地分支的当前状态,请执行以下命令:

git log --oneline --graph

图中的尖端,也就是最新的提交,告诉你你的位置:

* e69a76a (HEAD -> clickbait, origin/clickbait) Adding suggestions from Mic

你当前的HEAD指向clickbait分支,你和你的远程版本库处于同一位置。

创建分支的快捷方式

我承认,我用git checkout --track origin/clickbait这个命令让你走了很远的路,但看到这个命令的长篇大论,希望能帮助你理解当Git从远程签出并跟踪一个分支时,它实际上做了什么。

还有一个更简短的方法来签出并切换到远程的现有分支。git checkout clickbait同样有效,而且更容易输入和记忆。

当你向git checkout指定一个分支名称时,Git会检查是否有一个与该名称匹配的本地分支可以切换到。如果没有,它就会查看origin远程,如果它在远程找到一个与该名称相匹配的分支,它就认为那是你想要的分支,为你检查,并将你切换到该分支。它为你解决了所有这些问题,相当不错。

还有一个快捷命令,解决了git branch <branchname>git checkout <branchname>的两步问题:git checkout -b <branchname>。这又是一种创建本地分支的更快方式。

现在你已经看到了如何创建、切换到和删除分支,是时候进行本章的简短挑战了,这将有助于巩固你所学到的知识,并告诉你当你想删除一个已经有提交的本地分支时,该怎么做。

挑战

挑战:删除一个有提交的分支

在这个挑战中,你不想破坏现有的分支,所以你需要创建一个临时的本地分支,切换到该分支,做一个提交,然后删除该分支。

  1. 创建一个名为newBranch的临时分支。
  2. 切换到该分支。
  3. 使用touch命令在项目的根目录下创建一个空的README.md文件。
  4. 把这个新的README.md文件添加到暂存区域。
  5. 提交该修改,并附上适当的信息。
  6. 签出main分支。
  7. 删除newBranch--但Git不会让你在当前状态下删除这个分支。为什么?
  8. 按照Git给你的建议,看看你是否可以删除这个分支。

记得使用git statusgit branchgit log --oneline --graph --all来帮助你在做这个挑战时找到方向。

如果你被卡住了,或者想检查你的解决方案,你可以在本章的 "挑战 "文件夹下找到这个挑战的答案。

关键点

  • Git中的提交包括仓库中文件的状态信息,以及提交时间、提交者、提交的父级或母级等元数据。
  • 提交的哈希值成为唯一的ID,或者说密钥,用来识别仓库中的特定提交。
  • Git中,一个分支只是通过哈希值对一个特定提交的引用。
  • main只是一个方便的约定,但已被接受为仓库的原始分支。
  • 使用git branch <branchname>来创建一个分支。
  • 使用git branch 查看所有本地分支。
  • 使用git checkout <branchname>切换到本地分支,或签出并跟踪远程分支。
  • 使用git branch -d <branchname>来删除一个本地分支。
  • 使用git branch --all查看所有本地和远程分支。
  • originmain一样,只是一个方便的约定,是远程仓库的URL的别名。
  • 使用git checkout -b <branchname>可以一次性创建并切换到本地分支。

接下来去哪?

习惯于使用Git分支,因为你会经常这样做。轻量级分支是Git吸引这么多追随者的原因,因为它符合并发开发团队的工作流程。

但是,如果能在一个分支上工作,却不能把你的工作连接到主开发分支上,那就没什么意义了。这就是merging,而这正是你在下一章要做的事情。