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
接收所有的元数据,包括对这个提交的父代的引用,并将其与你的文件的状态打包成提交。然后,Git
用SHA1
对这些东西进行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)
意味着这个提交被main
和testBranch
两个分支所指向。这个提交之所以被两个标签所指向,是因为你只创建了一个新的分支,而没有在这个分支上创建任何其他的提交。所以在你再次提交之前,这个标签不能向前移动。
检查你的当前分支¶
如果你需要知道,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
是什么东西?
origin
是Git
使用的另一个便利约定。就像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
会用它对本地和远程分支当前状态的理解来回应,并通过-v
(verbose
)选项提供一些额外信息:
* 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>
。这又是一种创建本地分支的更快方式。
现在你已经看到了如何创建、切换到和删除分支,是时候进行本章的简短挑战了,这将有助于巩固你所学到的知识,并告诉你当你想删除一个已经有提交的本地分支时,该怎么做。
挑战¶
挑战:删除一个有提交的分支¶
在这个挑战中,你不想破坏现有的分支,所以你需要创建一个临时的本地分支,切换到该分支,做一个提交,然后删除该分支。
- 创建一个名为
newBranch
的临时分支。 - 切换到该分支。
- 使用
touch
命令在项目的根目录下创建一个空的README.md
文件。 - 把这个新的
README.md
文件添加到暂存区域。 - 提交该修改,并附上适当的信息。
- 签出
main
分支。 - 删除
newBranch
--但Git
不会让你在当前状态下删除这个分支。为什么? - 按照Git给你的建议,看看你是否可以删除这个分支。
记得使用git status
、git branch
和git log --oneline --graph --all
来帮助你在做这个挑战时找到方向。
如果你被卡住了,或者想检查你的解决方案,你可以在本章的 "挑战 "文件夹下找到这个挑战的答案。
关键点¶
Git
中的提交包括仓库中文件的状态信息,以及提交时间、提交者、提交的父级或母级等元数据。- 提交的哈希值成为唯一的
ID
,或者说密钥,用来识别仓库中的特定提交。 - 在
Git
中,一个分支只是通过哈希值对一个特定提交的引用。 main
只是一个方便的约定,但已被接受为仓库的原始分支。- 使用
git branch <branchname>
来创建一个分支。 - 使用
git branch
查看所有本地分支。 - 使用
git checkout <branchname>
切换到本地分支,或签出并跟踪远程分支。 - 使用
git branch -d <branchname>
来删除一个本地分支。 - 使用
git branch --all
查看所有本地和远程分支。 origin
和main
一样,只是一个方便的约定,是远程仓库的URL
的别名。- 使用
git checkout -b <branchname>
可以一次性创建并切换到本地分支。
接下来去哪?¶
习惯于使用Git
分支,因为你会经常这样做。轻量级分支是Git
吸引这么多追随者的原因,因为它符合并发开发团队的工作流程。
但是,如果能在一个分支上工作,却不能把你的工作连接到主开发分支上,那就没什么意义了。这就是merging
,而这正是你在下一章要做的事情。