©
本文档使用
php.cn手册 发布
gitcore-tutorial - 面向开发人员的Git核心教程
git *
本教程介绍了如何使用“core”Git命令来设置和使用Git存储库。
如果你只需要使用Git作为修订控制系统,你可能更喜欢从“Git教程简介”(gittutorial[7])或Git用户手册开始。
但是,如果您想了解Git的内部组件,那么对这些低级工具的理解会很有帮助。
核心Git通常被称为“plumbing”,其中更漂亮的用户界面称为“porcelain”。您可能不想经常直接使用管道,但可以很好地知道porcelain没有冲洗时plumbing的功能。
本文最初编写时,很多瓷器命令都是shell脚本。为了简单起见,它仍然用它们作为例子来说明管道是如何配合在一起形成瓷器命令的。源代码树包含一些在contrib/examples/中的脚本以供参考。虽然这些不再作为shell脚本实现,但对管道层命令做什么的描述仍然有效。
注意 | 更深入的技术细节通常被标记为Notes,您可以在第一次阅读时跳过。 |
---|
创建一个新的Git仓库不是一件容易的事情:所有的Git仓库都是空的,你唯一需要做的就是找到一个你想用作工作树的子目录 - 对于一个全新的项目来说是空的,或者您想要导入到Git中的现有工作树。
对于我们的第一个例子,我们将从头开始一个全新的存储库,没有预先存在的文件,我们将调用它git-tutorial
。首先,为它创建一个子目录,切换到该子目录,并使用以下命令初始化Git基础结构git init
:
$ mkdir git-tutorial $ cd git-tutorial $ git init
Git会回复
Initialized empty Git repository in .git/
这只是Git说你没有做过任何奇怪事情的方式,并且它会.git
为你的新项目创建一个本地目录设置。你现在会有一个.git
目录,你可以用它来检查ls
。对于您的新空项目,它应该显示三个条目,其中包括:
一个叫做HEAD
的文件,里面有ref: refs/heads/master
。这类似于符号链接并指向refs/heads/master
相对于该HEAD
文件。不要担心HEAD
链接指向的文件甚至不存在 - 您尚未创建将启动您的HEAD
开发分支的提交。
一个名为objects
的子目录,其中将包含项目的所有对象。你永远不应该有任何真正的理由直接看对象,但你可能想知道这些对象是什么包含data
你的存储库中的所有实体。
一个名为refs
的子目录,其中包含对对象的引用。
特别是,refs
子目录将包含另外两个子目录,分别命名heads
和tags
分别。他们完全按照他们的名字暗示:它们包含对任意数量不同heads
开发(aka branches
)的引用,以及为tags
创建存储库中特定版本而创建的任何引用。
注意:特殊的master
头是默认分支,这就是为什么.git/HEAD
文件被创建指向它,即使它尚不存在。基本上,这个HEAD
链接应该始终指向你现在正在工作的分支,并且你总是开始期待在master
分支上工作。
然而,这仅仅是一个约定,你可以随意命名你的分支什么,不必连过have
一个master
分支。不过,许多Git工具会认为.git/HEAD
是有效的。
注意 | 一个对象由其160位SHA-1散列(又名对象名称)标识,而对象的引用始终是该SHA-1名称的40字节十六进制表示。预计refs子目录中的文件将包含这些十六进制引用(通常在最后有一个最后的\n),因此当您实际启动时,您应该期望在这些refs子目录中看到许多包含这些引用的41字节文件填充你的树。 |
---|
注意 | 完成本教程后,高级用户可能想看看gitrepository-layout5。 |
---|
你现在已经创建了你的第一个Git仓库。当然,由于它是空的,这不是很有用,所以让我们开始用数据填充它。
我们会保持这种简单和愚蠢的,所以我们将开始填充一些简单的文件,以获得它的感觉。
首先创建你想在你的Git仓库中维护的随机文件。我们将从一些不好的例子开始,以了解它的工作原理:
$ echo "Hello World" >hello $ echo "Silly example" >example
你现在在你的工作树(又名working directory
)中创建了两个文件,但要真正检查你的努力工作,你必须经历两个步骤:
用有关工作树状态的信息填写index
文件(又名cache
)。
将该索引文件作为对象提交。
第一步很简单:当你想告诉Git关于工作树的任何修改时,你就可以使用git update-index
程序。该程序通常只需要一个你想更新的文件名列表,但为了避免微不足道的错误,它拒绝向索引添加新的条目(或删除现有的条目),除非你明确地告诉它你正在添加一个新的条目--add
标志(或移除--remove
标志)。
因此,要使用刚刚创建的两个文件填充索引,可以这样做
$ git update-index --add hello example
你现在已经告诉Git跟踪这两个文件。
事实上,正如你所做的那样,如果你现在查看你的对象目录,你会注意到Git会在对象数据库中添加两个新对象。如果你完成上述步骤,你现在应该可以做到
$ ls .git/objects/??/*
并看两个文件:
.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238.git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962
它们分别与名称为557db...
和的对象相对应f24c7...
。
如果你愿意,你可以用git cat-file
来查看这些对象,但是你必须使用对象名称,而不是对象的文件名:
$ git cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238
当-t
告诉git cat-file
告诉你对象的“类型”是什么。Git会告诉你,你有一个“blob”对象(即,只是一个普通文件),你可以看到内容
$ git cat-file blob 557db03
这将打印出“Hello World”。该对象557db03
只不过是文件的内容hello
。
注意 | 不要将该对象与文件hello本身混淆。该对象实际上只是文件的特定内容,不管您稍后更改文件hello中的内容,我们刚刚查看的对象都不会改变。对象是不可变的。 |
---|
注意 | 第二个例子说明,可以在大部分地方将对象名缩写为仅前几个十六进制数字。 |
---|
无论如何,正如我们前面提到的,通常你永远都不会真正看到对象本身,而输入长40个字符的十六进制名称并不是你通常想要做的事情。上面的题目只是为了表明git update-index
做了一些神奇的事情,并且实际上将文件的内容保存到了Git对象数据库中。
更新索引也做了其他事情:它创建了一个.git/index
文件。这是描述您当前工作树的索引,以及您应该非常清楚的内容。同样,你通常不会担心索引文件本身,但是你应该知道这样一个事实,即到目前为止你还没有真正“检查”你的文件到Git中,你只是告诉Git。
但是,由于Git知道它们,现在可以开始使用一些最基本的Git命令来操纵文件或查看它们的状态。
特别是,我们甚至不会将两个文件检入到Git中,然后我们将首先添加另一行hello
:
$ echo "It's a new day for git" >>hello
现在你可以,因为你告诉Git关于以前的状态hello
,请问使用下面的git diff-files
命令Git树中的变化与旧索引相比:
$ git diff-files
噢不,这不是很可读。它只是说出它自己的内部版本diff
,但是这个内部版本实际上只是告诉你,它已经注意到“hello”已被修改,并且它的旧对象内容已被替换为其他东西。
为了使它可读,我们可以git diff-files
通过使用-p
标志来告诉输出差异作为补丁:
$ git diff-files -p diff --git a/hello b/hello index 557db03..263414f 100644--- a/hello+++ b/hello @@ -1 +1,2 @@ Hello World+It's a new day for git
即我们通过添加另一条线所导致的变化的差异hello
。
换句话说,git diff-files
总是向我们显示索引中记录的内容与工作树中当前内容之间的区别。这非常有用。
一个通用的简写git diff-files -p
是只写git diff
,它会做同样的事情。
$ git diff diff --git a/hello b/hello index 557db03..263414f 100644--- a/hello+++ b/hello @@ -1 +1,2 @@ Hello World+It's a new day for git
现在,我们要进入Git的下一个阶段,即在索引中获取Git知道的文件,并将它们作为真正的树进行提交。我们分两个阶段来完成:创建一个tree
对象,并将该tree
对象作为commit
对象提交,同时解释树的全部内容以及我们如何进入该状态的信息。
创建一个树对象是微不足道的,并且完成git write-tree
。没有选项或其他输入:git write-tree
将采用当前索引状态,并编写描述整个索引的对象。换句话说,我们现在将所有不同的文件名与他们的内容(以及他们的权限)结合在一起,我们创建了一个Git“目录”对象的等价物:
$ git write-tree
并且这只会输出结果树的名称,在这种情况下(如果您完全按照我所描述的那样完成)它应该是
8988da15d077d4829fc51d8544c097def6644dbb
这是另一个难以理解的对象名称。再一次,如果你愿意的话,你可以使用git cat-file -t 8988d...
看到这次对象不是一个“blob”对象,而是一个“树”对象(你也可以用它git cat-file
来实际输出原始对象的内容,但你会看到主要二进制混乱,所以这不那么有趣)。
但是 - 通常你不会git write-tree
自己使用它,因为通常你总是使用该git commit-tree
命令将一棵树提交到一个提交对象中。事实上,根本不能实际使用git write-tree
它,但只是将其结果作为参数传递给它git commit-tree
。
git commit-tree
通常需要几个参数 - 它想知道parent
提交的内容是什么,但由于这是该新存储库中的第一次提交,并且没有父母,我们只需要传入树的对象名称。但是,git commit-tree
也希望在其标准输入上获得提交消息,并将提交的结果对象名称写入其标准输出。
这就是我们创建.git/refs/heads/master
指向的文件的地方HEAD
。这个文件应该包含对主分支树顶端的引用,因为这正是git commit-tree
吐出来的东西,所以我们可以用一系列简单的shell命令来完成这一切:
$ tree=$(git write-tree)$ commit=$(echo 'Initial commit' | git commit-tree $tree)$ git update-ref HEAD $commit
在这种情况下,这会创建一个与其他任何内容都无关的全新提交。通常情况下,你这样做只是一次为一个项目永远,其随后所有的提交将在较早提交顶部父。
再次,通常你永远不会亲自去做这件事。有一个有用的脚本git commit
,它会为你做所有这些。所以你可以写下来git commit
,而且它会为你完成上面的magic脚本。
请记住我们是如何完成git update-index
on文件的hello
,然后再进行更改hello
,并且可以将新状态hello
与我们保存在索引文件中的状态进行比较?
此外,请记住我是如何git write-tree
将索引文件的内容写入树中的,因此我们刚刚承诺的实际上是文件的原始内容hello
,而不是新的内容。我们这样做的目的是为了显示索引状态和工作树中的状态之间的区别,以及它们如何不匹配,即使我们犯了错误。
和以前一样,如果我们git diff-files -p
在我们的git-tutorial项目中做,我们仍然会看到我们上次看到的同样的区别:索引文件并没有因提交任何内容而改变。然而,现在我们已经犯了一些事件,我们也可以学习使用一个新的命令:git diff-index
。
与git diff-files
显示索引文件和工作树git diff-index
之间的区别不同,显示了提交树与索引文件或工作树之间的区别。换句话说,我们git diff-index
希望一棵树能够与之相抗衡,而在我们提交之前,我们不能这样做,因为我们没有任何可以相互区分的东西。
但现在我们可以做到
$ git diff-index -p HEAD
(这里-p
的含义与它的意思相同git diff-files
),它会向我们展示同样的差异,但是却出于完全不同的原因。现在我们将工作树与索引文件进行比较,但是对照我们刚刚编写的树。恰恰相反,这两个显然是相同的,所以我们得到了相同的结果。
再次,因为这是一种常见的操作,您也可以简单地使用
$ git diff HEAD
最终为你做上述事情。
换句话说,git diff-index
通常将一棵树与工作树进行比较,但是在给定该--cached
标志时,会告诉它仅仅比较索引缓存内容,并完全忽略当前工作树状态。由于我们只是将索引文件写入HEAD,git diff-index --cached -p HEAD
因此应该返回一组空白的差异,而这正是它所做的。
注意 | 因此,git diff-index确实总是使用索引进行比较,并且说它将树与工作树进行比较因此不是严格准确的。特别是,无论是否使用--cached标志,要比较的文件列表(“元数据”)总是来自索引文件。--cached标志只确定要比较的文件内容是否来自工作树。这并不难理解,只要您意识到Git根本就不知道(或关心)文件,而不会明确地告知它。Git永远不会去查找要比较的文件,它希望你告诉它文件是什么,这就是索引的用途。 |
---|
但是,我们下一步要承诺我们所做的改变,并且再次了解发生了什么,请牢记“工作树内容”,“索引文件”和“承诺树”之间的差异。我们在工作树中进行了更改,我们需要处理索引文件,因此我们需要做的第一件事是更新索引缓存:
$ git update-index hello
(注意--add
这次我们不需要这个标志,因为Git已经知道这个文件了)。
注意git diff-*
这里的不同版本会发生什么。我们更新后hello
的指数,git diff-files -p
现在显示无显着差异,但git diff-index -p HEAD
仍然没有显示当前状态是我们犯下的状态不同。实际上,git diff-index
无论我们是否使用该--cached
标志,现在都显示出同样的差异,因为现在索引与工作树是一致的。
现在,由于我们已经更新hello
了索引,我们可以提交新版本。我们可以通过再次手工编写树并提交树来完成此操作(这次我们必须使用-p HEAD
标志来告诉提交HEAD是新提交的父代,并且这不是初始提交任何更多),但你已经完成了,所以我们这次只使用有用的脚本:
$ git commit
它会启动一个编辑器来编写提交消息,并告诉你一些关于你已经完成的事情。
写下你想要的任何消息,所有#
以此开始的行将被删除,其余的将被用作改变的提交消息。如果您决定此时不想提交任何内容(您可以继续编辑内容并更新索引),则可以留下一条空的消息。否则git commit
会为你做出改变。
你现在已经做出了你的第一个真正的Git提交。如果你有兴趣研究git commit
真正的功能,请随时调查:这是一些非常简单的shell脚本,用于生成有用的(?)提交消息头文件,以及一些实际执行提交本身的单行程序(git commit
) 。
虽然创建更改很有用,但如果以后可以告诉更改了哪些内容,则更有用。对此的最有用的命令是另一个diff
家庭,即git diff-tree
。
git diff-tree
可以给两棵任意的树,它会告诉你它们之间的区别。也许更普遍的是,你可以给它一个单一的提交对象,它会找出那个提交本身的父对象,并直接显示它们之间的区别。因此,为了获得我们已经多次看到的差异,我们现在可以做
$ git diff-tree -p HEAD
(再次,-p
意味着将差异显示为人类可读的补丁),并且它将显示上次提交(in HEAD
)实际上发生了什么变化。
Note | Here is an ASCII art by Jon Loeliger that illustrates how various diff-* commands compare things. diff-tree +----+ | | | | V V +-----------+ | Object DB | | Backing | | Store | +-----------+ ^ ^ | | | | diff-index --cached | | diff-index | V | +-----------+ | | Index | | | "cache" | | +-----------+ | ^ | | | | diff-files | | V V +-----------+ | Working | | Directory | +-----------+ |
---|
更有意思的是,你也可以给git diff-tree
这个--pretty
标志,告诉它也显示提交信息和作者以及提交日期,你可以告诉它显示一系列的差异。或者,你可以告诉它是“沉默”,并且根本不显示差异,但只显示实际的提交信息。
实际上,与git rev-list
程序(产生修订列表)一起,git diff-tree
最终成为变化的真正来源。你可以模仿git log
,git log -p
等用一个简单的脚本,管道输出git rev-list
到git diff-tree --stdin
,这是究竟如何早期版本git log
中实现。
在Git中,有两种标签,一种是“轻”标签,另一种是“带标签的标签”。
一个“light”标签在技术上只不过是一个分支,除了我们把它放在.git/refs/tags/
子目录中而不是调用它head
。所以最简单的标签形式仅仅涉及
$ git tag my-first-tag
它只是把当前HEAD
的.git/refs/tags/my-first-tag
文件写入文件,然后你可以在这个特定的状态下使用这个符号名称。例如,你可以做
$ git diff my-first-tag
将当前状态与当前显然是空白区别的标签进行比较,但如果您继续开发和提交内容,则可以使用标签作为“定位点”来查看标记后发生了哪些变化。
一个“带注释的标签”实际上是一个真正的Git对象,它不仅包含一个指向你想要标记的状态的指针,而且还包含一个小标签名称和消息,还有一个可选的PGP签名,表示是的,你确实是这样做的标签。您可以使用-a
或-s
标志创建这些带注释的标签git tag
:
$ git tag -s <tagname>
这将签署当前HEAD
(但您也可以给它另一个参数,指定要标记的东西,例如,您可以使用标记当前mybranch
点git tag <tagname> mybranch
)。
通常,您只会为主要版本或类似的东西做签名标记,而轻量级标记对于您想要执行的任何标记都很有用 - 无论何时您决定要记住某个特定点,只需为其创建一个专用标记,并且在那个时候你有一个很好的符号名称。
Git仓库通常是完全自给自足和可重新定位的。例如,与CVS不同,“存储库”和“工作树”没有单独的概念。Git仓库通常是工作树,本地Git信息隐藏在.git
子目录中。没有别的。你看到的是你得到的。
注意 | 您可以告诉Git将Git内部信息从它跟踪的目录中分离出来,但现在我们将忽略它:这不是普通项目的工作方式,而只是用于特殊用途。因此,“Git信息始终与其描述的工作树直接相关”的心智模型在技术上可能不是100%准确的,但它对于所有正常使用来说都是一个很好的模型。 |
---|
这有两个含义:
如果您对创建的教程库感到厌倦(或者您犯了一个错误并想要重新开始),那么您可以执行简单的$ rm -rf git-tutorial
它会消失。没有外部存储库,并且您创建的项目之外没有任何历史记录。
如果你想移动或复制一个Git仓库,你可以这样做。有git clone
命令,但如果你想要做的只是创建一个你的仓库的副本(附带所有完整的历史记录),你可以用常规的方式来完成cp -a git-tutorial new-git-tutorial
。请注意,当您移动或复制Git存储库时,您的Git索引文件(缓存各种信息,特别是所涉及文件的一些“统计”信息)可能需要刷新。所以在你cp -a
创建一个新副本之后,你会想要做$ git update-index --refresh
在新的存储库中确保索引文件是最新的。
请注意,第二点即使在机器上也是如此。您可以复制一个远程的Git仓库与任何常规复制机制,是它scp
,rsync
或wget
。
在复制远程存储库时,您至少需要更新索引缓存,尤其是在其他人的存储库中,您通常希望确保索引缓存处于某种已知状态(您不需要知道他们做了什么,且还没有检查),所以通常你会先于git update-index
用
$ git read-tree --reset HEAD $ git update-index --refresh
这将强制从指向的树中重新构建索引HEAD
。它将索引内容重置为HEAD
,然后git update-index
确保将所有索引条目与检出文件进行匹配。如果原始存储库在其工作树中有未提交的更改,则git update-index --refresh
通知它们并告诉您需要更新它们。
以上内容也可以简单写成
$ git reset
实际上很多常见的Git命令组合都可以通过git xyz
接口编写脚本。您可以通过查看各种git脚本所做的工作来了解情况。例如,git reset
用于实现在上述的两行git reset
,但像一些git status
和git commit
稍微围绕基本Git命令更复杂的脚本。
许多(大多数?)公共远程存储库不会包含任何检出的文件或甚至索引文件,并且只包含实际的核心Git文件。这样的存储库通常甚至没有该.git
子目录,但直接在存储库中包含所有Git文件。
要创建自己的“原始”Git存储库的本地活动副本,首先要为项目创建自己的子目录,然后将原始存储库内容复制到.git
目录中。例如,要创建自己的Git存储库副本,您需要执行以下操作
$ mkdir my-git $ cd my-git $ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git
其次是
$ git read-tree HEAD
来填充索引。然而,现在你已经填充了索引,并且你拥有所有的Git内部文件,但是你会注意到你实际上没有任何工作树文件可以工作。为了得到这些,你会检查出来
$ git checkout-index -u -a
其中-u
标志意味着你要检出,以保持指数最高最新(这样你就不必事后刷新),-a
标志的意思是“签出的所有文件”(如果你有一个陈旧的副本或签出树的旧版本,你可能还需要添加-f
第一个标志,告诉git checkout-index
来强制覆盖任何旧文件)。
再次,这可以全部简化
$ git clone git://git.kernel.org/pub/scm/git/git.git/ my-git $ cd my-git $ git checkout
这将最终为你做上述所有的事情。
您现在已经成功复制了其他人的(我的)远程存储库,并将其签出。
Git中的分支实际上只不过是从.git/refs/
子目录中进入Git对象数据库的指针,正如我们已经讨论的那样,HEAD
分支只不过是这些对象指针之一的符号链接。
您可以随时通过在项目历史记录中选择一个任意点来创建一个新分支,然后将该对象的SHA-1名称写入一个文件中.git/refs/heads/
。你可以使用你想要的任何文件名(实际上是子目录),但是惯例是调用“普通”分支master
。尽管如此,这只是一个惯例,没有什么可以强制它。
举个例子,让我们回到我们之前使用的git-tutorial存储库,并在其中创建一个分支。你只需说出你想要签出一个新的分支就可以做到这一点:
$ git checkout -b mybranch
将在当前HEAD
位置创建一个新的分支,并切换到它。
注意 | 如果您决定在历史中的某个其他位置启动新分支,而不是当前的HEAD,那么您可以通过告诉git checkout检出结果的基础来做到这一点。换句话说,如果你有一个更早的标签或分支,你只需要做$ git checkout -b mybranch early-commit,它会在之前的提交中创建新的分支mybranch,然后检查当时的状态。 |
---|
你可以随时跳回原来的master
分支
$ git checkout master
(或者任何其他分支名称),如果你忘记了你碰巧在哪个分支上,那么简单
$ cat .git/HEAD
会告诉你它指向的地方。要获得您拥有的分支名单,您可以说
$ git branch
它曾经只是一个简单的脚本而已ls .git/refs/heads
。目前分支前面会有一个星号。
有时您可能希望创建一个新的分支,without
实际检查并切换到该分支。如果是这样,只需使用该命令
$ git branch <branchname> [startingpoint]
这将简单create
分支,但不会做任何进一步的事情。然后,你可以稍后 - 一旦你决定要在该分支上进行实际开发,就可以git checkout
使用branchname作为参数切换到该分支。
拥有一个分支的想法之一是你做了一些(可能是实验性的)工作,并最终将它合并回主分支。因此,假设您创建了mybranch
与原始master
分支相同的上述内容,那么让我们确保我们在该分支中,并在那里做一些工作。
$ git checkout mybranch $ echo "Work, work, work" >>hello $ git commit -m "Some work." -i hello
在这里,我们只是添加了另一行hello
,并且我们使用了一个简写来完成这两个操作,git update-index hello
并git commit
通过直接给文件名直接git commit
添加一个-i
标志(include
除了索引文件到目前为止您做了什么之外,它还告诉Git 该文件)提交)。该-m
标志是从命令行提供提交日志消息。
现在,为了让它更有趣一些,让我们假设别人在原始分支中做了一些工作,并通过回到主分支来模拟它,并在那里编辑相同的文件:
$ git checkout master
在这里,花一些时间看看内容hello
,注意它们不包含我们刚才所做的工作mybranch
- 因为这项工作在master
分支中根本没有发生。然后做
$ echo "Play, play, play" >>hello $ echo "Lots of fun" >>example $ git commit -m "Some fun." -i hello example
因为主分支显然心情更好。
现在,你有两个分支,并且你决定要合并完成的工作。在我们这样做之前,让我们介绍一个很酷的图形工具,帮助您查看正在发生的事情:
$ gitk --all
会以图形方式向你展示你的两个分支(这--all
就是说:通常它会向你显示你当前的HEAD
)和他们的历史。你也可以看到他们是如何来自一个共同的来源。
无论如何,让我们退出gitk
(^Q
或文件菜单),并决定我们要将我们在mybranch
分支上所做的工作合并到master
分支中(这也是我们的工作HEAD
)。要做到这一点,有一个很好的脚本调用git merge
,它想知道你想要解决哪些分支以及合并的内容:
$ git merge -m "Merge work in mybranch" mybranch
如果可以自动解析合并,则第一个参数将用作提交消息。
现在,在这种情况下,我们故意创建了需要手动修正合并的情况,因此,Git会自动完成它的功能(在这种情况下,它只是合并example
文件,在mybranch
分支中没有差异),并说:
Auto-merging hello CONFLICT (content): Merge conflict in hello Automatic merge failed; fix conflicts and then commit the result.
它告诉你它做了一个“自动合并”,由于hello
冲突导致失败。
不用担心。如果你曾经使用过CVS,那么它就会以相同的形式在hello
留下(小小的)冲突,所以让我们在我们的编辑器中打开hello
(不管怎么说),然后以某种方式修复它。我建议让hello
包含所有四行:
Hello World It's a new day for git Play, play, play Work, work, work
一旦你对你的手动合并感到满意,就执行
$ git commit -i hello
这会非常大声地警告你,你现在正在进行合并(这是正确的,所以不要介意),并且你可以写一个关于你的冒险的小型合并信息git merge
。
完成后,启动gitk --all
以图形方式查看历史记录的样子。注意,mybranch
仍然存在,你可以切换到它,并继续使用它,如果你想。该mybranch
分支不包含合并,但下一次您从master
分支中合并它时,Git会知道您如何合并它,因此您不必再次合并。
另一个有用的工具,特别是如果你不总是在X-Window环境下工作,是git show-branch
。
$ git show-branch --topo-order --more=1 master mybranch* [master] Merge work in mybranch ! [mybranch] Some work.--- [master] Merge work in mybranch*+ [mybranch] Some work.* [master^] Some fun.
前两行表示它显示了两个分支,它们的树顶部提交的标题,您当前在master
分支上(注意星号*
字符),后面的输出行的第一列用于显示master
分支中包含的提交以及分支的第二列mybranch
。显示了三个提交以及他们的标题。它们都在第一列中有空白字符(*
显示当前分支上的普通提交,-
是合并提交),这意味着它们现在是master
分支的一部分。只有“某些工作”提交+
在第二列中具有加号字符,因为mybranch
尚未合并到主分支的这些提交中。提交日志消息之前的括号内的字符串是一个短名称,可用于命名提交。在上面的例子中,master
并且mybranch
是分支头。master^
是master
分支头的第一位家长。如果您想查看更复杂的案例,请参阅gitrevisions[7]。
注意 | 如果没有--more = 1选项,git show-branch将不会输出master ^ commit,因为mybranch提交是master和mybranch提示的共同祖先。有关详细信息,请参阅git-show-branch1。 |
---|
注意 | 如果合并后主分支上有更多的提交,默认情况下合并提交本身不会由git show-branch显示。在这种情况下,您需要提供--sparse选项以使合并提交可见。 |
---|
现在,让我们假装你是一个完成所有工作的人mybranch
,并且你辛勤工作的成果最终被合并到master
分支中。让我们回到mybranch
,并运行git merge
以获取“上游变更”回到您的分支。
$ git checkout mybranch $ git merge -m "Merge upstream changes." master
这会输出这样的内容(实际的提交对象名称会有所不同)
Updating from ae3a2da... to a80b4aa....Fast-forward (no commit created; -m option ignored) example | 1 + hello | 1 + 2 files changed, 2 insertions(+)
因为你的分支没有包含任何已经合并到master
分支中的东西,合并操作实际上并没有进行合并。相反,它只是将分支树的顶部更新为分支树的顶部master
。这通常被称为fast-forward
合并。
您可以再次运行gitk --all
以查看commit ancestry的外观,或者运行show-branch
,这可以告诉您这一点。
$ git show-branch master mybranch! [master] Merge work in mybranch * [mybranch] Merge work in mybranch---- [master] Merge work in mybranch
通常情况下,与其他人合并比合并你自己的分支要普遍得多,所以值得指出的是,Git也使得它非常简单,事实上,与做一个没有什么不同git merge
。事实上,远程合并最终不过是“将工作从远程存储库获取到临时标记中”,然后是一个git merge
。
不出所料,从远程存储库中获取git fetch
:
$ git fetch <remote-repository>
可以使用下列其中一个传输命令来从以下位置下载存储库:
SSH
remote.machine:/path/to/repo.git/
or
ssh://remote.machine/path/to/repo.git/
此传输可用于上传和下载,并要求您拥有ssh
远程计算机的登录权限。它通过交换两端提交的头部提交并转移(接近)最小的一组对象来找出对方缺乏的对象集合。这是在库之间交换Git对象的最有效方式。
本地目录
/path/to/repo.git/
此传输与SSH传输相同,但用于sh
在本地计算机上运行两端,而不是在远程计算机上运行另一端ssh
。
Git Native
git://remote.machine/path/to/repo.git/
此传输设计用于匿名下载。与SSH传输一样,它可以找到下游端缺少的对象集合,并将其转移(接近)最小的一组对象。
HTTP(S)
http://remote.machine/path/to/repo.git/
Downloader从http和https URL首先通过查看repo.git/refs/
目录下指定的refname从远程站点获取最高的提交对象名称,然后尝试通过repo.git/objects/xx/xxx...
使用该提交对象的对象名称进行下载来获取提交对象。然后它读取提交对象以找出其父提交和关联树对象;它会重复这个过程,直到它获得所有必需的对象。由于这种行为,他们有时也被称为commit walkers
。
commit walkers
有时也被称为dumb transports
,因为它们不需要任何的Git知道智能服务器如Git机传输一样。任何股票甚至不支持目录索引的HTTP服务器就足够了。但是您必须准备好您的存储库git update-server-info
来帮助传输下载者。
一旦你从远程仓库获取,merge
当前分支。
然而 - fetch
是一件很常见的事情,然后立即merge
就被调用git pull
,你可以简单地做
$ git pull <remote-repository>
并且可选地给远端的分支名称作为第二个参数。
注意 | 根本不需要使用任何分支,通过保留尽可能多的本地存储库,因为您希望拥有分支,并使用git pull合并它们,就像在分支之间合并一样。这种方法的优点是,它可以让你为每个分支保存一组文件,并且如果你同时处理多行开发,你可能会发现来回切换更容易。当然,您将支付更多磁盘使用的代价来保存多个工作树,但是现在磁盘空间很便宜。 |
---|
您可能会不时从同一个远程存储库中获取数据。简而言之,您可以将远程存储库URL存储在本地存储库的配置文件中,如下所示:
$ git config remote.linus.url http://www.kernel.org/pub/scm/git/git.git/
并使用“linus”关键字git pull
而不是完整的URL。
示例:
git pull linus
git pull linus tag v0.99.1
以上相当于:
git pull http://www.kernel.org/pub/scm/git/git.git/ HEAD
git pull http://www.kernel.org/pub/scm/git/git.git/ tag v0.99.1
我们说这个教程展示了什么管道可以帮助你应对不冲水的瓷器,但我们迄今没有谈到合并的真正效果。如果您是第一次使用本教程,我会建议跳至“发布您的作品”部分,稍后再回来。
好,还跟得上吗?为了让我们看一个示例,让我们回到先前的存储库,带有“hello”和“example”文件,并让我们回到预合并状态:
$ git show-branch --more=2 master mybranch! [master] Merge work in mybranch * [mybranch] Merge work in mybranch---- [master] Merge work in mybranch+* [master^2] Some work.+* [master^] Some fun.
请记住,在运行git merge
之前,我们的master
head在“享受些乐趣”。承诺,而我们的mybranch
head在“做些工作”。commit.
$ git checkout mybranch $ git reset --hard master^2$ git checkout master $ git reset --hard master^
倒回后,提交结构应如下所示:
$ git show-branch* [master] Some fun. ! [mybranch] Some work.--* [master] Some fun. + [mybranch] Some work.*+ [master^] Initial commit
现在我们已经准备好尝试手动合并了。
git merge
命令,当合并两个分支时,使用3-way合并算法。首先,它找到它们之间的共同祖先。它使用的命令是git merge-base
:
$ mb=$(git merge-base HEAD mybranch)
该命令将公共祖先的提交对象名称写入标准输出,因此我们将其输出捕获到一个变量中,因为我们将在下一步中使用它。顺便说一下,在这种情况下,共同的祖先提交是“初始提交”提交。你可以告诉它:
$ git name-rev --name-only --tags $mb my-first-tag
找到一个共同的祖先提交后,第二步是这样的:
$ git read-tree -m -u $mb HEAD mybranch
这与git read-tree
我们已经看到的命令是一样的,但是与以前的例子不同,它需要三棵树。这将每棵树的内容读入stage
索引文件中的不同内容(第一棵树进入第一阶段,第二棵树进入第二阶段,等等)。在将三棵树读入三个阶段之后,所有三个阶段中相同的路径都collapsed
进入阶段0.在三个阶段中的两个阶段中相同的路径折叠到阶段0,从阶段2获取SHA-1或者阶段3,与第一阶段不同(即只有一侧从共同祖先改变)。
collapsing
操作之后,三棵树中不同的路径将保留在非零阶段。此时,您可以使用以下命令检查索引文件:
$ git ls-files --stage100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
在我们仅有两个文件的示例中,我们没有没有更改的文件,因此只能example
导致崩溃。但是在现实生活中的大型项目中,当一次提交中只有少量文件发生变化时,这collapsing
往往会使相当快速的大部分路径轻松合并,从而在非零阶段只发生少量实际变化。
要仅查看非零阶段,请使用--unmerged
标志:
$ git ls-files --unmerged100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
合并的下一步是合并这三个版本的文件,使用3-way合并。这是通过将git merge-one-file
命令作为命令的参数之一来完成的git merge-index
:
$ git merge-index git-merge-one-file hello Auto-merging hello ERROR: Merge conflict in hello fatal: merge program failed
git merge-one-file
用参数调用脚本来描述这三个版本,并负责将合并结果留在工作树中。这是一个相当简单的shell脚本,最终调用merge
RCS套件中的程序执行文件级3路合并。在这种情况下,merge
检测到冲突,并将带有冲突标记的合并结果留在工作树中。如果ls-files --stage
在此时再次运行,可以看到这一点:
$ git ls-files --stage100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
这是索引文件的状态,并且工作文件git merge
将控制权返回给您,并将冲突合并留给您解决。请注意,路径hello
尚未被合并,此时您看到的git diff
是自第2阶段(即您的版本)以来的差异。
所以,我们可以使用别人的作品从远程仓库,但你如何能准备一个资料库,让其他人撤出?
您在工作树中执行真正的工作,并将主存储库挂在其.git
子目录下。您可以远程访问该存储库,并要求人们从中获取信息,但实际上这并不是通常的做法。推荐的方法是拥有一个公共存储库,让其他人可以访问该存储库,并且当您在主工作树中所做的更改状态良好时,请从中更新公共存储库。这通常被称为pushing
。
注意 | 这个公共存储库可以进一步被镜像,这就是kernel.org上的Git存储库的管理方式。 |
---|
将本地(专用)存储库中的更改发布到远程(公用)存储库需要在远程计算机上具有写权限。您需要有一个SSH帐户才能运行单个命令,git-receive-pack
。
首先,您需要在将存放公共存储库的远程机器上创建一个空的存储库。这个空的存储库将被填充并随后推入,以保持最新状态。显然,这个存储库的创建只需要完成一次。
注意 | git push使用一对命令,本地机器上的git send-pack和远程机器上的git-receive-pack。两者之间的通信通过网络在内部使用SSH连接。 |
---|
您的私有存储库的Git目录通常是.git
,但您的公共存储库通常以项目名称命名,即<project>.git
。让我们为项目创建一个这样的公共存储库my-git
。登录到远程机器后,创建一个空目录:
$ mkdir my-git.git
然后,通过运行将该目录设置为Git存储库git init
,但这次由于其名称.git
并不常见,所以我们的做法略有不同:
$ GIT_DIR=my-git.git git init
确保此目录可用于您希望通过您选择的交通工具提取您的更改的其他人。你也需要确保你在$PATH
有这个git-receive-pack
程序。
注意 | 当你直接运行程序时,许多sshd安装不会将你的shell作为登录shell调用; 这意味着如果您的登录shell是bash,则只读取.bashrc而不是.bash_profile。作为一种解决方法,确保.bashrc设置$ PATH,以便您可以运行git-receive-pack程序。 |
---|
注意 | 如果你打算发布这个通过http访问的仓库,你应该在这个时候执行mv my-git.git/hooks/post-update.sample my-git.git/hooks/post-update。这可以确保每次你进入这个仓库时,都会运行git update-server-info。 |
---|
您的“公共存储库”现在已准备好接受您的更改。回到你有你的私人存储库的机器。从那里运行这个命令:
$ git push <public-host>:/path/to/my-git.git master
这会使您的公共存储库与master
您的当前存储库中与指定的分支头(即本例中)和它们可访问的对象相匹配。
作为一个真实的例子,这是我更新公共Git存储库的方式。Kernel.org镜像网络负责传播给其他公开可见的机器:
$ git push master.kernel.org:/pub/scm/git/git.git/
之前,我们看到.git/objects/??/
为目录创建的每个Git对象都存储目录下的一个文件。这种表示对于原子级和安全地创建是有效的,但在网络上传输并不方便。由于Git对象一旦创建就不可变,因此可以通过“将它们组合在一起”来优化存储。命令
$ git repack
会为你完成。如果你按照教程的例子,你现在已经在.git/objects/??/
目录中累积了约17个对象。git repack
告诉您打包了多少个对象,并将打包文件存储在.git/objects/pack
目录中。
注意 | 您将在.git/objects/pack目录中看到两个文件,即pack - *。pack和pack - *。idx。它们彼此密切相关,如果您出于任何原因手动将它们复制到不同的存储库,则应确保将它们复制在一起。前者保存包中对象的所有数据,后者保存随机访问的索引。 |
---|
如果你偏执,运行git verify-pack
命令会检测你是否有腐败的包装,但不要太担心。我们的项目总是完美的;-)。
一旦你打包了对象,你就不需要保留包文件中包含的解压对象了。
$ git prune-packed
会为你删除它们。
如果您好奇,您可以在跑步find .git/objects -type f
前后尝试跑步git prune-packed
。此外git count-objects
还会告诉您存储库中有多少个未打包对象以及它们占用了多少空间。
注意 | 对于HTTP传输来说,git pull稍微麻烦,因为打包的存储库可能包含相对较少包中的相对较少的对象。如果你期望从你的公共仓库获取很多HTTP请求,你可能需要经常重新打包和修剪,或者永远不要修剪。 |
---|
如果此时再次运行git repack
,则会显示“没有新包装”。一旦继续开发并累积更改,git repack
再次运行将创建一个新包,其中包含自上次打包存储库后创建的对象。我们建议您在初次导入后尽快打包项目(除非您从头开始项目),然后git repack
每隔一段时间运行一次,具体取决于项目的活跃程度。
当一个储存库是通过同步git push
和git pull
填充在源存储库对象通常存储在目的地解压。虽然这允许您在两端使用不同的打包策略,但这也意味着您可能需要每隔一段时间重新打包两个存储库。
尽管Git是一个真正的分布式系统,但通过非正式的开发人员层次来组织项目通常很方便。Linux内核开发就是这样运行的。在Randy Dunlap的演讲中有一个很好的例子(第17页,“合并到Mainline”)。
应该强调的是,这个层次纯粹是非正式的。在Git中没有任何基础强制执行这个层次结构所暗示的“补丁流程链”。您不必仅从一个远程存储库中获取数据。
“项目领导”的推荐工作流程如下所示:
在本地计算机上准备主要存储库。你的工作在那里完成。
准备一个可供他人访问的公共存储库。如果其他人通过哑传输协议(HTTP)从您的存储库中提取数据,则需要保留此存储库dumb transport friendly
。之后git init
,$GIT_DIR/hooks/post-update.sample
从标准模板复制将包含呼叫,git update-server-info
但您需要手动启用挂钩mv post-update.sample post-update
。这确保git update-server-info
了必要的文件保持最新。
从主存储库推入公共存储库。
git repack
公共存储库。这会建立一个包含初始对象集合作为基准的大包,并且可能git prune
用于从存储库中提取的传输支持打包存储库。
继续在主存储库中工作。您的更改包括修改您自己的修改,通过电子邮件收到的修补程序以及从您的“子系统维护者”的“公共”存储库中拉取合并。只要你愿意,你可以重新包装这个私人存储库。
将更改推送到公共存储库,并向公众发布。
每过一段时间,git repack
公共存储库。返回步骤5.继续工作。
为该项目工作并拥有自己的“公共存储库”的“子系统维护人员”推荐的工作周期如下所示:
通过git clone
在“项目负责人”的公共存储库上运行,准备工作存储库。用于初始克隆的URL存储在remote.origin.url配置变量中。
准备一个可供他人访问的公共存储库,就像“项目负责人”一样。
将“项目负责人”公共存储库中的打包文件复制到公共存储库,除非“项目负责人”存储库与您的计算机位于同一台计算机上。在后一种情况下,您可以使用objects/info/alternates
文件指向您从中借用的存储库。
从主存储库推入公共存储库。运行git repack
,并且可能git prune
用于从存储库中提取的传输支持打包的存储库。
继续在主存储库中工作。您的更改包括修改您自己的修改,通过电子邮件收到的修补程序,以及拉动“项目负责人”的“公共”存储库和可能的“子子系统维护人员”所产生的合并。只要你愿意,你可以重新包装这个私人存储库。
将更改推送到公共存储库,并请求您的“项目负责人”和可能的“子子系统维护人员”从中抽取。
每过一段时间,git repack
公共存储库。返回步骤5.继续工作。
没有“公共”存储库的“个人开发人员”的建议工作周期稍有不同。它是这样的:
通过git clone
“项目负责人”(或“子系统维护人员”,如果您在子系统上工作)的公共存储库准备工作存储库。用于初始克隆的URL存储在remote.origin.url配置变量中。
在master
分支机构的仓库中工作。
git fetch origin
每隔一段时间从上游的公共存储库运行。这只有前半部分,git pull
但不合并。公共存储库的头部存储在.git/refs/remotes/origin/master
。
使用git cherry origin
查看哪些补丁那些被接受,和/或使用git rebase origin
端口的未合并的变化着更新的上游。
使用git format-patch origin
准备用于电子邮件提交补丁,你的上游,并发送出去。返回第2步并继续。
如果您来自CVS背景,上一节中提出的合作风格对您来说可能是新的。你不必担心。Git支持您可能更熟悉的“共享公共存储库”合作风格。
有关详细信息,请参阅gitcvs-migration[7]。
您可能一次只能处理一件以上的事情。使用Git分支来管理那些或多或少的独立任务是很容易的。
我们已经看到了分支机构以前的工作方式,以两个分支机构的“乐趣和工作”为例。如果有两个以上的分支,这个想法是一样的。假设你从“主”头开始,并在“主”分支中有一些新代码,并在“提交 - 修复”和“差异修复”分支中有两个独立修复:
$ git show-branch! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Release candidate #1--- + [diff-fix] Fix rename detection. + [diff-fix~1] Better common substring algorithm.+ [commit-fix] Fix commit message normalization. * [master] Release candidate #1++* [diff-fix~2] Pretty-print messages.
这两个修补程序都经过了很好的测试,在这一点上,您想要在它们两个中进行合并。你可以先合并diff-fix
然后再合并commit-fix
,如下所示:
$ git merge -m "Merge fix in diff-fix" diff-fix $ git merge -m "Merge fix in commit-fix" commit-fix
这将导致:
$ git show-branch! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Merge fix in commit-fix--- - [master] Merge fix in commit-fix+ * [commit-fix] Fix commit message normalization. - [master~1] Merge fix in diff-fix +* [diff-fix] Fix rename detection. +* [diff-fix~1] Better common substring algorithm. * [master~2] Release candidate #1++* [master~3] Pretty-print messages.
但是,没有什么特别的理由要先合并一个分支,然后再合并,当你有一系列真正独立的变化时(如果顺序重要,那么它们就不是定义上的独立)。您可以将这两个分支同时合并到当前分支中。首先让我们撤销我们刚刚做的并重新开始。我们希望在这两次合并之前将主分支重置为master~2
:
$ git reset --hard master~2
你可以确保git show-branch
在git merge
你刚刚做的那两个之前匹配状态。然后,不是连续运行两个git merge
命令,而是合并这两个分支头(这被称为making an Octopus
):
$ git merge commit-fix diff-fix $ git show-branch! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Octopus merge of branches 'diff-fix' and 'commit-fix'--- - [master] Octopus merge of branches 'diff-fix' and 'commit-fix'+ * [commit-fix] Fix commit message normalization. +* [diff-fix] Fix rename detection. +* [diff-fix~1] Better common substring algorithm. * [master~1] Release candidate #1++* [master~2] Pretty-print messages.
请注意,你不应该因为你可以做Octopus。如果要同时合并两个以上的独立更改,章鱼是一件有效的事情,并且通常可以更容易地查看提交历史记录。但是,如果您与正在合并且需要手工解决的任何分支合并冲突,则表示发生在这些分支中的发展毕竟不是独立的,并且您应该一次合并两个分支,记录如何你解决了冲突,以及你偏好一方的变化。否则,它会使项目的历史难以跟上,并不容易。
gittutorial[7], gittutorial-2[7], gitcvs-migration[7], git-help[1], giteveryday[7], The Git User’s Manual