Git-秘籍-全-

Git 秘籍(全)

原文:Git Recipes

协议:CC BY-NC-SA 4.0

一、Git 入门

计算机软件制造商正面临着由微不足道的原因引起的困难挑战。一个典型的应用是由一个开发团队在很短的时间内处理成百上千个文件而产生的。每个文件都需要随时可供所有开发人员修改。当我们用时间线来补充这个场景时,情况就更加复杂了。任何开发人员都可以在任何时候修改每个文件。以下三个简单的因素使得源代码管理成为一项重要的任务:

  • 文件的数量
  • 开发者的数量
  • 时间线

这些问题很多年前就已经知道了,正如你所预料的,有各种各样的软件工具可以使团队处理文本文件变得更加容易。这些工具通常被称为版本控制软件版本控制软件。而 git 就属于这个家族。

git 是什么?

Git 是一个分布式版本控制系统,旨在支持 Linux 内核的开发。它由 Linus Torvalds 于 2005 年 4 月创建,现在由 Junio C. Hamano 维护。

git 区别于其他版本控制系统的主要特性是:

  • 分支
  • 数据完整性
  • 位置
  • 分布式的计算机系统
  • 开放源码
  • 最后但同样重要的是——受欢迎程度

分支模型是 git 最惊人的特性。我认为这本身就是切换到 git 的充分理由。使用 git,几乎可以瞬间创建分支,并且可以很容易地与其他开发人员合并和共享。虽然有许多复杂的操作可以在分支上执行,但是基本的用法是简单明了的。这鼓励了分支的广泛使用,我认为我说 git 分支模型改变了开发人员的工作方式并不夸张。

数据完整性 意味着 git 跟踪项目的所有文件和目录,这样就不可能引入未被注意到的更改。即使你想改变一个字节,你也必须创建一个版本。当你创建一个版本时,没有办法隐藏里面的东西。这是一个无法关闭的内置功能。因此,您可以完全信任 git:所有的更改都是作为修订版引入的,并且每个修订版都可以被检查。

Locality 提高了 git 的效率,并允许您即使在网络中断的情况下也能执行许多 git 命令。当您使用 git 时,您没有连接到任何服务器。大多数命令,比如 commit、branch、merge 和 rebase,都是以类似于 mkdir、ls、rm 等典型文件系统命令的方式在本地执行的。他们不进行任何数据传输。

因为 git 是一个分布式版本控制系统,所以每个存储库都是完全功能性的,既可以作为发送者也可以作为接收者。如果计算机之间有通信通道,它们的存储库可以双向交换内容。因此,您可以创建比集中式修订控制系统使用的客户机/服务器模式更复杂的工作流。

此外,git 是一个开源项目的事实,它正在成为世界上最流行的版本控制系统——你会发现有很好的理由开始学习 git。

1-1.在 Windows 上安装 git

问题

你想在 Windows 上安装 git。

解决办法

转到http://msysgit.github.io/并下载最新版本的 git for Windows 安装程序。在撰写本文时,这是版本 1.8.3。安装者名叫:Git-1.8.3-preview20130601.exe。运行安装程序,将所有选项设置为默认值。之后,git 就可以在您的系统上运行了。

它是如何工作的

在 Windows 上安装 git 有两种方法:

  • 使用位于http://www.cygwin.com的 Cygwin 包
  • 使用名为 msysgit 的独立安装程序。

在这个过程中,我们使用 msysgit 包。

当您运行从http://code.google.com/p/msysgit/下载的 msysgit 安装程序时,您会被问到两个问题:

  • 如何配置路径?
  • 如何配置行尾字符的转换?

标题为调整路径环境的对话框设置路径环境变量。Msysgit 安装程序不仅包含 git 二进制文件,还包含许多 Linux 程序,如sshcurllsmkdirrmfind。使用默认设置,安装程序将 git 和这些程序复制到以下目录:

C:\Program Files (x86)\Git\bin

该文件夹包含ls, mkdir, ssh, curl, find等。

C:\Program Files (x86)\Git\cmd

该文件夹包含 git 二进制文件和运行 git 的 shell 脚本

对话框中的第一个选项是仅使用 Git Bash】。使用此设置,path 变量不会被修改。当您启动 Windows 命令行并键入 git 时,命令行将响应一条消息,指出 git 命令不存在。

第二个选择是从 Windows 命令提示符运行 Git,将 C:\Program Files (x86)\Git\cmd 文件夹添加到您的路径中。因此,您可以在 windows 命令行中使用 git 命令。但是,当您在 windows 命令行中键入 ssh 时,系统将会响应一条未知的命令消息。

最后一个选择是从 Windows 命令提示符运行 Git 和包含的 Unix 工具。该选项将两个文件夹 C:\Program Files (x86)\Git\bin 和 C:\Program Files (x86)\Git\cmd 添加到您的路径中。现在,您可以在 Windows 命令行中使用所有包含的工具:ssh、git、ls、curl 等等。但是 C:\Program Files (x86)\Git\bin 中的一些命令,如 find,与 Windows 中原来可用的命令重叠。原来的查找现在在命令行中不可用。

当我写这本书的时候,我的意图是展示可以在所有平台上以完全相同的方式工作的命令。因此,我决定使用 bash 命令行。如果您在 Windows 上工作并且想要使用 bash 命令行,那么您可以保留默认的首选只使用 Git Bash】。

第二个对话框名为配置行尾转换,将名为core.autocrlf的配置选项设置为以下值之一:trueinputfalse。该设置的含义总结在表 1-1 中。

表 1-1 。core.autocrlf 选项的所有值及其对签出和提交操作的影响

|

价值

|

检验

|

犯罪

|
| --- | --- | --- |
| True | LF => CRLF | CRLF => LF |
| input | 没有人 | CRLF => LF |
| false | 没有人 | 没有人 |

当您选择第一个设置值true时,git 将在签出和提交操作期间转换文件中的行尾字符。当您签出文件时,git 会将 LF 转换为 CRLF,当您提交时,git 会将 CRLF 转换为 LF。

第二个选择是input,只有在提交时才转换新行。在本例中,git 将行尾从 CRLF 转换为 LF。

第三个设置(false)关闭所有转换。

行尾字符的转换在配方 13-2 到 13-6 中有更详细的解释。无论您当前选择哪一个,您都可以使用以下命令之一来更改设置:

$ git config --global core.autocrlf true
$ git config --global core.autocrlf input
$ git config --global core.autocrlf false

安装完成后,运行开始菜单中的 git bash 应用。要验证安装是否成功,您可以运行以下命令:

$ git --version

它应该打印出您的系统上安装的 git 版本。

image 提示如果您想将当前目录更改为 c 盘的根目录,请使用以下命令:$ cd /c

这相当于命令:c:

1-2.在 Linux 上安装 git

问题

你想在 Linux 上安装 git。

解决办法

根据您的系统,运行以下命令之一:

# for Ubuntu
$ sudo apt-get install git

# for Fedora
$ sudo yum install git

它是如何工作的

在 Linux 上安装 git 最简单的方法是使用可用的包。如果你想使用 git 的源代码来编译和安装 git,请遵循方法 11-3 中描述的步骤。

要验证安装是否成功,您可以运行以下命令:

$ git --version

它应该打印出您的系统上安装的 git 版本。

1-3.在 OS X 上安装 git

问题

你想在 OS X 上安装 git。

解决办法

访问http://code.google.com/p/git-osx-installer/站点并下载 git 的最新可用版本。运行下载的安装程序,将所有选项设置为默认值。

它是如何工作的

在 OS X 上安装 git 最简单的方法是使用图形化安装程序。要验证安装是否成功,您可以运行以下命令:

$ git --version

它应该打印出您的系统上安装的 git 版本。

1-4.访问手册

问题

你想访问 git 手册。

解决办法

运行以下命令:

$ git help
$ git help -a
$ git help -g

$ git help commit
$ git commit --help

$ git help merge
$ git merge --help

它是如何工作的

Git 命令分为两大类:

  • 瓷器命令
  • 管道命令

命令是日常使用的高级命令。除其他外,这一群体包括:

$ git add
$ git commit
$ git help
$ git push

另一组称为管道 ,包含低级命令。以下是一些例子:

$ git receive-pack
$ git update-index
$ git upload-pack

默认情况下,命令$ git help只列出了瓷瓶命令。如果你想列出管道命令和瓷器命令,使用-a开关$ git help -a

您可以使用以下语法访问特定 git 子命令的文档:

$ git help [COMMAND]
$ git [COMMAND] --help

以下是访问$ git commit命令文档的命令:

$ git help commit
$ git commit --help

1-5.配置 git

问题

您希望配置 git 以准备工作。

解决办法

运行以下命令:

$ git config --global user.name

它应该打印出空的结果。这是因为在安装之后,用户名没有被配置。使用以下命令设置 user.name 配置选项:

$ git config --global user.name "John Doe"

输入你的姓名,而不是无名氏。

接下来,运行命令:

$ git config --global user.email john.doe@example.net

此命令将设置您的电子邮件。

它是如何工作的

如果您想在 git 存储库中创建提交,您必须配置两个设置:user.name 和 user.email。用作 user.name 和 user.email 值的字符串将存储在您创建的每个提交中。

二、使用著名的存储库

我们将开始我们的旅程,探索现有的和相当著名的仓库。本章的主要目标是熟悉存储库——它们的类型和结构。在本章中,您将学习以下内容:

  • git 存储库最流行的托管解决方案是什么?
  • 如何创建托管在Github.comBitbucket.org上的存储库的本地副本?

一旦我们知道如何克隆一个存储库,我们就可以分析它的结构。然后我们将探索工作目录 git 目录及其内容。在这一点上,我们将能够将一个存储库分类为裸露的或者非裸露的。

接下来,我们将讨论打印存储库信息的各种命令,例如

  • 修订列表
  • 贡献者列表
  • 修订的数量
  • 贡献者的数量
  • git 目录和工作目录的磁盘使用情况

为了使输入长 git 命令变得更容易,我将定义它们的别名。

image 我已经在 Linux 和 Windows 两个平台上测试了本章和书中介绍的所有命令。我的目的是提供一套不管你的平台如何都能工作的指令。为了实现这个目标,文件系统操作是用 Linux 命令执行的,比如lsrm。此外,清单以$开头,路径使用/作为分隔符——表明它们是为 Linux 准备的。但是,如果您使用的是 Windows,请不要担心。只需使用 git 发布的 bash 命令解释器,所有的命令都会运行良好。

如果您使用的是不同于 Linux 的类 Unix 系统,一些命令(即duecho)可以使用与本书中介绍的不同的开关。因此,您需要自定义这些命令。

2-1.克隆托管在 Github 上的存储库

问题

您想要获得 jQuery 存储库的本地副本。

解决办法

启动命令行并创建git-recipes/git-recipes/02-01/目录:

$ cd /some/where/on/your/system
$ mkdir git-recipes
$ cd git-recipes
$ mkdir 02-01

将当前目录更改为02-01/:

$ cd 02-01

然后执行如清单 2-1 所示的命令。

清单 2-1。 命令克隆 jQuery 库

$ git clone https://github.com/jquery/jquery.git

image 注意在克隆命令期间,git 将一个完整的存储库从其原始位置复制到您的本地存储系统。根据您的带宽和项目大小,此操作可能需要相当长的时间。但是不用担心。克隆只在您设置本地存储库时进行一次。所有后续的数据传输都非常高效,因为 git 只传输丢失的数据部分。克隆的内部原理在第十章(配方 10-1)中有解释。

在您运行清单 2-1 中的命令后,您的git-recipes/02-01/目录将包含如图 2-1 中所示的文件和目录。注意,jQuery 存储在子目录jquery/下,而不仅仅是在git-recipes/02-01/中。

9781430261032_Fig02-01.jpg

图 2-1 。git 克隆命令后 git-recipes/02-01/目录的内容

image 注意事项 图 2-12013 年 4 月编制。正如您所猜测的,jQuery 项目一直在向前发展。因此您的git-recipes/02-01/目录的内容可能会有所不同。

它是如何工作的

要克隆 jQuery,您必须找到指向其存储库的 URL。启动网络浏览器,转到Google.com,搜索"jquery git repository"。结果将包括:

https://github.com/jquery/jquery

以类似的方式,您可以找到其他流行的开源项目的 URL。表 2-1 列出了另外三个项目的关键词和 URL:Linux、git 和 Mozilla。

表 2-1 。如何找到其他项目的 git 仓库?

|

Google.com中搜索的短语

|

存储库的 URL

|
| --- | --- |
| linux git 仓库 | https://github.com/torvalds/linux |
| git git repository | https://github.com/git/git |
| mozilla git 存储库 | https://github.com/mozilla/mozilla-central |

一旦知道了 jQuery 存储库的 URL,就可以启动 web 浏览器并访问:

https://github.com/jquery/jquery

您将看到如图图 2-2 所示的页面。

9781430261032_Fig02-02.jpg

图 2-2 。jQuery 知识库的主页面https://github.com/jquery/jquery

Github 上存储的每个库都可以在两个不同的 URL 下使用:HTTPS 和 SSH。您可以使用图 2-2 中箭头所指的按钮复制它们。jQuery 储存库的 URL 是:

HTTPS: https://github.com/jquery/jquery.git
SSH: git@github.com:jquery/jquery.git

如果您没有安装了 SSH 密钥的 Github 帐户,则不能使用 SSH URL。你必须选择 HTTPS 网址。

image 提示在第十三章我们将创建并配置一个 Github 账户。然后你也可以使用 SSH URL 在那之前,你必须使用 HTTPS 网址。

这样,您可以克隆在Github.com上可用的所有存储库。记住这个命令:

$ git clone https://github.com/abc/def.git

因为它创建了子目录def/。克隆体就储存在里面。

但是,如果您在 Windows 上工作,并且试图克隆 Linux 源代码,例如,您将会遇到问题,因为不同的系统有不同的文件名限制。我们会在第十一章对此进行分析。

如果没有配置了 SSH 密钥的 Github 账号,使用 SSH URL 会怎么样?jQuery 的 SSH URL 是:

git@github.com:jquery/jquery.git

如果您将它用于 git clone 命令:

$ git clone git@github.com:jquery/jquery.git

那么您将得到以下错误:

Permission denied (publickey)

2-2.克隆 Bitbucket 上托管的存储库

问题

您想要获得存储在Bitbucket.org上的亚特兰蒂斯 AUI 库的本地副本:

https://bitbucket.org/atlassian/aui

您希望克隆文件直接保存在git-recipes/02-02/下,而没有额外的aui/子目录。

解决办法

启动命令行并创建一个git-recipes/02-02/目录:

$ cd git-recipes
$ mkdir 02-02

将您当前的目录更改为02-02/:

$ cd 02-02

然后运行清单 2-2 中所示的命令。注意最后一个参数——一个点。这个点代表当前目录,因此克隆将直接放在git-recipes/02-02/下。如果没有点,克隆的存储库将被存储在子目录git-recipes/02-02/aui/中。

清单 2-2。 命令克隆 AUI 库

$ git clone https://bitbucket.org/atlassian/aui.git.

image 注意git 克隆命令的语法是:$ git clone URL [directory]。如果使用的话,可选的[directory]参数为克隆的存储库设置目标目录。

它是如何工作的

启动您的网络浏览器并转到https://bitbucket.org/atlassian/aui。亚特兰大 AUI 知识库的主页如图 2-3 所示。

9781430261032_Fig02-03.jpg

图 2-3 。亚特兰蒂斯 AUI 知识库主页https://bitbucket.org/atlassian/aui

Bitbucket 为每个存储库提供了两个 URL:HTTPS 和 SSH。图 2-3 和下面的列表将为您提供对它们的访问:

HTTPS: https://bitbucket.org/atlassian/aui.git
SSH: git@bitbucket.org:atlassian/aui.git

与 Github 一样,SSH URL 只能在您拥有一个配置了 SSH 密钥的 Bitbucket 帐户时使用。

image 提示git 最流行的两个托管解决方案是Github.comBitbucket.org。两者都为公共存储库提供无限制的免费账户。

2-3.克隆本地存储库

问题

你想要克隆一个你在配方 2-1 中创建的库git-recipes/02-01/jquery/,并且你更喜欢在git-recipes/02-03/下直接存储一个新的克隆,而不需要一个额外的jquery/目录。

解决办法

转到您的git-recipes/目录:

$ cd git-recipes

然后你需要执行清单 2-3 中的命令。

清单 2-3。 克隆本地存储库的命令

$ git clone 02-01/jquery 02-03

使用清单 2-3 中所示的命令后,目录git-recipes/02-03/中将包含如图 2-4 中所示的文件。

9781430261032_Fig02-04.jpg

图 2-4 。成功克隆后 git-recipes/02-03/目录的内容

它是如何工作的

Git clone 命令接受指向存储库的 URL 和本地路径。因此,您可以通过向命令传递路径而不是 URL 来克隆本地存储库。

2-4.复制本地存储库

问题

您想要克隆您在配方 2-3 中创建的存储库git-recipes/02-03/。这一次,你想要使用一个带有–R 标志的标准cp命令,而不是使用git clone

解决办法

进入目录git-recipes/:

$ cd git-recipes

第二,执行以下命令:

$ cp -R git-recipes/02-03 git-recipes/02-04

该命令将从git-recipes/02-03/创建文件的精确副本。git-recipes/02-04/的内容是一个有效的 git 存储库。

它是如何工作的

通过使用cp -R命令,您可以递归地复制一个目录。如果在包含存储库的目录上使用,它将创建一个正确的存储库,该存储库与使用git clone命令创建的存储库几乎相同。我们将在方法 2-5 中探索在方法 2-3 和 2-4 中创建的存储库之间的区别。

image 提示一旦您知道可以使用标准的文件系统操作(如cp)来复制一个存储库,您可以使用rsyncscp来达到同样的效果。在第十一章中,我们将使用 scp 命令启动一个新项目。

2-5.探索 git 存储库的内容

问题

git clone命令创建的目录包含什么?要回答这个问题,您需要用cdlscat命令探索git-recipes/02-03/目录的内容。你也可以使用你最喜欢的文件管理器。

解决办法

git-recipes/02-03/目录的内容如图图 2-5 所示,包含 jQuery 文件和目录以及一个名为.git的特殊目录。

9781430261032_Fig02-05.jpg

图 2-5 。在配方 2-3 中创建的存储库

名为.git的目录称为 git 目录 。它包含关于存储库的所有信息。.git目录的内容如图 2-6 所示。

9781430261032_Fig02-06.jpg

图 2-6 。. git 目录的内容

它是如何工作的

如果你想显示一个.git目录的内容,你可以使用以下命令:

$ cd git-recipes/02-03/
$ cd .git
$ ls -l

最后一个命令将打印图 2-6 中所示的文件和目录。在表 2-2 中简要描述了每个项目的作用。完整的描述包含在处理 git 具体细节的方法中。

表 2-2 。. git 目录的内容

|

目录/文件

|

描述

|
| --- | --- |
| hooks/ | 目录包含当某些事件发生时 git 可以自动执行的脚本;例如,在每次提交之前和之后。 |
| info/ | 目录包含一个名为exclude的文件,可以用来从存储库中排除文件。与.gitignore文件不同,该文件不被其他人共享。 |
| logs/ | 目录包含对存储库所做的本地更改的日志。 |
| objects/ | 这是包含文件、目录、修订和标签的所有信息的数据库。 |
| refs/ | git 在这里存储关于分支和轻量级标签的信息。 |
| config | 这是本地配置文件,包含将仅应用于此存储库的选项。 |
| description | 这是存储库的简短描述。它由 git 发布的 Gitweb CGI 应用使用。 |
| HEAD | 存储库的当前分支或版本 |
| index | 存储库的临时区域 |
| packed-refs | 打包格式的来自refs/的参考文献列表 |

现在让我们比较三个配置文件的内容:

git-recipes/02-01/jquery/.git/config
git-recipes/02-03/.git/config
git-recipes/02-04/.git/config

配方 2-1 中创建的第一个解决方案是存储在 Github 上的原始 jQuery 存储库的克隆。在git-recipes/02-01/jquery/.git/config中,你会发现以下几行:

[remote "origin"]
    url = https://github.com/jquery/jquery.git

条目[remote "origin"]存储传递给git clone命令的地址。

第二个解决方案是本地目录的克隆。文件git-recipes/02-03/.git/config包含:

[remote "origin"]
    url = /home/john/git-recipes/02-01/jquery

如你所见,这次[remote "origin"]指向本地目录。

image 提示我假设你的git-recipes/目录的完整路径是:/home/john/git-recipes/

第三种解决方案是对git-recipes/02-03/的精确复制。因此,文件git-recipes/02-04/.git/config包含:

[remote "origin"]
    url = /home/john/git-recipes/02-01/jquery

如果我们使用:

$ cd git-recipes
$ git clone 02-03 02-04

创建git-recipes/02-04/;文件git-recipes/02-04/.git/config应该包含:

[remote "origin"]
    url = /home/john/git-recipes/02-03

git-recipes/02-03/git-recipes/02-04/没有任何区别。稍后,在第十章中,我们将学习使用git remote命令改变配置文件中的[remote "origin"]条目。

结论

作为方法 2-4 和 2-5 的结论,记住存储库可以被复制和移动到你的驱动器上的不同位置;就像任何其他目录一样。git 目录.git不包含任何将存储库绑定到您的驱动器上的特定路径的信息。

2-6.删除和恢复工作目录的内容

问题

您想要验证 git 已经将您的所有文件存储在数据库.git/objects中。为此,您需要删除工作目录的内容,然后从 git 数据库中恢复它。

解决办法

输入在配方 2-3 中创建的储存库:

$ cd git-recipes/02-03

删除所有文件和目录,除了.git子目录:

$ ls -la
$ rm -rf *
$ rm .????*

现在,目录git-recipes/02-03/只包含一个子目录.git。您可以通过以下方式进行检查:

$ ls -l

接下来,执行命令:

$ git reset --hard

所有文件都将被还原。命令:

$ ls -l

现在打印出与图 2-4 中相同的结果。

它是如何工作的

通常,包含 git 存储库的目录由两个区域组成。其中一个已经讨论过了,这个 git 目录命名为.git。另一个叫做工作目录。它们都如图 2-7 所示。

9781430261032_Fig02-07.jpg

图 2-7 。git 目录和工作目录

工作目录是包含您的工作的临时存储。另一方面,git 目录包含存储项目所有快照的数据库。方法 2-6 应该让你相信你的工作目录的内容可以很容易地从数据库中恢复。

2-7.克隆一个空存储库

问题

您想要从配方 2-3 中创建一个存储库的裸克隆。

解决办法

发出以下命令:

$ cd git-recipes
$ git clone --bare 02-03 02-06

进入02-06/目录并检查其内容:

$ cd 02-06
$ ls -la

上述命令将打印与图 2-6 中的相同的输出。

它是如何工作的

git clone 命令带有一个可选参数--bare。您可以使用--bare参数来创建一个空的存储库。一个裸库 只包含 git 目录的内容。它不包含工作目录。这种类型的存储库仅用于同步目的。我们会在第十章用到。

image 记住可以用$ git clone --bare [URL]命令创建空的存储库。这种类型的存储库不包含工作目录。它的内容相当于非裸存储库中的一个.git目录的内容。

2-8.使用 git log 命令浏览历史记录

问题

您希望打印以下关于 jQuery 存储库的信息:

  • 存储库中修订的完整列表
  • 存储库中最新修订版的简化列表
  • 约翰·雷西格的修订清单
  • 包含 2012 年最后五次修订的列表

解决办法

进入目录git-recipes/02-01/jquery/:

$ cd git-recipes/02-01/jquery

要打印存储库中修订的完整列表,请执行:

$ git log

您将会看到输出类似于清单 2-4 中的输出。这是可从当前修订访问的修订的完整列表。您可以使用空格键和箭头键滚动输出。按下q退出git log

清单 2-4。git log 命令的输出

commit 18cccd04a6f69018242bce96ef905bc5d3be6ff8
Author: Richard Gibson < richard.gibson@gmail.com >
Date:   Mon Apr 29 13:31:59 2013 -0400

    Fix #13803: domManip remote-script evaluation per 1.9 (AJAX dataType "script")

commit 55e319aa52eb828a3a4c2298aa75b6d15cfa06f8
Author: Corey Frang <gnarf@gnarf.net>
Date:   Wed Apr 24 16:07:15 2013 -0400

    Fixes #13815: Ensure each element has its own private data object - Tests by @rwldrn

commit 3a6194076b8b7ab5a9d9f5e6ec602db2ab427d3e
Author: Oleg < markelog@gmail.com >
Date:   Wed Apr 24 22:15:41 2013 +0400

    Fix #13818: Add wrapMap entry for col element

清单 2-4 中的输出显示了三个版本。第一个修订版的名称是:

18cccd04a6f69018242bce96ef905bc5d3be6ff8

本修订版由 Richard Gibson 于 2013 年 4 月 29 日星期一创建。评论是:

Fix #13803: domManip remote-script evaluation per 1.9 (AJAX dataType "script")

提供修订版包含问题编号 13803 的修复程序的信息。

经过缩短和简化,修订列表可以用列表 2-5 中所示的命令打印出来。

清单 2-5。 产生简化和缩短的日志信息的命令

$ git log --abbrev-commit --abbrev=4 --pretty=oneline -10

它将打印类似于以下内容的输出:

18cc Fix #13803: domManip remote-script...
55e3 Fixes #13815: Ensure each element...
3a619 Fix #13818: Add wrapMap entry for...
78c8 Fix #13819: .parent sort direction...
ad71f Fix #13809: Avoid collisions with...

每一行都涉及一个版本,包含一个简短的缩写名 SHA-1 和注释。如果可能,缩写将缩短为四个字符:

18cc
55e3
78c8

必要时,使用更多字符:

3a619
ad71f

使用-10参数,输出仅限于最后 10 个版本。

清单 2-6 中显示的命令打印了 John Resig 所做的修改。

清单 2-6。 约翰·雷西格撰写的修订版

$ git log --author="John Resig"

列表 2-5 和列表 2-6 中显示的参数可以一起使用。命令:

$ git log --abbrev-commit --abbrev=4 --pretty=oneline -10 --author="John Resig"

以简化形式打印 John Resig 的最后 10 次修订。

在清单 2-7 的中给出了生成 2013 年最后五次修订清单的命令。

清单 2-7。2013 年最后五次修订

$ git log --pretty=oneline --since="2012-12-20" --until="2013-01-01" -5

它是如何工作的

git 存储库的历史由一系列修订组成。每个修订都是工作目录在特定时间点的快照。修订存储在.git/objects数据库中。

每个版本都通过其名称来识别。Git 使用一种 SHA-1 算法 来生成名字。因为修订版的 SHA-1 是使用许多不同类型的数据计算的——作者姓名、当前时间戳和快照等——我们可以将它们视为唯一标识符。两个不同的修订版具有相同 SHA-1 的概率非常小,可以忽略不计。事实上,SHA-1 唯一性是最基本的 git 假设之一。正如你将在第十一章中看到的。整个同步过程都依赖于它。

image 提示用户不能给版本命名。所有的名字都是 git 自动生成的。Git 规则,你可以相信它永远不会为不同的版本生成两个相同的名字。

SHA-1 名称的长度为 20 个字节,因此,它们的十六进制表示形式需要 40 个字符,例如:

18cccd04a6f69018242bce96ef905bc5d3be6ff8

在本书的后面,我们将需要使用这个名字作为参数传递给各种命令;如果是这样的话,就不必使用全部 40 个字符。通常,前七个字符就足够了。名称的最短缩写必须是四个字符长。记住缩写必须是唯一的——如果不是,那么你必须使用更多的字符。

储存在库中的修订列表可以用git log命令打印出来。它的各种选项和开关可用于过滤和重新格式化显示的修订。一个git log命令的输出按照版本创建的时间排序。最新版本显示在输出的顶部。

默认情况下,git log打印当前版本的所有可用版本。

可以用--pretty参数改变输出的格式。可用值及其含义总结在表 2-3 中。

表 2-3 。pretty 参数的值

|

价值

|

描述

|
| --- | --- |
| oneline | 名称和注释打印在一行中。 |
| short | 名称、作者和注释 |
| medium | 与short相同,但增加了修订日期 |
| full | 名称、作者、提交者和注释 |
| fuller | 姓名、作者、作者日期、提交者、提交日期和注释 |
| email | 与电子邮件格式的short相同 |
| raw | 低级修订的信息:名称、树、父修订的名称、作者和带时间戳的提交者 |
| format | 用户定义的格式 |

在表 2-3 中显示的参数可以传递给git log命令,如下所示:

$ git log --pretty=oneline
$ git log --pretty=short
$ git log --pretty=raw

-–pretty=oneline可以缩短为:

$ git log --oneline

影响格式的其他参数有:

  • --abbrev-commit—此选项打开缩写。
  • --abbrev=n—该选项设置缩写名称的长度。
  • --decorate—该选项包括每个版本的标签和分支。

最短的缩写必须包含四个字符。因此,--abbrev的最小值是 4:

$ git log --abbrev-commit --abbrev=4

image 提示参数--oneline将 SHA-1 缩写为七个字符。

参数--pretty=format允许您定义任意输出的格式。包含占位符的特殊字符串定义了输出。命令:

$ git log --pretty=format:"%an --- %H"

将以如下形式打印输出:

Joe Doe --- 123456...

输出的第一部分(例如,Joe Doe)是作者的名字,而第二部分是修订版的完整 SHA-1。该输出由两个占位符生成:

%an – author's name
%H – full SHA-1 hash

其他有用的占位符包括:

%h: abbreviated commit hash,
%ae: author email,
%ad: author date,
%cn: committer name,
%ce: committer email,
%cd: committer date,
%e: encoding,
%s: subject,
%n: newline.

image 提示git log手册中提供了占位符的完整列表。您可以使用git help log命令来访问它。

以下是过滤输出中包含的修订的一些参数:

  • -n—修订数,例如–7 将 git 日志限制为最近七次修订
  • --since="yyyy-mm-dd"—开始日期
  • --until="yyyy-mm-dd"—完成日期
  • --author="John Doe"—由给定作者提交

image 提示传递给--since--until参数的日期可以用yyyy-mm-dd格式或不太正式的格式设置为--since="1 week ago"--since="Two months ago"--until="5 days ago"--until="7 hours ago"--until="yesterday"。为了避免输入引号,你也可以用点来表示空格,比如--since=1.week.ago--since=Two.months.ago--until=5.days.ago

2-9.使用 git log 和 shortlog 命令分析存储库

问题

对于 jQuery 项目,您需要以下问题的答案:

  • 存储库包含多少修订版?
  • 有多少开发人员参与了这个项目?
  • 他们在这个项目上工作了多少天?
  • 工作目录使用了多少空间?
  • git 目录使用了多少空间?
  • 工作目录中有多少文件?

解决办法

进入目录git-recipes/02-01/jquery/:

$ cd git-recipes/02-01/jquery

要回答这些问题,执行列表 2-8 到 2-13 中所示的命令。

清单 2-8。 打印存储库中提交数量的命令

$ git log --pretty=oneline | wc -l

清单 2-9。 打印贡献者人数的命令

$ git shortlog -s | wc -l

清单 2-10。 产生贡献天数的命令

$ git log --pretty=format:%cd --date=short | uniq | wc -l

清单 2-11。 返回 git 目录使用的空间量的命令

$ du -h -s .git

清单 2-12。 返回工作目录使用的空间量的命令

$ du -h -s --exclude=.git

清单 2-13。 该命令产生工作目录中文件的数量

$ git ls-files | wc -l

image 提示 Linux 和 Windows 版本的du都支持--exclude参数。但是其他一些系统,比如 BSD,使用其他选项。在 BSD 中,用–I选项设置排除(I代表忽略)。

它是如何工作的

使用以下命令可以找到上述问题的答案:

git log
git shortlog
git ls-files
du
wc
uniq
grep

我们已经知道,命令:

$ git log --pretty=oneline

以简化形式打印所有修订的列表,其中每个修订占一行。将列表传送到wc -l:

$ git log --pretty=oneline | wc -l

我们得到修订的数量。

git shortlog命令显示了由作者分组的提交信息。如果没有任何参数,其输出具有以下形式:

Adam Coulombe (1):
      Fix #13150, ...

Adam J. Sontag (7):
      .closest() should
      Add a comment expl
      Add a comment to e
      Add link to chrome
      shorten the SHA
      Fix tabs vs spaces
      Revert grunt, grun
...

上面的列表包含了所有的开发者和他们的修订版。参数-s仅打印修订号和开发人员姓名:

1  Adam Coulombe
7  Adam J. Sontag
...

参数-n打印按修订次数以数字顺序排序的结果。

为了打印贡献者的数量,我们将git shortlog -s的结果传送到wc -l:

$ git shortlog -s | wc -l

下一个问题有点难回答。首先,我们想以特殊的形式打印出git log命令的输出。我们希望每行只包含一个提交日期,格式为yyyy-mm-dd。这可以通过以下方式实现:

$ git log --pretty=format:%cd --date=short

上述命令将生成日期列表:

2013-04-22
2013-04-22
2013-04-20
2013-04-20
2013-04-18
...

每次提交的日期都会显示在输出中。让我们删除重复的。我们可以使用uniq命令来完成:

$ git log --pretty=format:%cd --date=short | uniq

因此,我们将找出对项目做出贡献的不同日期。如果我们将结果传送到wc -l:

$ git log --pretty=format:%cd --date=short | uniq | wc -l

然后我们得到期望的天数。

image 提示这是我用来宣布我写一本书的工作天数的大致标准。当命令$ git log --pretty=format:%cd --date=short | uniq | wc -l返回 95 时,意味着我写了一本书不超过 95 天。

接下来的两个问题涉及工作目录和 git 目录包含的空间量。git 目录包含的空间量由以下命令返回:

$ du -h -s .git

工作目录包含的空间量由以下命令打印:

$ du -h -s --exclude=.git

以下是 jQuery 项目的结果:

  • 工作目录:1.3 MB
  • git 目录:16 MB

正如您所看到的,git 目录比工作目录使用了更多的空间。这并不奇怪:存储在.git/objects中的数据库包含 5192 个修订。每一次修订都可以看作是一个完整工作目录的快照。

最后一个问题可以用 git ls-files 命令来回答:

$ git ls-files | wc -l

git ls-files 命令打印工作目录中所有文件的名称。我们用wc –l来数它们。

2-10.为配方 2-8 和 2-9 中讨论的命令 定义别名

问题

清单 2-5 和 2-8 到 2-13 中显示的命令输入起来相当长。您希望定义更容易键入的别名,同时返回相同的输出。

解决办法

打开命令行并转到您的主目录。在 Windows 上使用 Linux、Mac 或 bash 命令行,可以通过以下方式完成:

$ cd ∼

如果您使用标准的 Windows 命令行,请尝试:

$ cd %userprofile%

启动文本文件编辑器并打开文件.gitconfig。如果您使用vi,您可以使用:

$ vi .gitconfig

在文件的底部追加清单 2-14 中的内容。保存文件并退出编辑器。

清单 2-14。 清单 2-5 和 2-8 到 2-13 中所示命令的别名

[alias]
    l    = log --oneline --abbrev-commit --abbrev=4 -25

    days = "!days() {
        git log --pretty=format:%cd --date=short | uniq;
    }; days"

    stat = "!stat() {
        echo -n Number of revisions:;
        git log --oneline | wc -l;
        echo -n Number of developers:;
        git shortlog -s | wc -l;
        echo -n Number of days:;
        git days | wc -l;
        echo -n The working directory:;
        du -h -s --exclude=.git;
        echo -n The git directory:;
        du -h -s .git;
        echo -n Number of files in the working dir:;
        git ls-files | wc -l;
    }; stat"

image 提示您不必键入清单 2-14 中显示的别名。它们都可以在https://github.com/gajdaw-git-recipes/aliases存储库中找到。

image 注意清单 2-14 中的应该在 stat 和 days 别名中不带换行符。该文件应该类似于:

days = "!days() { ... }; days"
stat = "!stat() { ... }; stat"

换行符仅用于可读性目的。

当你在.gitconfig文件的底部输入完清单 2-14 的内容后,进入目录git-recipes/02-01/jquery/:

$ cd git-recipes/02-01/jquery

并执行第一个别名:

$ git l

您应该会看到类似于以下内容的输出:

18cc Fix #13803: domManip remote-script ...
55e3 Fixes #13815: Ensure each element ...
3a619 Fix #13818: Add wrapMap entry for ...
...

接下来尝试第二个别名:

$ git stat

它将产生类似的结果:

Number of revisions:     5192
Number of developers:    190
Number of days: 1246
The working directory:   1.3M      .
The git directory:       16M   .git
Number of files in the working dir:     149

image 注意别名 git stat 使用echo–n参数来抑制换行符的输出。如果您的系统不支持echo –n,以上结果的格式会有所不同。

它是如何工作的

Git 允许您为任意命令定义别名。别名应该存储在用户的配置文件中。该文件应该被命名为.gitconfig,并存储在您的主目录中。如果您已经执行了任何命令来配置带有--global选项的 git,例如git config --global user.name,那么.gitconfig文件已经存在于您的主目录中。否则,您必须创建它。

清单 2-14 中显示的第一个别名可以通过以下方式创建:

$ git config --global alias.l "log --oneline --abbrev-commit --abbrev=4 -25"

命令git config –global alias.abc "def"只是在您的个人.gitconfing文件的[alias]部分创建一个条目abc = def

如果您想找到主目录的位置,请键入:

$ cd ∼
$ pwd

上述命令可以在 Linux、Mac 或 Windows 上的 bash 命令行上很好地工作。如果使用标准的 Windows 命令行,请使用:

$ cd %userprofile%
$ cd

如果您使用vi工作,您可以使用以下命令打开 git 配置文件:

# Linux
$ vi ∼/.gitconfig

# Windows
$ vi %userprofile%\.gitconfig

的语法。gitconfig 文件

我们将从一个.gitconfig文件的语法开始解释别名。有趣的字符有:散列符号、分号、引号和反斜杠。

在一个.gitconfig文件中,你可以使用散列符号和分号来表示延伸到行尾的注释。因此定义是:

word = lorem ; ipsum

用值lorem设置名为word的属性。第二个词ipsum被跳过,因为分号开始注释。类似的规则也适用于哈希标记。定义:

word2 = dolor # sit

用值dolor设置属性word2

如果要定义包含分号或散列符号的值,必须使用引号:

sentence = "Lorem ; ipsum"

上面定义了一个名为sentence的属性,其值为:

Lorem ; ipsum

引号应该被转义是不足为奇的。该行:

person = "John \"Moo\" Cowboy"

用值定义属性person:

John "Moo" Cowboy

同样的转义过程也适用于反斜杠。定义:

str = "a\\b"

str属性的值设置为:

a\b

以上描述阐明了以下符号:

something = "x ; \"y\" ; \\ ; z"

因为我们使用分号,所以需要用引号括起来。里面的引号被转义了。双反斜杠是转义反斜杠,因此属性something的值为:

x ; "y" ; \ ; z

请记住,这些规则适用于您存储在.gitconfig文件中的所有内容。

image 提示.gitconfig文件的语法在手册的语法:$ git help config 一节中有描述

别名语法

.gitconfig文件中定义 git 别名的语法是:

alias = command

或者

alias = !command

第一个版本——没有感叹号的版本——适用于 git 子命令。别名:

abc = def

定义可以命名为的命令:

$ git abc

执行时,git abc将产生与以下相同的效果:

$ git def

因此,我们可以定义别名:

l = log --pretty=oneline

当被调用时:

$ git l

将扩展到:

$ git log --pretty=oneline

别名的第二种语法——带感叹号的语法——适用于任意 shell 命令。别名:

list-files = !ls

可以称为:

$ git list-files

该调用将产生以下命令:

$ ls

我在 shell 函数中使用带感叹号前缀的别名。别名:

foo = "!bar(){ }; bar"

可以称为:

$ git foo

感叹号告诉 git 应该将这个别名传递给 shell。下一部分:

bar(){};bar

由函数定义组成:

bar(){};

和一个函数调用:

bar

在大括号内,您可以放置任意数量的完整 shell 调用,用分号分隔,例如:

foo = "!bar(){ echo abc; ls; }; bar"

这个别名可以称为:

$ git foo

这将产生两个命令:

$ echo abc
$ ls

同样,别名:

info = "!funInfo(){ git --version; git log --pretty=oneline -3; }; funInfo"

可以称为:

$ git info

它将产生与两个命令相同的输出:

$ git --version
$ git log --pretty=oneline -3

由于分号的存在,使用 shell 函数的别名需要用引号括起来。

清单 2-14 中的别名

清单 2-14 中的第一个别名是:

l = log --oneline --abbrev-commit --abbrev=4 -25

它没有使用感叹号;因此,它指的是 git 子命令。被呼叫时:

$ git l

它将扩大到:

$ git log --pretty=oneline --abbrev-commit --abbrev=4 -25

Git 允许您向别名传递额外的参数。因此,如果您想生成 John Doe 修订的简化列表,请使用参数--author调用别名:

$ git l --author="John Doe"

您可以用同样的方式传递任何其他参数。

下一个别名是:

days = "!days() {
    git log --pretty=format:%cd --date=short | uniq;
}; days"

由于感叹号,它被扩展为一个 shell 命令。该命令定义并调用名为days()的函数。当您键入时:

$ git days

它将最终执行:

$ git log --pretty=format:%cd --date=short | uniq

最后一个别名是一个 shell 函数,它调用许多其他命令。

stat = "!stat() {
    echo -n Number of revisions:;
    git log --oneline | wc -l;
    echo -n Number of developers:;
    git shortlog -s | wc -l;
    echo -n Number of days:;
    git days | wc -l;
    echo -n The working directory:;
    du -h -s --exclude=.git;
    echo -n The git directory:;
    du -h -s .git;
    echo -n Number of files in the working dir:;
    git ls-files | wc -l;
}; stat"

请注意,我们用 subalias 产生天数:

$ git days | wc -l

image 提示在 git 中创建别名有两种方法。第一种方法在配方 2-10 中讨论,第二种在配方 5-3 中讨论。我更喜欢在里面定义别名。gitconfig 文件,如配方 2-10 所示。这种方法不依赖于用户平台或权限,使得在课堂和培训中更容易采用。

2-11.分析一个流行的存储库

问题

最流行的 Github 库之一是 twitter/bootstrap,可从以下网址获得:

https://github.com/twitter/bootstrap.git

你想用配方 2-10 的别名来分析它。

解决办法

打开命令行并克隆twitter/bootstrap:

$ git clone https://github.com/twitter/bootstrap.git 02-11

命令:

$ git stat

将打印:

Number of revisions:     3569
Number of developers:     259
Number of days:           505
The working directory:   4.9M      .
The git directory:        28M   .git
Number of files in the working dir: 254

它是如何工作的

使用$ git clone 命令,您可以克隆 Github 或 Bitbucket 上任何可用的公共存储库。在配方 2-10 中创建的别名将有助于您获得项目的一些基本信息。

2-12.可视化存储库的历史

问题

您希望使用一个gitk应用以图形形式显示 HTML 5 样板库的历史。

解决办法

克隆 HTML 5 样板文件库:

$ cd git-recipes
$ git clone https://github.com/h5bp/html5-boilerplate.git 02-12

进入02-12/目录:

$ cd 02-12

并运行gitk命令:

$ gitk

image 注意如果追加了&符号$ gitk &,gitk 应用将在后台运行,可以使用命令行执行其他命令。

它是如何工作的

命令$ gitk将显示图 2-8 中所示的窗口。它包含五个面板:

  1. 修订
  2. 作者
  3. 日期
  4. 修改列表
  5. 已修改文件的列表

9781430261032_Fig02-08.jpg

图 2-8 。gitk 应用的主窗口

使用gitk你不仅可以轻松检查修订列表,还可以检查每个修订中引入的修改。

尝试向下滚动修订。你会发现这些修订并不一定形成一个线性的历史。非线性历史如图 2-9 所示。我们将在第五章、第六章和第七章中讨论非线性历史。

9781430261032_Fig02-09.jpg

图 2-9 。HTML 5 样板库的历史

image 注意$ gitk命令接受我们在配方 2-8 中讨论的所有过滤器。例如,您可以使用 gitk 只显示给定作者提交的内容:$ gitk --author=john

2-13.移除. git 目录

问题

您希望发现删除一个.git目录会如何影响一个存储库。

解决办法

克隆 FontAwesome 存储库:

$ cd git-recipes
$ git clone https://github.com/FortAwesome/Font-Awesome.git 02-13

输入目录:

$ cd 02-13

现在,目录02-13/包含一个 git 存储库。因此,您可以列出日志条目:

$ git log

或项目的贡献者:

$ git shortlog -n -s

如果使用以下命令删除.git目录:

$ rm -rf .git

您将看到工作目录的内容。Git 命令不再起作用了。如果您发出:

$ git log

您将得到以下错误:

fatal: Not a git repository (or any of the parent directories): .git

它是如何工作的

可以用简单的命令删除 git 目录:

$ rm -rf .git

执行此命令后,您将丢失项目的全部历史记录。项目目录将只包含存储在工作目录中的文件的最新版本。

摘要

在这一章中,我们已经讨论了使用 git 库的基本能力。您现在知道如何:

  • 克隆一个存储库(远程和本地)。
  • 进入存储库并发出各种 git 命令。
  • 打印存储在存储库中的列表或修订。
  • 用 git log 和 gitk 分析历史。
  • 发现存储在存储库中的修订、贡献者和文件的列表和数量。
  • 为最常用的命令定义别名。

你也学到了

  • git 目录
  • git 数据库
  • 工作目录

所有这些都是理解后面章节所需要的。

git 目录是一个名为.git的特殊目录,它通常存储在您的项目目录中。它包含您的项目的所有历史和 git 运行所需的各种配置条目。除非得到严格指示,否则不要修改.git目录的内容。

在 git 目录中有一个名为.git/objects的特殊子目录。就是 git 数据库,也叫对象库。这是各种 git 命令存储数据的地方。修订、文件的不同版本、目录、它们的内容等等——它们都存储在这个数据库中。git 不时尝试优化这个数据库。如果使用 git 不当,这可能会导致数据丢失。

第三个区域称为工作目录。它是项目的目录,不包括 git 目录。这是你工作的地方。工作完成后,您可以将工作目录的内容作为下一个版本存储在数据库中。你将在下一章学习如何做到这一点。

你记得秘籍 2-6 吗?如果没有,再分析一遍。这个菜谱展示了使用 git 的一个非常重要的方面。您存储在数据库中的修订可以检索到工作目录中。我们使用git reset --hard来恢复被删除的文件。从现在开始,您应该将工作目录视为一个临时存储。

工作目录和 git 目录用于将每个存储库分类为非裸

一个非裸存储库包含工作目录和 git 目录。非裸存储库的配置文件.git/config包含以下条目:

[core]
    bare = false

裸存储库只包含.git目录。它的.git/config文件包含:

[core]
    bare = true

正如你将在第十章中看到的,这种类型的库用于同步目的。

三、使用线性历史创建本地存储库

在这一章中,你将学习如何创建你自己的库,以及如何在你的日常工作中使用它们。这包括

  • $ git init初始化新的储存库
  • 使用$ git add –A$ git commit -m "..."将快照存储为修订版
  • 使用$ git status -s -b检查储存库的状态

您将学习如何从头开始一个新项目,以及如何导入现有文件。

三种方法将集中在将工作目录恢复到存储在修订版中的快照。在学习了这些方法之后,您应该能够将任何存储库(比如 jQuery)的工作目录重置为任意修订版,比如存储库中的第一个修订版,然后将工作目录返回到其最新状态。

我特别关注可能会引发问题的情况。两种方法精确地描述了如何以及何时丢失未提交或已提交的修改。这些知识会建立你的自信。如果您遵守一些简单的规则,您将永远不会丢失存储在 git 存储库中的内容。

在第二章中,我们将存储库描述为非裸露或者裸露。这种特征基于工作目录的存在。这里我们再介绍另一种分类:干净或者。这个分类只适用于非裸库。当存储库的工作目录内容与存储在其最新版本中的快照相同时,存储库就是干净的。另一方面,如果工作目录中的文件被修改并且没有提交,我们称这个存储库为脏的。为了找出一个存储库是干净的还是脏的,我们使用了$ git status命令。

3-1.创建您的第一个存储库

问题

您想要开始一个新项目,该项目将由包含您最喜欢的作家所写的书籍列表的文本文件组成。让我们假设您计划将每个作家的作品存储在一个单独的文件中。创建文件并键入其内容后,您应该保存文件并将其提交到存储库中。假设你用阿加莎·克里斯蒂、约翰·格里森姆和斯蒂芬·金的作品创建文件。你的存储库的历史看起来类似于图 3-1 。

9781430261032_Fig03-01.jpg

图 3-1 。配方 3-1 中的储存库

解决办法

在这个菜谱中,您将使用 git commit 命令创建您的第一个修订。Git 不允许提交,除非您将您的身份存储在配置文件中。如果到目前为止您还没有这样做,请运行以下两个命令,用您的个人信息替换John Doejohn.doe@example.net:

$ git config --global user.name "John Doe"
$ git config --global user.email john.doe@example.net

当您准备好提交时,初始化一个新的存储库:

$ cd git-recipes
$ git init 03-01
$ cd 03-01

现在,目录 03-01 包含了 git 存储库。要进行验证,请运行命令:

$ ls -la

它将打印三个项目:

.
..
.git

正如您所猜测的,存储库是空的。这意味着数据库不包含修订。我们可以用$ git log$ git status命令来验证这一点。首先,打印历史记录:

$ git log

答案将是:

fatal: bad default revision 'HEAD'

现在,使用以下命令检查状态:

$ git status

git status 打印的信息将是:

# On branch master
#
# Initial commit
#
nothing to commit (create/copy files and use "git add" to track)

注释# Initial commit意味着存储库已经准备好存储第一次提交。就这么办吧。

创建第一个文件:

$ vi agatha-christie.txt

该文件可以包含清单 3-1 中的文本,但这并不重要。

清单 3-1。 阿加莎-克里斯蒂的内容. txt

Novels
    1943 | Five Little Pigs
    1934 | Murder on the Orient Express

保存文件agatha-christie.txt后,使用以下命令检查存储库的状态:

$ git status -s

您将看到以下输出:

?? agatha-christie.txt

两个问号??通知您agatha-christie.txt文件未被跟踪。这是一个尚未提交的新文件。现在,仓库是脏的。

使用以下两个命令创建您的第一个修订版:

$ git add -A
$ git commit -m "First revision [Agatha Christie]"

该文件现在存储在新版本中。命令:

$ git status -s

返回空输出,这意味着工作目录中没有挂起的更改。换句话说,仓库是干净的。让我们用$ git log检查日志。输出将类似于:

commit de3680b0a770dd46ede81f46cba0ae32f9e4687c
Author: Włodzimierz Gajda <gajdaw@gajdaw.pl>
Date:   Thu May 2 12:50:19 2013 +0200

    First commit [Agatha Christie]

储存库的当前状态如图 3-2 所示。

9781430261032_Fig03-02.jpg

图 3-2 。第一次修订后配方 3-1 的储存库

让我们创建第二个修订版。请遵循以下步骤:

  1. 创建文件john-grisham.txt

    $ vi john-grisham.txt
    
  2. 键入文件的内容:

    Novels
    1989 | A Time to Kill
    1991 | The Firm
    1992 | The Pelican Brief
    
  3. 保存文件并关闭编辑器。

  4. Check the status of the repository:

    $ git status -s
    

    输出:

    ?? john-grisham.txt
    

    通知您一个新的、未被跟踪的文件:

    john-grisham.txt
    
  5. 将工作目录的当前状态保存为新版本:

    $ git add -A
    
    $ git commit -m "Second revision [John Grisham]"
    
  6. Check the status of the repository with:

    $ git status -s
    

    空输出证明存储库是干净的。

  7. Check the log of the repository with the alias from Recipe 2-10:

    $ git l
    

    输出包含两个修订:

    0468 Second revision: [John Grisham]
    
    de36 First commit [Agatha Christie]
    
  8. 存储库应该看起来像图 3-3 。

9781430261032_Fig03-03.jpg

图 3-3 。第二次修订后配方 3-1 的储存库

完成配方,创建第三个文件stephen-king.txt和第三个版本。程序如下:

  1. 创建文件stephen-king.txt

    $ vi stephen-king.txt
    
  2. 输入内容:

    Novels
        1974 | Carrie
        1975 | Salem’s Lot
        1977 | The Shining
    
  3. 保存文件并关闭编辑器。

  4. Check the status of the repository:

    $ git status -s
    

    存储库不干净:

    ?? stephen-king.txt
    
  5. 为工作目录的当前状态创建修订:

    $ git add -A
    $ git commit -m "Third revision [Stephen King]"
    
  6. Check the status of the repository:

    $ git status -s
    

    现在,输出是空的;因此,我们知道存储库是干净的。

  7. Check the log with:

    $ git l
    

    输出将包含三个修订:

    ffa6 Third revision [Stephen King]
    0468 Second revision: [John Grisham]
    de36 First commit [Agatha Christie]
    
  8. 储存库如图 3-1 所示。

image 注意配方 3-1 中介绍的两个命令:$ git add –A$ git commit -m "..."将你的工作目录的当前状态保存为一个新的版本。我们用它们在每个版本中存储一个新文件,但这不是必须的。您可以创建、删除、移动和复制任意数量的文件。这两个命令存储工作目录,不管修改了多少文件或者使用了哪种类型的工具。

它是如何工作的

使用以下命令初始化新的存储库:

$ git init

您可以传递一个路径来告诉 git 您希望项目存储在哪里。命令:

$ git init 03-01

创建一个新的空目录03-01并初始化其中的一个空存储库。没有任何参数,$ git init命令将在当前目录中初始化一个新的存储库。

当存储库初始化后,您就可以处理您的项目了:您可以创建文件并输入它们的内容。Git 非常智能地跟踪您在工作目录中所做的更改。它知道你做的所有修改。如果您怀疑这一点,请尝试使用以下命令:

$ git status

它返回关于工作目录中引入的更改的确切信息。这个命令的缩写形式也非常有用。如清单 3-2 所示。

清单 3-2。 回答问题的命令:仓库是脏的还是干净的?

$ git status -s

image 提示仓库可以被描述为干净或者肮脏。当我们说库是干净的;这意味着工作目录中的所有文件都存储在最新版本中。在这种状态下,命令:$ git status -s返回一个空结果。存储库是脏的意味着工作目录包含未提交的修改。命令:$ git status -s返回挂起变更的列表。

清单 3-2 中的命令以非常紧凑的形式打印出修改列表。您可以将它视为对以下问题的快速回答:存储库干净吗?如果输出为空,那么存储库是干净的。否则存储库是脏的,输出会列出修改。

在某些时候,您需要决定将工作目录的当前状态保存为一个新的版本。为此,使用清单 3-3 中的两个命令。

清单 3-3。 两个命令将工作目录的当前状态保存为新的版本

$ git add -A
$ git commit -m "Comment..."

现在,将它们视为一个原子操作。我们将在第四章中讨论他们的确切角色。现在,知道当执行时,它们将创建一个新的修订,并使存储库保持干净状态就足够了。

如果你是新的 git 用户,我建议在学习的早期阶段,你应该在每次修订后检查库的状态和日志。众所周知,这可以通过以下方式实现:

$ git status -s
$ git log

您也可以使用配方 2-10 中定义的$ git l别名。

3-2.创建 git 快照别名

问题

正如你已经知道的,你的工作目录的快照可以用清单 3-3 中的两个命令保存。因为我们将其视为单个操作,所以您希望定义将执行这两个命令的别名快照。您的别名,执行时为:

$ git snapshot

应该将工作目录的当前状态存储为新版本。

解决办法

打开命令行,转到您的主目录,编辑您的.gitconfig文件。遵循配方 2-10 开头给出的程序。

在您的.gitconfig文件的[alias]部分的末尾键入清单 3-4 的内容,保存文件,并关闭编辑器。

清单 3-4。 别名饭桶快照

[alias]
    snapshot = "!snapshot() {
        COMMENT=wip;
        if [ \"$*\" ]; then
            COMMENT=\"$*\";
        fi;
        git add -A;
        git commit -m \"$COMMENT\";
    }; snapshot"

它是如何工作的

类似于配方 2-10,别名用换行符分开。请记住,这里的换行符只是为了使别名更容易阅读——您必须在您的.gitconfig文件中将别名作为一个长行键入。

别名使用外壳函数snapshot,该函数在解析.gitconfig后变成:

snapshot() {
    COMMENT=wip;
    if [ "$*" ]; then
        COMMENT="$*";
    fi;
    git add -A;
    git commit -m "$COMMENT";
}

说明:

COMMENT=wip;

用值wip定义一个名为COMMENT的变量。Wip 是在制品的缩写。特殊变量$*包含传递给脚本的所有参数。考虑以下命令:

$ some-script a b c

这个调用向脚本some-script发送三个参数:abc。您可以使用带引号的$*变量"$*"来访问所有三个参数。

条件语句 if-then-fi:

if [ "$*" ]; then
    COMMENT="$*";
fi;

检查传递给脚本的参数。如果用参数调用脚本,它们将被分配给COMMENT变量。否则COMMENT变量将保持不变——它存储默认值 wip。

现在,您已经了解了将使您能够理解快照别名如何工作的一切。当我们运行该命令时:

$ git snapshot

它创建一个带有注释 wip 的版本。

如果我们传递任何参数:

$ git snapshot Lorem ipsum dolor

然后别名将创建带有注释“Lorem ipsum dolor”的修订。

3-3.在日常工作中使用 git 快照别名

问题

您想要启动一个新项目,该项目将由存储儿童歌曲的文本文件组成。类似于方法 3-1,您计划在新提交中保存每个新文件。为了避免同时键入$ git add$ git commit命令,您更喜欢使用$ git snapshot别名,在配方 3-2 中定义。

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 03-03
$ cd 03-03
$ git init

创建包含“唱一首六便士的歌”的歌词的第一个修订版。

  1. 创建文件sing-a-song-of-sixpence.txt

    $ vi sing-a-song-of-sixpence.txt
    
  2. 键入文件的内容:

    Sing a song of sixpence,
    A pocket full of rye.
    Four and twenty blackbirds,
    Baked in a pie.
    ...
    
  3. 保存文件并关闭编辑器。

  4. Check the status of the repository with $ git status -s

    存储库不干净。

  5. 将工作目录的当前状态保存为新版本:

    $ git snapshot Sing a song of sixpence
    
  6. Check the status of the repository with $ git status -s

    仓库是干净的。

  7. Check the log of the repository with $ git l

    输出将包含一个版本:

    7cfb Sing a song of sixpence
    

存储库现在看起来像图 3-4 。

9781430261032_Fig03-04.jpg

图 3-4 。“儿童歌曲”项目第一次改版

创建包含“咩,咩,黑羊”歌曲歌词的第二个修订版。

  1. 创建baa-baa-black-sheep.txt文件:

    $ vi baa-baa-black-sheep.txt
    
  2. 键入文件的内容:

    Baa, baa, black sheep,
    Have you any wool?
    Yes, sir, yes, sir,
    Three bags full;
    ...
    
  3. 保存文件并关闭编辑器。

  4. Check the status of the repository with $ git status -s

    存储库不干净。

  5. 将工作目录的当前状态保存为新版本:

    $ git snapshot Baa, baa black sheep
    
  6. Check the status of the repository with $ git status -s

    仓库是干净的。

  7. Check the log of the repository with $ git l

    输出将包含两个修订:

    564f Baa, baa black sheep
    7cfb Sing a song of sixpence
    

“儿童歌曲”项目的现状如图 3-5 所示。

9781430261032_Fig03-05.jpg

图 3-5 。第二次修订后的“儿童歌曲”项目

现在你决定这个项目应该存储不同语言的歌曲。创建一个名为EN的新目录,并将两个文件移动到其中:

$ mkdir EN
$ mv *. txt EN

检查存储库的状态:

$ git status -s

存储库不干净。将工作目录的当前状态保存为新版本:

$ git snapshot Internationalization: directory EN

现在存储库是干净的。命令:

$ git l

返回三个版本:

f305 Internationalization: directory EN
564f Baa, baa black sheep
7cfb Sing a song of sixpence

我们得到的知识库如图 3-6 所示。

9781430261032_Fig03-06.jpg

图 3-6 。第三次改版后的“儿童歌曲”项目

创建新文件夹PL:

$ mkdir PL

现在,检查状态:

$ git status -s

多奇怪,储存库是干净的!这是因为 git 不跟踪空目录。

image 提示绕过 git 禁止提交空目录的限制的公认方法是创建一个名为.gitkeep的空文件。

现在准备包含波兰歌曲“Bajka iskierki”的修订版:

  1. 创建bajka-iskierki.txt文件:

    $ vi PL/bajka-iskierki.txt
    
  2. 键入文件的内容:

    Na Wojtusia z popielnika
    Iskiereczka mruga....
    
  3. 保存文件并关闭编辑器。

  4. Check the status of the repository:

    $ git status -s
    

    存储库不干净。

  5. 将工作目录的当前状态保存为新版本:

    $ git snapshot [PL] Bajka iskierki
    
  6. Check the status of the repository with:

    $ git status –s
    

    仓库是干净的。

  7. Check the log of the repository with:

    $ git l
    

    输出将包含四个修订:

    d234 [PL] Bajka iskierki
    f305 Internationalization: directory EN
    564f Baa, baa black sheep
    7cfb Sing a song of sixpence
    

储存库的状态如图 3-7 所示。

9781430261032_Fig03-07.jpg

图 3-7 。第四次改版后的“儿童歌曲”项目

现在你决定每首歌都要以一句歌词开头:

TITLE: Abc...

打开EN/sing-a-song-of-sixpence.txt文件$ vi EN/sing-a-song-of-sixpence.txt,在文件的最开头插入一行:

TITLE: Sing a song of sixpence

保存文件并关闭编辑器。

以同样的方式修改第二个文件baa-baa-black-sheep.txt。第一行应该包含TITLE: Baa, baa, black sheep。最后修改第三个文件bajka-iskierki.txt。输入文本TITLE: Bajka iskierki。保存文件并关闭编辑器。

好了,现在三个文件都被修改了。命令$ git status –s打印:


 M EN/baa-baa-black-sheep.txt
 M EN/sing-a-song-of-sixpence.txt
 M PL/bajka-iskierki.txt

创建将存储项目当前状态的修订:

$ git snapshot Titles

$ git l打印的历史现在打印五个修订:

39d6 Titles
d234 [PL] Bajka iskierki
f305 Internationalization: directory EN
564f Baa, baa black sheep
7cfb Sing a song of sixpence

如你所见,单个修订可以存储任意数量的修改。我们创建的最后一个修订版包括三个修改过的文件。配方 3-3 的最终储存库显示在图 3-8 中。

9781430261032_Fig03-08.jpg

图 3-8 。配方 3-3 的最终储存库

它是如何工作的

您已经知道如何用$ git init初始化一个新项目。当不带参数执行时,该命令将在当前目录中创建一个存储库。

项目初始化后,您可以使用以下命令继续工作:

$ git status -s

和别名:

$ git snapshot
$ git l

每当您想要将工作目录的当前状态保存为修订时,请使用以下命令:

$ git snapshot

如果要设置修订的注释,请使用参数:

$ git snapshot A short info explaining the purpose of the revision

3-4.映射名称

问题

假设在第一次接触 git 时,您以这样的方式配置它,您的名字被设置为johny:

$ git config --global user.name johny

你在你的项目上工作了一段时间,然后你决定你更喜欢被称为John Doe。又过了一段时间,在这段时间里,你会再次改变主意。这次你要叫Paul "Moo" Cowboy。因此,你的修改被分配给三个不同的作者:johnyJohn DoePaul "Moo" Cowboy。您希望重新配置您的存储库,使所有这些名称都映射到您的真实名称。你可以通过准备一个.mailmap文件来达到这个效果。

解决办法

从配方 3-3 克隆存储库:

$ cd git-recipes
$ git clone 03-03 03-04
$ cd 03-04

检查修订的作者:

$ git shortlog -s

输出将类似于:

5  Włodzimierz Gajda

当然,我的名字会被你的代替。该输出通知您名为“odzimierz Gajda”的人编写了五个修订。

打开您的.gitconfig文件,并将您的名称改为:

[user]
    name = johny
    email = john.doe@example.net

接下来创建修订版本为johny。遵循程序:

  1. 创建目录FR/和文件FR/alouette-gentille-alouette.txt :

    $ mkdir FR
    $ vi FR/alouette-gentille-alouette.txt
    
  2. 键入文件的内容:

    Alouette, gentille alouette,
    Alouette, je te plumerai.
    ...
    
  3. 保存文件并关闭编辑器。

  4. 创建修订:

    $ git snapshot [FR] Alouette, gentille alouette
    

现在,$ git shortlog -s产生的输出将包括两个作者:

5  Włodzimierz Gajda
1  johny

遵循相同的程序,通过John Doe创建新版本:

  1. 把你在.gitconfig的名字改成无名氏:

    [user]
        name = John Doe
        email = john.doe@example.net
    
  2. 创建一个新文件little-skylark.txt :

    $ vi EN/little-skylark.txt
    
  3. 键入文件的内容:

    Little skylark, lovely little skylark,
    Little lark, I'll pluck your feathers off.
    ...
    
  4. 保存文件并关闭编辑器。

  5. 将工作目录的当前状态保存为新版本:

    $ git snapshot [EN] Little skylark, lovely little skylark
    
  6. Check the list of authors with:

    $ git shortlog -s -n
    

    由于有了-n选项,输出将按照修订数量降序排列:

    5  Włodzimierz Gajda
    1  John Doe
    1  johny
    

    接下来,在名称Paul "Moo" Cowboy下创建修订:

    1. Change your name in .gitconfig:

      [user]
          name = "Paul \"Moo\" Cowboy"
          email = moo@wild-west.example.net
      

      请注意,您必须用反斜杠对引号进行转义。

    2. 创建一个新文件frere-jacques.txt :

      $ vi FR/frere-jacques.txt
      
    3. 键入文件的内容:

      Frère Jacques, frère Jacques,
      Dormez-vous? Dormez-vous?
      Sonnez les matines! Sonnez les matines!
      Ding, daing, dong. Ding, daing, dong.
      
    4. 保存文件并关闭编辑器。

    5. 将工作目录的当前状态保存为新版本:

      $ git snapshot [FR] Frere Jacques
      
    6. Check the list of authors with:

      $ git shortlog -s -n
      

      输出将包含四个条目:

      5  Włodzimierz Gajda
      1  John Doe
      1  Paul "Moo" Cowboy
      1  johny
      

image 提示我们现在有四个不同的作者,我们将继续邮件映射。然而,当存储库包含由以下作者创作的修订时,我鼓励您检查由$ git shortlog -s -n返回的结果:name = "Paul "Moo" Cowboy", name = Peter ;Moo Cowboy, name = "Peter ;Moo Cowboy", name = "Peter "Moo" Cowboy"。这些例子有助于理解.gitconfig文件的解析。理解引用和评论的处理特别有帮助。

要继续映射姓名和电子邮件,请将您在.gitconfig文件中的姓名和电子邮件恢复为原始数据。我会输入:

[user]
    name = Włodzimierz Gajda
    email = gajdaw@gajdaw.pl

你必须用你的名字和电子邮件替换上述数据。

现在,创建文件.mailmap:

$ vi .mailmap

记住,.mailmap文件必须存储在当前项目的工作目录的根目录下。否则没有效果。键入以下内容(用您自己的电子邮件替换我的电子邮件):

My New Extra Name < gajdaw@gajdaw.pl >

当你保存.mailmap文件时,命令:

$ git shortlog -s -n

它将返回:

5  My New Extra Name
1  John Doe
1  Paul "Moo" Cowboy
1  johny

你可以看到,我的名字是从Włodzimierz Gajda映射到My New Extra Name的。再次打开.mailmap文件并追加另一行:

John Doe < john.doe@example.net >

该行:

John Doe < john.doe@example.net >

将所有提交的名称从john.doe@example.net更改为John Doe。的输出:

$ git shortlog -sn

现在是:

5  My New Extra Name
2  John Doe
1  Paul "Moo" Cowboy

以前分配给 johny 的提交现在被视为由John Doe创建。

如何将Paul "Moo" Cowboy的修订分配给John Doe?您将通过以下.mailmap条目实现它:

John Doe <john.doe@example.net> <moo@wild-west.example.net>

上面的条目映射了从moo@wild-west.example.netJohn Doe的所有修订。现在,$ git shortlog -ns的产量是:

5  My New Extra Name
3  John Doe

完成配方,创建.mailmap条目,将所有修订重新分配给你。通过添加您的第二个名字来重新映射您的姓名。这个问题的解决方案显示在清单 3-5 中。

清单 3-5。 这个。邮件映射内容将所有修订重新分配给我,并通过插入我的第二个名字 Edmund 来更改我的姓名

Włodzimierz Edmund Gajda <gajdaw@gajdaw.pl>
Włodzimierz Edmund Gajda <gajdaw@gajdaw.pl> <john.doe@example.net>
Włodzimierz Edmund Gajda <gajdaw@gajdaw.pl> <moo@wild-west.example.net>

现在,$ git shortlog -ns命令返回:

8  Włodzimierz Edmund Gajda

所有的修改都是由一个人完成的——我。

完成制作方法,创建新版本:

$ git snapshot Mapping names with .mailmap

现在,存储库包含九个修订,只有一个提交者。您可以通过以下方式进行验证:

$ git stat

这是我们在配方 2-10 中创建的别名。

记住.mailmap文件只影响$ git shortlog命令。命令:

$ git log

将打印:

commit abda33b8addab96e2016f974765f937f9dac6e3c
Author: Włodzimierz Gajda <gajdaw@gajdaw.pl>
Date:   Thu May 9 10:35:15 2013 +0200

    Mapping names with .mailmap

commit ba805256075eb86cf8a09a1d5c3161dbe6fc63e5
Author: Paul "Moo" Cowboy <moo@wild-west.example.net>
Date:   Thu May 9 10:07:01 2013 +0200

    [FR] Frere Jacques

commit 659ca289a3898eaf210d0c68228a645a74a3dd52
Author: John Doe <john.doe@example.net>
Date:   Thu May 9 10:01:44 2013 +0200

    [EN] Little skylark, lovely little skylark

...

在内部,所有的修改都用原作者标明。

它是如何工作的

要更改您的姓名,您可以使用以下命令:

$ git config --global user.name "Your Name"

或者手动编辑您的.gitconfig文件。这些命令:

$ git config --global user.name "John Doe"
$ git config --global user.email john.doe@example.net

创建以下.gitconfig条目:

[user]
    name = John Doe
    email = john.doe@example.net

使用$ git config命令还是编辑.gitconfig文件并不重要。需要记住的重要事实是,当您使用$ git commit命令创建修订时,会用到来自.gitconfig的姓名和电子邮件条目。因此,如果您首先将您的姓名定义为:

[user]
    name = johny
    email = john.doe@example.net

后来决定将其更改为:

[user]
    name = John Doe
    email = john@doe.example.com

那么您的存储库的历史将包含两个作者:johnyJohn Doe。命令:

$ git shortlog -s

将返回这两个名称,因为它们被视为不同的人:

13 johny
 8 John Doe

这个输出告诉我们,johnny 编写了 13 个修订,John Doe 编写了 8 个。你可以提供额外的信息,说明 johny 和 John Doe 实际上指的是同一个人。这个映射应该存储在工作目录的.mailmap文件中。文件的每一行都定义了名字到名字或者电子邮件到电子邮件的映射。该行:

Proper Name < commit@example.net >

定义将作者的电子邮件设置为commit@example.net的提交应该用名称Proper Name标记。

该行:

< proper@example.net > < commit@example.net >

重新映射电子邮件。它规定由commit@example.net创作的修改应该分配给使用电子邮件proper@example.net的人。

更复杂的例子:

Proper Name < proper@example.net > < commit@example.net >

将提交的名称和电子邮件都更改为作者的电子邮件等于commit@example.netProper Nameproper@example.net

可以将.mailmap文件的位置从工作目录的根目录更改为由mailmap.file配置选项定义的任意位置。.gitconfig.mailmap文件都可以是 utf-8 编码的,因此你可以在里面使用非 ascii 字符,比如,。如果你想分析现实生活中的.mailmap例子,请访问 jQuery 资源库:https://github.com/jquery/jquery

image 文档.mailmap文件的完整规范可在 shortlog 命令: $ git shortlog --help的手册中找到。

请记住,当您定义一个奇怪的名称时,例如:

Paul "Moo" Cowboy

你必须使用引号和反斜杠:

[user]
    name = "Paul \"Moo\" Cowboy"
    email = john.doe@example.net

image 提示命令:$ git log --pretty=format:"- { name: '%an', email: '%ae' }" | sort | uniq以 YAML 格式打印所有作者的完整列表。您可以使用这个列表为大型项目自动生成您的.mailmap文件。

3-5.使用 git 重置恢复修订

问题

您可能还记得,git 是一个版本控制系统。这意味着它存储项目中文件的所有版本。您可能想知道如何访问不久前存储的版本?您希望将项目恢复到第一个修订版,然后恢复到一段时间前提交的修订版,最后恢复到最后一个修订版。

解决办法

从配方 3-4 克隆存储库:

$ cd git-recipes
$ git clone 03-04 03-05
$ cd 03-05

并用$ git l打印历史。输出将类似于清单 3-6 。保存$ git l的输出,以备将来参考使用该配方。我将参考清单 3-6 ,但是因为你有不同的 SHA-1 散列,你将需要你自己的历史副本。一旦你学会了如何使用 reflog,保存就没有必要了。你将在配方 3-8 中学习如何使用 reflog。

清单 3-6。 储存库使用的历史菜谱 3-5

abda Mapping names with .mailmap
ba80 [FR] Frere Jacques
659c [EN] Little skylark, lovely little skylark
348f [FR] Alouette, gentille alouette
39d6 Titles
d234 [PL] Bajka iskierki
f305 Internationalization: directory EN
564f Baa, baa black sheep
7cfb Sing a song of sixpence

现在,将工作目录恢复到名为7cfb的第一个版本:

$ git reset --hard 7cfb

成功执行上述命令后,工作目录包含一个文件sing-a-song-of-sixpence.txt。您可以使用$ ls命令来验证它。此外,您可以通过$ git l查看历史记录。输出将只包含一个版本:

7cfb Sing a song of sixpence

这就是为什么我希望你复制清单 3-6 中的$ git l命令的输出。所有修订都包含在数据库中,但它们现在不包含在历史记录中。只有知道它们的 SHA-1 名称,才能恢复它们。如果你不知道它们的名字,你可以使用 ref log——我们将在秘籍 3-7 中学习。存储库现在看起来像图 3-4 。历史上除了7cfb没有其他版本。

我假设你知道清单 3-6 中打印的修订名称。如果没有,再次启动配方,这次保存清单 3-6 中显示的历史。

现在,恢复修订版,表示为:

f305 Internationalization: directory EN

您可以使用以下命令来完成:

$ git reset --hard f305

在该命令之后,工作目录包含以下目录和文件:

.
`-- EN
    |-- baa-baa-black-sheep.txt
    `-- sing-a-song-of-sixpence.txt

存储库看起来像图 3-6 。命令$ git l打印三个版本:

f305 Internationalization: directory EN
564f Baa, baa black sheep
7cfb Sing a song of sixpence

最后,将您的存储库重置为最新版本,如清单 3-6 所示:

$ git reset --hard abda

命令$ git l打印出与清单 3-6 中的相同的结果。工作目录包含配方 3-3 和 3-4 中创建的所有文件。

image 小心配方 3-5 清楚地表明,存储在.git/objects中的数据库和存储库的历史不是一回事。在执行$ git reset命令后,一些修订从历史记录中删除,但它们仍然存在于数据库中。存储库的历史只是数据库中所有可用信息的子集。要从数据库中获取信息,您需要一个有效的名称。

它是如何工作的

存储库的历史可以显示为修订列表。我们可以使用$ git log --pretty=oneline或者别名$ git l。正如您已经知道的,每个修订都有其唯一的名称。要将工作目录恢复到您可以使用的修订之一:

$ git reset --hard [REVISION]

该命令执行以下两项操作:

  • 它将工作目录的状态重置为指定的修订版,这意味着所有文件和目录的内容都将恢复到与修订版中保存的快照完全相同的快照。
  • 它从历史记录中删除在指定版本之后创建的所有版本。

如果您想恢复存储库的原始状态,就像在使用$ git reset命令之前一样,您必须记住最新版本的名称,或者您可以使用 reflog。

3-6.用 git 检验恢复修订版

问题

将工作目录恢复到给定版本的操作可以通过$ git reset$ git checkout命令来执行。在方法 3-5 中,你用$ git reset命令恢复旧的快照。现在,您想用一个$ git checkout命令获得类似的结果。

解决办法

从配方 3-4 克隆存储库:

$ cd git-recipes
$ git clone 03-04 03-06
$ cd 03-06

并用$ git l打印历史。输出将与清单 3-6 中的相同。保存$ git l的输出以备将来参考。

现在,将工作目录恢复到第一个版本,名为7cfb:

$ git checkout 7cfb

该命令将存储库的状态更改为分离头。您可以通过以下方式验证这一点:

$ git status -sb

输出应该是:

## HEAD (no branch)

工作目录现在包含一个文件sing-a-song-of-sixpence.txt,打印有$ git l的历史仅包含一个版本:

7cfb Sing a song of sixpence

使用以下命令将其恢复到正常状态:

$ git checkout master

现在用$ git l打印的历史包含了清单 3-6 中显示的所有修订。

您可以再次使用$ git checkout切换到其他版本,例如:

$ git checkout 564f

要返回正常状态,请使用:

$ git checkout master

它是如何工作的

恢复以前保存的快照的第二种方法是使用以下命令:

$ git checkout [REVISION]

该命令的工作方式与配方 3-5 中讨论的$ git reset不同。

$ git checkout命令执行以下三个操作:

  • 它进入分离的头部状态。
  • 它将工作目录的状态重置为指定的版本。
  • 它从历史记录中删除在指定修订之后创建的所有修订。

分离的头是存储库的一种特殊状态,在这种状态下,您不在任何分支上。我们将在第五章、第六章、第七章和第十章更详细地讨论分支。现在,要使用$ git checkout,你只需要知道:

  • $ git checkout [SHA-1]命令进入分离头状态。
  • 如何检查您的存储库的状态。
  • 如何从脱离的头部回到正常状态?

命令:

$ git status -b

返回当前分支的信息。它输出:

# Not currently on any branch.

当您处于分离的头部状态或:

# On branch master

当你处于正常状态时。你可以连接一个$ git status命令的两个有用的开关-s-b:

$ git status -s -b

或者甚至:

$ git status -sb

当存储库干净并处于分离磁头模式时,此命令会打印:

## HEAD (no branch)

在正常状态下,输出为:

#master

如果您处于分离状态,您可以通过以下方式返回正常状态:

$ git checkout master

总而言之,该命令:

$ git checkout [REVISION]

将工作目录恢复到指定版本,并进入分离头状态。

命令:

$ git checkout master

返回到最新版本并恢复正常状态。

image 提示配方 3-6 介绍了一个存储库的新特性。我们可以说存储库处于脱离头状态正常状态

3-7.创建 git 的别名

问题

如何定义一个别名来简化一个$ git status –sb命令的执行?

解决办法

在您的.gitconfig文件的[alias]部分的末尾键入清单 3-7 的内容。

清单 3-7。 别名饭桶年代

[alias]
    s = status -sb

您可以使用以下命令获得相同的结果:

$ git config --global alias.s "status -sb"

它是如何工作的

别名$ git s执行命令:

$ git status -sb

输出传达了以下问题的答案:

  • 存储库是干净的还是脏的?换句话说,是否有任何未提交的更改?
  • 存储库是否处于分离状态?或者我们在一根树枝上?如果是,打印分行的名称。

3-8.使用参考日志

问题

在配方 3-5 和 3-6 中,用 SHA-1 名称保存日志的程序非常麻烦。如果你知道如何使用 reflog,这是可以避免的。你将想要创建一个如图 3-9 所示的库。接下来,您希望使用$ git reset --hard [REVISION]命令将工作目录恢复到每次提交。您可能更喜欢使用 reflog,而不是复制和粘贴 SHA-1 名称。

9781430261032_Fig03-09.jpg

图 3-9 。配方 3-8 中讨论的储存库

image 提示图 3-9 中显示的文件内容并不重要。

解决办法

初始化新的存储库:

$ cd git-recipes
$ git init 03-08
$ cd 03-08

使用以下内容创建第一个文件:

$ echo lorem > lorem.txt

该命令创建一个名为lorem.txt的新文件。该文件包含一个单词lorem。您可以使用两个命令来验证它:

$ ls
$ cat lorem.txt

第一个文件列出了当前目录的内容(该列表将由一个文件lorem.txt组成),第二个文件显示了lorem.txt的内容(当然是lorem)。

现在用以下内容创建第一个修订版:

$ git snapshot lorem

当然,存储库是干净的,$ git status –s返回空结果。现在使用以下命令检查参考日志:

$ git reflog

该命令的输出如清单 3-8 中的所示。

清单 3-8。 第一次提交后 git reflog 的输出

bb057dd HEAD@{0}: commit (initial): lorem

它通知您,带有注释lorem的修订现在可以称为:

bb057dd

或者

HEAD@{0 }.

让我们创建第二个版本:

$ echo ipsum > ipsum.txt
$ git snapshot ipsum

目前,存储库包含两个修订版本,注释分别为loremipsum。命令:

$ git reflog

现在返回清单 3-9 中的输出。

清单 3-9。 第二次提交后 git reflog 的输出

227c9fb HEAD@{0}: commit: ipsum
bb057dd HEAD@{1}: commit (initial): lorem

发生了什么事?我们在历史中从版本lorem移动到版本ipsum。当前版本可以被称为HEAD@{0}——现在是版本ipsum。之前的版本——也就是lorem——可以称为HEAD@{1}

创建第三个版本:

$ echo dolor > dolor.txt
$ git snapshot dolor

并执行:

$ git reflog

输出显示在清单 3-10 中。

清单 3-10。 第三次修改后 git reflog 的输出

fe7dbef HEAD@{0}: commit: dolor
227c9fb HEAD@{1}: commit: ipsum
bb057dd HEAD@{2}: commit (initial): lorem

历史向前发展。这次HEAD@{0}指的是dolor修改。之前的版本是ipsum,所以可以简称为HEAD@{1}。我们创建的第一个版本——lorem——现在可以作为HEAD@{2}使用。

是时候使用引用日志名称来恢复修订了。首先,我们想恢复标题为lorem的版本。您可以使用以下命令来完成此操作:

$ git reset --hard HEAD@{2}

之后,工作目录应该只包含一个文件,$ git reflog命令应该返回清单 3-11 中的输出。

清单 3-11。git 复位后 git reflog 的输出——硬磁头@{2}

bb057dd HEAD@{0}: reset: moving to HEAD@{2}
fe7dbef HEAD@{1}: commit: dolor
227c9fb HEAD@{2}: commit: ipsum
bb057dd HEAD@{3}: commit (initial): lorem

正如你在清单 3-11 中看到的,所有的HEAD@{n}引用都被更新了。以下是他们指出的情况:

HEAD@{0}—points to the revision lorem
HEAD@{1}—points to the revision dolor
HEAD@{2}—points to the revision ipsum
HEAD@{3}—points to the revision lorem

接下来,使用以下命令将存储库重置为标题为dolor的版本:

$ git reset --hard HEAD@{1}

在这之后,存储库包含三个文件lorem.txtipsum.txtdolor.txt;并且$ git reflog命令返回如清单 3-12 所示的输出。

清单 3-12。git 复位后 git reflog 的输出——硬磁头@{1}

481f34f HEAD@{0}: reset: moving to HEAD@{1}
aae6588 HEAD@{1}: reset: moving to HEAD@{2}
481f34f HEAD@{2}: commit: dolor
84fb524 HEAD@{3}: commit: ipsum
aae6588 HEAD@{4}: commit (initial): lorem

它是如何工作的

Git reflog 是一个特殊的日志,它在存储库中存储关于您的移动的信息。每次创建修订、重置存储库或更改当前修订时,参考日志都会更新。名称HEAD@{0}总是指向当前版本。先前的版本可作为HEAD@{1}获得。两个操作前的当前版本可作为HEAD@{2}获得,等等。因此,即使你不知道他们的名字,你也可以参考以前的修订。

3-9.在现有项目中创建新的存储库

问题

您正在处理一个已经包含大量文件和目录的项目。您想开始在那个项目中使用 git。

解决办法

输入包含您要跟踪的一些文件的目录:

$ cd my/important/project

初始化新的存储库:

$ git init

并创建包含所有文件的修订版:

$ git add -A
$ git commit -m "Initial commit"

当然,您可以使用别名:

$ git snapshot Initial commit

存储库包含存储所有文件的当前状态的单个修订。然后,您可以继续工作,用$ git snapshot$ git add$ git commit命令保存所有修改。

它是如何工作的

Git 的 init 命令可以在任何不包含.git子目录的目录中执行。您可以在已经包含由许多文件和子目录组成的项目的目录中运行$ git init。在存储库初始化之后,您可以用两个命令$ git add –A$ git commit –m "Initial commit"导入所有文件。你也可以使用$ git snapshot Initial commit

3-10.丢失未提交的更改

问题

如果您忘记提交更改并重置工作目录,您希望检查您的修改会发生什么情况。

解决办法

从配方 3-1 克隆存储库:

$ cd git-recipes
$ git clone 03-01 03-10
$ cd 03-10

创建一个新文件graham-masterton.txt:

$ vi graham-masterton.txt

键入其内容:

Novels
    1975 | The Manitou
    1977 | The Djinn
    1979 | Revenge of the Manitou

保存文件并关闭编辑器。

然后修改文件stephen-king.txt:

$ vi stephen-king.txt

追加两本小说The StandThe Dead Zone:

Novels
    1974 | Carrie
    1975 | Salem’s Lot
    1977 | The Shining
    1978 | The Stand
    1979 | The Dead Zone

保存文件并关闭编辑器。

现在,$ git status命令打印出:

M stephen-king.txt
?? graham-masterton.txt

这意味着工作目录中有两个变化:

  • 文件stephen-king.txt已被修改。
  • 工作目录包含一个新文件graham-masterton.txt

$ git l别名打印历史。

输出包含我们在配方 3-1 中创建的三个修订:

ffa6 Third revision [Stephen King]
0468 Second revision: [John Grisham]
de36 First commit [Agatha Christie]

假设现在你忘记提交你的工作。当您决定使用以下命令恢复您的第一个修订版时,文件stephen-king.txtgraham-masterton.txt仍然未提交:

$ git reset --hard de36

在该命令之后,工作目录包含两个文件。命令$ ls打印他们的名字:

agatha-christie.txt
graham-masterton.txt

文件stephen-king.txt已经消失。您可以将存储库的状态重置为修订版:

ffa6 Third revision [Stephen King]

可以使用 reflog 来完成:

$ git reset --hard HEAD@{1}

文件stephen-king.txt将被恢复,但是它现在将只包含在配方 3-1 期间键入的三本书。命令:

$ cat stephen-king.txt

印刷品:

Novels
    1974 | Carrie
    1975 | Salem's Lot
    1977 | The Shining

你在配方 3-10 期间键入的两本新小说:

1978 | The Stand
1979 | The Dead Zone

没有存储在数据库中。你还记得吗?您忘记提交更改。在stephen-king.txt中输入的更改已丢失,无法恢复。

显示新文件的内容:

$ cat graham-masterton.txt

通过$ git reset操作,新文件保持不变。

它是如何工作的

这个配方的目的是警告您未提交的更改可能会丢失。请记住,该命令:

$ git reset --hard [REVISION]

只能在干净的目录中安全使用。签出命令:

$ git checkout [REVISION]

是内部限制的。只有在操作不会导致数据丢失的情况下,才能使用它。每当有丢失未提交的更改的风险时,系统将会警告您,并且操作将会中止。这将在配方 5-6 和 5-7 中详细讨论。

3-11.创建 git 简单提交别名

问题

您已经注意到,在学习和实践 git 时,经常需要创建一系列的修订。通常文件的内容并不重要,可以忽略不计。您希望简化使用别名simple-commit创建这种类型的提交的任务。被呼叫时:

$ git simple-commit lorem ipsum dolor

别名应创建如图 3-9 所示的三个版本。每个参数都应该被解释为请求创建一个新的版本来存储一个新的文件。电话:

$ git simple-commit abc

应该创建一个带有注释abc的修订。修订版应包括一个包含文本abc的新文件abc.txt

解决办法

打开您的.gitconfig文件,并在[alias]部分的末尾键入如列表 3-13 所示的别名。

清单 3-13。 别名:git 创建文件和 git 简单提交

[alias]
    create-file = "!createFile() {
        for name in \"$@\"; do
            echo $name>$name.txt;
        done;
    }; createFile"

    simple-commit = "!simpleCommit() {
        for name in \"$@\"; do
            git create-file \"$name\";
            git snapshot $name;
        done;
    }; simpleCommit"

它是如何工作的

第一个别名创建文件。电话:

$ git create-file yes no

创建两个文件yes.txtno.txt。第一个文件包含文本yes,第二个文件包含文本nofor循环:

for name in \"$@\"; do
    echo $name>$name.txt;
done;

处理传递给脚本的所有参数。每个参数都可以作为$name变量在一次循环中访问。因此呼吁:

$ git create-file yes no

相当于:

echo yes>yes.txt
echo no>no.txt

第二个别名包含处理所有参数的相同循环:

for name in \"$@\"; do
    git create-file \"$name\";
    git snapshot $name;
done;

每次循环时,我们调用两个别名:

$ git create-file $name
$ git snapshot $name

电话:

$ git simple-commit yes no

相当于:

$ git create-file yes
$ git snapshot yes

$ git create-file no
$ git snapshot no

电话:

$ git simple-commit lorem ipsum dolor

创建如图 3-9 所示的储存库。

image 提示秘籍 2-10、3-2 和 3-7 中定义的所有别名在你使用 git 的日常工作中都很有用。配方 3-11 中定义的别名只有在学习和实践 git 时才有用。

3-12.松开提交

问题

您希望验证无法通过符号引用访问的修订是否会丢失。您可以按照以下步骤操作:

  • 提交更改。
  • $ git reset –hard [REVISION]重置历史。一些提交将不再被$ git log命令返回。它们在数据库中保持不变。您可以使用 reflog 或 SHA-1 名称来访问它们。
  • 一旦清除参考日志,未被$ git log打印的修订将变得不可及。这意味着他们仍然在数据库中,但你可以访问他们只有当你知道他们的 SHA-1 名字。
  • 执行完$ git prune命令后,所有可删除的修订都将从数据库中删除。他们迷路了。没有办法让他们回来。

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 03-12
$ cd 03-12

用注释abc创建三个修订:

$ git simple-commit a b c

使用清单 3-14 中所示的命令,你可以在没有别名的情况下达到同样的效果。

清单 3-14。 该命令相当于 git simple-commit a b c

$ echo a>a.txt
$ git add -A
$ git commit -m a

$ echo b>b.txt
$ git add -A
$ git commit -m b

$ echo c>c.txt
$ git add -A
$ git commit -m c

$ git l打印历史。输出包含三个修订:

5c1e c
4580 b
c4ac a

将存储库的状态重置为第一个版本:

$ git reset --hard c4ac

虽然用$ git l打印的历史现在只包含一个修订版—c4ac a—但其他两个修订版— 5c1ec4580b—仍然在数据库中可用。您可以使用它们的名称或 reflog 来恢复它们。我们在配方 3-8 中这样做了。

现在,$ git reflog命令打印出以下输出:

c4ac743 HEAD@{0}: reset: moving to c4ac
5c1ee9a HEAD@{1}: commit: c
45800dd HEAD@{2}: commit: b
c4ac743 HEAD@{3}: commit (initial): a

这意味着修订版5c1ee9ac45800ddb在符号名称HEAD@{1}HEAD@{2}下可用。我们称这种类型的修订为悬空修订。让我们用以下命令清除 reflog:

$ git reflog expire --all --expire=now

执行此命令后,reflog 变为空。$ git reflog 命令返回空结果。这意味着现在修订版5c1ee9ac45800ddb只能通过它们的名字获得。没有符号名称导致修改bc。如果是这种情况,git 可以从数据库中删除修订。这种类型的修订被称为不可达修订

让我们检查一下,哪些存储在.git/objects数据库中的对象只能通过 SHA-1 名称访问:

$ git prune --dry-run

输出将包含——除其他内容外——两个修订:

45800ddc19fa325296437fdbd7cc7e5654619597 commit
5c1ee9a3f19f854c783fa87003cb1ecc5508971d commit

如果您比较一下$ git l的输出,您将会看到输出中包含了修订名称5c1ec4580b。换句话说,如果你现在执行命令$ git prune,那么两个修改bc将最终丢失。就这么办吧。执行命令:

$ git prune

如果您现在尝试使用其名称将存储库重置为修订版c:

$ git reset --hard 5c1e

您将得到以下错误:

fatal: ambiguous argument '5c1e': unknown revision or path not in the working tree.

该修订版不再可用。你刚刚失去了它!永远!

image 提示你可以用$ git fsck --unreachable列出数据库中存储的所有不可访问的对象。

它是如何工作的

Git 将所有修订存储在存储库的数据库.git/objects中。该数据库使用 SHA-1 散列来识别修订、文件和所有其他条目。它是一种内容可寻址存储,这意味着密钥是使用存储数据的内容生成的。有时,数据库会被清除,不能通过符号引用访问的对象最终会被删除。为了熟悉这个过程,我们需要更深入地研究存储库的结构。

在前一章中,我们将 git 存储库分为:

  • git 目录.git/
  • 数据库.git/objects
  • 工作目录

数据库的内容可以进一步分为:

  • 通过各种符号引用(如 reflog、分支和标签)可用的对象被分类为可达的
  • 仅通过 SHA-1 名字可用的对象被分类为不可达

清理数据库的过程会删除所有无法访问的对象。您可以使用$ git prune$ git gc命令手动完成。但是即使不使用那些命令,git 也会自动完成——这只是时间问题。关于多久清理一次存储库的确切参数可以在配置中设置。

当我们打电话时:

$ git reflog expire --all --expire=now

参考日志已清除。因此,我们删除了所有指向修订版bc的符号引用。这就是为什么修订版bc变得遥不可及。接下来调用:

$ git prune

已从数据库中删除这些修订。

结论

方法 3-12 的目的是向你展示,即使是提交的变更也可以从存储库中删除。您可以通过保留符号引用来避免这种情况。为此,最简单的解决方案是使用分支。配方 5-4 也讨论了丢失已提交变更的问题。

摘要

这一章向前迈出了非常重要的一步。现在,您可以在日常工作中使用 git,而没有任何丢失数据的风险。git 最简单的工作流程是:

  1. 修改您的文件

  2. Save the snapshot of the working directory with two commands:

    $ git add -A
    $ git commit -m "...".
    

    该操作由这两个命令实现,如图 3-10 中的所示

    9781430261032_Fig03-10.jpg

    图 3-10 。使用$ git add–A 和$ git commit–m 命令

感谢:

$ git reset --hard [REVISION]
$ git checkout [REVISION]

您知道如何检索以前的快照之一。使用$ git checkout$ git reset您可以访问存储在您的存储库中的每个修订。记住,当使用$ git reset时,参考日志将帮助你返回到最新版本。

从现在开始,您应该记住两种新的重要的描述存储库的方法。存储库可以是:

  • 干净还是脏
  • 在分离的头部或正常状态下

两个特征都由$ git status –sb命令显示。

当工作目录的内容与当前版本中保存的快照相同时,存储库是干净的

当工作目录的内容包含未提交的更改时,存储库是脏的

第二个特征,正常状态和分离的磁头状态,是在.git/HEAD文件的基础上完成的。我们将在关于分支的章节中回到这一点。目前你知道如何检查状态。当执行以下命令时,存储库处于脱离头状态:

$ git status -sb

开始于:

## HEAD (no branch)

否则储存库处于正常状态

本章中出现的另一个重要特征将数据库的内容分为:

  • 可达对象:通过符号引用可获得的对象
  • 无法访问的对象:只能通过 SHA-1 名字访问的对象

请记住,在自动垃圾收集过程中,无法访问的对象可以从存储库中删除。这就是为什么你不能在分离的头部状态下工作。当您在分离的 HEAD 状态下工作时,您会创建无法访问的修订,除非您手动创建符号引用(分支或标记)。

四、管理文件

在本章中,我们将练习和分析在工作目录中使用的文件系统命令。我们将创建、修改、删除和重命名文件,并检查这些操作如何影响存储库。

正如您已经知道的,git 不会自动注册工作目录中的更改。要创建新版本,您必须发出特殊命令:

$ git add -A
$ git commit -m "Some comment..."

至此,我们将它们视为一个原子操作,可以描述为将工作目录的快照保存为一个新的修订。现在,我们将把这个操作分解成两个独立的步骤:

$ git add -A

$ git commit -m "Some comment..."

Git 允许您选择哪些文件应该进入下一个版本——这就是为什么您需要两个命令来创建一个版本的原因。第一个命令为下一个修订选择文件。第二个命令使用选定的文件创建修订。为下一次修订选择的文件称为暂存文件。暂存文件列表存储在名为暂存区 或索引.git/index文件中。

4-1.暂存和提交新文件

问题

您想要创建一个新文件,并将其提交到存储库中。您还想熟悉这个配方中执行的每个命令所执行的变化。为了实现这一点,您需要使用$ git status$ git status –s命令来分析存储库的状态。

解决办法

使用以下内容启动新存储库:

$ cd git-recipes
$ git init 04-01
$ cd 04-01

然后,按照以下步骤操作:

  1. $ echo new > new.txt创建一个新文件

  2. Check the status of the repository. The output of the $ git status will be the following:

    # On branch master
    #
    # Initial commit
    #
    # Untracked files:
    #   (use "git add <file>..." to include in what will be committed)
    #
    #       new.txt
    nothing added to commit but untracked files present (use "git add" to track)
    

    如你所见,new.txt文件被列为未跟踪的

  3. Check the shortened output of git status. The command

    $ git status -s will print:
    
    ?? new.txt
    

    未跟踪的文件用??表示。

  4. 使用$ git add new.txt命令暂存文件。

  5. Check the status of the repository. The output of $ git status would be:

    # On branch master
    #
    # Initial commit
    #
    # Changes to be committed:
    #   (use "git rm --cached <file>..." to unstage)
    #
    #       new file:   new.txt
    #
    

    这一次,文件显示在待提交的变更部分下。这意味着该文件是暂存的。

  6. Check the status with the $ git status -s command. The command will print:

    A_ new.txt
    

    这个显示的新状态由两个字符A和空格组成。为了使输出更具可读性,我使用了下划线_来代替空格。

  7. $ git commit -m "Staging and committing a new file"
    

    创建一个新版本

  8. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。

它是如何工作的

在存储库中存储新文件的过程总是由两个步骤组成。没有别的办法做这件事。当你创建一个新文件时,起初它是未跟踪的,这意味着 git 不会将文件存储在存储库中,也不会跟踪文件的内容。$ git status -s命令返回的文件状态用两个问号表示:

?? new.txt

如果要将文件存储在修订中,首先要将文件添加到临时区域。这通过以下方式完成:

$ git add new.txt

暂存区是一个修改列表,该列表将包含在下一次使用$ git commit命令进行的修改中。暂存区存储在.git/index文件中。添加到暂存区的文件称为暂存文件。在$ git add new.txt之后,缩短状态命令$ git status -s打印:

A_ new.txt

如您所见,添加到暂存区的新文件由A_表示。

image 提示你可以使用通配符 *?作为$ git add命令。

当文件位于临时区域时,您可以使用以下命令创建新版本:

$ git commit -m "一些解释…

临时区域中包含的所有更改都将进入修订版。该文件存储在存储库中,现在可以描述为未修改的。您可能注意到了这些命令:

$ git status
$ git status -s

不打印任何关于未修改文件的信息。

配方 4-1 将储存在储存库中的文件分为三组:

  • 无人探索过的
  • 阶段
  • 未更改的

图 4-1 显示了新文件从未跟踪到暂存和从暂存到未修改的过程。

9781430261032_Fig04-01.jpg

图 4-1 。提交新文件的过程

$ git status –s命令返回的所有代码都使用两个字母。第一个字母描述暂存区,第二个字母描述工作目录。当$ git status –s返回??时,你可以把它解释为:

  • 第一个?通知您该文件在暂存区中未知。
  • 第二个?通知您文件在工作目录中是未知的。

以类似的方式,由A_表示的状态具有以下含义:

  • 第一个字符(A)表示文件被添加到暂存区(它已被暂存并将在下一次提交时被存储)。
  • 第二个字符(_)表示工作目录中的文件与暂存区中存储的文件具有相同的内容。

有时您可能会无意中存放一个新文件。你如何逆转分期?如果是新文件,可以使用以下命令之一:

$ git rm --cached -- [filename]
$ git reset -- [filename]

4-2.暂存和提交修改的文件

问题

您希望修改并提交已存储在存储库中的文件。为了熟悉这个操作是如何执行的,您需要在每隔一个命令之前和之后使用$ git status 和$ git status –s命令。

解决办法

使用以下内容启动新存储库:

$ cd git-recipes
$ git init 04-02
$ cd 04-02
$ git simple-commit modified

现在存储库是干净的,工作目录包含一个名为modified.txt的文件。该文件由配方 3-11 中定义的$ git simple-commit 别名创建并提交。

现在,按照程序进行:

  1. $ echo Some other text > modified.txt修改modified.txt文件

  2. Check the status of the repository with the $ git status command. The output:

    # On branch master
    # Changes not staged for commit:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       modified:   modified.txt
    #
    no changes added to commit (use "git add" and/or "git commit -a")
    

    告诉您modified.txt文件已被修改,但没有准备提交。请注意,与配方 4-1 一样,该文件列在未提交的变更下。

  3. Check the simplified form of status. The output of $ git status -s will be:

    _M modified.txt
    

    状态由两个字符表示:空格和字母M。同样,我使用了下划线而不是空格。

  4. $ git add modified.txtmodified.txt添加到暂存区

  5. Check the status with the $ git status command. The output:

    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #       modified:   modified.txt
    #
    

    该文件现在被列为要提交的变更

  6. Check the simplified form of status with the $ git status -s command. The output will be:

    M_ modified.txt
    

    状态由两个字符表示:字母M和一个空格。

  7. $ git commit -m "Staging and committing a modified file"提交更改

  8. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。

它是如何工作的

配方 4-2 从修改已经存储在版本中的文件开始。我们将使用以下内容修改文件:

$ echo Some other text > modified.txt

在此操作之后,文件变为修改过的。为了了解 git 对该文件的看法,我们发出以下命令:

$ git status

该文件列在未提交的变更下。因此,我们可以说文件是修改的未升级的。也是跟踪。但是每个未转移的文件都会被跟踪和修改。因此,说文件是未分级就足够了。通过$ git status –s命令,先前在一些修改后提交的文件被标记为_M

要将未登台文件添加到登台区,我们使用$ git add命令。之后:

$ git add modified.txt

文件变成了上演的。它的短标签现在是M_(字母M后跟一个空格)。

最后,我们使用以下内容提交暂存文件:

$ git commit -m "Some comment..."

并且该文件再次未被修改。

配方 4-2 在图 4-2 中描述。

9781430261032_Fig04-02.jpg

图 4-2 。使用登台区提交修改文件的过程

两个字母状态_MM_ 提供了关于暂存区和工作目录中文件状态的信息。

状态_M意味着:

  • 第一个字符(空格):文件未添加到临时区域。
  • 第二个字符(M):文件在工作目录中被修改。

状态M_可以解释为:

  • 第一个字符(M):文件被添加到暂存区。
  • 第二个字符(空格):暂存区中文件的状态与工作目录中文件的状态相同。

如果您想将状态M_再次反转为_M,您可以使用:

$ git checkout -- [filename]

4-3.提交修改的文件

问题

您想要修改并提交已经提交的文件。您希望仅使用$ git commit命令来执行该操作。

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 04-03
$ cd 04-03
$ git simple-commit modified

现在存储库是干净的,工作目录包含一个文件— modified.txt

现在,请遵循以下步骤:

  1. $ echo Yet another text > modified.txt
    

    修改modified.txt文件

  2. Check the status of the repository

    $ git status
    

    输出与配方 4-2 中的相同:

    # On branch master
    # Changes not staged for commit:
    #   (use "git add <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       modified:   modified.txt
    #
    no changes added to commit (use "git add" and/or "git commit -a")
    
  3. 检查状态的简化形式。

    $ git status -s will be:
    
    _M modified.txt
    

    的输出

  4. 使用

    $ git commit -a -m "Committing modified file: modified.txt"
    

    提交更改

  5. $ git status -s检查状态

输出是空的,因此存储库是干净的。

它是如何工作的

我们从配方 4-2 中知道,在修改之后,未修改的文件(即,被提交并且此后未被修改的文件)变得未分级。我们可以用一个命令提交所有未转移的文件:

$ git commit -a -m "Some comment..."

标志-a告诉 git 包含在提交的未分级文件中。(你还记得吗?跟踪所有未暂存的文件!)注意,使用这个解决方案,您不能选择哪些文件将进入提交。命令:

$ git commit -a -m "..."

将提交所有暂存和未暂存的文件。如果您想提交一个未暂存的文件,您可以将其名称传递给$ git commit命令,如下所示:

$ git commit -m "Some text..." -- [filename]

image 提示命令$ git commit -m "..."创建一个包含所有暂存文件的新版本。此命令不会修改未转移和未跟踪的文件。命令$ git commit -a -m "..."创建一个包含所有被跟踪文件(暂存和未暂存)的新版本。未被跟踪的文件(即标有??的文件)不会被该命令修改。

使用两个命令可以获得与使用$ git commit –a命令相同的效果:

$ git add -u
$ git commit -m "..."

命令$ git add -u存放所有跟踪的文件。

请记住,您不能使用一个命令:

$ git commit -am "..."

提交新文件。必须使用$ git add命令暂存新文件。除此之外,别无选择。

配方 4-3 中描述的程序在图 4-3 中给出。如您所见,这两个命令都跳过了临时区域。

9781430261032_Fig04-03.jpg

图 4-3 。提交修改文件的过程(跳过暂存区)

image 注意命令$ git commit -a -m "..."可以写成$ git commit -am "...".选项的顺序很重要。你不能把它写成$ git commit -m -a "..."$ git commit -ma "...".

4-4.暂存和提交已删除的文件

问题

您的存储库处于干净状态,包含一个提交的文件— deleted.txt。您希望从工作目录中删除该文件,然后提交该操作。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-04
$ cd 04-04
$ git simple-commit deleted

现在存储库是干净的,工作目录包含文件deleted.txt

遵循程序:

  1. $ git rm deleted.txt删除deleted.txt文件

  2. 用$ ls 检查工作目录的内容。文件deleted.txt已被移除。

  3. Check the status of the repository with the $ git status command. The output:

    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #       deleted:    deleted.txt
    #
    

    在要提交的的变更下列出一个文件deleted.txt。这意味着删除文件的操作是上演的

  4. Check the simplified form of status. The output of $ git status -s will be:

    D_ deleted.txt
    

    这一次,分段文件删除由两个字符表示:一个字母D和一个空格。

  5. $ git commit -m "Staging and committing removed file"命令提交更改。

  6. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。工作目录不包含deleted.txt文件。

它是如何工作的

我们从一个包含文件deleted.txt的干净的存储库开始菜谱。命令$ git rm deleted.txt从工作目录中删除文件,并执行该操作。暂存和删除的文件通过$ git status -s命令用D_表示。在另一个$ git commit命令之后,操作被提交:存储在修订版中的快照不包含文件deleted.txt

配方 4-4 的流程如图图 4-4 所示。

9781430261032_Fig04-04.jpg

图 4-4 。配方 4-4 的流程

两个字母的状态D_表示:

  • 第一个字符(D):文件被删除,操作被暂存。
  • 第二个字符(空格):工作目录中文件的状态与暂存区中的状态完全相同:文件已从工作目录中删除。

如何反转由$ git rm [filename]命令执行的操作?$ git rm之后的状态由D_表示。首先,我们使用以下命令取消转移:

$ git reset -- [filename]

该命令将状态从D_转换为_D。在_D状态下,文件仍然从工作目录中丢失,但是删除不再进行。因此,该文件在暂存区中是可用的。要将文件从临时区域恢复到工作目录,请执行以下命令:

$ git checkout -- [filename]

上面的命令恢复文件。我们可以说,它将_D状态转换为未修改的状态。工作目录现在是干净的,文件被恢复。

4-5.提交使用标准 rm 命令删除的文件

问题

您的存储库处于干净状态,并且包含了removed.txt文件。文件已提交。您希望使用标准的$ rm命令删除文件,然后将修改提交到存储库中。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-05
$ cd 04-05
$ git simple-commit removed

存储库是干净的,工作目录包含文件removed.txt

遵循以下程序:

  1. $ rm removed.txt删除removed.txt文件

  2. Check the status of the repository. The command:

    $ git status
    

    印刷品:

    # On branch master
    # Changes not staged for commit:
    #   (use "git add/rm <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       deleted:    removed.txt
    #
    no changes added to commit (use "git add" and/or "git commit -a")
    

    该文件列在未提交的更改下。所以文件是unstaged

  3. Check the simplified form of status. The output of $ git status -s will be:

    _D removed.txt
    

    状态由两个字符表示:一个空格和一个字母D

  4. $ git commit -a -m "Staging and committing removed file"
    

    提交更改

  5. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。工作目录不包含removed.txt文件。

它是如何工作的

您可以使用标准命令,如$ rm来删除工作目录中的文件。如果您想存放所有删除的文件,您可以使用 git commit $ git commit -am "..."-a标志或以下命令之一:

$ git add -u
$ git add -A

您也可以只暂存用$ rm命令删除的选定文件;来实现$ git rm [filename]命令的使用。

综上所述,以下两个程序对filename.txt具有相同的效果:

# first procedure
$ rm filename.txt
$ git commit -am "..."

# second procedure
$ rm filename.txt
$ git rm filename.txt
$ git commit -m "..."

它们在一个方面有所不同:第一个过程将提交所有被跟踪的文件(暂存和未暂存);第二个将只提交暂存文件。

两个程序都显示在图 4-5 中。

9781430261032_Fig04-05.jpg

图 4-5 。使用标准 rm 命令删除的文件可以使用一个命令(git commit -a)或两个命令(git rm 和 git commit)提交

状态D_在配方 4-4 中有详细描述。此配方中出现的另一个状态_D具有以下含义:

  • 第一个字符(空格):操作未被分段。
  • 第二个字符(D):文件从工作目录中删除。

命令$ rm [filename]将未修改的文件转换为_D状态。您可以使用以下方法反转此操作:

$ git checkout -- [filename]

该命令将标为_D的文件转换成未修改的文件。文件将被恢复到工作目录中。

4-6.将未修改的文件转换为未被跟踪的文件

问题

存储库处于干净状态,工作目录包含一个文件— untracked.txt。该文件未被修改。你想把它转换成未被追踪的状态。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-06
$ cd 04-06
$ git simple-commit untracked

现在存储库是干净的,工作目录包含一个文件— untracked.txt

遵循以下程序:

  1. $ git rm --cached untracked.txt
    

    删除未跟踪的. txt 文件

  2. $ ls命令检查存储库的内容。正如您所看到的,文件没有被删除,它仍然存在于工作目录中。

  3. Check the status of the repository. The command

    $ git status prints:
    
    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #       deleted:    untracked.txt
    #
    # Untracked files:
    #   (use "git add <file>..." to include in what will be committed)
    #
    #       untracked.txt
    

    该文件同时被列为已暂存 ( 待提交变更)和未跟踪(未跟踪文件)。

  4. Check the simplified form of the status. The output of

    $ git status -s will be:
    
    D_ untracked.txt
    ?? untracked.txt
    

    该文件被列出两次:作为D_??

  5. $ git commit -m "Committing removed file"提交更改

  6. Check the status with the $ git status -s command.

    输出是:??untracked.txt

    该文件不再被跟踪,并且不包括在最新的快照中。

它是如何工作的

这一次是单一命令:

$ git rm --cached [filename]

将一个未修改的文件转换成两种不同的状态,表示为D_ ??。第一种状态表示文件已暂存。更准确地说,我们可以说文件删除操作是分阶段的:下一次提交将存储工作目录的快照,而不存储该文件。

第二种状态表示为??,指定工作目录包含一个不被跟踪的文件。在发出下一个$ git add命令之前,该文件不会受到下一个提交操作的影响。

配方 4-6 如图 4-6 所示。

9781430261032_Fig04-06.jpg

图 4-6 。git rm - cached 命令将一个未修改的文件转换成两种状态,分别表示为 D_ 和??

4-7.暂存并提交用 git mv 重命名的文件

问题

您的存储库处于干净状态,包含一个名为old-name.txt的文件。您想要将文件old-name.txt重命名为new-name.txt,并提交该操作。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-07
$ cd 04-07
$ git simple-commit old-name

现在存储库是干净的,工作目录包含一个文件— old-name.txt

遵循以下程序:

  1. $ git mv old-name.txt new-name.txt重命名文件

  2. $ ls命令检查工作目录的内容。工作目录现在包含一个名为new-name.txt的文件。文件old-name.txt已经消失了。

  3. Check the status of the repository with the $ git status command. The output:

    
    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #       renamed:    old-name.txt -> new-name.txt
    #
    

    说明移动文件的操作是分阶段进行的。

  4. Check the simplified form of status. The output of $ git status -s will be:

    R_ old-name.txt -> new-name.txt
    

    状态由两个字符表示:字母R后跟一个空格。

  5. $ git commit -m "Staging and committing moved file"命令提交更改。

  6. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。

它是如何工作的

$ git mv的语法如下所示:

$ git mv [old-filename] [new-filename]
$ git mv [filename] [directory]

第一个命令重命名文件,而第二个命令将文件移动到目录中。用$ git mv命令重命名或移动的文件用R_表示。

操作如图 4-7 中的所示。

9781430261032_Fig04-07.jpg

图 4-7 。配方 4-7 中的事件流程

两个字母的状态R_可以解释为:

  • 第一个字符(R)表示暂存区的状态。R表示文件已重命名,操作已暂存。
  • 第二个字符与工作目录有关。空格意味着工作目录中文件的状态与暂存区中的状态完全相同。

如何撤销$ git mv命令执行的操作?该过程包括两个步骤。首先,使用以下命令取消重命名:

$ git reset -- [new-filename]

该命令会生成两个文件:

D_ old-file.txt
?? new-file.txt

您可以使用以下命令恢复old-file.txt:

$ git reset -- [old-filename]
$ git checkout -- [old-filename]

new-file.txt可通过以下方式移除:

$ rm new-file.txt

4-8.提交用标准 mv 命令重命名的文件

问题

您的存储库是干净的,包含一个名为old-name.txt的文件。您想要重命名并提交更改。您更喜欢用标准的$ mv命令来执行重命名。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-08
$ cd 04-08
$ git simple-commit old-name

存储库是干净的,工作目录包含一个文件— old-name.txt

遵循以下程序:

  1. $ mv old-name.txt new-name.txt重命名文件

  2. Check the status of the repository with the $ git status command. The output:

    # On branch master
    # Changes not staged for commit:
    #   (use "git add/rm <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       deleted:    old-name.txt
    #
    # Untracked files:
    #   (use "git add <file>..." to include in what will be committed)
    #
    #       new-name.txt
    no changes added to commit (use "git add" and/or "git commit -a")
    

    通知您两项更改。第一个变化与文件old-name.txt有关。文件old-name.txt已删除,但操作未暂存。此外,您的存储库现在包含一个名为new-name.txt的新的未跟踪文件。因此,当您重命名一个文件时,git 认为它是两个独立的操作:删除和创建。

  3. Check the simplified form of status. The output of $ git status -s will be:

    _D old-name.txt
    ?? new-name.txt
    

    文件old-name.txt已被删除;这被表示为_D。git 将文件new-name.txt视为一个新的未被跟踪的文件。因此,将其表示为??

  4. $ git add new-name.txt命令暂存一个新文件。正如你在配方 4-1 中所记得的,除了使用$ git add命令,没有其他方法来暂存一个新的未跟踪文件。现在命令$ git status -s打印:

    A_ new-name.txt
    _D old-name.txt
    
  5. Stage the removed file with the $ git rm old-name.txt command and check the status with the $ git status -s command. The output:

    R_ old-name.txt -> new-name.txt
    

    显示 git 足够聪明,能够猜到文件被移动了。

  6. $ git commit -m "Committing a file moved with mv"创建版本。

  7. $ git status –s命令检查状态。输出是空的,因此存储库是干净的。

它是如何工作的

操作$ mv old-name.txt new-name.txt导致两个变化,表示为:

_D old-name.txt
?? new-name.txt

第一次变更可通过$ git rm old-name.txt进行

要进行第二次更改,您可以使用$ git add new-name.txt

当两个更改都被暂存时,git 会猜测只有一个更改—文件被重命名为:

R_ moved.txt -> sudir/moved.txt

这证明了在 git 中,重命名的方式并不重要。

4-9.暂存所有文件

问题

存储库处于干净状态,其工作目录包含三个文件:modified.txtdeleted.txtold-name.txt。你想:

  • 创建一个新文件— new.txt
  • 更改modified.txt的内容。
  • 移除文件— deleted.txt
  • 将文件old-name.txt重命名为new-name.txt
  • 用一个命令$ git add –A准备所有的改变。
  • 使用命令— $ git commit -m "Staging all changes"提交更改。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-09
$ cd 04-09
$ git simple-commit modified deleted old-name

存储库是干净的,工作目录包含三个文件:modified.txtdeleted.txtold-name.txt

遵循以下程序:

  1. 创建一个新文件$ echo new > new.txt

  2. 修改modified.txt文件$ echo Some new contents > modified.txt的内容

  3. $ rm deleted.txt删除deleted.txt文件

  4. 重命名old-name.txt文件$ mv old-name.txt new-name.txt

  5. Check the status of the repository with the $ git status command. The output:

    # On branch master
    # Changes not staged for commit:
    #   (use "git add/rm <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #
    #       deleted:    deleted.txt
    #       modified:   modified.txt
    #       deleted:    old-name.txt
    #
    # Untracked files:
    #   (use "git add <file>..." to include in what will be committed)
    #
    #       new-name.txt
    #       new.txt
    no changes added to commit (use "git add" and/or "git commit -a")
    

    将所有变更列为未暂存。工作目录还包含一个未跟踪的文件。

  6. 查看状态的简化形式。$ git status –s的输出将是:

    _D deleted.txt
    _M modified.txt
    _D old-name.txt
    ?? new-name.txt
    ?? new.txt
    
  7. 用一个命令$ git add -A准备所有的改变。

  8. Check the status with the $ git status command. You will see:

    # On branch master
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #       deleted:    deleted.txt
    #       modified:   modified.txt
    #       renamed:    old-name.txt -> new-name.txt
    #       new file:   new.txt
    #
    

    所有的变化都是阶段性的。

  9. 使用$ git status -s命令检查缩短状态。您将获得:

    D_  deleted.txt
    M_  modified.txt
    R_  old-name.txt -> new-name.txt
    A_  new.txt
    
  10. $ git commit -m "Committing all the changes"命令提交更改。

  11. $ git status -s命令检查状态。输出是空的,因此存储库是干净的。

它是如何工作的

配方 4-9 解释了我们已经非常熟悉的命令的作用。这个命令在工作目录中存放所有的改变。$ git add -A执行的转换汇总在表 4-1 中。

表 4-1 。git add -A 执行的转换

|

$ git add -A 之前的状态

|

$ git add -A 后的状态

|
| --- | --- |
| 新的未暂存文件?? | A_ |
| 已修改的未转移文件 M | M |
| 已删除的未转移文件 D | D |
| 重命名的未分级文件表示为两处更改;其中一个用??另一个用 D 表示 | R |

4-10.使用混合状态

问题

您希望检查修改暂存文件时会发生什么情况。

解决办法

使用以下内容启动存储库:

$ cd git-recipes
$ git init 04-10
$ cd 04-10

存储库是干净的,工作目录不包含任何文件。

遵循以下程序:

  1. 创建一个新文件$ echo Some info > file.txt

  2. $ git add file.txt命令暂存文件。此时$ git status -s打印:

    A_  file.txt
    
  3. $ echo Some other info > file.txt修改文件

  4. $ git status -s命令检查状态。输出将是下面的代码:

    AM file.txt
    
  5. 该代码由两个字母组成,通知您某个文件在某个时间点被暂存,但此后被修改过。

  6. $ git commit -m "First revision"命令创建修订。该命令创建一个新的版本,存储内容为Some info的文件。仓库仍然很脏。缩短状态命令打印:

    _M file.txt
    
  7. 包含Some other info的文件仍未暂存。

它是如何工作的

配方 4-10 强调了$ git add命令的一个非常重要的方面。它在给定的时间点暂存文件。您可以将$ git add视为特殊类型的$ cp命令。Git add 命令将文件从工作目录复制到登台区。当然,该操作是在发出$ git add 命令后立即执行的。如果您修改已经暂存的文件,您的修改不会自动暂存。换句话说,存储在临时区域中的文件版本不同于存储在工作目录中的版本。这种状态由两个不同的字母表示,例如:

AM file.txt

第一个字母A表示文件已暂存。第二个字母M告诉您存储在工作目录中的文件被修改了。如何将文件恢复到正常状态?如果已经准备好的内容很重要,你可以遵循秘籍 4-10。否则,您可以重做$ git add 命令。国家将变成:

A_ file.txt

在这种情况下,已暂存但未提交的更改将会丢失。

通常,我在日常工作中不使用AM这样的状态。原因很简单。我不会存放我的文件,让它们不被提交。一旦文件被暂存,我就提交。在现实场景中使用 AM 状态的唯一例子是当我从存储库中恢复一个被删除的文件,然后在提交之前修改它。程序如下:

$ git simple-commit lorem
$ git rm lorem
$ git checkout HEAD∼ -- lorem.txt
$ vi lorem.txt
$ git status -s

命令$ git checkout HEAD∼ -- lorem.txt从倒数第二个版本恢复一个被删除的文件。如果编辑文件,其状态将由 AM 指示。

摘要

本章将存储在存储库中的所有文件分为几个类别。分工从问题开始:文件是否被 git 跟踪。每个文件不是未被跟踪就是被跟踪。因为 git 不存储任何关于未被跟踪的文件的信息——关于它们没有什么可说的了。

被跟踪的文件可以进一步分为未修改的已修改的

未修改的文件是已经存储在储存库中(即数据库中)的文件。从那以后,它们没有被修改过。

修改过的文件是提交后被更改过的文件。它们可以进一步分为有级无级档。

暂存的文件是进入下一个版本的文件,而未暂存的文件将保持不变。未转移的文件不受下一个提交命令的影响,也不会影响下一个提交命令。

图 4-8 显示了所讨论文件的状态图。

9781430261032_Fig04-08.jpg

图 4-8 。文件的不同状态

请注意:

  • 跟踪每个未修改的文件。
  • 跟踪和修改每个暂存和未暂存的文件。

因此,我们可以毫不含糊地使用这四个形容词来描述文件:

  • 无人探索过的
  • 未更改的
  • 阶段
  • 未分级

包含git status -s命令返回的短代码的状态简化图如图图 4-9 所示。

9781430261032_Fig04-09.jpg

图 4-9 。git status–s 返回的带有短代码的不同文件状态的简图

知识库的结构

为了引入登台区,我扩展了存储库的结构。事实上它由三个不同的区域组成,如图图 4-10 所示:

  • 工作目录
  • 存储对象的数据库
  • 集结地

9781430261032_Fig04-10.jpg

图 4-10 。知识库的结构

工作目录是您工作的地方。正如您已经知道的,您可以使用任意的工具和命令来操作工作目录中的文件。

一旦您决定应该提交整个工作目录或其中某些文件的当前状态,您就可以存放这些文件。您可以使用$ git add、$ git rm 和$ git mv 命令来实现这一点。它们都可以接受通配符,因此您可以编写如下命令:

$ git add *
$ git rm *.txt
$ git mv .????* tmp/

您可以避免使用$ git rm$ git mv命令,因为$ git add可以为您执行重命名和文件删除。$ git add命令接受可选参数--all–update,可分别缩写为-A-u。这些参数以下列方式影响$ git add命令的默认行为:

  • $ git add *命令将暂存工作目录中所有新的和修改过的文件(删除的文件不会被暂存)。
  • git add --update命令将暂存所有修改的文件(包括修改和删除的文件,但不包括新文件)。
  • git add --all暂存所有文件(包括新的、修改的和删除的文件)。

根据您的需要,您可以用$ git add –A存放所有文件,或者根据您的意愿修改版本。

最后,您可以使用$ git commit命令创建修订。如果使用不带-a标志的$ git commit命令,只有手动添加到暂存区的文件会受到影响。$ git add$ git rm$ git mv命令执行的操作如图图 4-11 所示。

9781430261032_Fig04-11.jpg

图 4-11 。由$ git add、$ git rm 和$ git mv 命令实现的操作

五、分支

每当我被问及从旧版本的控制系统(VCS,如 CVS 或 Subversion)转换到 git 的好处时;我用这个简短的陈述来回答:git 分支模型。一旦你学会使用它,你会问自己,没有它我究竟是怎么工作的?事实上,git 处理分支的方式使其高于其他(如果不是全部)VCS 系统。事实上,我真的相信这个特性足以成为转向 git 的理由。

分支到底是什么?一条分支 是一条发展线。这是一个高层次的定义,与特定于实现的方面无关。从技术上讲,git 中的分支是指向数据库中任意提交的指针。虽然自信地使用分支需要一些练习;你应该从最基本的分支特征开始。它们是相互独立的。修改一个分支的方式不会影响其他分支。为了保存项目历史中的任何一点,创建一个分支就足够了。Git 永远不会修改您的分支,除非您明确要求它这样做。

本章将为你提供一个使用分支的大部分方面的强有力的把握。我们从在非裸存储库中创建和切换分支开始。特别是,讨论包括 git 存储分支方式的各个方面。这将帮助您理解为什么 git 分支如此高效。

接下来,我们在克隆的上下文中分析分支。它将引导我们进入以下不同类型的分支:

  • 远程分支(即远程存储库中的分支)

  • 本地分支(即您当前工作的存储库中的分支)

  • 普通地方分行

  • 本地跟踪分支

  • 远程跟踪分支 1

所有的分支类别,在开始时可能不清楚,将在第十章讨论远程存储库和协作时变得清晰。这里介绍它们是为了从一开始就提供一个清晰的观点。关于分支克隆的主题将在最后一个秘籍中返回。在那里,我们考虑在裸存储库中的分支上操作。这个方法很少使用,但是它进一步阐明了在非裸存储库中切换分支的过程。

同样,不要在分离的头部状态下工作。这是没有意义的,尤其是一旦你理解了分支。记住,使用分支保证你的修改永远不会丢失。

为了全面了解分支,我还将展示切换分支——如果不小心的话——是如何让你的生活变得复杂的。你可以用三个简单的方法来解决这些问题:

  • 清除和重置分支
  • 切换分支以避免冲突
  • 从意外提交中恢复

有了这三个简单的方法,你一读完这一章就能利用分支。这些是应急秘籍——当你遇到困难时,你可以运用它们。

偶尔你会面临从不同分支访问文件的问题。有两个简单的命令可以解决这个问题:您可以从不同的分支中签出文件,或者在标准输出中显示它。与流重定向器>一起使用的后一种解决方案允许您重命名从不同分支检出的文件。显示这些命令的方法强调了您在异构环境中工作时可能遇到的问题。

本章的最后一部分介绍了解释如何删除和重命名分支的方法。这就引出了合并分支和不合并分支的非常重要的概念。

5-1.创建和切换分支

问题

你想在 git 中创建和切换分支。为了实现这个目标,你需要创建一个如图 5-1 所示的库。该图显示了一个修订图,其中每个点代表一个提交,每个带箭头的圆角矩形代表一个分支。您应该小心地创建提交,以使提交消息与相应点的标签完全相同。此外,每次提交都应该包括一个新文件,其名称和内容基于点的标签。例如,标有和d2的点表示带有注释d2的提交。它应该包含一个名为d2.txt的文件,其中包含文本d2。这种提交方法有两个重要的特点,可以帮助你练习分支和提交:

  • 没有冲突。
  • 对于每个版本,你可以很容易地猜出工作目录的内容。

9781430261032_Fig05-01.jpg

图 5-1 。在配方 5-1 中创建的存储库

当图 5-1 中的所示的存储库完成时,您想要验证每个分支只包含分支及其祖先所指向的版本中存储的文件。

解决办法

启动命令行并创建一个新的存储库:

$ cd git-recipes
$ git init 05-01
$ cd 05-01

然后创建三个版本m1m2m3、??。您可以使用以下步骤:

  1. $ echo m1 > m1.txt创建一个新文件m1.txt
  2. 使用$ git snapshot m1提交快照
  3. $ echo m2 > m2.txt创建一个新文件m2.txt
  4. 使用$ git snapshot m2提交快照
  5. $ echo m3 > m3.txt创建一个新文件m3.txt
  6. 使用$ git snapshot m3提交快照

存储库现在看起来像图 5-2 。

9781430261032_Fig05-02.jpg

图 5-2 。配方 5-1 中的存储库以及在主分支中创建的前三个版本

默认情况下,任何新初始化的存储库都包含一个名为master的分支。您可以使用以下命令验证这一点:

$ git branch

该命令打印分支列表。现在它的输出将是:

* master

让我们创建一个名为doc的新分支。以下是您需要的命令:

$ git branch doc

现在,如果您用$ git branch命令列出分支,您会注意到存储库包含两个分支,如下所示:


  doc
* master

星号表示当前分支。说明你还在master分支。存储库现在看起来像图 5-3 。

9781430261032_Fig05-03.jpg

图 5-3 。配方 5-1 中的存储库,有两个分支 master 和 doc

现在您需要在名为doc的分支上创建三个版本。首先,您必须使用命令切换到这个分支:

$ git checkout doc

现在命令:

$ git branch

印刷品:

* doc
  master

上面的输出通知您当前位于名为doc的分支上。您的提交现在将转到这个新分支。您可以通过以下步骤创建三个提交d1d2d3、:

  1. $ echo d1 > d1.txt创建一个新文件d1.txt
  2. 使用$ git snapshot d1提交快照
  3. $ echo d2 > d2.txt创建一个新文件d2.txt
  4. 使用$ git snapshot d2提交快照
  5. $ echo d3 > d3.txt创建一个新文件d3.txt
  6. 使用$ git snapshot d3 命令提交快照。

存储库现在看起来像图 5-4 。

9781430261032_Fig05-04.jpg

图 5-4 。来自配方 5-1 的储存库,具有版本 d1、d2 和 d3

要创建下一个分支,可以使用三个命令:

# switch to master branch
$ git checkout master

# create a new branch named info
# pointing to the same commit as master branch
$ git branch info

# switch to info branch
$ git checkout info

或者一个命令:

$ git checkout -b info master

两个解决方案都将创建一个名为info的新分支,它指向与分支master相同的修订版本。存储库中的当前分支现在是info。您可以使用以下命令检查它:

$ git branch

上述命令的输出如下:


  doc
* info
  master

存储库现在看起来像图 5-5 。

9781430261032_Fig05-05.jpg

图 5-5 。配方 5-1 中的存储库,带有一个名为 info 的新分支

您可以创建三个版本i1i2i3来完成配方:

  1. $ echo i1 > i1.txt创建一个新文件i1.txt
  2. 使用$ git snapshot i1提交快照
  3. $ echo i2 > i2.txt创建一个新文件i2.txt
  4. 使用$ git snapshot i2提交快照
  5. $ echo i3 > i3.txt创建一个新文件i3.txt
  6. 使用$ git snapshot i3提交快照

存储库已完成。看起来像图 5-1 。您可以使用以下命令来验证它:

$ gitk --all &

gitk应用绘制的图形如图图 5-6 所示。注意,在这个图中,当前分支的名称是用粗体书写的,而当前的修订是用白点表示的。在你的屏幕上,圆点将是黄色的——这是默认情况下gitk使用的约定。

9781430261032_Fig05-06.jpg

图 5-6 。用 gitk 应用绘制的配方 5-1 的存储库

类似于图 5-6 中所示的图表可以使用附加开关通过$ git log命令打印出来:

$ git log --oneline --graph --decorate --all

上述命令的输出将类似于:

* 0d6501b (info) i3
* ab087d1 i2
* 6f4364e i1
| * 23d9855 (HEAD, doc) d3
| * f7651b8 d2
| * 6042953 d1
|/
* 7c9bc41 (master) m3
* 9cf5f5a m2
* ebf4409 m1

传递给$ git log的参数--graph打开连接提交的行的可见性,--oneline将输出压缩为一行预提交,SHA-1 缩短为 7 个字符,--decorate打开符号引用,如HEADmasterdocinfo。用于$ git log$ gitk命令的参数--all具有相同的含义:它使得存储在存储库中所有可用分支中的修订被包括在内。默认情况下,只显示当前分支中包含的提交。

如果您现在使用命令切换到名为master的分支:

$ git checkout master

工作目录将只包含三个文件m1.txtm2.txt,m3.txt。可以用$ ls命令 来验证。同理,如果你用$ git checkout doc切换到doc分支,那么工作目录将包含六个文件:d1.txtd2.txtd3.txtm1.txtm2.txtm3.txt。如果您切换到分支info,工作目录将包含文件:i1.txti2.txti3.txtm1.txtm2.txtm3.txt。这就是为什么我坚持创建诸如m1m2d1d2等提交。您可以很容易地验证您发出的命令如何影响工作目录。一个简单的$ ls命令将显示工作目录的完整内容——没有必要检查文件。

image 提示您可以使用$ git show info^{tree}命令列出 info 分支中存储的文件,而无需切换分支。以类似的方式,您可以用$ git show SHA-1^{tree}列出任意修订版中的文件。

当用$ git branch 列出分支时,可以使用附加参数-v。命令$ git branch -v将打印每个分支的最新版本,例如:

* doc    23d9855 d3
  info   0d6501b i3
  master 7c9bc41 m3

执行以下两个命令完成配方:

$ git checkout master
$ git pack-refs --all

第一个会将当前分支更改为master,而第二个会将所有引用,包括所有分支,存储在一个名为.git/packed-refs的文件中。

它是如何工作的

每个存储库都包含一个特殊的文件.git/HEAD,它指向当前的版本。这就是 git 知道您最近签出了哪个版本的方法。这也将在您下次提交时用作父修订。存储在.git/HEAD文件中的引用以两种形式之一写入:

  • 作为对分支的符号引用
  • 作为阿沙一号

如果你在一个分支上,那么.git/HEAD的内容是用符号形式写的。当你进入分离头状态时,参考被写成 SHA-1。在这里,我们将集中讨论符号引用。SHA-1 参考将在配方 5-4 中讨论。

引用的符号形式如下所示:

ref: refs/heads/master

上面引用的符号形式表示您当前的版本是名为master的分支所指向的版本。

分支以两种不同格式之一存储在.git目录中:

  • 松散格式
  • 压缩格式

松散格式的分支存储在.git/refs/heads目录中。每个分支都存储在一个单独的文件中。在符号引用ref: refs/heads/xyz中,refs/heads/xyz部分是文件.git/refs/heads/xyz的路径。该文件包含分支xyz最新版本的 SHA-1 名称。

在打包格式中,许多引用,如ref: refs/heads/xyzref: refs/heads/fooref: refs/heads/bar,都存储在一个文件中——??。在一个新初始化的存储库中,文件.git/packed-refs不存在。这意味着默认情况下,引用最初以松散的格式存储。

当您初始化一个新的存储库时,它不包含任何修订,它的数据库是空的。文件.git/HEAD包含条目ref: refs/heads/master,文件夹.git/refs/heads为空——文件refs/heads/master不存在。新的存储库包含一个名为master的分支,它不包含任何修订。

使用以下命令创建第一个版本后:

$ echo m1 > m1.txt
$ git snapshot m1

那么你的知识库就不再是空的了。文件.git/HEAD没有改变——它仍然包含指向.git/refs/heads/master的相同条目。但是现在目录.git/refs/heads包含一个名为master的文件。该文件存储了标有m1的版本的 SHA-1。您可以使用以下两个命令来检查它:

$ git log --pretty=oneline
$ cat .git/refs/heads/master

比较以上命令的输出,你会注意到存储在.git/refs/heads/master中的 SHA-1 与$ git log命令返回的完全相同。

现在,master分支以松散的格式存储。关于其最新版本的信息保存在文本文件.git/refs/heads/master中。如果要将所有引用的格式从松散格式转换为压缩格式,可以使用以下命令:

$ git pack-refs --all

在上面的命令之后,.git/refs/heads目录又变空了;然而,这并不意味着分支被删除。存储库仍然包含这个分支。你可以用$ git branch命令来验证它。然而,master分支的提示以打包格式存储在.git/packed-refs文件中。

image 提示分支中的最后一次提交被称为分支的尖端。图 5-1 呈现了三个分支,并有如下提示:分支文档的提示为d3,分支主文档的提示为m3,分支信息的提示为i3

下一次提交后,使用:

$ echo m2 > m2.txt
$ git snapshot m2

文件.git/HEAD保持不变,但是存储master分支的格式再次从打包变为松散。文件.git/refs/heads/master 被重新创建,它现在包含了标签为m2的修订版的 SHA-1。您可以使用之前使用的两个命令来检查它:

$ git log --pretty=oneline
$ cat .git/refs/heads/master

您应该注意到,每次修订都会将分支的格式从打包转换为松散。

标记为m3的第三个版本保持文件.git/HEAD不变,而.git/refs/heads/master包含第三个版本的 SHA-1。

$ git branch doc创建一个新分支会怎么样?Git 创建一个新文件.git/refs/heads/doc,并在其中存储当前版本的 SHA-1。最初,每个分支都以松散的格式存储。

把一个分支想象成一个指针,指向修订图中的一个节点。因为以十六进制文本格式存储的 SHA-1 消耗 40 个字节,因此创建分支意味着在文本文件中存储 40 个字节。这也是 git 中分支如此高效的原因之一。分支创建的整个过程包括在本地存储系统中保存一个 41 字节长的引用(40 字节用于 SHA-1 和一个换行符)!不仅没有任何数据传输,也没有任何通信。是瞬间的!

注意,当你用$ git branch doc命令 ?? 创建一个新的分支时,文件.git/HEAD保持不变。这意味着 git 不会自动切换到新的分支。要切换到一个新的分支,你必须发出$ git checkout doc命令。该命令将.git/HEAD文件的内容更改为:

ref: refs/heads/doc

正如你所看到的关于当前分支的信息,在$ git branch命令的输出中用星号表示的分支,存储在.git/HEAD文件中。

提交时 git 执行哪些内部操作?Git 使用.git/HEAD文件解析当前分支的名称。让我们假设文件.git/HEAD包含ref: refs/heads/abc。这意味着您当前的分支被命名为abc。一旦分支的名称被解析,git 就会读取存储在abc分支中的最新版本的 SHA-1 名称。如果分支以松散格式存储,则从.git/refs/heads/abc文件中读取名称。否则分支的名称来自.git/packed-refs。我们将把abc分支中最新版本的 SHA-1 表示为XXXX。版本XXXX将被用作新版本的父版本。接下来,git 在数据库中创建并存储新的修订版YYYY。在此过程中,名称XXXX被用作YYYY版本的父版本。最后,新创建的修订版本的 SHA-1 名称,即YYYY,被存储在文件.git/refs/heads/abc中。正如您所记得的,每次提交的副作用是当前分支再次以松散的格式存储。

总而言之,我们可以说,当您在 branch 上工作时:

  • 当您提交时,新的修订将转到当前分支。文件.git/HEAD不变。新创建版本的 SHA-1 将存储在.git/refs/heads/branch-name文件中。如果该分支已经以打包格式存储,则该格式被更改为松散格式。
  • 当你用$ git branch branch-name创建一个新的分支时,一个新的文件.git/refs/heads/branch-name被创建,它存储作为参数传递给$ git branch命令或当前版本的版本的 SHA-1。文件.git/HEAD保持不变。新分支的格式总是宽松的。
  • 当您使用$ git checkout branch-name命令切换分支时,.git/refs/heads中的所有文件保持不变。对分支branch-name的符号引用存储在.git/HEAD中。它的形式是ref: refs/heads/branch-name。该命令将工作目录重置为符合branch-name分支最新版本的状态。该命令不会改变存储分支提示的格式。

正如您在配方 5-1 的解决方案部分所了解的,创建和切换到新分支的两个操作都可以通过一个命令来实现:

$ git checkout -b new-branch existing-branch

这个命令创建一个名为new-branch的新分支,它指向与名为existing-branch的现有分支相同的版本。

HEAD 在许多 git 命令中扮演着非常特殊的角色。在任何你需要当前工作版本的 SHA-1 的地方,你都可以使用HEAD来代替。此外,HEAD通常是缺省参数的缺省值。这些命令:

$ git reset --hard
$ git reset --hard HEAD

是一样的。以类似的方式,您可以创建一个新的分支:

$ git branch new-name [REVISION]

[REVISION]参数定义了一个新分支将指向哪里。如果省略该参数,将使用HEAD。因此,这两个命令:

$ git branch new-name
$ git branch new-name HEAD

是等价的。其他命令也是如此。以下两个命令也是等效的:

$ git checkout -b new-branch
$ git checkout -b new-branch HEAD

image 注意特殊名称HEAD被转换成路径.git/HEAD。因此,如果你在类似 u*ix 的系统上工作,你必须用大写字母输入。在 Windows 上工作的读者可以使用小写字母键入HEAD(即head)。

修订可以用许多不同的方式来识别。你已经知道 SHA-1 和缩写的 SHA-1 名字都可以使用。引用修订的其他方式包括分支、reflog 条目、祖先引用、第 n 个父引用和标签。以下是一些如何创建指向特定修订的分支的示例:

$ git branch new-branch info      # existing branch
$ git branch new-branch a1b2c3ef  # abbreviated SHA-1
$ git branch new-branch HEAD@{5}  # reflog entry
$ git branch new-branch doc∼5     # ancestor reference
$ git branch new-branch master²  # n-th parent reference
$ git branch new-branch v1.2.3    # tag

请记住,上述命令不会修改存储在中的数据库。git/对象。他们只在.git/refs/heads中创建一个文件。该文件将包含给定版本的阿沙-1。

image 提示$ git checkout -b命令的语法也可以用更一般的形式写成$ git checkout -b new-branch [REVISION]

当您开始处理分支时,值得记住的是切换分支的操作存储在一个 reflog 中。这些命令:

$ git checkout doc
$ git checkout master

将导致$ git reflog条目的以下输出:

23d9855 HEAD@{0}: checkout: moving from master to doc
7c9bc41 HEAD@{1}: checkout: moving from info to master

$ git pack-refs --all命令将存储在.git/refs/heads中的所有引用打包并存储在.git/packed-refs文件中。您可以通过在分支中创建新的提交来解包每个分支。

5-2.克隆带有分支的存储库

问题

您想要创建配方 5-1 中创建的存储库的精确副本。您希望副本包含原始存储库中存储的所有分支:masterdocinfo。您还想创建两个名为foobar的新分支——它们应该指向与master分支相同的修订版本。

您将不得不处理由$ git clone命令的行为引起的困难:克隆的存储库只包含一个名为master的本地分支。在克隆过程中,git 只为存储在。原始存储库中的 git/HEAD。

解决办法

使用以下命令克隆在方法 5-1 中创建的存储库:

$ cd git-recipes
$ git clone 05-01 05-02
$ cd 05-02

新的存储库只包含一个名为master的分支。你可以用$ git branch命令来验证它。它的提示以松散的格式存储在文件.git/refs/heads/master中。要创建一个名为info的分支,该分支将对应于克隆存储库中同名的分支,请执行以下命令:

$ git checkout info

该命令将打印分支info被设置为跟踪远程分支的消息:

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

在上面的命令之后,存储库将包含两个分支:masterinfo。可以用$ git branch命令来验证。

以同样的方式,您可以创建名为doc的分支:

$ git checkout doc

存储库05-02现在包含三个分支masterinfodoc 。完成制作分支foobar的配方;

$ git checkout -b foo master
$ git checkout -b bar master

命令$ git branch -a -vv输出以下信息:

* bar                   7c9bc41 m3
  doc                   23d9855 [origin/doc] d3
  foo                   7c9bc41 m3
  info                  0d6501b [origin/info] i3
  master                7c9bc41 [origin/master] m3
  remotes/origin/HEAD   -> origin/master
  remotes/origin/doc    23d9855 d3
  remotes/origin/info   0d6501b i3
  remotes/origin/master 7c9bc41 m3

第一行:

* bar                   7c9bc41 m3

通知您您的存储库中包含一个名为doc普通本地分支 。这是您当前的分支,它指向由注释m37c9bc41标识的修订。

第二行:


  doc                   23d9855 [origin/doc] d3

通知您您的存储库中包含一个名为doc本地跟踪分支 。这个分支跟踪的分支被命名为origin/doc

该行:


  remotes/origin/doc    23d9855 d3

通知您您的存储库中包含一个名为origin/doc远程跟踪分支

因此,您的存储库包含三种不同类型的分支:

  • 普通本地分行
  • 本地跟踪分支
  • 远程跟踪分支

通过删除新存储库和克隆的原始存储库之间的关系来完成这个配方。您可以通过以下方式实现这一点:

$ git remote rm origin

的输出

$ git branch -a -vv:
* bar    7c9bc41 m3
  doc    23d9855 d3
  foo    7c9bc41 m3
  info   0d6501b i3
  master 7c9bc41 m3

通知您存储库现在只包含普通的本地分支。

它是如何工作的

在这个菜谱中,我们使用两个不同的存储库。第一个存储库被命名为05-01。这是我们克隆的原件。第二个存储库被命名为05-02。它是原始存储库的副本。该配方中的所有命令都将在05-02库中执行。术语本地存储库指的是你工作的存储库,也就是05-02。原始存储库将被称为远程存储库。您可以使用05-02库中的命令$ git remote -v来检查这个关系。输出:

origin  .../git-recipes/05-01 (fetch)
origin  .../git-recipes/05-01 (push)

通知您当前所在的存储库使用别名origin指向05-01存储库。就在克隆操作之后,但是在任何人创建新的提交之前,他们存储在.git/objects中的数据库是相同的。

正如你在配方 5-1 中所记得的,存储库05-01包含三个分支:masterinfodoc。当我们在05-02库中工作时,我们将把存储在05-01库中的分支称为远程分支 。您永远不会通过 commit 或 add 命令与它们进行交互。远程分支不可用—无法从当前存储库中登录远程存储库并与之交互。与远程分支交互的唯一方法是使用 push 和 fetch 命令——这些主题的详细讨论包含在第十章中。

05-02本地创建和存储的分支将被称为本地分支。有三种类型的本地分支:

  • 普通地方分行
  • 远程跟踪分支机构
  • 本地跟踪分支机构

我再强调一次这一点:都是局部。远程跟踪分支也是。

当您发出命令时:

$ git checkout -b foo master
$ git checkout -b bar master

他们创建了两个普通本地分支 ,命名为foobar。可以肯定的是,没有人会用“普通”这个形容词。我只是“一时兴起”发明了它,以便让你清楚所有类型的分支。他们通常被称为地方分行。但是让我们暂时使用这个术语——它将有助于避免混淆。

远程跟踪分支 是远程分支的本地副本。它们会保留远程分支在初始克隆或上次获取操作期间的状态。创建远程跟踪分支的要点非常简单:每当您想要检查远程分支的状态时,您应该咨询远程跟踪分支。远程跟踪分支被命名为remotes/X/Y,其中X代表远程存储库的别名,Y是远程分支的名称。对于存储在远程仓库05-01中的名为lorem的远程分支,别名为origin,远程跟踪分支将被命名为remotes/origin/lorem。这个名字可以简化为origin/lorem。远程跟踪分支以打包格式存储;因此,您不会在refs/remotes/origin目录中找到它们。它们存储在.git/packed-refs文件中。您可以将远程跟踪分支视为只读—我们不会在其中提交。

本地跟踪分支 用于在远程分支发布你的提交。它们类似于普通的本地分支:您可以在其中提交。例如,当以松散格式存储时,它们被存储在.git/refs/heads目录中。主要区别在于它们连接到远程跟踪分支。每个本地跟踪分支跟踪一个远程跟踪分支。最初,它们指向与远程跟踪分支完全相同的修订。。

$ git branch命令列出本地分支。该命令打印本地跟踪分支和普通本地分支。要列出远程跟踪分支,使用-r参数$ git branch -r。您可以使用$ git branch -a命令列出所有分支。附加参数-v打印每个分支的最新版本。如果您想将全部分类分为三组,普通本地分支、远程跟踪分支和本地跟踪分支,请使用参数-vv

在图 5-7 中描述了所有四种类型的分支。来自资源库05-02的三个分支infoorigin/infofoo的属性汇总在表 5-1 中。

9781430261032_Fig05-07.jpg

图 5-7 。四种类型的分支:远程分支、普通本地分支、本地跟踪分支和远程跟踪分支

表 5-1 。存储库 05-02 的三个分支 info,origin/info,foo 的属性如图 5-7 所示

image

image 提示记住:远程跟踪分支是本地分支,充当远程分支的只读副本。

表 5-1 的栏回答了以下问题:

  • 本地/远程:分支机构是本地还是远程?
  • 我可以在这个分支中提交吗?
  • 本地连接:哪个本地分支与该分支相连?
  • 远程连接:哪个远程分支与该分支相连?
  • 发送/接收:我可以使用远程分支向/从这个分支发送/接收吗?

您会在表 5-1 中注意到,只有远程跟踪分支被允许与远程分支进行交互。在克隆过程中,git 会自动创建:

  • 其中本地跟踪分支master 2
  • 所有远程分支的远程跟踪分支

因此,在$ git clone命令之后,存储库05-02包含以下分支:

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/doc
  remotes/origin/info
  remotes/origin/master

以上是$ git branch -a命令的输出。该行:

remotes/origin/HEAD -> origin/master

打印远程存储库的.git/HEAD文件的内容。这就是你如何知道哪个分支被远端认为是当前的。

记住,用$ git clone创建的新存储库包含完整的数据库——包括所有的修订和其他对象。只有存储在.git/refs/heads目录中的本地指针丢失。您必须手动创建它们。这可以通过$ git checkout命令来完成。与许多其他 git 命令一样,$ git checkout命令 用于实现许多不同的目标。在这个配方中,$ git checkout用于为同名的远程分支创建一个新的本地跟踪分支。因此命令:

$ git checkout info

创建一个名为info的本地跟踪分支。该分支将与存储在您的本地存储库中的远程跟踪分支remotes/origin/info相连接。另一个命令之后:

$ git checkout doc

存储库05-02包含三个本地分支masterdocinfo。你可以使用$ git branch命令来检查它。这三个分支,即masterdocinfo,就是局部跟踪分支

image 提示命令$ git branch -a打印本地分支、本地跟踪分支、远程跟踪分支。虽然输出清楚地显示了哪些是远程跟踪分支,但是用于普通本地分支和本地跟踪分支的格式是相同的。如果您同时使用-vv选项,可以获得更精确的信息。

从配方 2-5 中可以知道,克隆定义了新克隆和存储库之间的关系,存储库是您传递给$ git clone命令的 URL。这种关系写在.git/config文件的表单中:

[remote "origin"]
    url = ...

也可以用$ git remote -v命令来检查。

我们将在第十章中更详细地讨论远程,该章涉及远程存储库和同步。现在我只想删除这种关系,否则原始存储库中的远程分支将会模糊各种命令的输出,特别是$ gitk --all$ git log --all。要删除关系运行:

$ git remote rm origin

此命令删除:

  • 存储库配置文件中的[remote "origin"]条目
  • 所有远程跟踪分支

命令$ git remote -v现在返回空结果,命令$ git branch -a只打印五个本地分支docmasterinfofoobar。目前,没有远程存储库;因此所有分支都是普通的本地分支。

image 提示本地跟踪分支和远程跟踪分支只存在于配置文件中至少包含一个远程部分的存储库中。在一个孤立的存储库中谈论远程跟踪分支和本地跟踪分支是没有意义的。

5-3.创建带有分支的克隆体别名

问题

您希望创建一个别名来简化克隆带有分支的存储库的过程。您希望能够用一个命令克隆一个存储库并复制其分支:

$ git clone-with-branches URL directory

第一个参数——URL—应该指向一个现有的存储库(一个 URL 或者一个本地路径)。第二个参数—directory—设置要存储克隆的目录的名称。下面是我们应该如何从配方 5-1 中克隆存储库:

$ git clone-with-branches 05-01 05-03

和 jQuery:

$ git clone-with-branches git@github.com :jquery/jquery.git jquery-local-clone

解决办法

用文本编辑器打开你的.gitconfig文件,在[alias]部分的末尾输入清单 5-1 中的别名。请记住,您必须删除所有换行符。保存文件并关闭编辑器。

清单 5-1。 用分支别名克隆一个库

list-remote-branches = "!listRemoteBranches() {
    git branch -r | sed \"/->/d; s/  origin\\///g\";
}; listRemoteBranches"

checkout-remote-branches = "!checkoutRemoteBranches() {
    for name in `git list-remote-branches`; do
        git checkout $name;
    done;
}; checkoutRemoteBranches"

clone-with-branches = "!cloneWithBranches() {
    git clone $1 $2;
    cd $2;
    git checkout-remote-branches;
    git remote rm origin
}; cloneWithBranches"

您可以通过运行以下命令来验证别名是否按预期工作:

$ git clone-with-branches 05-01 05-03

它是如何工作的

如您所知,您可以使用以下命令列出远程跟踪分支:


 $ git branch -r

如果您克隆存储库05-01,那么上面的命令将会打印出来:


  origin/HEAD -> origin/master
  origin/doc
  origin/info
  origin/master

因此,我们知道原始存储库包含三个名为docinfomaster的分支。第一个别名$ git list-remote-branches以如下方式转换上述输出:

  • 首先,它删除包含->字符的条目(这个条目告诉我们,在原始存储库中HEAD包含对origin/master的符号引用)。
  • 然后,它删除前缀origin/

这两种操作都由流编辑器sed执行。以下 shell 命令:

$ git branch -r | sed "/->/d"

删除包含->的行。用sed过滤掉一些行的语法是:

$ sed "/PATTERN/d"

上面的命令过滤掉所有包含PATTERN的行。

为了删除前缀origin/,我们使用sed’s替换命令——其语法如下:

$ sed "s/PATTERN/REPLACEMENT/"

其中PATTERN 定义了将要被替换的字符串,REPLACEMENT 是新的字符串,斜线作为分隔符,s代表替换。

命令:

$ git branch -r | sed "s/  origin\///g"

用空字符串替换所有出现的origin/。因为我们的PATTERN包含一个斜杠,所以我们必须用\对它进行转义。

当我们使用分号作为分隔符将两个sed命令组合成一个处理指令时:

$ git branch -r | sed "/->/d; s/  origin\///g"

输出将只包含分支名称:

doc
info
master

这就是我们如何获得克隆存储库中所有远程跟踪分支的名称列表。

第二个别名$ git checkout-remote-branches,包含一个for循环,处理由$ git list-remote-branches 别名返回的名字:

for name in `git list-remote-branches`; do
    git checkout $name;
done;

对于我们执行 checkout 命令的每个名字,它都会创建一个本地跟踪分支。当循环结束时,新创建的克隆包含了来自原始存储库的所有分支。

最后一个别名$ git clone-with-branches,执行四个操作:

  • 它克隆原始存储库:$ git clone $1 $2
  • 它带着一个新的克隆进入目录:cd $2
  • 创建本地分支:$ git checkout-remote-branches
  • 它删除了与远程存储库的关系:$ git remote rm origin

注意,配方 5-3 和配方 2-4 产生了相似的结果。使用$ cp -R命令或$ git clone-with-branches别名创建的存储库包含与原始存储库相同的分支,没有远程。这两个过程的主要区别在于,克隆会清除 reflog,而复制会保留它。

如果您是 git 新手,那么本地跟踪分支和远程跟踪分支的概念可能不清楚。我们将更详细地讨论本地跟踪分支和远程跟踪分支,在有关同步的章节中再次强调它们的作用。然后,我希望,他们的目的会变得更清楚。

image 提示别名$ git clone-with-branches在学习 git 时很有用。到本章结束时,或者一旦我们开始练习合并和重定基础,你可能会得出同样的结论。

将 git 子命令创建为 shell 脚本

您在配方 2-10 和配方 5-3 中实现的别名存储在您的全局.gitconfig文件中。这种方法的主要缺点是禁止换行。每个别名,不管有多复杂,都必须存储在一个.gitconfig文件的一行中。这是由.gitconfig解析强加的内部限制。您可以通过将别名存储为单独的 bash 脚本来规避这一限制。要准备使用缩进语法的$ git clone-with-branches git 子命令,创建文件: 3

# on Windows
C:\Program Files (x86)\Git\libexec\git-core\git-clone-with-branches

# on Linux
/usr/lib/git-core/git-clone-with-branches

内容如清单 5-2 所示。在将清单 5-2 保存到一个文件时,你不必删除换行符。

清单 5-2。 Shell 脚本 git-core/git-clone-with-branch

#!/bin/sh

git clone $1 $2
cd $2
git checkout-remote-branches
git remote rm origin

5-4.以分离的头状态提交

问题

你想以一种超然的头脑状态投入。您需要从配方 5-1 中克隆存储库,并创建修订版x1x2x3,如图 5-8 中的所示。请注意,该图不包含任何指向x1x2x3的分支。不能通过符号引用(比如分支和标签)访问的修订被称为悬空修订

9781430261032_Fig05-08.jpg

图 5-8 。悬挂修订 x1、x2 和 x3

一旦您创建了这三个提交,您想要切换到master分支,并验证x1x2x3修订是否可用。他们迷路了吗?你如何找回x1x2x3,以及你如何最终移除它们?

解决办法

克隆配方 5-1 中创建的存储库及其所有分支:

$ cd git-recipes
$ git clone-with-branches 05-01 05-04
$ cd 05-04

存储库现在看起来像图 5-1 。在图 5-8 中可以看到,修订x1的父版本是i1。你可能还记得配方 3-6 中的命令$ git checkout [REVISION],当与 SHA-1 名字一起使用时,进入分离头状态并重置工作目录。为了使用它,我们需要为i1版本取一个名字。我们可以使用下面的$ git log命令来检查这个名字:

$ git log --oneline --grep=i1 info

第一个参数,--oneline,设置输出的格式。第二个参数--grep=i1充当过滤器。仅包含注释包含i1字符串的修订。第三个参数info设置搜索的起点。因此,将只分析分支info中可用的提交。上述命令的输出将只包括一次提交,例如,

6f4364e i1

还有一种比使用 SHA-1 的简称更简单的方式来称呼i1。可以将i1修订作为由info分支指向的修订的第二个父修订。正式写法为info∼2。因此命令:

$ git checkout info∼2

进入分离磁头状态,并将工作目录重置为提交i1。该命令打印有关进入分离磁头状态的清晰信息。就在命令之后,.git/HEAD指向i1提交。这种状态如图 5-9 中所示。

9781430261032_Fig05-09.jpg

图 5-9 。处于分离状态的存储库

现在,使用我们在配方 3-11 中创建的别名创建x1修订版:

$ git simple-commit x1

x1的 SHA-1 将存储在.git/HEAD中。x1的父代将是i2。仓库的这种状态如图图 5-10 所示。

9781430261032_Fig05-10.jpg

图 5-10 。在分离头状态下创建的新提交 x1

创建另外两个版本x2x3,使用:

$ git simple-commit x2 x3

存储库现在看起来像图 5-11 。

9781430261032_Fig05-11.jpg

图 5-11 。在分离的头状态下创建的新提交 x1、x2 和 x3

在图 5-11 所示的状态下,.git/HEAD文件包含了x3版本的 SHA-1 名称。如果你现在用$ git checkout master命令切换到分支master,那么库的状态将会改变,如图图 5-12 所示。这正是图 5-8 中所示的状态,其中.git/HEAD被明确显示。图中通常会跳过对.git/HEAD的引用,因为相同的信息——在必要时——可以在主图前加上星号。

9781430261032_Fig05-12.jpg

图 5-12 。存储库中的当前分支可以通过加在主分支前面的星号或者包含一个. git/HEAD 引用来显示

当仓库处于图 5-12 所示的状态时,命令$ ls只返回三个文件m1.txtm2.txtm3.txtx1.txtx2.txtx3.txt不可用。

如何检索悬空修订?首先,分析$ git checkout master命令的输出。它包含了您的三个修订版本x1x2x3需要用类似(当然,您的 SHA-1 会不同)的命令检索回来的详细信息:

$ git branch new_branch_name 01f3af0

上面的命令创建了一个名为new_branch_name的分支,该分支将指向提交x3(它的 SHA-1 名称写为01f3af0)。如果由于某种原因上述信息不可用,您总是可以使用 reflog 来恢复丢失的提交。现在你可以用$ git checkout HEAD@{1}返回到x3提交。

你怎样才能最终摆脱悬而未决的修订呢?我们已经在配方 3-12 中练习过了。如果您清除 reflog 并用$ git prune删除数据库,那么所有悬挂的修订最终都会丢失。开始吧。用$ git reflog expire --all --expire=now清除参考日志。这可以通过简单的$ rm .git/logs/*命令来完成。现在,您可以使用以下两个命令之一来检查哪些对象会丢失:

$ git prune --dry-run
$ git fsck --unreachable

输出应该包含三个提交(x1x2x3)、三个文件、 4 、 ( x1.txtx2.txtx3.txt)和三棵树(每个提交一棵树)。

image 提示一棵树是给定目录的快照。每次提交都包含一个工作目录主文件夹的树。

现在,如果您运行$ git prune,所有悬挂的修订将从数据库中移除。然而,不可能再把它们找回来了。

提示你应该把这个秘籍当作另一个警告:不要在超然的状态下工作。使用分支可以很容易地避免它。

它是如何工作的

我们必须解决的第一个问题是如何引用标记为i1的版本。当然,你可以使用$ git log$ git log --oneline命令找到它的全称或简称。您应该已经非常熟悉这些引用提交的方法。然而,在这个配方中,我们使用了祖先引用——这是另一种非常方便的识别修订的方法。

除了第一个版本,每个版本都包含一个父版本。我们可以使用波浪符号()来引用父版本。符号[REVISION]∼表示给定修订的父版本。您可以将此符号与任何方法结合使用来标识修订。你可以写:

7c9bc41684455b2b38749ec9cdeed707c07038b2∼
7c9b∼
master∼
info∼
HEAD∼

符号:

7c9bc41684455b2b38749ec9cdeed707c07038b2∼

7c9bc41684455b2b38749ec9cdeed707c07038b2指向的修订的父修订。下一个引用7c9b∼使用一个简称指向同一个修订版。以类似的方式:

  • master∼表示由master分支指向的提交的父提交
  • info∼指向由info分支指向的提交的父提交
  • HEAD∼指当前版本的父版本
  • 等等

此外,您可以在波浪号后附加一个数字—它允许我们指向更早的祖先。引用[REVISION]∼n指向[REVISION]的第 n 个祖先。在图 5-8 中,doc∼4指向标有m2的版本,master∼2info∼5doc∼5均指向m1。分支infodocmaster的所有祖先参考在图 5-13 中显示。

9781430261032_Fig05-13.jpg

图 5-13 。分支文档、信息和主文档的祖先引用

image 提示引用[REVISION]∼1[REVISION]∼的意思完全相同:给定版本的第一个父版本。

您在配方 5-1 中了解到,您当前的版本可以始终通过符号引用HEAD 来引用。正如您所猜测的那样,HEAD可以与HEAD∼HEAD∼2HEAD∼3等祖先引用结合使用。如果您使用$ git checkout info切换到info分支,那么使用HEAD和波形符将得到图 5-14 中的结果。

9781430261032_Fig05-14.jpg

图 5-14 。对名为 info 的分支使用 HEAD 和祖先引用

你有没有注意到*info分公司名称前面的星号?正如我告诉你的:这是一个更简单的方式来显示你目前在哪一个分支。星号通知您.git/HEAD现在包含了ref: refs/heads/info符号引用。这个想法不是我的——它取自$ git branch命令的输出,现在应该是:


  doc
* info
  master

也许你注意到命令$ git checkout [REVISION]的工作方式因其参数而异。如果参数是阿沙-1 名称,那么命令进入分离头状态。另一方面,如果您传递一个分支名称,该命令将改变您当前的分支。你可以用一个--detached选项强制分离头部状态,例如$ git checkout --detached master

即使您传递了分支所指向的提交的 SHA-1,上述规则也适用。假设$ git log --oneline --decorate打印:

7c9bc41 (HEAD, master) m3

命令:

$ git checkout 7c9bc41

进入分离头状态;即使7c9bc41指向master分支。

5-5.重置并清洁树枝

问题

您正在一个分支中工作—创建、修改、复制、删除和重命名文件。做了一些修改后,你意识到你所有的修改(还没有提交)都是错的。您希望将分支的状态重置回起始点。

解决办法

克隆配方 5-1 中创建的存储库及其所有分支:

$ cd git-recipes
$ git clone-with-branches 05-01 05-05
$ cd 05-05

存储库现在是干净的,并且您在master分支中。使用以下命令修改工作目录:

$ echo foo > bar.txt
$ echo bar > m1.txt
$ cp m1.txt copy.txt
$ rm m2.txt
$ mv m3.txt new-m3.txt

$ git status -sb现在打印:

## master
 M m1.txt
 D m2.txt
 D m3.txt
?? bar.txt
?? copy.txt
?? new-m3.txt

您可以用两个命令放弃所有这些更改:

$ git reset --hard
$ git clean -f

现在存储库是干净的,您可以用$ git status -s命令检查它。所有的修改都丢失了。

它是如何工作的

你已经知道了命令$ git reset --hard。它会将所有跟踪的文件重置为最新版本中保存的状态。但是$ git reset命令不能修改未被跟踪的文件。使用$ git clean -f命令删除所有未被追踪的文件。参数-f作为预防措施;没有它,$ git clean命令只会打印一条消息。这是另一个安全级别,有助于避免数据丢失。但是,如果您想只列出已经删除的文件,而不实际删除它们,请使用$ git clean -n命令。

5-6.无冲突地切换脏存储库中的分支

问题

假设在存储库中工作期间,您修改了一些文件,并希望将它们提交到与当前分支不同的分支中。你必须在一个肮脏的仓库中切换分支。根据您的更改,git 可以允许或禁止该操作。在这个配方中,我们考虑允许切换的情况。

解决办法

克隆配方 5-1 中创建的存储库及其所有分支:

$ cd git-recipes
$ git clone-with-branches 05-01 05-06
$ cd 05-06

存储库现在是干净的,包含三个分支。您目前在master分支。将您的当前分支更改为info,使用:

$ git checkout info

然后使用以下命令修改工作目录:

$ echo bar > bar.txt
$ echo foo > m1.txt
$ rm m2.txt
$ mv m3.txt new-m3.txt

文件bar.txt已创建—这是一个新的未跟踪文件。m1.txt文件被修改——它现在包含foo而不是m1。下一个文件——i1.txt——被删除了。最后一个文件m3.txt被重命名为new-m3.txt。命令$ git status -sb现在打印出来:

## info
 M m1.txt
 D m2.txt
 D m3.txt
?? bar.txt
?? new-m3.txt

以上变化当然不是阶段性的。存储库是脏的,你在info分支。

假设您忘记了您的更改,并且出于某种原因想要签出doc分支。命令:

$ git checkout doc

成功了。所有的更改都被保留,您现在在doc分支中。$ git status -sb的输出现在几乎是一样的——只是分支名称改为了doc:

## doc
 M m1.txt
 D m2.txt
 D m3.txt
?? bar.txt
?? new-m3.txt

您仍然不记得您的更改或您当前的分支,并且您无意中使用$ git add -A进行了所有的更改。

最后,您意识到您的更改仍然没有提交。此外,您希望他们去master分公司。要将您的修改提交到master分支,您必须切换分支并创建修订:

$ git checkout master
$ git snapshot Recipe 5-5 Switching branches in dirty repository without conflicts

您的存储库是干净的,所有的修改都存储在主分支中。

它是如何工作的

在配方 5-5 之前的所有配方中,当一个库干净时,我们切换分支。但是 git 允许切换分支,即使存储库是脏的。默认情况下,所有更改都会被保留,无论它们是否被转移。如果 git 不能保存您的更改,那么它将拒绝切换分支。换句话说,当且仅当未提交的变更不与您要切换到的分支的内容冲突时,才允许切换脏存储库中的分支。记住一个简单的经验法则:如果分支被交换了,这意味着你的修改没有引起任何冲突。但是,有一个小小的例外:在分支切换期间,您可以在没有任何警告的情况下丢失未提交的更改的唯一情况是,当您删除一个不在您要切换到的分支中的文件时。

您可以使用以下命令来验证它:

$ git checkout info
$ rm i1.txt   # removed file i1.txt is not present in doc branch
$ git status -sb

输出将包含关于一个被删除的文件i1.txt的信息。如果你切换到doc分支(这个分支不包含i1.txt文件):

$ git checkout doc

然后,git 不会警告你有冲突。存储库变得干净,关于已删除文件i1.txt的信息丢失。如果您再次切换到info分支,该文件将被恢复。在用$ rm m2.txt命令删除配方中的m2.txt文件的情况下,当我们切换到doc分支时,文件被删除的信息被保留。那是因为文件m2.txt存在于每个分支中。

image 问题你能想象出一个别名,把切换分支只限制在干净状态吗?

5-7.在有冲突的脏存储库中切换分支

问题

您刚刚修改了工作目录,并且想要在不同的分支中提交更改。您必须切换到另一个分支,然后创建一个新的版本。在这个配方中,我们考虑了当你的改变与你想要切换到的分支冲突的情况。如果是这种情况,git 不允许您签出另一个分支。

解决办法

克隆在配方 5-1 中创建的存储库:

$ cd git-recipes
$ git clone-with-branches 05-01 05-07
$ cd 05-07

切换到info分支,用以下命令修改工作目录:

$ git checkout info
$ echo foo > i1.txt

i1.txt文件被修改,列为:

_M i1.txt

$ git status -sb。该文件不在master分支中。如果你现在执行命令$ git checkout master,你会看到警告:

error:
    Your local changes to the following files
    would be overwritten by checkout:
        i1.txt

因此,git 拒绝切换到master的当前分支,因为您会丢失对i1.txt文件的更改。

假设您的修改是重要的——您想要在master分支中保存和提交它们。您可以采取三种不同的方法:

  • 您可以暂时隐藏您的更改,更改一个分支,然后检索这些更改。这就是我们在这个秘籍中要做的。
  • 您可以将您的更改与您在签出期间切换到的分支合并。这种解决方案类似于存储。
  • 最后,您可以在当前(错误的)分支中提交,然后将您的修订移动到适当的分支。该程序将在配方 5-8 中讨论。

隐藏未提交的更改

命令$ git checkout master产生警告:

error:
    Your local changes to the following files
    would be overwritten by checkout:
        i1.txt

设置如下:你在info分支,文件i1.txt被修改,你想在master分支提交。

首先,用以下命令保存工作目录的当前状态:

$ git stash

存储库变得干净了,工作目录反映了info分支中的最新修订,您对i1.txt文件的修改被存储在一个名为 stash 的临时区域中。因为存储库是干净的,所以您可以安全地检查master分支:

$ git checkout master

上面的命令不会以任何方式影响你隐藏的工作。

要检索您隐藏的修改,请执行以下命令:

$ git stash pop

工作目录的状态将被调整,以反映用 stash 命令存储的更改。上面的命令将产生一个关于冲突变更的警告:

CONFLICT (modify/delete):
    i1.txt deleted in Updated upstream and modified in Stashed changes.
    Version Stashed changes of i1.txt left in tree.

处理冲突的一般方法在第九章中介绍。命令$ git status -sb打印:

## master
DU i1.txt

冲突的文件表示为DU。要将i1.txt文件保存在master分支中,您需要暂存一个文件:

$ git add i1.txt

然后创建一个新版本:

$ git commit -m "i1.txt file..."

在签出期间合并变更

要在结帐期间合并您的修改,请使用-m参数:

$ git checkout -m master

上述命令将产生与这三个命令完全相同的结果:

$ git stash
$ git checkout master
$ git stash pop

然而,通过使用$ git stash命令,你可以推迟合并你的隐藏文件的时间。

它是如何工作的

解决方案中使用的$ git stash命令保存暂存区和工作目录的当前状态,并重置工作目录。新的未跟踪文件不受此命令的影响。因此,如果您想要保存所有文件(包括未跟踪的文件),请使用以下两个命令:

$ git add -A
$ git stash

第一个命令将所有修改添加到临时区域。当一起运行时,这些命令保证您的所有修改都被隐藏。

保存更改后,stash 命令会重置工作目录。存储库变得干净,它的工作目录反映了当前分支中最新版本的快照。

要恢复隐藏状态,您应该使用$ git stash pop命令。您可以在每个分支中执行该命令。弹出的隐藏更改将与您当前的分支合并。

Git 允许任意次数地存储未提交的更改——它们存储在堆栈上。可以用$ git stash list命令列出隐藏的状态。它产生类似于下面的输出:

stash@{0}: WIP on info: 0d6501b i3
stash@{1}: WIP on doc: 23d9855 d3
stash@{2}: WIP on master: ae34fcd m4

您还可以将默认消息WIP on XXX更改为更有意义的内容,例如:

$ git stash save A very descriptive information

命令$ git stash相当于$ git stash save WIP on [branch-name]。你已经在配方 3-2 中学习了 WIP 的含义。它代表正在进行的工作。

虽然$ git stash是一个非常方便的工具,可以用来在脏回购中切换分支,但是你也可以使用强制签出。有两个$ git checkou t 命令的开关可以帮助你:-m-f。第一个将您的更改与您切换到的分支合并;第二种方法会丢弃您的修改。

image 提示当然,如果没有冲突的话,可以应用隐藏。因此,在方法 5-6 中,你也可以使用 stashing 来保存和恢复你未提交的更改。

5-8.在错误的分支中提交

问题

您刚刚创建了一个新的修订版,却发现它应该转到一个不同的分支。如果您的修改在切换 branch 时没有冲突,您很容易忘记在提交之前检查适当的分支。在错误的分支中创建修订后,您希望将它移动到正确的目的地。

解决办法

克隆在配方 5-1 中创建的存储库:

$ cd git-recipes
$ git clone-with-branches 05-01 05-08
$ cd 05-08

并修改m1.txt文件

$ echo A new text > m1.txt

让我们假设您打算在master分支中提交这个变更。修改不与doc分支冲突。您可以使用$ git checkout doc毫无问题地检查doc分支。checkout 命令输出一行内容,为您提供有关更改的信息:

M       m1.txt

但是很容易被忽略。

一段时间后,你完全意识不到现在的分支已经不是那个master。您使用以下内容创建了错误的版本:

$ git snapshot Recipe 5-8: a revision in a wrong branch

现在你意识到了你的失败,你想把修订移动到master分支。

您可以分两步完成:

  • 从错误的分支复制修订版以纠正错误。
  • 从错误的分支中删除修订。

在继续下面给出的程序之前,确认新版本出现在doc分支中,而不在master分支中。以下是您需要的命令:

$ git log --oneline doc
$ git log --oneline master

下面是您需要将最新版本从doc分支复制到master分支的过程:

  1. $ git checkout master命令将当前分支更改为master
  2. $ git cherry-pick doc命令将 tip 版本从doc分支复制到当前分支(即master)。

到目前为止,新版本在两个分支中都存在。命令$ git log --oneline -1 master$ git log --oneline -1 doc打印如列表 5-3 所示的输出。尽管您的实际散列会有所不同,但是您应该注意到您提交的两个 SHA-1 名称是不同的。

清单 5-3。 输出的$ git log - oneline -1 为两个分支

# the output of $ git log --oneline -1 master
43336a3 Recipe 5-8: a revision in a wrong branch

# the output of $ git log --oneline -1 doc
7ad4187 Recipe 5-8: a revision in a wrong branch

完成配方,从doc分支删除错误版本:

  1. $ git checkout doc命令将当前分支更改为doc
  2. $ git reset --hard HEAD∼命令删除修订。

$ git l master$ git l doc的输出证明新版本只包含在master分支中。您也可以用一个命令来验证它,显示所有分支:$ git log --oneline --graph --decorate --all

它是如何工作的

该方法使用两个重要的命令:

$ git cherry-pick doc
$ git reset --hard HEAD∼

第一个命令——cherry-pick——复制当前分支中的给定修订。它需要一个指向要复制的修订的参数。如果您将分支的名称传递给该命令,它将复制该分支所指向的修订;也就是树枝的尖端。您也可以使用其他方法来标识修订,例如:

$ git cherry-pick 7c9bc41    # shortened SHA-1 name
$ git cherry-pick info∼      # last but one commit in info branch
$ git cherry-pick info∼3     # branch name and ancestor reference

术语复制修订在这里可能有点误导。您创建的每个修订都有唯一的名称。这条规则没有例外。您只能:

  • 向 git 的数据库添加新的修订版
  • 从 git 的数据库中删除现有的修订

Git 不实现对存储在数据库中的对象的修改。提交的名称是阿沙-1 哈希,它是使用您的名称、当前时间戳、文件快照和父修订版的名称生成的。当我们将一个版本从历史中的一个地方复制到另一个地方时,我们至少改变了其中一个参数:父版本的名称。因此,没有办法为复制的版本保存旧的 SHA-1。因此,由cherry-pick命令实现的操作可以更好地描述为在当前分支之上应用由给定修订引入的变更。该命令创建一个全新的修订,该修订再现了作为参数传递的修订中存储的更改。新修订总是在当前分支的顶部创建,它成为分支中的最新修订。

您已经熟悉了第二个命令。我们在配方 3-5 中使用了它。这次我们使用一个祖先引用HEAD∼。该引用指向当前分支中的倒数第二个版本。

5-9.删除本地分支

问题

假设您已经意识到您的存储库中包含了您将不再需要的陈旧分支。你决定删除它们。

解决办法

克隆在配方 5-1 中创建的存储库,并切换到doc分支:

$ cd git-recipes
$ git clone-with-branches 05-01 05-09
$ cd 05-09
$ git checkout doc

假设您决定masterinfo分支不再重要。删除master分支执行:

$ git branch -d master

接下来尝试用相同的命令删除info分支:

$ git branch -d info

上面的命令无法删除info分支,因为它包含一些可能会丢失的修订。要强制删除,请使用命令:

$ git branch -D info

它是如何工作的

存储库中每两个分支可以被表征为合并不合并。如果a中包含的所有修订也包含在b中,我们说a分支合并b分支。图 5-15 呈现了两个分支ab。分支a合并到分支b中。分支a包含两个修订a1a2——这两个修订都可以从b指向的修订开始并在父修订之后找到。

9781430261032_Fig05-15.jpg

图 5-15 。分支 a 并入分支 b;分支 b 没有合并到分支 a 中

这种关系通常不是反映性的。在图 5-15 的库中分支b 没有合并master中的。如您所见,修订版b1b2b3包含在分支b中,但不包含在分支a中。这种关系反映的唯一情况是当两个分支指向完全相同的版本时,如图图 5-16 所示。

9781430261032_Fig05-16.jpg

图 5-16 。分支 a 并入分支 b,分支 b 并入分支 a

第三种情况是既没有分支a并入分支b也没有分支b并入分支a。然后我们说分支ab?? 已经分叉。这种情况如图图 5-17 所示。

9781430261032_Fig05-17.jpg

图 5-17 。分叉的分支 a 和 b

通过传递给$ git branch命令的--merged--no-merged选项,可以获得当前分支中哪些分支被合并,哪些分支没有被合并的信息;

# print the names of branches merged in current branch
$ git branch --merged

# print the names of branches not merged in current branch
$ git branch --no-merged

现在您已经了解了合并和未合并的分支,您可以很容易地猜到该命令:

$ git branch -d [branch-name]

仅当给定分支与当前分支合并时,才删除该分支。如果要删除的分支没有合并,该命令会显示一条警告:

error: The branch 'branch-name' is not fully merged.

然后离开。带有-D选项的命令:

$ git branch -D [branch-name]

删除分支,即使它没有被合并。

让我提醒你,分支是一个简单的指针——只是以松散或压缩格式存储的阿沙-1。如果创建一个分支,git 会在.git目录中存储一个新指针。当分支被删除时,指针被移除。命令$ git branch -d$ git branch -D不会修改数据库。所有的修订都保持不变。因此,即使您删除了一个分支,也可以使用 reflog 检索它。

image 本秘籍描述了删除本地分支的方法。您可以使用它来删除普通的本地分支和本地跟踪分支。所有远程跟踪分支都可以用$ git remote rm命令删除,正如我们在配方 5-2 中所做的那样。要仅删除一个远程跟踪分支源/doc,您可以使用$ git branch -d -r origin/doc 命令。但是用$ git branch -d -r 删除的远程跟踪分支将在下一个 fetch 命令后重新创建。删除远程分支的命令将在第十章关于存储库同步中讨论。

5-10.使用分支作为备份

问题

您的存储库处于干净状态。你开始在你当前的分支中开发一个新的特性。经过一些修改后,你意识到新的想法是行不通的,想放弃它。您希望取消新特性,并将存储库恢复到该特性启动前的状态。解决这个问题的最好方法是使用分支。

解决办法

克隆配方 5-1 中创建的存储库及其所有分支:

$ cd git-recipes
$ git clone-with-branches 05-01 05-10
$ cd 05-10

存储库现在是干净的,您在master分支中。这是起点。您创建一个名为feature的新分支,它将与当前的master分支完全相同:

$ git checkout -b feature

现在,您在feature分支中创建一些提交:

$ git simple-commit one two three

您的所有修改都已提交,存储库是干净的。

现在您决定在feature分支中实现的整个概念不工作了。你想摆脱它。您可以使用两个命令来实现这一点:

$ git checkout master
$ git branch -D feature

它是如何工作的

这个秘籍不需要新的命令。这个方法只是强调了分支在特定场景中的作用。没有风险,git 允许你测试新的概念,否则会破坏或复杂化你的工作。每个分支都是独立于其他分支的。您在feature分支中提交的修改并不影响master分支,事实上,也不影响任何其他分支。如果您的存储库的某个特定状态对您很重要,并且您想要绝对确定无论您做什么,您总是可以得到它,只需创建一个分支。就这么简单。一旦你理解了这个想法,你会想,没有它,你究竟是怎么工作的。

请记住,如果您不确定某个想法,您不必删除该分支。多余的分支对你的工作没有任何影响。你可以不去管他们。以松散格式编写的分支只占用 41 个字节,所以不会占用很多空间。

最后,如果您删除分支,即使您删除了它,您创建的修订也不会从数据库中删除。原因很简单:reflog 包含禁止修剪这些对象的条目。要完全删除分支及其修订,您必须删除一个分支,清除 reflog,并清理数据库。

5-11.重命名分支

问题

您在图 5-1 中的所示的存储库中工作,并且您想要重命名分支:

  • infoinformation
  • masterdoc

解决办法

克隆在配方 5-1 中创建的存储库:

$ cd git-recipes
$ git clone-with-branches 05-01 05-11
$ cd 05-11

你在master分公司。将info分支机构的名称改为information,名称为:

$ git branch -m info information

最后,用以下代码将master分支的名称改为doc:

$ git branch -M master doc

它是如何工作的

命令$ git branch -m [old-name] [new-name]将名为old-name的分支重命名为new-name。如果具有新名称的分支已经存在,那么您可以使用$ git branch -M [old-name] [new-name]覆盖它。在这种情况下,现有的new-name分支将会丢失。

对了,你注意到了吗?与任何其他分支一样,master分支可以被重命名和删除。

5-12.从不同的分支签出文件

问题

在处理一个有许多分支的项目时,您刚刚意识到在一个分支中,您需要来自另一个分支的一些文件。您希望从当前分支之外的分支中签出文件。

解决办法

使用以下内容克隆在配方 5-1 中创建的存储库:

$ cd git-recipes
$ cp -R 05-01 05-12
$ cd 05-12

如果您愿意,可以使用$ cp命令创建本地克隆。用$ cp创建的克隆将略有不同:它的 reflog 不为空,并且不包含 remote。然而,这些差异在这个秘籍中没有计算在内,因此,你可以用$ cp命令来练习克隆。

我想你目前在master分部。更改m1.txt文件的内容并提交更改:

$ git checkout master
$ echo The new content from Recipe 5-12 > m1.txt
$ git snapshot The new content from Recipe 5-12

master分支中唯一可用的文件是m1.txtm2.txtm3.txt。您想要一个i1.txt文件的副本,它存储在info分支中。您可以通过以下方式实现这一目标:

$ git checkout info -- i1.txt

在签出期间,您可能需要重命名文件。这正是我们修改master分支中的m1.txt文件的原因。现在,master分支中的m1.txt文件的版本与其他分支中的版本不同。如何从doc分支获取m1.txt文件而不丢失存储在工作目录中的版本?您可以使用以下命令在 stdout 上显示存储在doc分支中的m1.txt文件:

$ git show doc:m1.txt

要用不同的名称保存它,请使用流重定向将输出发送到文件:

$ git show doc:m1.txt > m1-from-doc-branch.txt

$ git snapshot Files from other branches命令创建另一个版本,完成配方。

它是如何工作的

命令$ git checkout可以用来只签出一个文件,而不用切换分支。语法如下:

$ git checkout [REVISION] -- [filename]

当然,您可以向它传递任意的修订标识符。您可以使用HEAD、祖先引用、隐藏引用、远程跟踪分支和 reflog,仅举几个例子:

$ git checkout HEAD∼ -- file.txt
$ git checkout stash@{3} -- file.txt
$ git checkout remotes/origin/master -- file.txt
$ git checkout HEAD@{yesterday} -- file.txt

该命令还允许您使用全局通配符:

$ git checkout doc -- d*.txt

两个破折号将文件名与命令选项分开。很多时候,破折号并不重要,比如在$ git checkout doc d1.txt中。它们对于区分路径中的选项是必要的。这里的-f作为一个选项:

$ git checkout doc -f

在下面的例子中,-f被解释为一个路径:

$ git checkout doc -- -f

存储在数据库中的文件的任意版本也可以显示在您的屏幕上,而无需将其签出。这可以通过$ git show命令来完成。您必须确定版本和文件。这是通过冒号分隔的两个参数完成的:

$ git show [REVISION]:[FILENAME]

使用此命令,您可以签出文件,然后用不同的名称保存它:

$ git show [REVISION]:[FILENAME] > [new-filename]

举例来说,这是检出具有您的系统中不允许的名称的文件的方法。如果你在 Linux 中创建了一个名为some*strange*name.txt的文件,那么它将不能在 Windows 中签出这个文件。在 Windows 上工作时,您仍然可以使用新名称签出该文件:

$ git show HEAD:some*strange*name.txt > some-strange-name.txt

我遇到的最令人吃惊的文件名问题的例子是,当我在培训一个使用 Linux 的人时,他使用了带有尾随点的文件名,例如:

$ echo lorem > lorem.

环境是异构的——受训者使用 Windows 和 Linux。使用 Windows 的受训者再也无法获得一个干净的库签出。就在克隆命令之后,存储库变脏了。如果像这样的修订已经是公开的,补救方法是签出文件并更改它们的名称。多亏了$ git show [REVISION]:[FILENAME] > [new-filename],这可以在任何平台上实现。

5-13.切换空存储库中的分支

问题

您已经克隆了一个空存储库,它的当前分支是master。你想换到另一个分行。

解决办法

使用以下命令克隆在配方 05-01 中创建的存储库:

$ cd git-recipes
$ git clone --bare 05-01 05-13
$ cd 05-13

新创建的克隆将是一个裸克隆。用$ git branch -a命令列出所有分支。您将得到以下输出:


  doc
  info
* master

如您所见,在一个空的存储库中,为所有远程分支创建了本地分支。你不必像我们在方法 5-2 中那样手工检查它们。HEAD引用指向master分支——我们知道这一点,这要感谢上面输出中master前面的星号。但是您可以使用两个命令之一来验证它:

$ cat HEAD
$ git symbolic-ref HEAD

要更改当前分支,请执行以下命令:

$ git symbolic-ref HEAD refs/heads/info

此后,命令$ git branch输出:


  doc
* info
  master

因此,我们知道当前分支被改变了。如果你有疑问,你可以用$ git log --oneline -3命令再次检查。它将打印出i3i2i1提交。

它是如何工作的

在非空存储库中切换分支的命令是:

$ git checkout [branch-name]

实际上,它执行两个操作:它更改存储在HEAD中的引用,并签出文件。这两个操作也可以手动执行:

$ git symbolic-ref HEAD refs/heads/branch-name
$ git reset --hard

在一个空的存储库中,您不能使用需要工作目录的命令,比如$ git checkout$ git status。它们根本没有意义。在一个空的存储库中工作,你可以改变存储在HEAD中的引用,但是你不能重置工作目录。

因此,要更改空存储库中的当前分支,请使用以下命令:

$ git symbolic-ref HEAD refs/heads/branch-name

这是一个对符号引用进行操作的低级命令。与一个参数一起使用,如在$ git symbolic-ref HEAD中,该命令作为一个 getter:它输出引用。当使用两个参数时,它充当 setter:作为第一个参数传递的符号引用被设置为作为第二个参数传递的值。

值得记住的是,克隆中的当前分支将是发出$ git clone命令时原始存储库中的当前分支。不管原始存储库是裸的还是非裸的,这都是事实。您可以使用传递给 clone 的附加参数-b对此进行更改。这些命令:

$ git clone -b doc 05-01 05-13-doc-nonbare
$ git clone --bare -b doc 05-01 05-13-doc-bare

会创建新的克隆,使HEAD指向refs/heads/doc

摘要

这一章的内容是我们在接下来的章节中需要的坚实基础——特别是合并和重定基础。概括地说,我们了解到:

  • 创建、删除和重命名分支
  • 切换分支
  • 在当前分支中提交
  • 显示给定分支或所有分支的历史记录——使用$ git log$ gitk命令

这就是 ABC——使用分支的第一步。关于分支的创建和切换的方法为您提供了关于 git 存储分支的方式的详细信息,以及关于哪个分支是当前分支的信息。您知道 git 用来存储分支的两种格式——松散和打包。你也知道如何从一个变成另一个。

另一方面,删除分支机构需要引入两个术语:合并的和未合并的分支机构。这些术语非常重要——它们将在关于合并和重定基础的章节中详细使用。如有疑问,请参考图 5-15 、图 5-16 和图 5-17 寻求帮助。

我们还考虑了克隆中的分支,这给了我们两种分类:

  • 远程分支机构
  • 地方分行

本地分支机构进一步分为三种不同类型:

  • 普通地方分行
  • 本地跟踪分支
  • 远程跟踪分支

您已经学会了创建所有类型的本地分支,并且由于$ git branch命令的-vv参数,您应该很容易地对您的存储库中的所有分支进行分类——即使这种分类仍然有点模糊。记住,命令只能被限制在给定类型的分支上。例如,删除带有$ git branch -d$ git branch -D的分支只适用于本地分支。这些命令不适用于远程分支。

在分支之旅的最开始,在清晰的状态下切换分支是比较容易的。然而,这不是强制性的。一旦您练习了基础,您将需要切换到一个分支,同时保持存储库肮脏。我已经把这个任务分解成两个独立的配方:无冲突和有冲突的情况。未冲突的情况可能导致在错误的分支中提交。这个问题通过挑选和重置命令来解决。分支切换的冲突案例是引入 stashing 的绝佳场所,stashing 是一种存储脏状态的方法。

分支的下一个方面与裸分类和非裸分类相关。在学习切换裸存储库中的分支时,我们将非裸存储库中的相同操作分解为符号引用更新和工作目录重置。它让您对一些命令有了更深入的了解,比如 checkout。它们在工作目录上操作,因此不能在空存储库中执行。

最后,您学习了识别修订的新方法:

  • 符号参考HEAD
  • 分行名称
  • 祖先参考
  • 隐藏引用

祖先引用可以与所有其他方法一起使用,例如:

HEAD∼
master∼2
stash@{4}∼5

记住,不要用脱离的状态来提交你的工作。总是用树枝。

1 是的,这些也是当地的分店。它们是跟踪远程分支的本地分支。 2 分支的名称可以不同——它是远程存储库中当前分支的名称。所以配方 5-1 才以$ git checkout master结束。它保证在克隆之后,存储库 05-02 将包含主分支。 3 文件的路径在您的系统上可以不同。例如,在 CentOS 上,它是/usr/libexec/git-core。 4 文件作为二进制大对象(blobs)存储在 git 数据库中。

六、合并分支

创建和切换分支的命令允许您将项目分成独立的开发线。例如,您可以创建分支来启动应用中的新功能或实现修复。迟早你会完成一个特性或修复,然后很可能,你会想把你的努力融入到开发的主线中。连接不同分支的过程可以使用两种不同的技术来完成。第一个是通过 $ git merge 命令实现的。这是本章的主题。第二个是用 $ git rebase 命令完成的。我们将推迟这个问题,因为它是下一章的主题。

也许你已经注意到术语合并并不是全新的。我们已经在配方 5-7 中使用它来表示将工作目录中的变更与我们切换到的分支连接起来的过程。在那里,合并是在结帐时完成的。结果被留在工作目录中等待提交。在本章中,我们考虑将不同的分支合并在一起。合并分支的结果将被存储和提交,也就是说,在一个分支中。我们将在不包含未提交或未跟踪文件的干净配方中执行合并。在配方之后,存储库将再次处于干净的状态。

在这里,你会充分体会到simple-commitclone-with-branches的别名。他们会带你到更高的抽象层次。我们将使用$ git simple-commit创建一系列提交,而不是使用$ git add$ git commit以及$ echo foo > bar。因此,你将能够专注于合并。别名的工作方式不会发生冲突。当然,这是一个简化的设置,而不是真实的场景。但是我更喜欢把合并和解决冲突分成两个独立的步骤。冲突将在第九章中深入讨论。

这一章反映了我通常教授分支和合并的方式。我坚信学习 git 分支模型的一个关键点是掌握这个操作转换修订图的方式。一旦您理解了如何创建一个具有预定义结构的图,合并就变得简单明了。

本章涵盖了三种合并情况:

  • 快进
  • 两个分叉分支的合并
  • 多个分叉分支的合并

为了实现这些合并,我们需要创建:

  • 有两个分支的存储库,其中一个必须合并到另一个中
  • 具有两个分叉分支的存储库
  • 和具有多个分叉分支的储存库

所有这些存储库都将被创建在单独的配方中,如果有必要的话,这样可以更容易地多次重复每个案例。每当您想要再次执行合并时,只需克隆一个初始存储库并执行合并分支的命令。以这种方式工作,你可以,例如,测试和分析$ git merge命令的不同选项如何影响它的行为。

此外,本章还解释了:

  • 如何撤消合并操作
  • 如何使用--no-ff开关强制将快进实现为典型的合并
  • 如何使用--ff-only开关确保合并是快进
  • 如何使用第 n 个父引用

6-1.在分支机构中实现新功能

问题

您的存储库是干净的,只包含一个名为master的分支。该分支由三个修订组成。该配方的起点如图图 6-1 所示。您希望实现应用的一个新特性。因为您不确定这个解决方案是否是好的,所以您决定使用一个名为feature的新分支。你想要达到的结果如图图 6-2 所示。

9781430261032_Fig06-01.jpg

图 6-1 。制作方法 6-1 的起点

9781430261032_Fig06-02.jpg

图 6-2 。完成配方 6-1

解决办法

启动命令行并创建新的存储库:

$ cd git-recipes
$ git init 06-01
$ cd 06-01

接下来,在master分支中创建三个提交:

$ git simple-commit m1 m2 m3

你的存储库现在看起来像图 6-1 。创建一个名为feature的新分支,包含三个新提交:

$ git checkout -b feature
$ git simple-commit f1 f2 f3

$ git checkout master命令完成检查master分支的配方。现在,存储库看起来像图 6-2 。

它是如何工作的

该配方使用已知的命令。它提供了在应用中实现新功能的最佳方法。每当你开始研究一个新的课题时,就在一个专门的分支中进行。请注意,图 6-2 中显示的库也可以绘制成图 6-3 中的库。修改不必形成一条直线。该配方对分支中的提交数量不敏感。例如,master分支可以包括 100 次提交,而feature分支只能包括一次提交。这个存储库唯一重要的方面是master分支完全合并到了feature分支中。

9781430261032_Fig06-03.jpg

图 6-3 。图 6-2 的替代图形表示

6-2.快进分支

问题

你已经完成了配方 6-1 的工作,并决定将你在feature分支的工作合并到master分支中。你想要创建如图图 6-4 所示的存储库。

9781430261032_Fig06-04.jpg

图 6-4 。将特征分支合并到主分支的结果

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-01 06-02
$ cd 06-02

现在你在master分支。用$ git merge feature命令将feature分支合并到master分支。该命令将打印此操作以快进方式进行的信息。当你完成时,master分支包含了在master分支中创建的所有文件以及来自feature分支的三个文件。$ ls命令将打印六个文件名:f1.txtf2.txtf3.txtm1.txtm2.txtm3.txt

它是如何工作的

这个秘籍展示了合并分支的最简单的例子。有两个分支masterfeature,当前分支为master。一个很重要的事实是,master分支中的所有修改都包含在feature分支中。我们说master分支合并到了feature分支。该关系在配方 5-9 中定义。

在图 6-2 等设置中,master分支发出的命令$ git merge feature只是将master指针移动到feature分支引用的地方。这个操作叫做快进。这是连接两个发展历史的最简单的例子。最终的存储库包含两个指向完全相同的提交的分支。请注意,在快进期间,不会创建新的提交。

请记住,历史将不包含我们使用$ git merge 命令的任何信息。特征分支的名称可以从历史中消失。这可以算是一个缺点。我们将在配方 6-6 中避开这个缺点。

您也可以尝试将master分支合并到feature分支中。那种情况下会发生什么?因为master分支已经合并到feature分支了,所以命令:

$ git checkout feature
$ git merge master

只会打印一条短信:Already up-to-date。存储库不会改变。

6-3.撤消快进

问题

你已经完成了配方 6-2 中呈现的快进操作,你对它不满意。你想撤销合并。换句话说,你想要将图 6-4 中所示的存储库转换回图 6-2 中所示的形式。

解决办法

cp命令克隆存储库06-02:

$ cd git-recipes
$ cp -R 06-02 06-03
$ cd 06-03

撤消合并最简单的解决方案是使用 reflog。由于使用了cp命令来克隆 reflog,所以它不是空的,如果使用了$ git clone命令,它就会是空的。命令$ git reflog打印的结果类似于:

0deae94 HEAD@{0}: merge feature: Fast-forward
757d501 HEAD@{1}: checkout: moving from feature to master
0deae94 HEAD@{2}: checkout: moving from feature to feature
0deae94 HEAD@{3}: clone: from c:/git-recipes/06-01

您可以使用$ git reset --hard HEAD@{1}撤销合并。

它是如何工作的

在合并的情况下,$ git merge命令之后的 reflog 条目HEAD@{1}指向您当前的分支,就像它在合并之前一样。将此作为参数传递给$ git reset --hard,您将撤销合并。即使您的 reflog 为空,您仍然可以使用 SHA-1 名称撤销合并,如在$ git reset --hard 757d501中所示。

6-4.平行发展 分叉分支

问题

您刚刚创建了一个包含应用新特性的分支。存储库看起来像图 6-2 中的所示。这是配方 6-4 的起点。但是,您不确定您在feature分支的工作是否已经完成。您不确定它是否已准备好进行合并。同时,你想继续在master分公司工作。你想切换到master分支并创建一些新的版本。这个配方之后的库应该看起来像图 6-5 。

9781430261032_Fig06-05.jpg

图 6-5 。配方 6-4 后获得的储存库

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-01 06-04
$ cd 06-04

并创建两个新的修订版本:

$ git simple-commit m4 m5

它是如何工作的

这个配方强调了这样一个事实,即不同分支中的工作可以并行地继续。在这种情况下,我们说两个分支masterfeature 分叉。提交m4f1被创建的确切时间并不重要,插图通常会在同一级别显示它们,如图 6-5 中的所示。实际上,它们总是在不同的时刻创建的。因此,可视化修订图的命令,如$ git --oneline --graph --decorate --all$ gitk --all&,总是在不同的级别显示它们,如图图 6-6 。修订版m4m5是后来创建的,因此它们将出现在修订版f1f2f3之上。

9781430261032_Fig06-06.jpg

图 6-6 。提交 m4 和 m5 是在提交 f1、f2、f3 之后创建的,因此它们被绘制在上面

$ gitk --all &命令从图 6-6 中的图 6-7 中呈现仓库。

9781430261032_Fig06-07.jpg

图 6-7 。使用 gitk 应用可视化配方 6-4 的储存库

请记住,在图 6-5 所示的存储库中进行快速合并是不可能的。您可以使用$ git merge命令的附加参数--ff-only进行检查。这个参数在 git 上设置了一个附加条件:执行合并,但是只有在快速前进的情况下。命令:

$ git merge --ff-only feature

master分支中执行的可以解释为下面用伪代码写的条件语句:

if (the merge of feature into master is a fast-forward) {
    $ git merge feature
}

图 6-5 中所示的库中执行的命令$ git merge --ff-only feature将无法产生输出:

fatal: Not possible to fast-forward, aborting.

使用--ff-only开关,您可以确保您的合并操作总是以快进方式进行。如果合并 不能快进完成,则中止。为了快速执行分叉分支的合并,我们将使用$ git rebase命令。这将是下一章的主题。

6-5.合并 分叉的分支

问题

你想要合并如图 6-5 所示的分支。f eature分支将并入master分支。您想要获取的存储库在图 6-8 中显示。图 6-8 强调了m4m5f1f2f3修订的创建顺序。在这个配方中,这个顺序并不重要,因此图 6-8 也可以像图 6-9 那样绘制。

9781430261032_Fig06-08.jpg

图 6-8 。将分叉的分支合并后得到的知识库

9781430261032_Fig06-09.jpg

图 6-9 。在图 6-8 中显示了储存库的可选视觉表示

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 06-05
$ cd 06-05

并用$ git merge feature命令合并分支。

它是如何工作的

在不可能快进的情况下,$ git merge命令创建一个额外的修订,称为合并提交 。此提交与您到目前为止创建的提交不同,因为它包含多个父提交。它连接两个或多个不同的版本。这使我们有机会将每个提交分类为非合并提交合并提交。一个合并提交是一个有两个或更多父提交的提交。非合并提交是只有一个父提交的提交。显然,在这个配方中创建的提交有两个父提交,因此它是一个合并提交。

当使用$ git log$ gitk检查历史时,您可以过滤掉这两种类型的提交。命令:

$ git log --oneline --merges

输出只合并提交,而

$ git log --oneline --no-merges

仅打印非合并提交。您还可以通过以下方式设置预期的最小和最大父节点数:

$ git log --oneline --max-parents=X --min-parents=Y

其中XY是任意正整数。

Git 支持引用,这些引用允许它使用脱字符号(^)选取合并提交的任何父元素。引用[REVISION]^[n]指向用[REVISION]标识的提交的第 n 个父提交。对于图 6-9 中的库,master¹指向版本m5master²指向修订f3,如图图 6-10 所示。我将它们称为第 n 个父引用。

9781430261032_Fig06-10.jpg

图 6-10 。第 n 个父引用 master¹ 和 master²

记住参考号[REVISION]∼[REVISION]∼1[REVISION]^[REVISION]¹是等价的。这是因为1是默认值,并且总是引用合并提交中的第一个父节点。

如果你想放心的使用$ git merge命令,你要记住当前分支是你合并到的分支,传递给$ git merge 命令的分支是要合并的分支。您合并到的分支(master分支)的末端成为合并提交的第一个父级,您合并到的分支(feature分支)的末端成为第二个父级。您合并的分支不会改变,它仍然指向与命令之前相同的修订。您合并到的分支会收到一个新的 commit,其注释类似于:

Merge branch 'X'

其中X是您合并的分支的名称(在我们的菜谱中是feature)。您可以记住上面的规则,记住,当在master分支上时,$ git log --oneline -1命令会打印:

6fb2 Merge branch 'feature'

正如您所猜测的,工作目录现在包含了来自两个分支的所有文件。命令$ ls输出文件:f1.txtf2.txtf3.txtm1.txtm2.txtm3.txtm4.txtm5.txt

可以完全按照配方 6-3 中的方法撤消合并。只是这次你不仅可以使用 reflog 和 SHA-1,还可以使用祖先和第 n 个父引用。假设你在master分支,以下两个命令将撤销本配方中讨论的合并:

$ git reset --hard master^
$ git reset --hard master∼

但是,如果你在 Windows 命令行工作,事情就复杂了。因为插入符号是一个特殊的字符,你必须以特殊的方式使用它。插入符号被 Windows shell 分析器用作转义字符。在 Linux shells 中,这个角色通常分配给反斜杠()字符。如果你想在 Windows 命令行中使用插入符号,你必须键入两次(^^).)此外,因为在 Windows 上 git 子命令是通过间接 shell 调用触发的,所以转义被执行两次。因此,如果您想在 Windows 命令行中使用引用master²,您必须键入四个 caretmaster^^^²。当然,如果您在 bash shell 中工作,这并不适用。

最有趣的情况是,当您想在嵌入字符串的 SQL 语句的正则表达式中使用反斜杠(\))时,比如:

$query = "SELECT * FROM paradox WHEARE content REGEXP '\\\\\\\\'";

所有三种语言—RegExp、SQL 和 PHP—都使用相同的转义字符,即反斜杠(\))。因此,一个反斜杠被编码为八个反斜杠!

6-6.避免了 的快进合并

问题

你的存储库看起来像图 6-2 。你想将分支feature合并到master中,这样历史看起来就像图 6-11 。您希望来自feature分支的所有修订被可视化地分组在一个灯泡中。

9781430261032_Fig06-11.jpg

图 6-11 。使用- no-ff 选项将特征分支合并到主分支的结果

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-01 06-06
$ cd 06-06

然后将feature分支与:

$ git merge --no-ff feature

它是如何工作的

$ git merge命令的选项--no-ff 改变了强制创建合并提交的默认行为,即使合并可以作为快进来执行。通过这种方式,您保留了提交f1f2f3相互关联的信息。它们都处理开发的同一个方面,共同构成一个完整的作品。如果出于某种原因,您需要将整个分支恢复或复制到历史中的其他地方,像这样组织提交将使操作更容易。

6-7.发散 多个分支

问题

您想要创建如图 6-12 所示的存储库。它包含五个分叉的分支。请注意,创建修订的实际顺序并不重要。该图显示了在完全相同的时刻创建的来自不同分支的修订,我们知道这是不正确的。您是单独工作的,并且您的所有修订都是按顺序创建的,一次一个,而不是并行的。但是这个图像的作用是强调,不管你提交的顺序是什么,配方 6-7 中描述的过程都工作得很好。

9781430261032_Fig06-12.jpg

图 6-12 。具有五个不同分支的存储库

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-01 06-07
$ cd 06-07

并创建四个新分支:

$ git branch a
$ git branch b
$ git branch c
$ git branch d

然后一个接一个地切换到每个分支并创建两个新的提交。这些命令如清单 6-1 中的所示。

清单 6-1。 创建分叉分支的命令如图图 6-9

$ git checkout master
$ git simple-commit m4 m5

$ git checkout a
$ git simple-commit a1 a2

$ git checkout b
$ git simple-commit b1 b2

$ git checkout c
$ git simple-commit c1 c2

$ git checkout d
$ git simple-commit d1 d2

$ git checkout master

它是如何工作的

例如,图 6-12 所示的库结构可以在你的产品发布新版本后获得。我使用像这样构造的存储库来给框架添加新的独立特性。一旦一个框架的稳定特性发布了,我就实现多样化的特性,比如授权、数据库连接、开发环境设置,以及不同分支中的各种扩展。通过这种方式,我可以创建满足特定需求的框架发行版。发行版可以包含在分支中实现的任何特性。每个功能都可以根据您的需要打开或关闭。这个发行版是用 merge 命令创建的:您合并的分支将出现在最终的发行版中。

用清单 6-1 中的命令创建的储存库表单图 6-12 将由$ git log --oneline --graph --all命令显示,如图 6-13 中的所示。

9781430261032_Fig06-13.jpg

图 6-13 。由$ git log - oneline - graph - all 命令绘制的来自图 6-12 的存储库

6-8.合并 多个分支

问题

在图 6-12 中所示的存储库中工作,您想要将四个分支abcd合并成master分支。您的目标是将图 6-12 所示的存储库转换成图 6-14 所示的存储库。

9781430261032_Fig06-14.jpg

图 6-14 。将分支 a、b、c 和 d 合并到主分支的结果

解决办法

用分支克隆配方 6-7 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-07 06-08
$ cd 06-08

并将四个分支合并成master分支执行:

$ git merge a b c d

它是如何工作的

命令$ git merge a b c dmaster中创建新的提交,标记为Merge branches 'a''b''c''d'。这将是在当前分支中创建的合并提交。它的五个父级将可以通过以下第 n 个父级引用来访问:

master¹
master²
master³
master⁴
master⁵

第一个父进程master¹指向与master分支在图 6-14 中指向的提交相同的提交。第二个父节点master²指向由a分支在图 6-14 中指向的提交。第三个父节点master³,通过b分支指向图 6-14 中表示的提交。诸如此类。正如您所猜测的,父节点的顺序取决于$ git merge a b c d命令传递给分支的顺序。

许多分支的合并可以用与方案 6-3 或方案 6-5 中相同的方式撤销。您可以使用 reflog、SHA-1 名称或第 n 个祖先引用:

$ git reset --hard master^

由带--graph开关的$ git log命令打印的图 6-14 中所示的库的可视化表示在图 6-15 中呈现。

9781430261032_Fig06-15.jpg

图 6-15 。由$ git log with - graph switch 命令绘制的配方 6-8 中的存储库

很少需要合并两个以上的分支。事实上,在很多流行的项目中,比如 jQuery、Twitter Bootstrap、Ruby on Rails、Symfony,都没有超过两个父级的提交。我所知道的同时合并两个以上分支的项目只有 git 和 Linux。令人惊讶的是,Linux 包含有 32 个父级的提交!您可以使用$ git log命令的--min-parents=n选项进行验证,例如:

$ git log --oneline --min-parents=32

另一个补充参数--max-parent=n设置由$ git log打印的提交的最大父项数量的要求。

当然,合并众多分支的能力并不重要。操作:

$ git merge a b c d

可以作为四个不同的合并来执行,每个合并只涉及两个分支:

$ git merge a
$ git merge b
$ git merge c
$ git merge d

上面唯一的缺点是历史将包含四个合并提交,而不是一个。

摘要

现在,你已经知道了$ git merge,你可以开始充分欣赏 git 分支模型。使用$ git merge处理合并到的分支和合并到分支。您合并到的分支是您当前的分支。您合并的分支是传递给$ git merge命令的分支。如果命令$ git branch打印:


  bar
* foo

那么对于$ git merge bar,我们有:

  • 是您合并到的分支
  • 是你合并的分支

一般来说,$ git merge命令执行两种操作之一:要么是快进,要么是合并

快进,也用 FF 表示,是通过在图中向前移动分支来更新分支的过程。当您要合并的分支合并到已经合并的分支中时,就会发生这种情况。在这种情况下,不会创建新的提交。该命令的唯一结果是您合并的分支(您的当前分支)的更新的 SHA-1 散列。如果你的分支出现分歧,快进是不可能的。

在另一种情况下,当 FF 不可能时,$ git merge命令通过创建新的提交来执行merge。这个新提交非常特殊:它至少有两个父提交。毫不奇怪,它被称为合并提交

本章之后,你应该能够合并任意数量的分支,如果你对结果不满意,可以撤销操作。您知道如何强制执行非快进合并,即使在默认情况下,操作将作为快进执行。最后,您知道如何使用--ff-only开关来避免执行非快进合并。

当我们深入工作流时,合并的所有这些方面都很重要。

七、分支变基

你可以用$ git merge$ git rebase命令连接两个不同的开发历史。在这一章中,我们将讨论重定基。合并在前一章已经讨论过了。

一般来说,重置基础是一种将分歧分支转换成线性历史的方法。您可以将它看作是一个自动的挑选操作,将一系列提交从图中的一个位置移动到另一个位置。当您开始与同一个存储库中的其他开发人员合作时,使用 rebasing 的优势将变得显而易见。通过使用 rebasing 命令,你将能够产生一个清晰的项目线性历史。

我们将从对重定基础的分叉分支的深入解释开始。我们将使用三种不同的方法来执行此操作:

  • 使用$ git rebase命令
  • 使用$ git format-patch$ git am命令
  • 使用$ git cherry-pick命令

这将为你提供一个坚实的背景和对重定基础工作方式的深刻理解。

然后,我们将继续单独使用$ git rebase来连接分离的分支(即,没有$ git merge命令)。在那里,您将学习如何通过重定基础来快进分叉的分支。

接下来,我们将讨论仅移动新分支的一部分的问题。这可以通过$ git rebase命令的--onto参数来实现,它适用于您的存储库包含三个或更多分支的场景。这个设置也将作为讨论提交范围的起点。在继续进行$ git rebase --onto操作之前,我们将讨论两点和三点操作符。

本章的最后一个秘籍将涉及灯泡。我们在配方 6-6 中学习了如何创建它们。如果你想保留合并的话,改变基础要困难得多。

7-1.改变分叉分支的基础

问题

您在一个拥有两个名为masterfeature的分支的存储库中工作。分支分叉了,你的存储库现在看起来像图 7-1(a) 。你想以这样的方式改造 ?? 分公司:

  • 历史是线性的(这意味着分支不再分叉)。
  • master分支合并为feature分支。
  • feature分支中进行的所有提交都在master分支的最顶端。

你想要实现的储存库在图 7-1(b) 中给出。

9781430261032_Fig07-01.jpg

图 7-1 。在重设基础之前(a)和重设基础之后 (b)来自配方 7-1 的储存库

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 07-01
$ cd 07-01

然后按照以下步骤操作:

  1. $ git checkout feature命令检查feature分支。
  2. $ git rebase master命令将feature分支复位到master分支上。
  3. $ git checkout master命令检查master分支。

它是如何工作的

$ git rebase master执行的转换可以被描述为在另一个分支的顶部应用由来自当前分支的提交引入的改变。这在图 7-2 中进行了描述。

9781430261032_Fig07-02.jpg

图 7-2 。将提交从当前分支“移动”到另一个分支之上

正如你从配方 5-8 中已经知道的,没有办法将版本从一个地方移动到另一个地方。你所能做的就是创建一个新的修订版,它将有相同的注释,并且将相同的变化引入到你的文件中。这就是为什么图 7-1(b) 和图 7-2 中的修改包含了素数。它强调了这样一个事实,即这些是用不同的 SHA-1 名称进行的新修订。

最初的修订版f1f2f3会发生什么?没什么?它们完好无损。更准确地说,可以如图 7-3 中的所示描述重置基础。最初的修订f1f2f3不再被任何分支引用——它们变成了悬空的修订。但是它们在数据库中保持不变。至少只要不使 reflog 过期并清理数据库。

9781430261032_Fig07-03.jpg

图 7-3 。将修订 f1、f2 和 f3 的副本更精确地重新基础化到另一个分支中—原始修订保持不变

图 7-3 中的包含了如何从重置基础中恢复的提示。要撤销重置基础,你所要做的就是改变feature分支所指向的版本。你应该记得上一章的内容,分支只是指针。你可以把它们当成贴纸:任何东西都可以从一个地方撕下来,然后再贴到另一个地方。此操作不会修改数据库,所有修订保持不变。如果你修改了feature分支,使其再次指向f3修订版,那么重定基础将被撤销。

让我们找到最初的版本f3。和往常一样,您可以使用$ git reflog命令。但这一次,它的输出可能会产生误导。用$ git log命令来探索 reflog 可能会更容易。我们希望获得所有提交的列表,这些提交:

  • 包含在 reflog 中
  • 有包含f3字符串的注释

清单 7-1 中给出了适当的命令。用--pretty参数定义的格式打印缩短的散列(%h占位符)、注释(%s占位符)和提交日期(%cd占位符)。每个提交都可以在 reflog 中出现多次。实际上,每次签出都会在 reflog 中存储一个对提交的新引用。多亏了管道化的sortuniq命令,清单 7-1 中的命令产生的输出将包含每个提交一次。

清单 7-1。 该命令列出所有引用日志提交,注释中包含 f3

$ git log --walk-reflogs --grep=f3 --pretty="%h %s %cd" | sort | uniq

当您找到正确的提交时,您可以使用$ git reset --hard [SHA-1]命令更改feature分支。如果您的存储库是干净的,那么这个命令可以被看作是将您当前的分支移动到任意版本的一种方式。使用两个单独的命令也可以达到相同的效果:

$ git branch -D feature
$ git checkout -b feature [SHA-1]

第一条命令删除feature分支;第二个创建一个新的feature分支,指向期望的修订。您可以将上述两个命令合并为一个:

$ git checkout -B feature [SHA-1]

开关-b是一个安全的开关:只有当存储库还没有包含这样的分支时,它才会创建一个分支。如果分支存在,$ git checkout -b失败。开关-B强制$ git checkout命令覆盖现有分支。

好的,我们知道重定基础是如何转换修订图的结构的。但是文件会怎么样呢?从这个意义上说,重定基础的结果与合并产生的结果完全相同。两个命令:

# current branch is feature
$ git rebase master
$ git merge master

导致工作目录的内容完全相同。工作目录包含两个分支的文件:featuremaster

一般来说,图 7-2 和 7-3 中描述的换基在两个分支上操作;因此,该命令需要两个参数:

$ git rebase a b

如果跳过第二个参数,将使用HEAD。因此,命令:

$ git rebase a
$ git rebase a HEAD

是等价的。要将feature分支重定位到master分支上,就像在这个配方中一样,您可以:

  • 将当前分支更改为feature,并使用一个参数进行重置,如下所示:

    $ git checkout feature
    $ git rebase master
    
  • 使用两个参数来重新设定基数——那么你当前的分支就不重要了:

    $ git rebase master feature
    

无论哪种情况,feature都是成功重置基础后的当前分支。

7-2.手动重置分叉分支

问题

为了更深入地了解重设基础,你想执行与配方 7-1 相同的转换,而不使用$ git rebase命令。在这个秘籍中,你想把重建基础分成两个操作:创建补丁和应用补丁。通过这种方式,补丁可以由一个开发人员创建,通过电子邮件发送,然后由另一个开发人员应用。

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 07-02
$ cd 07-02

然后按照以下步骤操作:

  1. $ git checkout feature 结账feature分支

  2. $ git format-patch --ignore-if-in-upstream master
    

    f1f2f3版本生成补丁

  3. 进入分离头状态,其中HEAD指向与master分支相同的版本。您将通过执行$ git checkout git rev-parse master`` 来实现

  4. 使用$ git am *.patch 应用补丁

  5. 使用$ git checkout -B featurefeature分支移动到当前版本

  6. $ rm *.patch删除补丁

它是如何工作的

feature分支包含三个提交f1f2f3,它们不包含在master分支中。您可以通过以下方式进行检查:

$ git log --oneline master..feature

输出将显示三个提交:

0deae94 f3
c1cab03 f2
3df8f34 f1

参数master..feature 指定一组提交。您可以将其视为减法运算符:

feature - master

或者更准确地说:

revisions included in feature - revisions included in master

这就是您如何发现在 rebase 期间哪些提交已经或将要被转移到其他位置。命令:

$ git log --oneline master..feature

将打印将被移动的提交。当在重置基础后发出时,它将打印被移动的提交。

现在我们想为这三次提交生成补丁。修补程序是一个文本文件,它精确地描述了要在项目文件中引入的变更集。当在feature分支发出时,命令:

$ git format-patch --ignore-if-in-upstream master

生成三个名为0001-f1.patch0002-f2.patch0003-f3.patch的文本文件。第一个文件是修订版f1的补丁。第二个文件是修订版f2的补丁。第三个文件是修订版f3的补丁。参数- ignore-if-in-upstream保证只生成还没有在master分支中合并的提交补丁。当您想要为许多分支多次生成补丁时,此选项变得很有必要。

所有生成的文件都是新的和未被跟踪的,正如$ git status -s命令所证明的:

?? 0001-f1.patch
?? 0002-f2.patch
?? 0003-f3.patch

在配方 5-6 中,你学到了新的未跟踪文件不会影响签出命令。您知道当前分支可以切换,未跟踪的文件将保持不变。这正是我们想要做的,因为我们想要将补丁应用到master分支。然而,因为master分支应该在结果存储库中保持不变,所以我们将使用分离的 HEAD 状态。

这个秘籍给了你更多关于分离头部状态的详细实用的知识。我警告过你要避免它,所以你可能会问为什么要在 git 中引入它。分离的 HEAD 状态背后的原因是一些命令,比如 rebase,改变了修订的图形。为了保留原始分支,需要在分离的头状态下执行这些操作。如果出现问题,您可以很容易地返回到操作之前的状态,因为原来的分支没有改变。

好的,现在我们需要进入一个分离的 HEAD 状态,其中HEAD指向与master分支相同的修订。如何产生master分公司所指修订的 SHA-1 名称?您可以使用$ git rev-parse命令来完成:

$ git rev-parse master

上面的命令将一个符号引用,比如HEADHEAD∼featureinfo²,转换成阿沙-1 名称。使用反勾运算符将$ git rev-parse的结果作为参数传递给 checkout,您将进入所需的分离头部状态。完整的命令如清单 7-2 中的所示。您的存储库现在看起来如图 7-4 所示。

清单 7-2。

$ git checkout `git rev-parse master`

9781430261032_Fig07-04.jpg

图 7-4 。该库来自图 7-1(a)后的命令来自清单 7-2

当存储库看起来像图 7-4 时,我们应用存储在带有.patch后缀的文件中的补丁:

$ git am *.patch

上面的命令使用HEAD指针作为父指针来再现提交f1f2f3。存储库现在看起来像图 7-5 。

9781430261032_Fig07-05.jpg

图 7-5 。从图 7-4 中的存储库在应用了$ git am *补丁后。补丁命令

最后一步是现在改变feature分支。我们希望它指向我们当前的修订。为此我们可以使用$ git checkout命令。然而,命令$ git checkout -b feature将不起作用。原因相当明显:feature分支已经存在。不过,我们可以使用-B开关强制结帐:

$ git checkout -B feature

更新后的知识库如图 7-6 所示。提交f1f2f3在数据库中仍然作为悬空修订可用,但是它们没有显示在图中。

9781430261032_Fig07-06.jpg

图 7-6 。来自的储存库图 7-5 后的$ git checkout -B 特性

我们不再需要补丁了。用$ rm *.patch 命令删除它们。

使用樱桃采摘 rebase

您可以使用$ git cherry-pick命令获得类似的结果

  1. 进入分离头状态:$ git checkout git rev-parse master``
  2. $ git cherry-pick feature∼2重新应用HEAD中的修订f1
  3. $ git cherry-pick feature∼1重新应用HEAD中的修订f2
  4. $ git cherry-pick feature重新应用HEAD中的修订f3
  5. 使用$ git checkout -B featurefeature分支移动到当前版本

上述解决方案的主要缺点是,这里您必须知道您想要重新应用哪些修订。命令$ git format-patch将卸下你肩上的重担!另外$ git cherry-pick不会创建补丁。要通过电子邮件发送补丁,你必须用$ git format-patch命令生成补丁。

image 提示配方 7-2 背后的主要原因是为了让你更深入地了解$ git rebase是如何工作的。不要使用$ git format-patch$ git am$ git cherry-pick来重置你的分支,除非你想通过电子邮件把补丁发给其他人。请使用$ git rebase命令。配方 7-2 中的分析将有助于你理解重置基础的内部原理。根据您的工作流程,每次您想要发布您的作品时,都可能需要重新设定基准。

7-3.将分歧的分支连接成线性历史

问题

你在一个拥有两个分支masterfeature的存储库中工作,如图图 7-7(a) 所示。您希望将feature分支合并到master分支中,这样得到的历史是线性的,也就是说,它不包含合并提交。该配方的起点如图 7-7(a) 中的所示。您想要获取的存储库在图 7-7(b) 中显示。

9781430261032_Fig07-07.jpg

图 7-7 。起点和结果将分歧分支加入线性历史

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 07-03
$ cd 07-03

然后按照以下步骤操作:

  1. $ git rebase master featurefeature分支重置到master分支上
  2. $ git rebase feature master?? 将master分支重置到feature分支上

它是如何工作的

我们从图 7-7(a) 所示的储存库开始。该配方的第一步执行配方 7-1 中描述的操作。在$ git rebase master feature之后,存储库将看起来像图 7-1(b) 。我们需要将master分支快进到feature分支。这正是第二个命令$ git rebase feature master的目的。

快进可以用$ git merge或者$ git rebase来完成。下面是用$ git merge命令快进master分支的命令:

$ git checkout master
$ git merge feature

这是与$ git rebase命令相同的命令:

$ git rebase feature master

在配方 6-2 中讨论了使用$ git merge快进。

7-4.分叉的三个分支

问题

你的存储库包含两个分叉的分支masterfeature,如图图 7-8(a) 所示。首先,你想在你的feature分部的最新修订版的基础上,研究一些新的想法。您需要创建一个名为brave-idea的新分支,并将您的更改提交为修订版b1b2。接下来您想要切换到feature分支并创建三个新的修订f4f5f6。你想要实现的库如图图 7-8(b) 所示。

9781430261032_Fig07-08.jpg

图 7-8 。通过对存储库(a)应用配方 7-4,您将得到存储库(b)

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 07-04
$ cd 07-04

然后按照以下步骤操作:

  1. $ git checkout -b brave-idea feature创建并检查brave-idea分支
  2. $ git simple-commit b1 b2brave-idea分支中创建两个版本
  3. $ git checkout feature检查feature分支
  4. $ git simple-commit f4 f5 f6feature分支中创建三个版本
  5. $ git checkout master将当前分支更改为master

它是如何工作的

这个秘籍解释了如何创建许多不同的分支。我们只使用众所周知的命令来实现这一点:clonecheckoutsimple-commit。这就是如何轻松生成具有给定结构的存储库。如果您想分析 git 命令及其对图形结构的影响,这个功能非常有用。

请记住,当您使用带有--graph选项的$ git log命令时,您得到的图形可能会稍有不同。从图 7-8(b) 中得到的一个库的$ git log --oneline --graph --decorate --all 结果如图图 7-9 所示。

9781430261032_Fig07-09.jpg

图 7-9 。该库来自图 7-8(b) 由$ git 绘制的 log-one line-graph-decoration-all

一旦你创建了如图 7-8 所示的存储库,许多关于修订图的问题可能会出现。例如以下内容:

  • 如何找到分支ab的共同祖先?
  • 如何寻找任意支数的共同祖先?
  • 如何找到两个分支的差异a - b,即包含在分支a中但不包含在分支b中的修订?
  • 如何找到两个分支的对称差异a ∆ b,即包含在ab中但不同时包含在两者中的修订?
  • 如何查找包含在abc分支中,不包含在def分支中的修订?

两个分支的共同祖先是包含在两个分支中的最新修订。对于分支featurebrave-idea,它是f3。对于masterfeaturem3。您可以使用以下方法找到共同祖先:

$ git merge-base feature brave-idea

如果你想得到两个以上分支的共同祖先,使用--octopus参数。命令:

$ git merge-base --octopus feature brave-idea master

打印m3提交的 SHA-1。

提交的范围已经在配方 7-2 中讨论过了。特殊运算符..被解释为分支的差异。命令:

$ git log --oneline master..brave-idea

打印提交的b2b1f3f2f1,同时:

$ git log feature..master

输出版本m4m5

由两个分支引入的新提交集由...操作符解析。这是分支的对称差异。的输出:

$ git log feature...brave-idea

f6f5f4b2b1组成。

指定包含和排除修订的更详细的方法是使用--not操作符。命令:

$ git log a b c --not d --not e --not f

打印包含在abc中,不包含在def中的修订。这也可以写成:

$ git log a b c ^d ^e ^f

使用上面的语法,你可以用清单 7-3 中的命令列出masterfeaturebrave-idea分支中引入的新修订。该命令输出修订:

  • f6f5f4——在feature分支引入的提交
  • b2b1——在brave-idea分行引入的提交
  • m5m4——在master分行引入的提交

清单 7-3。

$ git log --oneline
    master feature brave-idea
    ^`git merge-base master feature`
    ^`git merge-base feature brave-idea`

我们如何达到上述结果?我们包括所有三个分支机构:

master feature brave-idea

然后排除通过masterfeature分支的共同祖先可用的提交(它是修订版 m3):

^`git merge-base master feature`

并且排除通过featurebrave-idea分支的共同祖先可用的提交(它是修订版f3):

^`git merge-base feature brave-idea`

使用用反勾运算符定义的 shell 子命令,我们不必复制/粘贴嵌入的m3f3的 SHA-1 名称。

在研究修订图时,您可能还会发现生成带有给定注释的提交的 SHA-1 名称的命令很有用:

$ git log --format="%h" --grep=XXX --all

上述命令考虑了所有分支(--all选项),并搜索包含XXX字符串的修订。多亏了--format参数,输出只包含缩短的 SHA-1 名字。

7-5.部分重置和

问题

您决定在图 7-10(a) 所示的库中的brave-idea分支中引入的代码现在可以与他人共享了。因此,您想要将修订版b1b2移动到master分支。你想要实现的库如图图 7-10(b) 所示。如果在你制作brave-idea的过程中,你最初的想法不断发展,变得足够大,可以作为独立的特征来对待,这种情况就会发生。

9781430261032_Fig07-10.jpg

图 7-10 。通过对存储库(a)应用配方 7-5,您将得到存储库(b)

解决办法

用分支克隆配方 7-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 07-04 07-05
$ cd 07-05

然后用清单 7-4 中所示的命令将brave-idea分支重置到master分支上。

清单 7-4。 将图 7-10(a) 中所示的库转换成图 7-10(b) 中所示状态的命令

$ git rebase --onto master feature brave-idea

它是如何工作的

命令$ git rebase --onto作用于三个分支:

$ git rebase --onto a b c

第一个分支是我们将重新应用补丁的分支。另外两个分支定义了要重新应用的补丁集。它将是由双点运算符b..c定义的集合。换句话说,该命令获取包含在c中但不包含在b中的修订,并在a上重新应用它们。如果操作成功,则c被移动并指向结果提交。

清单 7-4 中的命令可以在任何分支中执行。结果总是一样的:提交b1b2将作为b1'b2'被重新应用到master分支之上。操作后的分支c将是你当前的分支。

如果您忽略了最后一个参数,那么您当前的分支将被重置。以下命令是等效的:

$ git rebase --onto foo bar
$ git rebase --onto foo bar HEAD

我们可以说清单 7-4 中的命令相当于两个命令:

$ git checkout brave-idea
$ git rebase --onto master feature

7-6.为分叉分支创建灯泡

问题

你的知识库看起来像图 7-11 中的(a)。您想要将feature分支中引入的变更合并回master分支,以这样的方式,重新应用的修订f1'f2'f3'在来自master分支的修订之上形成一个灯泡。您希望实现的存储库如图 7-11(b) 所示。

9781430261032_Fig07-11.jpg

图 7-11 。将配方 7-6 应用于图 6-5 中的存储库后,您将获得该存储库

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 07-06
$ cd 07-06

然后按照以下步骤操作:

  1. $ git rebase master featurefeature分支重置到master分支上
  2. $ git checkout master切换到master分支
  3. $ git merge --no-ff featurefeature分支合并成master分支

它是如何工作的

配方 7-6 包括两个步骤:

  • 首先,我们使用配方 7-1 来转换存储库,如图 7-1 所示。
  • 接下来,我们使用配方 6-6 进行合并,形成一个灯泡。

7-7.在子分支 中创建灯泡

问题

你的知识库看起来像图 7-12(a) 。你想将brave-idea分支合并回feature分支作为灯泡。您希望实现的知识库如图图 7-12(b) 所示。

9781430261032_Fig07-12.jpg

图 7-12 。配方 7-7 中考虑的储存库

解决办法

用分支克隆配方 7-4 中的存储库:

$ cd git-recipes
$ git clone-with-branches 07-04 07-07
$ cd 07-07

然后按照以下步骤操作:

  1. $ git rebase feature brave-ideabrave-idea分支重置到feature分支上
  2. $ git checkout feature切换到feature分支
  3. $ git merge --no-ff brave-ideabrave-idea分支合并成feature分支
  4. $ git branch -d brave-idea删除brave-idea分支
  5. $ git checkout master检查master分支

它是如何工作的

配方 7-7 显示了如何将配方 7-6 应用于featurebrave-idea分支。您可能认为这是多余的,但最终的存储库对于下一个秘籍是必需的。

7-8.给树枝换上灯泡

问题

你的存储库现在看起来像图 7-12(b) 。您想要将feature分支重新定位到master分支上。

解决办法

用分支克隆配方 7-7 中的存储库:

$ cd git-recipes
$ git clone-with-branches 07-07 07-08
$ cd 07-08

然后用$ git rebase master featurefeature分支复位到master分支上。您将获得如图图 7-13 所示的存储库。注意feature分支不再包含merge branch 'brave-idea'修订。

9781430261032_Fig07-13.jpg

图 7-13 。该库来自图 7-12 后的$ git rebase master 特性

它是如何工作的

正如你在图 7-13 中所看到的,重置基础只在非合并提交时起作用。所有合并提交都将丢失。重定基础总是产生一条直线的提交,没有灯泡或合并。在有球茎的树枝的情况下,它不一定是你所期望的。如果你想保留合并和灯泡,你不能使用简单的 rebase 命令。您必须移动整个分支,然后重新生成合并提交。

好好看看图 7-13 。提交b1b2用两个素数b1''b2''表示。双撇号强调了这些是新提交的事实。它们引入了与图 7-12 中的b1'b2'以及图 7-8 中的b1b2相同的变化,但它们的 SHA-1 名称不同。

7-9.重设基础期间保留合并

问题

你的知识库看起来像图 7-14(a) 。你要将feature树枝重新放在master树枝上,保留灯泡。你想要实现的库如图图 7-14(b) 所示。

9781430261032_Fig07-14.jpg

图 7-14 。通过将配方 7-9 应用于存储库(a ),您将得到存储库(b)

解决办法

用分支克隆配方 7-7 中的存储库:

$ cd git-recipes
$ git clone-with-branches 07-07 07-09
$ cd 07-09

然后用--preserve-merges参数进行重置:

$ git rebase --preserve-merges master feature

它是如何工作的

参数--preserve-merges强制 git 在重置基础期间保留合并。

摘要

本章介绍了重定基准的概念——从一个分支的顶部复制另一个分支的提交序列的操作。你可以把它当成一个工具,把分叉的分支转化成线性的历史。重设基础的语法允许您在一个完整的分支之上重设基础。您可以通过以下方式实现这一点:

$ git rebase dest src

其中,src是您要从中获取提交的分支,而dest是提交将被重新应用的分支。此次操作后,src将成为当前分支机构。

您还可以使用以下命令执行部分重置:

$ git rebase --onto dest part src

这里的dest也是提交将被重新应用的分支。要移动的提交集由partsrc分支定义。该操作移动包含在src中并从part分支中排除的提交。你可以把它作为dest重新应用的(src - part)差值记忆下来。

在这两种情况下,最终的src分支都可以省略。如果是这种情况,那么将使用当前分支。这些命令:

$ git rebase dest
$ git rebase dest HEAD

与以下命令完全等效:

$ git rebase --onto dest part
$ git rebase --onto dest part HEAD

在这一章中,我们第一次有意地在超脱的头部状态下工作。分支状态可以被视为以原子方式执行操作(如重置基础)的手段。该操作在分离的头部状态下执行。成功完成后,我们根据需要调整分支。否则不会修改分支,并且可以取消操作。在第九章关于冲突的讨论中会有更多关于这方面的内容。

请记住,默认情况下,重置基础会跳过合并提交。如果需要,您可以使用--preserve-merges选项保存它们。

在本章中,顺便说一下,你还学习了如何用$ git merge-base命令找到两个分支的共同祖先,以及如何指定范围提交。提交范围可以用两个特殊的操作符.....来定义,或者用更详细的方式来定义。

当您键入a..b时,它是一组包含在b中但不包含在a中的提交。你可以把它想成一个区别(b - a)

三个点c...d指定对称差,即两组提交的集合:

  • c中可用而d中不可用的
  • 以及那些在d中可用而在c中不可用的

这可以算是(c - d) + (d - c)

更详细的语法使用--not操作符,缩写为^来排除分支。范围:

a b c ^d ^e ^f
a b c --not d --not e --not f

包括abc中可用的提交,排除def中可用的提交。

八、修改历史记录

本章介绍修改修订图结构的各种命令。有时你需要将三个不同的版本合并成一个版本。在其他时候,您可能需要相反的操作:将一个单独的提交分成许多单独的提交。无论是哪种情况,请记住 git 版本是永久的。他们永远不会改变。一旦创建了修订,就无法对其进行修改。你所能做的就是用秘籍 3-12 和 5-4 中介绍的方法把它扔掉。因此,每当我说“让我们修改一个修订”这样的话,我脑海中的操作是创建一个类似于原始版本的新修订。原始版本在 git 数据库中保持不变。它可以通过符号引用而不是 reflog 来访问,但它仍然存在。直到下一次数据库清除。

如果任何操作产生的结果令您不满意,您可以随时返回到以前的状态。您所需要的只是包含正确快照的修订名称。可以使用 reflog 来查找该名称,但也可以创建一个临时分支来保存对所需修订的引用。因为修订不会改变,所以您不需要担心修改修订的后果。无论您做什么,都不会改变已经存储在数据库中的修订。提交、重置基础和合并只会产生新的修订,这些操作不会修改现有的修订。修改修订是不可能的。

这对于我研究 git 来说是一个真正的突破。一旦我学会了如何重构修订图以及如何撤销各种操作,我就获得了自由使用该工具的信心。

8-1.修订最新版本

问题

您刚刚提交了一组变更到存储库中,一分钟后您意识到有一些额外的修改应该被合并到先前的版本中。您不想再创建另一个提交;您可能希望通过添加一些额外的更改来修改现有的修订。

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 08-01
$ cd 08-01

然后按照以下步骤操作:

  1. 用$ echo lorem > lorem.txt 创建文件 lorem.txt
  2. 使用$ git add lorem.txt 暂存文件
  3. 用$ git commit -m "lorem "提交文件

存储库包含一个标记为lorem的修订。清单 8-1 显示了$ git log --pretty=fuller命令的输出。

清单 8-1。 我们希望修改的原始版本

commit 5a786865f21b5c1725e56c2bf60f6516ce736b9b
Author:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
AuthorDate: Thu Aug 22 07:02:00 2013 +0200
Commit:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
CommitDate: Thu Aug 22 07:02:00 2013 +0200

    Lorem

现在您意识到存储在lorem.txt文件中的文本应该大写并扩展。为了更深入地了解 git 的内部,我们将在不同的用户名下修改版本。

修改本地存储在这个特定存储库中的user.nameuser.email配置设置。您可以通过以下方式实现这一目标:

$ git config --local user.name "John Doe"
$ git config --local user.email john@example.net

最后,按照以下步骤修改版本:

  1. 用$ echo Lorem Ipsum Dolor > lorem.txt 修改 lorem.txt 的内容
  2. 使用$ git add lorem.txt 暂存文件
  3. 使用$ git commit-amend-m " Lorem Ipsum Dolor "提交文件

历史仍然只包含一个版本。命令$ git log --pretty=fuller打印清单 8-2 中所示的输出。

清单 8-2。$ git commit-amend 命令创建的提交

commit f63bce5e17a3ba02b0dbee13bb56ceabfd622ce7
Author:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
AuthorDate: Thu Aug 22 07:02:00 2013 +0200
Commit:     John Doe <john@example.net>
CommitDate: Thu Aug 22 07:07:45 2013 +0200

    Lorem Ipsum Dolor

两个版本,原始版本和修正版本,都可以通过 reflog 获得。命令$ git reflog输出:

f63bce5 HEAD@{0}: commit (amend): Lorem Ipsum Dolor
5a78686 HEAD@{1}: commit (initial): lorem

因此,您可以随时撤销用$ git reset --hard HEAD@{1}修改的内容。

它是如何工作的

$ git commit命令的参数--amend允许您修改历史中的最新版本。命令$ git commit --amend:

  • 从历史中获取最近的提交(在配方 8-1 中,它是名为5a78的提交;文件lorem.txt包含lorem;提交如清单 8-1 所示
  • 获取暂存区的当前状态(在配方 8-1 中,它是带有Lorem Ipsum Dolor的暂存文件lorem.txt
  • 并将它们组合成一个新的修订(在配方 8-1 中,它是名为f63b的提交;提交如清单 8-2 所示

这个新版本(f63b)取代了历史中的原始版本(5a78)。

从技术上讲,该命令不会修改版本。它创建一个新的提交。您可以使用$ git reflog命令找到两个提交的名称。原始提交在 git 数据库中一直悬而未决,直到最终被垃圾收集操作删除。

image 提示记住——git 版本是永久的!不可能更改提交中存储的任何信息并保留相同的 SHA-1。$ git commit --amend命令创建一个全新的版本,然后更新主分支以指向新的版本。

为什么我们在这个配方中改变了user.nameuser.email的配置?它将帮助您理解 git 处理日期和分配作者身份的方式。每个提交包含四个属性:AuthorAuthorDateCommitCommitDate。他们储存:

  • Author—作者的名字
  • AuthorDate—最初提交的日期
  • Commit—委托人的姓名
  • CommitDate—历史中引入提交的日期

当您第一次创建提交时,AuthorCommit都将被设置为您的名字。存储在AuthorDateCommitDate中的日期将是相同的。这种情况如清单 8-1 所示。

如果用--amend选项修改提交会发生什么?Git 保留原来的AuthorAuthorDate字段,并为CommitCommitDate字段设置新值。这显示在清单 8-2 中。同样的规则也适用于你的提交。

image 提示 Git 不关心你的文件的最后修改日期。Git 跟踪内容——文件的最后修改日期不会以任何方式影响您的修订。存储在数据库中的每个提交都包含AuthorDateCommitDate。这些日期是在提交、rebase 或 cherrypick 时设置的。文件的最后修改日期是在您签出文件时设置的。例如,这是在切换分支时完成的。

细心的读者会注意到,$ git commit --amend引入的修改的作者被错误地归于原作者。在配方 8-1 中,两个词IpsumDolor是由用户 John Doe 创作的,但被认为是 odzimierz Gajda。然而,实际上这种情况从来不会发生,因为不允许修改其他开发人员提交的内容。你可以对它们进行精选或重新排序,但这样作者才能被正确地归属。

8-2.删除最近的版本

问题

您希望从当前分支中删除两个最新的修订。图 8-1 中描述了您想要实现的转变。

9781430261032_Fig08-01.jpg

图 8-1 。删除两个最新修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes
$ git init 08-02
$ cd 08-02
$ git simple-commit a b c d

您的存储库现在包含四个修订版。全部包含在master分支中。使用命令$ git reset --hard HEAD∼2删除两个最新版本。现在$ git log --oneline命令只返回两个版本:a 和 b。版本 c 和 d 从历史中删除。

image 提示你可以用这个方法删除任意数量的最近提交。命令$ git reset --hard HEAD∼13将删除最后 13 个提交。

它是如何工作的

参考号HEAD∼2指向图 8-1 中的版本b。你也可以使用修订版的 SHA-1b来达到同样的效果。假设修订版b的名称是a1b2c3d4,以下命令是等效的:

$ git reset --hard HEAD∼2
$ git reset --hard a1b2c3d4  # SHA-1 of revision b

正如您已经知道的,git 命令通常不会从数据库中删除对象。因此,在图 8-2 中可以更准确地描述配方 8-2 中执行的操作。修订版cd在数据库中保持可用,直到它们在 reflog 中的所有符号引用都被清除并且数据库被清理。要撤消操作,您可以随时使用 reflog。

9781430261032_Fig08-02.jpg

图 8-2 。修订版 c 和 d 一直悬而未决,直到它们被 git gc 清除

image 提示配方 8-2 中讨论的命令在配方 3-5 中使用,以检验所需的版本。

8-3.将许多修订压缩成一个修订

问题

您的存储库包含相当多的修订。你想把最后三个版本压缩成一个版本。图 8-3 中的描述了您想要执行的操作。

9781430261032_Fig08-03.jpg

图 8-3 。压缩最后三次修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes
$ git init 08-03
$ cd 08-03
$ git simple-commit a b c d

您的存储库现在包含四个修订,工作目录包含四个文件。当然,您可以使用$ git log$ ls命令来检查它。如果您想验证哪些文件包含在最新版本中,请使用以下命令:

$ git show --name-only HEAD

它将打印上一次修订带有注释d的信息,其中包括一个名为d.txt的文件。同样,您可以列出任何以前版本中包含的文件:

$ git show --name-only HEAD∼
$ git show --name-only HEAD∼2

让我们将最后三个提交压缩到一个版本中。为此,请运行以下命令:

$ git rebase -i HEAD∼3

重置命令的开关-i打开交互模式。该命令将启动 vim 编辑器,内容如清单 8-3 中的所示。更改编辑器的内容——键入清单 8-4 中的命令。您应该将出现在第二次和第三次提交之前的单词 pick 改为 fixup

清单 8-3。$ git rebase-I HEAD 后 vim 的内容∨3

pick f2136a0 b
pick a36ee90 c
pick 46c002f d

# Rebase d344a8a..46c002f onto d344a8a
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell

清单 8-4。 您必须在$ git rebase -i HEAD 之后键入的 vim 内容 3

pick f2136a0 b
fixup a36ee90 c
fixup 46c002f d

在你输入了清单 8-4 中的内容后,关闭编辑器。然后 git 会执行操作。在此之后,使用以下内容检查存储库的历史记录:

$ git log --oneline

该命令将只打印两个提交ab。根据$ ls命令,工作目录仍然包含四个文件。命令:

$ git show --name-only HEAD

证明最后一次提交现在包含三个文件b.txtc.txtd.txt

它是如何工作的

在交互式重定基础期间,git 将您在编辑器中键入的内容视为子命令列表。例如,清单 8-4 包含三个子命令。其中之一是:

fixup a36ee90 c

其含义是:“对修订版 a36ee90c 执行修正操作。”以下是交互式重置基础可用子命令的完整列表:

  • pick—提交将出现在结果历史记录中
  • reword—将使用提交,但 git 将允许修改其注释
  • edit—将使用提交,但 git 将允许修改(添加和删除文件)
  • squash—提交将被压缩到前一个提交中,git 将允许修改结果提交的注释
  • fixup—与 squash 相同,但这次 git 不允许修改结果修订的注释(将使用第一个修订的注释)
  • exec—该命令允许你执行任意的外壳命令

每个子命令都可以用它的首字母缩写。您键入的命令由 git 按照它们在编辑器中出现的顺序逐一执行。

那么清单 8-4 中显示的命令是什么意思呢?其中有三个:第一个是 pick 命令,第二个是 fixup 命令,第三个是另一个 fixup 命令。第一个命令

pick f2136a0 b

选择提交b。因此,它将出现在结果历史中。您可以将 pick 命令视为一个精选命令。应用由版本f2136a0定义的补丁。下一个命令:

fixup a36ee90 c

将提交c压缩为之前的提交b。fixup 命令不允许您修改结果提交的注释。您将获得包含来自提交bc的变更集的提交,并且用提交b的原始注释来表示。

最后一个命令:

fixup 46c002f d

执行一次挤压。这一次,git 将 commit d压缩为将c压缩为b的结果。因此,您将以注释b表示的单个提交结束,并合并来自提交bcd的变更集。

交互式重置基础执行的所有操作都存储在 reflog 中。现在,命令$ git reflog打印以下结果:

e3fc0e0 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
e3fc0e0 HEAD@{1}: rebase -i (squash): b
5eb1d5a HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
f2136a0 HEAD@{3}: checkout: moving from master to f2136a0
46c002f HEAD@{4}: commit: d
a36ee90 HEAD@{5}: commit: c
f2136a0 HEAD@{6}: commit: b
d344a8a HEAD@{7}: commit (initial): a

正如您可能猜到的那样,原始修订会保留在数据库中,直到它们被清除。如果您想撤销重设基础,您需要以下命令:$ git reset --hard HEAD@{4}

给定版本中修改的文件列表由$ git show --name-only [REVISION]命令打印。您也可以使用$ git log --name-only REVISION∼..REVISION$ git diff --name-only REVISION∼..REVISION来获得类似的结果。范围REVISION∼..REVISION$ git log$ git diff命令的输出限制为仅一个版本。

8-4.将一个修订拆分成多个修订

问题

存储库中的最新版本在工作目录中引入了三个新文件。您无意中提交了所有三个文件,一分钟后才意识到每个文件都应该存储在单独的修订中。您希望将最新修订拆分为三个不同的修订,每个修订都与一个文件相关。配方 8-4 与配方 8-3 相反。

9781430261032_Fig08-04.jpg

图 8-4。将最近的修订分成许多不同的修订

解决办法

创建一个新的存储库,它将包含两个修订版本:ab。第一次修订应包括一个文件a.txt,第二次修订应包括三个文件:b.txtc.txtd.txt:

$ cd git-recipes
$ git init 08-04
$ cd 08-04
$ echo a > a.txt
$ git add -A
$ git commit -m a
$ echo b > b.txt
$ echo c > c.txt
$ echo d > d.txt
$ git add -A
$ git commit -m b

使用$ git log --oneline命令验证存储库包含两个修订版,并且最后一个修订版确实包含三个文件。$ git show --name-only命令的输出将打印三个文件名:b.txtc.txtd.txt

现在,将您的历史重置为修订版a,将修订版b引入的修改作为新的未提交的变更保留在工作目录中。你可以用$ git reset HEAD∼命令来做这件事。在这个命令之后,由$ git status -sb返回的库的状态将如下:

?? b.txt
?? c.txt
?? d.txt

如您所见,文件现在未被跟踪。您可以创建三个不同的修订版本:

$ git add b.txt
$ git commit -m b

$ git add c.txt
$ git commit -m c

$ git add d.txt
$ git commit -m d

秘籍完成了。您可以使用$ git log命令检查历史包含四个版本abcd。每个修订版都包含一个文件。要验证它,请使用以下命令:

$ git show --name-only HEAD
$ git show --name-only HEAD∼
$ git show --name-only HEAD∼2

它是如何工作的

为了更深入地了解$ git reset命令 转换您的存储库的方式,您需要彻底理解回购的结构。存储库包括:

  • 工作目录
  • 集结地
  • 数据库

存储在.git/HEAD中的头指向存储在数据库中的一个提交。

工作目录可以解释为项目的一个快照。那很清楚。但是同样的,你也可以把 staging 区域和你的 HEAD 指针当作两个不同的快照。因此,我们可以说,在任何给定的时间点,您的存储库在三个不同的快照上运行:

  • 第一个快照—工作目录
  • 第二个快照—暂存区
  • 第三个快照—存储在 HEAD 指向的修订中的快照

让我们假设您刚刚用$ git simple-commit lorem创建了一个修订。存储库是干净的,工作目录包含一个名为lorem.txt的文件,存储一个字符串lorem

当存储库干净时,所有三个快照—工作目录、暂存区和磁头—都是相同的。让我们用$ echo foo > lorem.txt来修改lorem.txt 文件。这次操作后$ git status -sb命令打印出来:

_M lorem.txt

存储在三个快照中的lorem.txt文件包含:

  • 第一个快照(工作目录):该文件包含foo
  • 第二个快照(暂存区):该文件包含lorem
  • 第三张快照(头):该文件包含lorem

因此_M意味着工作目录中的文件不同于暂存区中存储的文件,同时暂存区中存储的文件与HEAD指向的修订中的文件相同。

$ git status -sb 打印的两个字母代码,如_M,让你知道三个快照的区别。如果两个字母的代码是XY,那么:

  • X让您了解第三个快照(HEAD)和第二个快照(暂存区)之间的差异
  • Y让您了解第二个快照(暂存区)和第一个快照(工作目录)之间的区别

让我们用$ git add lorem.txt暂存lorem.txt文件。$ git status -s 的输出如下:

M_ lorem.txt

代码是M_,这次存储在三个快照中的lorem.txt文件包含:

  • 第一个快照(工作目录):该文件包含foo
  • 第二个快照(暂存区):该文件包含foo
  • 第三张快照(HEAD):该文件包含lorem

因此M_意味着工作目录中的文件与暂存区中的文件相同;存储在暂存区中的文件不同于存储在HEAD中的文件。

如果您使用$ git commit提交此修改,那么所有三个快照将再次同步。它们都包含带有foo字符串的文件lorem.txt

一旦你掌握了三个快照背后的思想,就很容易理解$ git reset命令的工作方式。该命令更改三个快照:头、登台区和工作目录。它有三个影响其行为的重要选项--soft--mixed--hard、。它们的含义总结在表 8-1 中。

表 8-1 。$ git reset 命令的选项- soft、- mixed、- hard

image

表 8-1 告诉我们--soft选项只影响HEAD--mixed选项影响HEAD和中转区。第三个选项(--hard)影响所有三个快照:HEAD、暂存区和工作目录。

我们已经非常熟悉的操作是:

$ git reset --hard [REVISION]

该操作的内部可以描述为:

  • HEAD的修改:更新HEAD使其指向[REVISION]
  • 暂存区的修改:获取快照,使其现在由HEAD指向,并将其存储在暂存区中
  • 工作目录的修改:取HEAD现在指向的快照,签入工作目录

在该命令之后,所有三个快照完全相同。您当前的版本是[REVISION]。注意:这个命令会修改工作目录。正如您已经知道的,您将丢失未提交的更改!

第二个选项,--mixed是默认的。因此,以下两个命令是相同的:

$ git reset --mixed [REVISION]
$ git reset [REVISION]

您可以将$ git reset --mixed操作视为暂存和提交的逆操作。以下是根据三个快照描述的内部情况:

  • HEAD的修改:更新HEAD使其指向[REVISION]
  • staging area 的修改:取HEAD现在指向的快照,存储在 staging area 中
  • 不要触摸工作区域

现在很容易分析这个菜谱中的命令:$ git reset HEAD∼。该命令相当于$ git reset --mixed HEAD∼。它执行两种操作:

  • 设置HEAD,使其指向父版本
  • 它获取快照,使其现在由HEAD指向,并将其存储在暂存区中。

请注意,工作目录没有改变。您的所有修改(它们已经被提交)都将保留在那里。结果与您暂存和提交变更之前完全一样。三个文件b.txtc.txtd.txt现在显示为未暂存。

第三个选项--soft,只移动存储在HEAD中的指针。它不会修改临时区域或工作目录。如果您试图在这个菜谱中使用$ git reset --soft HEAD∼,那么$ git status -sb返回的状态将是:

A_ b.txt
A_ c.txt
A_ d.txt

文件是暂存的。如果您想创建一个只存储一个文件的修订版,您必须卸载一些文件。要将c.txt文件从A_更改为_A,您可以使用$ git rm --cached c.txt命令。执行$ git reset HEAD∼的另一种方法是使用两个命令:

$ git reset --soft HEAD∼
$ git rm --cached [b-d].txt

尽管这个解决方案更糟糕,我还是鼓励你去尝试一下。使用$ git add$ git commit你可以在两个不同的步骤中存放和提交文件。命令$ git reset --soft$ git rm --cached执行相反的操作:取消提交和取消登台。

在这个配方中,我们在一次提交中执行了一个撤销操作。记住,用同样的方式,你可以用$ git reset HEAD∼5撤销任意数量的提交。

8-5.重新排序修订

问题

您的存储库包含许多修订。最后三个版本被标记为bcd。它们在历史中按以下顺序出现:d是最近的版本,cd之前创建,bc之前。您需要对修订版bcd重新排序,以符合图 8-5 。

9781430261032_Fig08-05.jpg

图 8-5 。重新排序修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes
$ git init 08-05
$ cd 08-05
$ git simple-commit a b c d

命令$ git log --oneline现在返回以下输出:

cc595c7    d
7bb0fe3    c
b040c68    b
9dfe77d    a

修订顺序为abcd。最老的是a,最新的是d

现在用$ git rebase -i HEAD∼3进行交互式重置基础。在这个命令之后,git 将用清单 8-5 中显示的内容启动 vim。将清单 8-5 中显示的内容替换为清单 8-6 中显示的代码。变化非常小:提交被重新排序。然后保存文件并关闭编辑器。

清单 8-5。$ git rebase 后 vim 的原始内容-I HEAD∽3(提交顺序为 b,c,d)

pick b040c68    b
pick 7bb0fe3    c
pick cc595c7    d

清单 8-6。 交互式改基时你应该在编辑器中输入的内容(提交顺序为 d、b、c)

p   cc595c7    d
p   b040c68    b
p   7bb0fe3    c

当您保存并关闭编辑器时,git 将执行重置。该操作完成后,使用$ git log命令检查您的修订顺序。$ git log -oneline的输出应该如下:

7bb0fe3    c
b040c68    b
cc595c7    d
9dfe77d    a

这些提交现在按cbda排序(从最新的到最早的)。

它是如何工作的

交互式重置基础的子命令pick可以缩写为p。通过更改编辑器中子命令的顺序,可以修改应用修补程序的顺序。重置基础根据补丁在编辑器窗口中的顺序应用补丁。清单 8-6 中的第一个子命令,即p-cc595c7-d,定义了要应用的第一个补丁。因此,在生成的历史中,修订版d将出现在a之后。

原始修订保留在数据库中,可以通过引用日志引用进行访问。

8-6.删除几个版本

问题

您的存储库包含几个修订版。最后五次修订被标记为bf。您想要删除修订版bdf。图 8-6 中描述了您想要实现的转变。

9781430261032_Fig08-06.jpg

图 8-6 。删除修订

解决办法

创建一个包含修订版af的新存储库:

$ cd git-recipes
$ git init 08-06
$ cd 08-06
$ git simple-commit a b c d e f

$ git log --oneline命令现在按以下顺序打印修订:

35cba5a     f
4932572     e
bb7f037     d
93b7397     c
9219566     b
17e5231     a

您希望从历史记录中删除一些提交。您要删除的最早提交是b。这是历史上第五次犯罪(f第一、e第二、d第三、c第四、b第五)。你需要的命令是$ git rebase -i HEAD∼5。这是一个交互式的重置基础,所以 git 将启动编辑器。交互式重设基础的原始子命令如清单 8-7 所示。用清单 8-8 中所示的子命令替换它们。最后保存文件并关闭编辑器。

清单 8-7。 配方 8-6 中交互重置的原始子命令

pick 9219566 b
pick 93b7397 c
pick bb7f037 d
pick 4932572 e
pick 35cba5a f

清单 8-8。 执行图 8-6 中所示转换的命令

pick 93b7397 c
pick 4932572 e

当您完成重置$ git log --oneline时,应打印:

4932572     e
93b7397     c
17e5231     a

它是如何工作的

如果从编辑器中删除一个子命令选择,那么相应的修订将不会出现在历史中。

8-7.编辑旧版本

问题

您的存储库包含许多修订。历史上的第三次修订被标记为x,它引入了一个新文件x.txt。现在你想重新编辑这个版本:它应该引入两个新文件x.txty.txt。你还想在提交x之后引入一个新的版本z。其他修订应保持不变。图 8-7 中显示了您想要实现的转换。

9781430261032_Fig08-07.jpg

图 8-7 。编辑旧版本

解决办法

创建一个包含修订版本abxcd的新存储库:

$ cd git-recipes
$ git init 08-07
$ cd 08-07
$ git simple-commit a b x c d

并与$ git rebase -i HEAD∼3进行交互式重置基础。交互式重设基础的原始子命令如清单 8-9 所示。用清单 8-10 中的子命令替换它们。你要把修改x的命令从pick改成edit。然后保存文件并关闭编辑器。

清单 8-9。 配方 8-7 中交互重置的原始子命令

pick 9aa7b18 x
pick 2455e82 c
pick f8bf7b5 d

清单 8-10。 执行图 8-7 中所示转换的命令

edit 9aa7b18 x
pick 2455e82 c
pick f8bf7b5 d

重置基础过程在提交x时停止。您现在可以使用以下命令调整x提交:

$ echo y > y.txt
$ git add y.txt
$ git commit --amend --no-edit

x提交被调整时,用$ git simple-commit z创建一个新的修订z。最后,用$ git rebase --continue完成重置。

它是如何工作的

交互式重置基础是作为一个迭代来实现的,该迭代循环通过清单 8-10 中所示的命令。这种迭代是在分离的磁头状态下进行的。当你关闭包含清单 8-10 所示子命令的编辑器时,git 进入分离状态并执行迭代。

清单 8-10 中的第一个子命令是edit 9aa7b18 x。该命令首先应用由9aa7b18标识的x版本定义的补丁,然后暂停重设基础。补丁x之后,你就处于分离的头部状态。如果你想验证,这运行命令$ git status -sb。您将看到以下输出:

## HEAD (no branch)

证明你现在是在超脱的头部状态下工作。bash 命令提示符:

gajdaw@GAJDAW /c/git-recipes/08-07 (master|REBASE-i 1/3)

打印您正在使用三个修补程序执行重置基础操作以及应用了第一个修补程序的信息。

如你所知,git 允许你用$ git add$ git commit这样的命令在分离的头状态下工作。因此,您可以用$ echo y > y.txt创建一个新文件,用$ git add y.txt暂存它,最后用$ git commit --amend --no-edit修改当前提交。这就是x提交被修改的方式。如果您跳过--no-edit选项,那么 git 将启动编辑器,您将有机会修改修订版x的注释。

一旦您完成了x提交,您就可以继续创建版本z。完成后,你用$ git rebase --continue命令完成秘籍。

值得注意的是,当交互式重置基础暂停时,您可以用其他方法修改历史。您可以使用$ git commit插入额外的提交,或者使用$ git reset删除一些提交。但是,在完成第一个基础之前,您不能执行另一个交互式基础重建。

您可以使用$ git rebase --abort中止暂停的重置基础。若要撤消该操作,请使用 reflog。

8-8.恢复版本

问题

您的存储库包含任意数量的修订。其中一个提交在您的项目中引入了一个 bug。您希望以这样一种方式撤销由该提交引入的更改,即项目的历史直到当前头都保持不变。

你想要实现的转变在图 8-8 中给出。应恢复标有b的版本。修订版c之前的历史必须保持不变。该操作将通过创建一个标记为Revert "b"的附加版本来实现。这个新的提交恢复了由b引入的更改。

9781430261032_Fig08-08.jpg

图 8-8 。恢复修订

解决办法

创建一个包含修订版本abc的新存储库:

$ cd git-recipes
$ git init 08-08
$ cd 08-08
$ git simple-commit a b c

然后执行$ git revert --no-edit HEAD∼ 命令。

它是如何工作的

命令$ git revert [REVISION]创建一个新的修订版本,恢复由[REVISION]引入的更改。附加参数--no-edit将新版本的注释设置为Revert "..."。这是撤销已包含在项目公共历史中的修订的唯一方法。

8-9.正在恢复合并提交修订

问题

您在项目中使用两个分支:masterfeature。分支出现分歧,你决定将feature分支合并为master分支。当你完成合并后,feature分支被删除。

过了一段时间,你意识到feature branch 引入了许多 bug 和严重的问题。因此,您希望将分支feature的合并恢复为master分支。

您想要实现的转换如图 8-9 所示。

9781430261032_Fig08-09.jpg

图 8-9 。恢复合并提交

解决办法

使用以下命令创建如图 8-9(a) 中所示的存储库:

$ cd git-recipes
$ git init 08-09
$ cd 08-09
$ git simple-commit m1 m2 m3
$ git checkout -b feature
$ git simple-commit f1 f2 f3
$ git checkout master
$ git simple-commit m4 m5
$ git merge feature
$ git branch -d feature
$ git simple-commit m6

$ git log --oneline --graph命令验证你的库的历史看起来像图 8-9(a) 中的。也可以用$ ls列出工作目录的内容。工作目录现在包含九个文件:f1.txtf3.txtm1.txtm6.txt

当存储库准备就绪时,您可以使用以下命令恢复合并提交:

$ git revert --no-edit -m 1 HEAD∼

该命令将历史向前移动。存储库现在将包含一个带有注释Revert "Merge branch 'feature'"的新版本。这个修订删除了您在feature分支中创建的提交中引入的所有变更。工作目录现在只包含六个文件m1.txtm6.txt。文件f1.txtf2.txtf3.txt都没了。您可以使用$ ls命令来验证它。

它是如何工作的

合并提交有两个或多个父提交。如果您恢复合并提交,您必须指出应该恢复历史的哪一部分。在图 8-9 中标为Merge branchfeature的提交有两个父级:

  • 第一个是提交m5
  • 第二个父节点是提交节点f3

恢复Merge branch ' feature'提交 可以产生由修订组成的快照:

m1, m2, m3, m4, m5, m6

或由修订组成的快照:

m1, m2, m3, f1, f2, f3, m6

决定权在你。您通过传递给$ git revert命令的附加参数-m做出决定。如果你想把历史保存在合并提交的第一个父项下,那么使用-m 1参数,比如:

$ git revert --no-edit -m 1 HEAD∼

上述命令将产生由m1m2m3m4m5m6修订组成的快照。这种情况如图 8-10 中的所示。

9781430261032_Fig08-10.jpg

图 8-10 。用$ git revert - no-edit -m 1 HEAD 获得的快照

如果你想把历史保存在合并提交的第二个父项下,那么使用-m 2参数,比如:

$ git revert --no-edit -m 2 HEAD∼

该命令产生由修订版 m1m2m3f1f2f3m6组成的快照。这种情况如图 8-11 所示。

9781430261032_Fig08-11.jpg

图 8-11 。用$ git revert - no-edit -m 2 HEAD 获得的快照

如果您忘记指出想要保留的分支,git 将拒绝恢复合并提交。该命令将无法生成以下消息:

error: Commit XXXXXX is a merge but no -m option was given.
fatal: revert failed

8-10.摘樱桃修订版

问题

您想要将修订从一个分支复制到另一个分支。图 8-12 中的展示了您心目中的转变。您的master分支包含一个标记为m4的修订。你要把它复制到feature分公司。

9781430261032_Fig08-12.jpg

图 8-12 。精心挑选的修订

解决办法

从配方 6-4 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 08-10
$ cd 08-10

然后用$ git checkout feature转到特征分支,用$ git cherry-pick master∼复制修订m4

它是如何工作的

$ git cherry-pick命令将修订版定义的补丁作为参数应用到当前分支。

8-11.挤压一根树枝

问题

您刚刚完成了一项新功能的工作。您的工作由存储在专用分支中的三个提交组成。您想要压缩这些提交,并将它们作为一个新的提交添加到您的master分支之上。

该任务在图 8-13 中给出。feature分支包含三个版本f1f2f3。你想把它们压缩成一个单独的版本,出现在master分支中。

9781430261032_Fig08-13.jpg

图 8-13 。压断树枝

解决办法

从配方 6-4 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 06-04 08-11
$ cd 08-11

接下来用$ git merge --squash feature挤压特征分支。最后,用$ git commit -m "The feature branch was squashed"提交变更。

它是如何工作的

操作$ git merge --squash feature修改工作目录和储存库的暂存区,复制在feature分支中引入的变更。就在这个命令之后$ git status -sb打印出来:

A  f1.txt
A  f2.txt
A  f3.txt

这意味着:

  • 工作目录包含来自特征分支的所有变更
  • 所有的变化都已经上演了

如果你真的对这个修改满意,你可以用$ git commit命令提交它们。

8-12.重新使用恢复的分支

问题

处理项目时,您将创建一个名为 feature 的分支,其中包含许多修订。feature 分支中的所有代码看起来都是正确的,您将它合并到主分支中,形成一个灯泡。碰巧这个分支机构引起了许多问题。因此,您决定使用方法 8-9 中解释的过程来恢复合并提交。

您的项目的工作继续进行,主分支继续前进。一段时间后,您希望再次合并已恢复的特征分支。您想要实现的操作如图 8-14 所示。

9781430261032_Fig08-14.jpg

图 8-14 。配方 8-12 将储存库(a)转换成储存库(b)

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 08-12
$ cd 08-12

并创建图 8-14(a) 所示的存储库。使用以下命令:

$ git simple-commit a b
$ git checkout -b foo-bar
$ git simple-commit x y
$ git checkout master
$ git merge --no-ff foo-bar
$ git simple-commit c
$ git revert -m 1 --no-edit HEAD∼
$ git simple-commit d

使用$ ls命令你可以验证工作目录不包含文件x.txty.txt。至此foo-bar分支成功还原。

你的项目的历史向前推进了。revert 命令后创建了版本d。现在您想将foo-bar分支重新合并到主分支中。为此,请运行以下命令:

$ git format-patch foo-bar∼2..foo-bar
$ git checkout -b foo-bar-tmp
$ git am *.patch
$ rm *.patch
$ git branch -M foo-bar-tmp foo-bar
$ git checkout master
$ git merge --no-ff -m "2nd merge of 'foo-bar'" foo-bar

存储库应该看起来像图 8-14(b) 。您可以使用$ git log --graph --oneline --all --decorate命令来验证这一点。工作目录包含文件x.txty.txt

它是如何工作的

当你的库看起来像图 8-14(a) 中的库时,命令:

$ git rebase master foo-bar

将不执行重置基础操作。它只会将 foo-bar 分支快进到 master 分支指向的修订。如果你想强制重置基准,你必须使用$ git format-patch$ git am命令手动重置。

foo-bar 分支中的修订可通过范围说明符foo-bar∼2..foo-bar获得。命令:

$ git log --oneline foo-bar∼2..foo-bar

列出了两个版本xy。为了创建这些修订版的补丁,我们使用:

$ git format-patch foo-bar∼2..foo-bar

接下来,我们创建一个新的临时分支,名为 foo-bar-tmp:

$ git checkout -b foo-bar-tmp

并在其中应用补丁:

$ git am *.patch

不再需要修补程序,因此您可以使用以下命令删除它们:

$ rm *.patch

然后,将临时分支 foo-bar-tmp 重命名为原来的名称 foo-bar:

$ git branch -M foo-bar-tmp foo-bar

当分支 foo-bar 准备就绪时,您可以进入主分支:

$ git checkout master

并再次合并 foo-bar 分支:

$ git merge --no-ff -m "2nd merge of 'foo-bar'" foo-bar

摘要

前两章介绍了合并和重定基——这两种操作使修订图的结构变得复杂。在第八章中,我们更加关注修订图,考虑了多种方法来转换其结构。我确信你会发现这里讨论的许多秘籍在你的日常工作中很有用。

这里介绍的所有配方都强调了修订的性质。让我再次提醒你:修订不会改变。一旦创建,就不能修改。你所能做的就是创建新的修订版,在某些方面与原版相似。这条规则构成了各种撤消操作的基础。如果你想在 git 中撤销某些东西,你必须在开始操作之前寻找 HEAD 指向的修订。如果你知道它的名字,那么$ git reset --hard [REVISION]会撤销操作。

本章要记住的第二件重要的事情涉及到存储库的三个方面:

  • 工作目录
  • 集结地
  • 和您当前的分支(即由 HEAD 指向的修订)

它们中的每一个都定义了项目中文件的快照。您可以使用文件系统命令修改工作目录中存储的快照,例如$ rm$ cp$ echo foo > bar等等。使用 git 命令修改暂存区中存储的快照,如$ git add$ git rm$ gim mv等。最后,可以用$ git commit命令修改 HEAD 指向的版本中存储的快照。

使用这三个快照,您可以将由$ git status -sb返回的两个字母状态代码解释为:

  • 代码的第一个字母比较了磁头快照和转移区:

  • 空格表示存储在磁头快照和转移区中的文件是相同的

  • 任何其他字符都表示存储在磁头快照中的文件不同于暂存区中的文件

  • 代码的第二个字母比较了临时区域和工作目录

  • 空格表示存储在临时区域中的文件和存储在工作目录中的文件是相同的

  • 任何其他字符都表示存储在登台区的文件不同于存储在工作目录中的文件

本章还阐明了作者身份的概念以及 git 处理日期的方式。每个提交存储四个不同的属性:AuthorCommitAuthorDateCommitDate. AuthorCommit保存在提交中引入的代码作者的身份(Author属性),以及在项目历史中引入提交的人的身份(Commit属性)。如你所知,当你执行$ git commit命令时Author被设置(没有--amend参数)。当您使用$ git commit --amend$ git cherry-pick$ git rebase修改提交时,git 只改变提交者的用户名——作者身份保持不变。请注意,当您压缩一些提交时,没有办法保留原始作者。被挤压的提交将归因于第一个提交的作者。

其他属性AuthorDateCommitDate是时间戳。第一个存储创作提交时的信息,第二个存储历史中引入提交时的信息。Git 不关心或存储任何其他日期。特别是,git 操作不受文件系统中存储的修改日期的影响。如果您:

  • 下午 5 点提交
  • 在下午 5:10 创建、编辑并保存文件a.txt
  • 在下午 5:20 创建、编辑并保存文件b.txt
  • 下午 5:30 提交

then your repository will contain two commits. The first will contain the timestamp 5:00 p.m. The second revision will be denoted as created at 5:30. There is nothing in between. The information that your files were modified at 5:10 and at 5:20 is lost—git doesn’t track it.

介绍的秘籍中有四个使用了交互式的重新基础。你应该非常仔细地分析它们,尤其是配方 8-7。即使你不打算用它的方式提出。在冲突的情况下,重定基础将被暂停——这就是配方 8-7 特别重要的原因。一旦你知道如何用配方 8-7 编辑旧版本,你将更容易解决冲突。

image 注意请注意,本章中的配方在用于修改已与他人共享的修订时会导致严重的问题。您只能使用配方 8-8 和 8-9 中的$ git revert命令来修改已发布的修订。所有其他配方只能用于没有发送到共享存储库的修订。记住:你的存储库的公共历史只能向前发展。否则,在团队内部同步工作将会非常困难和麻烦。

九、解决冲突

到目前为止,我们主要关注修订图的结构。我们通常使用别名$ git simple-commit提交;因此,我们生成的文件非常简单。事实上,到目前为止我们创建的几乎每个文件都只包含一个单词。一旦创建,这些文件很少被修改。此外,秘籍是以这样一种方式编写的,我们通常在不同的分支中使用不同的文件名。这种简化的提交过程是学习修订图上各种操作的非常有效的方式,例如合并和重新建立基础。然而,它们并没有为您在团队中工作做好充分的准备,在团队中,您的同事正在对同一个文件进行更改。在这一章中,我们将填补这个空白。您将学习如何控制文件的内容,直到解决问题。

在实际项目中工作,你迟早会遇到冲突。当您合并包含对文件的完全相同行的不同修改的分支时,就会发生这种情况。如果在某个文件中,例如readme.txt,一个开发人员键入第一行如下:

Lorem ipsum

和其他开发人员类型:

Dolor sit amet

git 将不能自动合并两个版本。您必须在Lorem ipsumDolor sit amet之间手动选择。因为冲突通常会引起很多关注和恐慌——它们被认为是令人害怕的事情——四个秘籍将准确地解释如何在文本和二进制文件的合并和重定基础期间处理冲突。一旦你熟悉了冲突,好好看看冲突是如何解决的。在 git 中,当你暂存一个文件时,冲突就解决了。这意味着在所有四个关于冲突的方案中,最重要的工作是由一个著名的$ git add指挥完成的。

9-1.在文本文件 中创建冲突的更改

问题

您想要创建一个包含两个分支的存储库,这两个分支在合并或改变基础时会在文本文件中产生冲突。您想要创建的存储库如图 9-1 所示。

9781430261032_Fig09-01.jpg

图 9-1 。带有分支 en 和 fr 的存储库将在文本文件中产生冲突

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 09-01
$ cd 09-01

然后按照以下步骤操作:

  1. Create the file numbers.txt with the contents shown in Listing 9-1

    清单 9-1。 在主控分支中创建 numbers.txt 文件

    1
    
    2
    
    3
    
  2. $ git snapshot Numbers: 1, 2, 3提交numbers.txt文件

  3. $ git branch en创建名为en的分支

  4. $ git branch fr创建名为fr的分支

  5. en分支中创建新的提交

    • a.用$ git checkout en切换到en分支

    • b.  Change the contents of numbers.txt. Replace 2 with two as shown in Listing 9-2

      清单 9-2。 恩分公司提交的 numbers.txt 文件

      1
      
      two
      
      3
      
    • c.用$ git snapshot Numbers: two提交更改

  6. fr分支中创建新的提交

    • a.用$ git checkout fr切换到fr分支

    • b.  Change the contents of numbers.txt. Replace 2 with deux as shown in Listing 9-3

      清单 9-3。fr 分公司提交的 numbers.txt 文件

      1
      
      deux
      
      3
      
    • c.用$ git snapshot Numbers: deux提交更改

$ git checkout en完成秘籍。

它是如何工作的

要创建一个冲突的变更,你必须在两个不同的分支中修改一个文件的完全相同的行。在这个配方中,我们修改了包含数字2的一行。在名为en的第一个分支中,我们用two替换2,在名为fr的第二个分支中,我们用deux替换2

在这种情况下,git 不能自动地合并或者重置分支。正如您将看到的,合并和重置基础将被暂停,您将不得不手动解决冲突。

9-2.合并后解决文本冲突

问题

你和你的同事正在处理同一个项目。你们都很欣赏 git 分支模型提供的独立性。因此你们每个人都创造了一个分支。不幸的是,在不同的分支工作时,你们都编辑了同一个文件,插入了重叠的更改。现在,您想将自己的工作与同事的工作合并。在这次行动中,你将面临冲突。您希望以这样一种方式解决冲突,即您和您的同事在文件中键入的所有内容都得到保留。

这个真实的场景简化为合并我们在方法 9-1 中创建的存储库中的分支enfr。您希望使用在配方 9-1 中创建的存储库作为起点。你想要达到的结果如图 9-2 所示。numbers.txt文件的第二行应该包含清单 9-4 中显示的内容。你想保留两个互相冲突的单词twodeux

9781430261032_Fig09-02.jpg

图 9-2 。来自配方 9-1 的储存库,具有合并的分支 en 和 fr

清单 9-4。 合并 en 和 fr 分支后的 numbers.txt 文件

1
two - deux
3

解决办法

从配方 9-1 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 09-01 09-02
$ cd 09-02
$ git checkout en

你现在的部门是en。用$ git merge frfr分支合并成en。这一次,自动合并失败,并显示以下消息:

Auto-merging numbers.txt
CONFLICT (content): Merge conflict in numbers.txt
Automatic merge failed; fix conflicts and then commit the result.

如您所见,合并被暂停。您必须解决冲突,然后提交结果。$ git status -s的输出是:

UU numbers.txt

冲突文件标有UU,根据$ git status --help表示已更新但未合并$ git merge fr之后的numbers.txt的内容如清单 9-5 所示。

清单 9-5。numbers . txt 文件的内容紧跟在$ git merge fr 命令之后

1
<<<<<<< HEAD
two
=======
deux
>>>>>>> fr
3

现在,您必须编辑文件并准备您认为是冲突的适当解决方案的内容。你可以使用任何你喜欢的编辑器,你可以插入任何你喜欢的内容。打开文件numbers.txt,根据清单 9-6 进行更改。

清单 9-6。numbers . txt 文件的内容带有手动编辑的内容

1
two - deux
3

文件保存后,您可以验证其状态没有改变。命令$ git status -s返回与之前相同的输出:UU numbers.txt

一旦您手动解决了冲突,您可以将文件的状态从UU更改为M_。这是通过$ git add numbers.txt命令完成的。用$ git commit --no-edit命令提交更改,你将完成配方。

它是如何工作的

$ git merge命令产生冲突时,合并暂停。留给您的是一个存储库,其中的一些文件被标记为UU。这些文件包含必须手动解决的冲突。

每个冲突都用特殊的标记表示:

<<<<<<<
=======
>>>>>>>

冲突的第一部分来自你当前的分支,存储在 HEAD 中。Git 通知您,您当前的分支(在我们的菜谱中是en)包含单词two:

<<<<<<< HEAD
two
=======

冲突的第二部分来自于fr分支。冲突的词是deux。该信息显示为:

=======
deux
>>>>>>> fr

如何解决 git 中的冲突?这是通过一个命令$ git add 完成的。从现在起,你需要记住一个简单的规则:暂存文件解决冲突。就状态而言,我们可以说暂存文件会将其状态从UU更改为M_。起初,文件的内容并不重要,这可能令人惊讶。如果您愿意,您可以保留清单 9-5 中所示的文件,并使用<<<<<<<, =======, >>>>>>>标记提交它。当您编辑文件并删除这些标记时,并不意味着您已经解决了冲突。只有当你暂存一个文件时才这样做(例如,用$ git add命令)。

有时,您需要通过使用一个分支中引入的内容并忽略另一个分支中的更改来解决冲突。这对二进制文件尤其重要。您可以使用两个命令来实现这一点:

$ git checkout --ours [filename]
$ git checkout --theirs [filename]

--ours标志表示当前分支。这就是菜谱里的en--theirs标志表示传递给$ git merge命令的分支。在这个秘籍中,它是fr分支。换句话说,命令$ git checkout --ours numbers.txt将在工作目录中产生如列表 9-2 所示的文件,而命令$ git checkout --theirs numbers.txt—如列表 9-3 所示的文件。请注意,这些命令不能解决冲突。它们只恢复文件的内容,而不改变其状态。恢复的文件保持在UU状态。

如果你想生成清单 9-5 中所示的文件,你可以使用:

$ git checkout --merge [filename]

使用上面的命令,您将得到一个文件,其中的冲突用<<<<<<< ours>>>>>>> theirs标签表示,如:

1
<<<<<<< ours
two
=======
deux
>>>>>>> theirs
3

上面的输出不包含分支enfr分叉之前的原始行。如果这对您很重要,请使用以下命令:

$ git checkout --conflict=diff3 numbers.txt

它将创建如清单 9-7 所示内容的文件。这一次,该文件包含另一个标记为base的部分。base部分显示存储在合并库中的版本——由$ git merge-base en fr命令返回的提交。

清单 9-7。 冲突以 diff3 格式呈现

<<<<<<< ours
two
||||||| base
2
=======
deux
>>>>>>> theirs
3

image 提示在配方 5-12 中,我们讨论了从任意版本恢复任意文件的命令:$ git checkout [REVISION] [filename]。它可以代替使用--ours--theirs参数。在该配方中,命令$ git checkout en numbers.txt相当于$ git checkout --ours numbers.txt$ git checkout fr numbers.txt相当于$ git checkout --theirs numbers.txt

解决所有冲突后,您可以使用$ git commit --no-edit命令恢复暂停的合并。选项--no-edit不是强制性的,您可以跳过它。但是它减轻了您输入或检查提交消息的负担。

如有疑问,您可以随时使用$ git merge --abort中止正在进行的合并。撤销合并的方法在第六章中讨论。

image 提示解决合并冲突包括三个步骤:1)编辑文件;2)用$ git add命令暂存文件;3)用$ git commit --no-edit命令完成合并。

9-3.重置基础后解决文本冲突

问题

对于在配方 9-1 中创建的存储库,您想要将分支en重置到fr上。你的目标是产生如图 9-3 所示的库。numbers.txt文件的冲突行应该包含清单 9-8 中显示的内容。

9781430261032_Fig09-03.jpg

图 9-3 。配方 9-1 中的存储库,其中分支基于 fr

这个方法是一个简化的场景,其中两个开发人员在不同的分支工作,并在其中一个文件中产生重叠的变更。该秘籍提供了一种方法,将你的工作重新建立在你同事的工作基础上。

清单 9-8。 将 en 分支重置到 fr 后要保留的 numbers.txt 文件

1
deux - two
3

解决办法

从配方 9-1 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 09-01 09-03
$ cd 09-03

$ git rebase fr en命令将en分支复位到fr分支上。重置基础将失败,并显示以下消息:

First, rewinding head to replay your work on top of it...
Applying: Numbers: two
Using index info to reconstruct a base tree...
M       numbers.txt
Falling back to patching base and 3-way merge...
Auto-merging numbers.txt
CONFLICT (content): Merge conflict in numbers.txt
Failed to merge in the changes.
Patch failed at 0001 Numbers: two
The copy of the patch that failed is found in:
   /git-recipes/09-03/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

重设基础以与配方 8-7 相同的方式暂停。你必须解决冲突,然后你可以继续重新基础。

$ git status -s 的输出是:

UU numbers.txt

冲突文件的标记方式与使用UU合并时完全相同。然而,当您打开numbers.txt文件时,您会看到文件的内容发生了变化。$ git rebase fr en之后的numbers.txt文件如清单 9-9 所示。

清单 9-9。numbers . txt 文件的内容紧跟在$ git rebase fr en 命令之后

1
<<<<<<< HEAD
deux
=======
two
>>>>>>> Numbers: two
3

重置基础从检验fr分支的 tip 提交开始。因此,HEAD 部分中呈现的内容来自于fr分支:

<<<<<<< HEAD
deux
=======

在 rebase 期间应用的第一个补丁来自标记为Numbers: two的提交。因此,冲突的第二部分被格式化为:

=======
two
>>>>>>> Numbers: two

现在,您必须编辑文件并输入清单 9-8 中的内容。

最后,用$ git add numbers.txt命令暂存文件。该命令会将文件的状态从UU更改为M_。用暂停的复位:$ git rebase --continue 完成配方继续。

它是如何工作的

如果在重设基准期间有冲突,操作暂停。您必须手动解决冲突。该过程与合并冲突的情况完全相同:编辑文件,然后登台它。同样的规则也适用于此:暂存文件 解决冲突

清单 9-2、9-3、9-7 和 9-9 中所示的numbers.txt文件的四种状态可以用以下命令检索:

$ git checkout --ours numbers.txt
$ git checkout --theirs numbers.txt
$ git checkout --merge numbers.txt
$ git checkout --conflict=diff3 numbers.txt

但是要小心:这次--ours--theirs的意思颠倒了:--ours是指fr分支,--theirs是指en分支。这是因为在fr分支中,重定基础是从最近一次提交的检验开始的。

当所有冲突都解决后,您可以使用$ git rebase --continue继续重置基准,或者使用$ git rebase --abort中止操作。这个操作的撤销在第七章中讨论过。

image 提示解决 rebase 冲突包括三个步骤:1)编辑文件;2)用$ git add命令暂存文件;3)用$ git rebase --continue命令完成复位。

9-4 在二进制文件中创建冲突的更改

问题

您想要创建一个包含两个分支的存储库,这两个分支在合并或重置时会在二进制文件中产生冲突。您想要创建的存储库如图 9-4 所示。

9781430261032_Fig09-04.jpg

图 9-4 。带有分支 a 和 b 的存储库将产生二元冲突

解决办法

创建新的存储库:

$ cd git-recipes
$ git init 09-04
$ cd 09-04

然后按照以下步骤操作:

  1. $ git commit --allow-empty --allow-empty-message -m " "命令创建第一个版本。这就是你如何产生一个带有空注释的空提交。
  2. $ git branch a创建名为a的分支
  3. $ git branch b创建名为b的分支
  4. a分支中创建新的提交
    • a.用$ git checkout a切换到a分支
    • b.创建显示一只猫的图像,并将其保存在名为picture.jpg的文件中
    • c.用$ git snapshot Cat提交更改
  5. b分支中创建新的提交
    • a.用$ git checkout b切换到b分支
    • b.创建显示一只狗的图像,并将其保存在名为picture.jpg的文件中
    • c.用$ git snapshot Dog提交更改

$ git checkout a命令完成配方。

它是如何工作的

这一次,存储库包含以相同名称保存在文件中的两个不同的图像。存储在名为a的分支中的文件显示一只猫,存储在b分支中的文件显示一只狗。

注意,命令$ git commit --allow-empty --allow-empty-message -m " "产生了带有空消息的空提交。

9-5.合并期间解决二元冲突

问题

你想用$ git merge命令合并配方 9-4 中创建的分支ab。您想要获得图 9-5 中所示的库。合并后的存储库应该包含来自b分支的picture.jpg文件。

9781430261032_Fig09-05.jpg

图 9-5 。来自配方 9-4 的储存库,具有合并的分支 a 和 b

解决办法

从配方 9-4 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 09-04 09-05
$ cd 09-05
$ git checkout a

你现在的部门是a。用$ git merge b命令将b分支合并成a。正如你所猜测的,合并失败了。该消息通知您在picture.jpg文件中有二进制冲突:

warning: Cannot merge binary files: picture.jpg (HEAD vs. b)
Auto-merging picture.jpg
CONFLICT (add/add): Merge conflict in picture.jpg
Automatic merge failed; fix conflicts and then commit the result.

合并暂停,冲突的二进制文件用以下符号表示:

AA picture.jpg

通过$ git status -s命令。您必须选择文件的一个版本。使用$ git checkout --theirs picture.jpg命令选择显示狗的图像。

一旦您签出了文件的适当版本,您可以将文件的状态从AA更改为M_。这是通过$ git add picture.jpg命令完成的。用$ git commit --no-edit提交更改,您将完成合并。当合并完成后,打开你最喜欢的图像编辑器,确认picture.jpg显示一只狗。

它是如何工作的

二进制文件比文本文件引起更多的无故障冲突。这是因为 git 不能将两个不同的二进制文件合并成一个文件。没有方法产生图 9-6 中所示的合并文件。你只能使用图像编辑器,比如 Gimp,git 不能帮助你。对于二进制文件,您只能请求文件的第一个或第二个版本。这是通过两个命令完成的:

$ git checkout --ours [filename]
$ git checkout --theirs [filename]

9781430261032_Fig09-06.jpg

图 9-6 。Git 不能将两个独立的二进制文件合并成一个文件

或者用:

$ git checkout a [filename]
$ git checkout b [filename]

冲突的二进制文件用AA表示。这是另一个区别,因为文本冲突用UU表示。

其余的,即解决冲突和完成合并,如前面使用$ git add$ git commit --no-edit命令所做的那样执行。

9-6.在重置基期间解决二进制冲突

问题

当在配方 9-4 中创建的存储库中工作时,您可能希望将分支a重置到b上。您想要获得图 9-7 中所示的存储库。当你完成重置基础时,分支a应该包含一张显示一只猫的图片。

9781430261032_Fig09-07.jpg

图 9-7 。您希望在配方 9-6 中生成的存储库

解决办法

从配方 9-4 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 09-04 09-06
$ cd 09-06

$ git rebase b a命令将分支a复位到b上。重置基础将失败,并显示以下消息:

First, rewinding head to replay your work on top of it...
Applying: Cat
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
warning: Cannot merge binary files: picture.jpg (HEAD vs. Cat)
Auto-merging picture.jpg
CONFLICT (add/add): Merge conflict in picture.jpg
Failed to merge in the changes.
Patch failed at 0001 Cat
The copy of the patch that failed is found in:
   c:/git-recipes/09-06/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

重置基础已暂停,您必须解决冲突。$ git status -s的输出是:

AA picture.jpg

要从分支a恢复显示猫的图像,使用$ git checkout --theirs picture.jpg命令。这次--ours是分支b--theirs是分支a

$ git add picture.jpg$ git rebase --continue命令完成配方。最后打开你最喜欢的图片编辑器,确认picture.jpg显示的是一只猫。

它是如何工作的

重定基过程中的二进制冲突的处理几乎与配方 9-5 相同。合并和重定基础的唯一区别是--ours--theirs的角色颠倒了。这在表 9-1 中进行了总结。

表 9-1 。在合并和重组过程中我们和他们的角色

|

命令

|

-我们的

|

-他们的

|
| --- | --- | --- |
| $ git checkout a
| a | b |
| $ git checkout a
| b | a |

9-7.合并期间强制二进制模式

问题

该配方的起点是在配方 9-1 中创建的存储库。你想要合并两个分支enfr,以二进制模式合并numbers.txt文件的两个版本。

解决办法

从配方 9-1 克隆存储库:

$ cd git-recipes
$ git clone-with-branches 09-01 09-07
$ cd 09-07
$ git checkout en

现在创建一个名为.gitattributes的文件,包含单行numbers.txt binary。您可以使用一个命令来完成:

$ echo "numbers.txt binary" > .gitattributes

提交这个新文件

$ git snapshot .gitattributes rule to force binary type of numbers.txt

最后,用$ git merge fr命令将fr分支合并成en。这一次你会得到这样的信息:

warning: Cannot merge binary files: numbers.txt (HEAD vs. fr)
Auto-merging numbers.txt
CONFLICT (content): Merge conflict in numbers.txt
Automatic merge failed; fix conflicts and then commit the result.

如您所见,numbers.txt文件被视为二进制文件。命令$ cat numbers.txt打印:

1
two
3

因此,文件的两个版本没有合并。工作目录包含来自en分支的版本(目前是--ours)。

它是如何工作的

git 如何知道哪些文件是二进制的,哪些文件有文本内容?它检查文件的前 8,000 个字节是否出现空字节。如果文件包含代码为 0 的字节,则认为它是二进制文件。否则,它将被视为文本文件。

要强制将文件视为二进制文件,可以使用:

filename binary

.gitattributes文件中的规则。同样,您也可以使用以下规则强制将二进制文件视为文本文件:

filename text

在这两种情况下,filename都可以用一个模式代替。这里有两个例子:第一个强制将所有在bindir/下的文件视为二进制文件,另一个强制将所有文件名以.xyz结尾的文件视为文本文件:

bindir/ binary
*.xyz text

一般来说,.gitattributes文件的语法允许您为每个模式定义一个属性列表:

pattern attribute-1 attribute-2 attribute-3 ...

模式定义了规则应该影响哪些文件。以下是四个模式示例:

*             # all files
*.txt         # all files ending with .txt
somedir/      # all files under somedir/ directory
readme.txt    # one file readme.txt

对于每个模式,您可以应用任意数量的属性,如下所示:

*.txt         text -merge eol=crlf

这一行定义了一个规则,将用于所有匹配*.txt模式的文件。该规则由三个条目组成:

text
-merge
eol=crlf

第一个条目由一个单词text组成,为所有匹配的文件设置text属性。因此,所有的*.txt文件都将被视为文本文件,因此 git 将对它们执行行尾规范化。

第二个条目由一个单词merge组成,前面有一个破折号,它取消了merge属性。这意味着所有的*.txt文件将被合并为二进制文件。

最后一个规则设置了在结帐过程中应该使用的行尾字符。

所有可用属性的列表汇总在表 9-2 中。

表 9-2 。可用属性的列表

|

属性

|

描述

|
| --- | --- |
| binary | 关闭三个属性:diff, merge, text |
| conflict-marker-size | 定义冲突标记的长度。 |
| delta | 对于由属性增量设置为false的路径生成的 blobs,不会尝试增量压缩。 |
| diff | 该属性影响 git 对给定文件执行$ git diff操作的方式。 |
| encoding | 该属性的值指定 GUI 工具(例如 gitk 和 git-gui)应该使用的字符编码,以显示相关文件的内容。 |
| eol | 该属性定义了在签出过程中使用的行尾。 |
| export-ignore | 从使用$ git archive命令生成的档案中排除文件。 |
| export-subst | 在执行$ git archive命令时,用其他文件替换文件。 |
| filter | 此属性可用于在签出和签入期间执行附加处理。 |
| ident | 该属性允许在文件中嵌入$Id$变量。这些变量在签入和签出期间进行处理。 |
| merge | 定义文件是否可以合并为带有标记<<<<<<<, =======, >>>>>>>的文本文件,或者是否应该被视为二进制文件。 |
| text | 该属性控制行尾规范化。 |
| white-space | 此属性允许您定制空白错误的控制。 |

所有属性的完整描述可以通过$ git attributes --help命令获得。

摘要

这一章是与其他开发者合作之前的最后一步,也是必要的一步。它为您提供以下问题的准确答案:

  • 两个人修改一个文本文件的完全相同的一行会发生什么?
  • 当两个开发人员更改存储不同内容的同一个二进制文件时会发生什么?
  • git 如何决定哪些文件是二进制的,哪些不是?

第一个问题是文本文件中的重叠更改,这将导致合并或重设基础操作期间的文本冲突。在这两种情况下,操作都会暂停,您必须解决所有冲突。冲突解决后,您可以使用$ git commit命令完成合并。使用$ git rebase --continue命令恢复暂停的重置基础。

冲突的文本文件由$ git status –s命令表示为UU。重叠部分标有<<<<<<<, =======, >>>>>>>。这些标记的长度可以通过表 9-2 中的conflict-marker-size属性进行调整。您必须手动编辑文件,并为每个冲突决定您认为合适的内容。请记住,移除标记<<<<<<<=======>>>>>>>并不能解决冲突。即使您删除所有标记并保存文件,它仍然处于UU状态。要将文件的状态从UU更改为M_,您必须暂存文件。这可以通过$ git add [filename]命令来完成。

二进制文件也会导致冲突,但是在这种情况下,git 不能将两个不同的二进制文件合并成一个文件。你将会看到一个文件的第一个或第二个版本,这取决于你是否使用了合并或重建基础。冲突的二进制文件表示为AA。通过使用$ git add [filename]命令暂存文件,可以完全像在文本情况下一样解决冲突。

在文本冲突的情况下,以下四个命令可用于生成四个版本的冲突文件:

$ git checkout --ours [filename]
$ git checkout --theirs [filename]
$ git checkout --merge [filename]
$ git checkout --conflict=diff3 [filename]

对于二元冲突,只能使用前两个命令。

git 如何决定哪些文件是二进制,哪些是文本?它浏览每个文件的前 8,000 个字节。如果它发现一个空字节,文件被认为是二进制的。否则,该文件被视为文本文件。您也可以使用一个.gitattributes文件来详细指定每个路径的类型。您可以将该文件视为在每个模式级别上指定各种文件属性的一种方式。

十、远程存储库和同步

所有 VCS 系统背后的内在原因是使一组开发人员之间的协作尽可能无缝。最后,我们已经到了可以讨论如何使用 git 进行小组工作的时候了。我们将从最简单的设置开始,所有的存储库都可以通过本地存储获得。

首先你必须学会如何使用遥控器。我们将在一个菜谱中讨论这个问题,这个菜谱将向您展示当您克隆一个存储库时会发生什么。然后,我们将深入介绍两个或更多开发人员如何合作的分步方法。我们将考虑两种重要情况:

  • 第一种:所有成员共享和访问一个空存储库
  • 第二种:两个开发者直接合作,不需要任何额外的存储库

您可以将第一个视为集中式客户机/服务器解决方案,而将另一个视为对等解决方案。Git 是一个分布式系统,允许您混合两种方法。

从中央存储库下载新贡献的最简单方法是使用$ git pull命令。当使用默认设置时,这会导致项目的历史非常复杂。我会告诉你什么时候,为什么你会遇到麻烦。这将引导我们改进秘籍,永远为你提供一个干净的历史。

我在配方 5-2 中提到的不同类型分支的主题将再次出现。这一次,我们将坚持到底。您将了解关于远程分支机构、本地跟踪分支机构和远程跟踪分支机构的所有信息。我不仅将向您展示如何列出、创建、销毁和同步它们,还将展示不同的命令,如$ git commit$ git fetch,如何影响它们的状态。本章将让您对远程分支机构、远程跟踪分支机构和本地跟踪分支机构有一个全面而完整的了解。通过获得这些技能,你将准备好加入任何使用 git 的团队。

10-1.手动克隆

问题

你想对克隆有更深入的了解。实现这一点的一种方法是手动克隆一个存储库。您想要执行一个手动克隆,在这个过程中,每一个内部 git 操作,比如新存储库的初始化和从远端获取修订,都是用一个更专门的命令来执行的。继续这个配方克隆https://github.com/creationix/js-git库。js-git 项目是 git 的初步 JavaScript 实现。

解决办法

使用以下内容创建一个新目录:

$ cd git-recipes
$ mkdir 10-01
$ cd 10-01

然后按照以下步骤操作:

  1. 使用$ git init初始化新的存储库
  2. 添加远端的网址:$ git remote add origin https://github.com/creationix/js-git.git
  3. 从远端获取 git 数据库和远程跟踪分支:$ git fetch --no-tags origin master:refs/remotes/origin/master
  4. 创建一个本地的master分支,它将指向与origin/master远程跟踪分支相同的版本:$ git rev-parse origin/master > .git/refs/heads/master
  5. $ git branch --set-upstream-to=origin/master设置master分支为远程跟踪分支origin/master的本地跟踪分支
  6. 将默认分支的信息存储在本地的远程存储库中:$ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master
  7. 签出工作目录中的文件:$ git checkout

它是如何工作的

这个秘籍揭开了克隆操作的神秘面纱。它将克隆分为:

  • 初始化
  • 遥控器的定义
  • 下载 git 数据库和远程跟踪分支
  • 创建适当的分支

Git 启动一个克隆,用$ git init命令初始化一个新的空存储库。在这个命令之后,存储库是空的——它不包含任何分支。$ git branch的输出为空。

要从外部来源复制修订,我们需要一个 URL。这个 URL 是用$ remote add [alias] [URL]命令设置的。第一个参数是短别名;第二个参数是一个 URL。一旦你定义了一个远程 origin用:

$ git remote add origin https://github.com/creationix/jz-git.git

您可以使用短别名origin来代替完整的 URL。命令:

$ git fetch --no-tags origin master:refs/remotes/origin/master

相当于:

$ git fetch --no-tags https://github.com/creationix/js-git.git master:refs/remotes/origin/master

可以用$ git remote命令列出遥控器。默认情况下,该命令打印定义的别名。附加参数-v打开详细输出。命令$ git remote -v打印所有别名的名称和 URL。可以使用$ git remote rm [alias]命令移除遥控器。

所有遥控器都存储在.git/config文件中。当您执行:$ git remote add foo https://example.comnet/bar.git时,git 会在.git/config文件中添加以下条目:

[remote "foo"]
    url = https://example.comnet/bar.git
    fetch = +refs/heads/*:refs/remotes/foo/*

该行:

url = https://example.comnet/bar.git

存储 URL。第二行:

fetch = +refs/heads/*:refs/remotes/foo/*

定义了所谓的 refspec 。Refspec 指定了远程分支(即远程存储库中的分支)映射到远程跟踪分支(即存储在refs/remotes/foo目录中的本地分支)的方式。远程存储库在其.git/refs/heads目录中包含分支。我们希望以这样一种方式复制它们,使得它们不会与我们在.git/refs/heads本地存储的普通本地分支相冲突。因此,我们将远程跟踪分支放在一个名为.git/refs/remotes/foo的单独目录中。只要用于命名远程的别名是唯一的,我们就可以确保来自不同远程的分支不会相互冲突,也不会与我们的本地分支冲突。

您可以治疗:

fetch = +refs/heads/*:refs/remotes/foo/*

两个目录之间的 1:1 映射:一个在远程存储库中,另一个在本地存储库中。上面说明了在远端的.git/refs/heads目录中别名为foo的所有文件都被映射到本地名为.git/refs/remotes/foo的目录中。该映射在$ git fetch$ git push操作期间使用。放置在 refspec 最开始的字符+允许您推送修订,这些修订将覆盖存储在远程存储库中的历史。

值得注意的是,除了存储在.git/config文件的[remote "foo"]部分中的配置之外,两个存储库之间没有其他依赖关系。遥控器的名称只是一个别名,使您的命令更短。您可以使用遥控器的名称,而不是键入完整的 URL。此外,远程别名只存储在本地存储库中。远程终端不会在其配置中存储任何有人使用其 URL 的信息。

image 提示 Origin 是 git 在克隆操作期间为远程存储库使用的标准名称。这没有什么神奇的:你可以用$ git remote rm origin删除一个原始遥控器。你可以用$ git remote add origin [URL]创建一个新的原点遥控器。

一旦定义了远程,我们就可以将 git 数据库从远程复制到本地存储库中。这是通过$ git fetch --no-tags origin master:refs/remotes/origin/master命令完成的。在这个命令之后,存储库包含一个远程跟踪分支。命令$ git branch -a -vv输出类似于:

remotes/origin/master 60478cc Bump version to 0.3.1

参数--no-tags 确保包含在远程储存库中的标签不被复制。下一个参数origin给出了我们想要从中复制修订的遥控器的名称。最后一个参数master:refs/remotes/origin/master是一个 refspec。它由冒号分隔的两个名称组成:

  • master—远程存储库中的远程分支的名称,别名为origin
  • refs/remotes/origin/master—远程跟踪分支的名称(它是本地存储库中的本地分支)

refspec 确保远程master分支将被复制到本地.git/refs/remotes/origin/master文件中。在 fetch 命令之后,本地存储库中的.git/objects目录包含了从远程存储库中复制的对象。您应该注意到,$ git fetch命令创建了一个本地文件.git/refs/remotes/origin/master。这是远程分支的副本。副本作为远程跟踪分支存储在本地存储库中。回到配方 5-2,我强调了远程跟踪分支是本地分支。这就是远程跟踪分支是如何创建的:它们在$ git fetch命令之后出现在您的存储库中。您可以通过在$ git fetch之后立即发出的$ git branch -a -vv命令来验证这一点。即使你用$ git branch -d -r命令删除了一个远程跟踪分支,它也会在下一个$ git fetch命令后被重新创建。

下一步就是成立当地的master分公司。正如您已经知道的,一个普通的本地分支只是一个存储适当的 SHA-1 名称的文本文件。我们希望我们的分支指向与获取操作期间创建的.git/refs/remotes/origin/master分支相同的修订。存放在.git/refs/remotes/origin/master的分支可以简称为origin/master。你如何找到由一些象征性的参考origin/master所指向的修订版的 SHA-1 名字?为此,我们可以使用$ git rev-parse命令。运行命令$ git rev-parse origin/master。它将打印出由.git/refs/remotes/origin/master分支指向的修订的 SHA-1 名称。要创建一个指向相同版本的普通本地分支,将 SHA-1 存储在一个文本文件中就足够了:

$ git rev-parse origin/master > .git/refs/heads/master

image 提示$ git rev-parse origin/master > .git/refs/heads/master命令的结果也可以用$ cp .git/refs/remotes/origin/master .git/refs/heads/master命令来实现。

上面的命令创建了一个名为master的普通本地分支。$ git branch -a -vv命令的输出应该类似于:

* master                60478cc Bump version to 0.3.1
  remotes/origin/master 60478cc Bump version to 0.3.1

现在我们把一个普通的本地分支master变成一个本地跟踪分支,用于远程跟踪origin/master分支。这通过以下方式完成:

$ git branch --set-upstream-to=origin/master

在此之后,$ git branch -a -vv版画:

* master                60478cc [origin/master] Bump version to 0.3.1
remotes/origin/master 60478cc Bump version to 0.3.1

感谢第一线的[origin/master],我们知道master是远程跟踪分支origin/master的本地跟踪分支。

命令$ branch --set-upstream-to=origin/master.git/config文件中创建以下条目:

[branch "master"]
    remote = origin
    merge = refs/heads/master

这表示您的本地master分支被设置为跟踪存储在由origin URL 指向的存储库中的refs/heads/master中的远程分支。通过对本地和远程分支使用不同名称的示例,更容易理解上面命令的含义。假设您的本地存储库包含一个名为foo的普通本地分支。您希望foo分支跟踪远程存储库中的bar分支。如果您发出以下命令:

$ git branch --set-upstream-to=origin/bar foo

然后将创建以下配置条目:

[branch "foo"]
    remote = origin
    merge = refs/heads/bar

origin指向的远程存储库包含一个文件.git/refs/heads/bar。这是bar的远程分支。本地存储库包含.git/refs/remotes/origin/bar.git/refs/heads/foo。第一文件.git/refs/remotes/origin/bar是远程跟踪分支,第二文件.git/refs/heads/foo是本地跟踪分支。本地跟踪支路foo与远程跟踪支路origin/bar相连。

image 提示你也可以用$ git branch master origin/master命令创建指向origin/mastermaster分支。我避免了上面的命令,因为它不仅创建了一个本地的master分支,还设置了跟踪。我更喜欢把两项业务分开。因此我用$ git rev-parse$ git branch --set-upstream-to来分别执行这两个动作。

这个过程的最后一步是在本地远程存储库中存储关于默认分支的信息:

$ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master

该命令将创建一个本地文件.git/refs/remotes/origin/HEAD。该文件将包含一个指向refs/remotes/origin/master的符号引用。这就是我们如何知道在远端哪个分支被认为是默认的。

Git 允许使用$ git config命令直接操作其配置。因此在foo分支发出的命令$ git branch --set-upstream-to=origin/bar相当于两个命令:

$ git config branch.foo.remote origin
$ git config branch.foo.merge refs/heads/bar

使用一个额外的--unset参数你也可以取消设置任意选项。在配方 10-5 中,我们将使用:

$ git config --unset branch.foo.remote
$ git config --unset branch.foo.merge

取消跟踪。

10-2.与中央存储库 合作

问题

您希望使用一个中央存储库来模拟两个开发人员 John 和 Sarah 的合作。在这种情况下,协作 将由三个库组织:

  • 第一个开发者的非裸存储库
  • 10-02/sarahs-repo—第二个开发者的非裸库
  • 一个用来同步约翰和莎拉工作的空仓库

在这个配方中,两个开发者将只使用master分支 来工作。您希望分析在以下情况下会发生什么:

  • 每个开发人员在master分支继续他或她的工作
  • 一个开发人员将他或她的修改发送到共享的回购协议中
  • 另一个开发人员获取新的版本

image 提示这个菜谱展示了如何围绕一个中心库来组织团队的工作。这个工作流程类似于集中式系统使用的客户端/服务器方法,如 CVS 或 SVN。

解决办法

使用以下内容创建新目录:

$ cd git-recipes
$ mkdir 10-02
$ cd 10-02

工作由开发人员之一初始化 。我们假设是约翰开始了整个项目:

# the command issued in git-recipes/10-02 directory
$ git init --bare shared-repo

这个存储库将用于同步 John 和 Sarah 的工作。

接下来,约翰创建了自己的知识库:

# john's commands in git-recipes/10-02 directory
$ git init johns-repo

然后他设置他的个人数据并定义origin遥控器 ??:

# john's commands
$ cd johns-repo
$ git config --local user.name john
$ git config --local user.email john@example.net
$ git remote add origin ../shared-repo

这两个库现在看起来像图 10-1 。图中未示出元信息user.nameuser.emailremote.origin

9781430261032_Fig10-01.jpg

图 10-1 。初始化后的两个存储库

然后,John 在他的私有存储库 中创建了一些修订:

# john's command
$ git simple-commit a1 a2 a3

现在,John 的存储库中包含一个名为master的普通分支。$ git branch -a -vv的输出类似于:

* master dc30648 a3

存储库看起来像图 10-2 。

9781430261032_Fig10-02.jpg

图 10-2 。约翰的 a1、a2、a3 提交后的储存库状态

约翰将他的a1a2a3修订发送到shared-repo存储库 中:

# john's command
$ git push -u origin master

该命令在远程shared-repo存储库中创建一个新的分支。新的远程分支被命名为master。由于有了-u参数,上面的命令在johns-repo中创建了一个远程跟踪分支origin/master。您可以使用$ git branch -a -vv命令进行验证。输出将类似于:

* master                dc30648 [origin/master] a3
  remotes/origin/master dc30648 a3

如您所见,John 的存储库包含一个本地跟踪分支master和一个远程跟踪分支origin/master。我们可以说,-u参数将名为master的普通本地分支转换为本地跟踪分支。存储库现在看起来像图 10-3 。

9781430261032_Fig10-03.jpg

图 10-3 。约翰发布的$ git push -u origin master 的效果

接下来莎拉加入了这个项目。她克隆了:

# sarah's commands
# executed in git-recipes/10-02
$ git clone shared-repo sarahs-repo
$ cd sarahs-repo

因为莎拉使用了$ git clone命令 ??,她的master分支被设置为跟踪远程master分支。这三个存储库现在看起来如图 10-4 所示。

9781430261032_Fig10-04.jpg

图 10-4 。莎拉发行的$ git 克隆共享回购 sarahs-repo 的效果

现在轮到莎拉为这个项目做贡献了。她创建了两个版本b1b2:

# sarah's command
$ git simple-commit b1 b2

存储库现在看起来像图 10-5 。

9781430261032_Fig10-05.jpg

图 10-5 。在 Sara 已经创建了 b1 和 b2 提交 ?? 之后仓库的状态

下一步,Sarah 将她的修订发送给shared-repo,内容如下:

# sarah's command
$ git push origin master

注意 Sarah 不需要使用-u。她用$ git clone命令初始化了她的存储库,因此自动初始化了对master分支的跟踪。John 用$ git init初始化了他的存储库。这就是为什么他第一次推的时候需要用-u。Sarah 的 push 命令的结果如图 10-6 所示。

9781430261032_Fig10-06.jpg

图 10-6 。萨拉的$ git push -u origin master 命令之后的仓库

现在轮到约翰下载莎拉的修改。他跑着说:

# john's command
$ git pull origin master

这导致了图 10-7 中所示的状态。

9781430261032_Fig10-07.jpg

图 10-7 。后一库约翰的$ git 拉原点主控命令

上述模式可以重复任意多次。在约翰和莎拉的主分支分开之前,没有什么特别的事情发生。我们来分析一个这样的案例。

约翰和莎拉的作品

这一次,John 和 Sarah 都在他们的存储库中独立工作。约翰创建修订版a4a5,而莎拉创建修订版b3:

# john's command
$ git simple-commit a4 a5

# sarah's command
$ git simple-commit b3

图 10-8 中描述了您将获得的存储库。

9781430261032_Fig10-08.jpg

图 10-8 。约翰和莎拉的分支分叉的存储库

现在约翰和莎拉都想把他们的修改推进shared-repo。我们假设 Sarah 是第一个执行$ git push命令的人。之后:

# sarah's command
$ git push origin master

存储库看起来像图 10-9 。

9781430261032_Fig10-09.jpg

图 10-9 。Sarah 成功的$ git push origin master 命令之后的存储库

现在约翰想把他的作品寄给shared-repo:

# john's command
$ git push origin master

Git 拒绝提交,因为johns-repo已经过时。上述命令的输出包含以下消息:

! [rejected]        master -> master (fetch first)

Git 通知 John,他的推送被拒绝,他必须首先获取缺失的修订。为了更新他的本地master分支,John 运行以下命令:

# john's command
$ git pull origin master

pull 命令获取 Sarah 的b3修订版并执行合并操作。现在存储库看起来像图 10-10 。

9781430261032_Fig10-10.jpg

图 10-10 。John 的$ git pull origin master 命令之后的存储库

John 的$ git pull origin master命令使用 Sarah 的最新版本更新了他的存储库,因此 John 现在可以使用以下命令将他的工作推送到shared-repo:

# john's command
$ git push origin master

注意,John 不再需要参数-u了,因为跟踪已经被 John 对$ git push -u的第一次调用所定义。存储库看起来像图 10-11 。

9781430261032_Fig10-11.jpg

图 10-11 。John 成功的$ git push origin master 命令之后的存储库

最后,Sarah 使用以下方法完成了 John 的工作:

# sarah's command
$ git pull origin master

产生的储存库如图 10-12 所示。

9781430261032_Fig10-12.jpg

图 10-12 。配方 10-2 中存储库的最终状态

它是如何工作的

在我们深入研究用于更新三个存储库的命令之前,让我们从分析图 10-12 中所示的所有三个存储库的内容开始。它们都包含完全相同的提交。您可以使用$ git log命令来验证这一点。进入git-recipes/10-02/johns-repo目录,运行下面的$ git log命令:

$ cd git-recipes/10-02/johns-repo
$ git log --oneline

您将获得类似于清单 10-1 中所示的输出。

清单 10-1。 图 10-12 中的在 johns-repo 中执行的$ git log-one line 的输出

515710e Merge branch 'master' of ../shared-repo
596e379 b3
5d11316 a5
2f46c63 a4
82d0a6b b2
6075835 b1
73e4416 a3
44fc529 a2
c8e56d1 a1

对 Sarah 的存储库重复类似的命令:

$ cd git-recipes/10-02/sarahs-repo
$ git log --oneline

上述命令将打印清单 10-2 中的输出。

清单 10-2。$ git log 的输出-one line run in sarahs-repo from 图 10-12

515710e Merge branch 'master' of ../shared-repo
596e379 b3
5d11316 a5
2f46c63 a4
82d0a6b b2
6075835 b1
73e4416 a3
44fc529 a2
c8e56d1 a1

尽管实际打印在屏幕上的 SHA-1 会有所不同,但您应该注意到两个输出是完全相同的。从图 10-12 中可以猜出shared-repo中执行的$ git log的输出:

$ cd git-recipes/10-02/shared-repo
$ git log --oneline

也会一模一样。存储在:johns-repo/.git/objectssarahs-repo/.git/objectsshared-repo/.git/objects中的三个数据库包含完全相同的对象。这由以下事实来证明:在所有三个存储库中由$ git log返回的 SHA-1 名称是相同的。

换句话说,当您使用$ git push$ git pull发送或接收修订时,git 会在存储库之间复制数据库条目。这与$ git rebase$ git cherry-pick$ git commit --amend命令相反。当您在本地存储库中工作时,没有复制现有修订的方法—您所能做的就是用新的 SHA-1 名称创建一个新的数据库对象。另一方面,在$ git push$ git pull命令期间,修改被复制而不是重新提交。复制的对象与原始对象具有相同的 SHA-1。

在这个配方中,我们使用了两个命令来发送和接收远程存储库的修订:

  • $ git push—将修订从本地发送到远程
  • $ git pull—将修订从远程下载到本地,然后与适当的分支合并

这些命令带有两个参数:

$ git push origin master
$ git pull origin master

在这两种情况下,origin是远程的名称,而master是本地分支的名称。

第一个命令$ git push origin master,将本地分支发送到远程存储库。更准确地说,我们可以说该命令将丢失的修订从本地存储库发送到远程端,然后更新分支(远程分支和远程跟踪分支——我们将很快讨论这一点)。

当 John 对图 10-2 中的仓库执行$ git push origin master时,首先 git 将三个版本a1a2a3johns-repo的数据库复制到shared-repo的数据库,然后更新分支。

默认情况下,git 将$ git push操作限制为快进情况。这意味着只有当远程分支可以快速推进您的工作时,$ git push才会成功。这是约翰将图 10-2 中的状态变为图 10-3 中的状态的情况。图 10-2 中库shared-repo为空;因此它可以安全地接收三个提交a1a2a3。当 Sara 将存储库从图 10-5 中的更改为图 10-6 中的所示的存储库时,也会发生同样的情况。图 10-5 中所示的库shared-repo包含了a1a2a3三个版本。不包含sarahs-repoa3之前的b1b2修订。shared-repo中的master分支可以用b1b2版本快进,因此操作成功。

图 10-9 所示的情况要复杂得多。共享存储库中的master分支 和 John 的存储库中的master分支已经分离。约翰的包含a4a5,共享存储库包含b3。因此约翰在图 10-9 所示的库中执行的$ git push origin master失败。Git 打印了 John 首先从远程存储库获取修订所需的信息。

当与-u参数一起使用时,$ git push命令存储关于跟踪的信息。命令:

$ git push -u origin master

当约翰将他的a1a2a3修订版本推送到约翰的存储库中的远程跟踪分支remotes/origin/master时,由约翰执行。master分行被设置为origin/master分行的本地跟踪分行。只有在第一次调用$ git push时才需要这个参数。

该库中使用的新命令是$ git pull origin master。这个命令从origin指向的远程存储库中复制修订。修订从远程的master分支复制,然后与本地存储库中当前的master分支合并。如果操作可以作为快进执行,则不存在合并提交。这是将图 10-6 所示的状态变为图 10-7 所示的状态。当本地和远程分支出现分歧时,该命令会生成一个合并提交。这就是为什么我们在清单 10-1 和 10-2 的顶部显示了合并提交。当您将图 10-9 所示的状态更改为图 10-10 所示的状态时,合并提交出现在johns-repo中。

提交如何影响跟踪分支

我们再次从 Sarah 的回购的角度分析跟踪分支如何在提交 期间改变。在$ git clone shared-repo sarahs-repo之后,仓库shared-reposarahs-repo看起来像图 10-13 。

9781430261032_Fig10-13.jpg

图 10-13 。克隆后立即共享回购和 sarahs 回购

我们从莎拉的角度来看这个情况。因此sarahs-repo是本地存储库,shared-repo是远程存储库,如图 10-13 中的所示。该图显示了三种类型的分支:远程分支、本地跟踪分支和远程跟踪分支。Sarah 的存储库不包含任何普通的本地分支。当 Sarah 克隆存储库时,$ git clone命令会自动为她创建两个本地分支:masterorigin/master。第一个是本地跟踪分支;第二个是远程跟踪分支。您可以使用$ git branch -a -vv命令验证这一点。输出将包含两行重要的内容:

* master                36c7205 [origin/master] a3
  remotes/origin/master 36c7205 a3

第一行表示masterorigin/master分支的本地跟踪分支。第二行表示origin/master是一个远程分支。

当 Sarah 用$ git simple-commit b1 b2提交时,她将她的master分支(即本地跟踪分支)向前移动。$ git simple-commit b1 b2后的分支状态如图图 10-14 所示。

9781430261032_Fig10-14.jpg

图 10-14 。在 Sarah-repo 中执行$ git simple-commit b1 b2 后分支的状态如图 10-13 所示

您可以使用$ git log --oneline --decorate命令来验证 Sarah 的存储库的状态。输出:

b019 (HEAD, master) b2
978f b1
66ad (origin/master, origin/HEAD) a3
91d5 a2
b189 a1

包含指向适当修订的标签masterorigin/master。关于分支状态的简短信息也可以通过$ git status -sb命令获得。当在图 10-14 的sarahs-repo中执行时,该命令将产生如下输出:

## master...origin/master [ahead 2]

这将通知您,您的本地跟踪分支包含两个未包含在远程跟踪分支中的修订。换句话说:本地跟踪分支master比远程跟踪分支origin/master领先两个版本。

如您所见,提交操作仅向前移动本地跟踪分支。远程跟踪分支和远程分支保持不变。

推动如何影响跟踪分支

我们如何更新shared-repo中的远程分支主机和 Sarah 的存储库中的远程跟踪分支origin/master?这是在莎拉用$ git push origin master命令提交时完成的。该命令改变两个储存库的状态,如图图 10-15 所示。图 10-15 展示了在图 10-14 所示的状态下,执行sarahs-repo$ git push origin master的结果。

9781430261032_Fig10-15.jpg

图 10-15 。在 Sarah-repo 中执行$ git push origin master 命令后,图 10-14 中的存储库的状态

您应该注意到,为了决定应该发送哪些修订,git 只需找到 Sarah 的存储库中两个分支masterorigin/master之间的差异。由于配方 7-2 中讨论的两点,这可以用$ git log --oneline origin/master..master命令来完成。该命令打印包含在master中但不包含在origin/master中的修订列表。

image 提示请记住,远程跟踪分支,比如origin/master,可以像普通本地分支一样在 git 提交中使用。例如,命令$ git branch foo origin/master∼3创建一个名为foo的新的普通本地分支,它指向与origin/master的曾祖父相同的修订。

总而言之,推送以下列方式更新分支:

  • 在本地存储库远程—跟踪分支被更新为本地跟踪分支中的最新版本
  • 在远程存储库中—远程分支被更新为本地跟踪分支中的最新版本

如果推送操作失败会怎么样?如果约翰在他的存储库中执行$ git push origin master,如图 10-9 中的所示,那么推送将被拒绝,并显示以下消息:

! [rejected]        master -> master (fetch first)

所有分支保持不变。

拉动如何影响跟踪分支

这一次我们是从约翰的视角观察变化。因此,johns-repo是本地存储库,shared-repo是远程存储库。

当您从远程存储库提取时,您的本地跟踪分支和远程跟踪分支将会更新。远程分支保持不变。第一种情况(没有合并)在图 10-16 和图 10-17 中描述。图 10-16 显示了拉动操作之前的状态。共享存储库包含不包含在johns-repo中的两个修订b1b2。拉操作将这些修订带入johns-repo,并将分支更新到图 10-17 所示的状态。

9781430261032_Fig10-16.jpg

图 10-16 。在$ git pull origin master 命令之前的 John 的存储库

9781430261032_Fig10-17.jpg

图 10-17 。约翰的仓库在$ git pull origin master 命令后在约翰-repo 中执行如图图 10-16 所示

图 10-18 和 10-19 说明了第二种情况。这是一个快进操作。如果在$ git pull origin master期间发生合并,本地跟踪分支master和远程跟踪分支origin/master以完全相同的方式改变。它们将指向自动生成的合并提交,如图图 10-18 和图 10-19 所示。

9781430261032_Fig10-18.jpg

图 10-18 。共享回购和约翰回购的主要分支出现分化

9781430261032_Fig10-19.jpg

图 10-19 。在图 10-18 中显示的约翰回购中发出的$ git 拉式来源主数据的结果

总而言之,拉操作更新本地跟踪分支和远程跟踪分支,而保持远程分支不变。

image 提示记住:远程分支是远程存储库中的一个分支。远程跟踪分支是本地分支,用作您的工作和远程存储库内容之间的链接。图 5-7 清楚地显示了所有类型的分支。

10-3.为一个提交生成(n-1)个合并提交

问题

如果项目是由 n 个开发人员使用配方 10-2 实现的,您想要检查历史看起来像什么。为此,您需要模拟三个开发人员的工作:John、Sarah 和 Peter。你必须在三个不同的存储库中并行提交:johns-reposarahs-repopeters-repo。然后,您需要同步所有的存储库。正如您将看到的,由$ git pull命令生成的历史将包含大量多余的合并提交。在最坏的情况下,一个提交可以生成多达 n-1 个合并提交,其中 n 是所涉及的开发人员的数量。

解决办法

使用以下内容创建新目录:

$ cd git-recipes
$ mkdir 10-03
$ cd 10-03

然后用以下内容初始化项目:

# the command issued in git-recipes/10-03 directory
$ git init --bare shared-repo

接下来,John 初始化他的存储库,创建一个初始提交,并将其推送到共享存储库:

# commands issued in git-recipes/10-03 directory
$ git clone shared-repo johns-repo
$ cd johns-repo
$ git simple-commit "Initial commit"
$ git push -u origin master

然后,另外两个开发人员使用以下内容创建他们的存储库:

# commands issued in git-recipes/10-03 directory
$ git clone shared-repo sarahs-repo
$ git clone shared-repo peters-repo

现在,所有的开发人员都准备好提交了。在这个配方中,所有的开发人员并行工作。他们每个人都创建了自己的提交:

# command issued in johns-repo
$ git simple-commit "The first commit by John"

# command issued in sarahs-repo
$ git simple-commit "The first commit by Sara"

# command issued in peters-repo
$ git simple-commit "The first commit by Peter"

现在,他们想分享他们的工作。

John 是第一个将他的更改推送到中央存储库的人:

# command issued in johns-repo
$ git push origin master

然后莎拉和彼得拉他们的工作:

# command issued in sarahs-repo
$ git pull --edit origin master

# command issued in peters-repo
$ git pull --edit origin master

他们都输入合并消息。莎拉打字"Sarah merges...",彼得打字"Peter merges..."

现在,莎拉推进她的工作:

# command issued in sarahs-repo
$ git push origin master

然后彼得试着推:

# command issued in peters-repo
$ git push origin master

该推送被拒绝,因此 Peter 合并了shared-repo中的最新更改:

# command issued in peters-repo
$ git pull --edit origin master

Peter 将合并提交的消息键入为"Peter merges again...",然后他用:

# command issued in peters-repo
$ git push origin master

当 John 和 Sara 都接受了 Peter 所做的更改后,菜谱就完成了:

# command issued in johns-repo
$ git pull origin master

# command issued in sarahs-repo
$ git pull origin master

现在,所有四个存储库都包含了清单 10-3 中的历史。清单显示了在四个存储库中执行的$ git log --graph --oneline命令的输出。

清单 10-3。 在菜谱 10-3 中创建的历史

*   901f9b1 Peter merges again...
|\
| *   70984f8 Sarah merges...
| |\
| * | ebf6fff The first commit by Sarah
* | |   192af3a Peter merges...
|\ \ \
| | |/
| |/|
| * | 4721211 The first commit by John
| |/
* | 4314f0a The first commit by Peter
|/
* ebb21d1 Initial commit

它是如何工作的

这个秘籍的目的很简单:我想让你相信秘籍 10-2 中提出的解决方案不是你应该遵循的模式。用$ git pull origin master命令创建的历史将很难阅读。配方 10-3 向您展示了如果一组 n 个开发人员并行工作,并且每个开发人员创建一个提交,那么第一个开发人员所做的提交将生成 n-1 个合并提交。

在我们的配方中,提交"The first commit by John"生成了两个合并:

70984f8 Sarah merges...
192af3a Peter merges...

很容易认识到,如果团队由 n 个开发人员组成,我们将得到 n-1 个合并提交。

看看清单 10-3 中的。显示的历史仅由三次提交生成:每个开发人员一次。如果您的团队由大量定期提交的开发人员组成,那么命令$ git pull origin master将产生一个非常复杂的修订图,其中包含大量多余的合并提交。

image 提示如果你认为清白的历史很重要,你应该把秘籍 10-2 当作一个不可遵循的模式。

10-4.保持历史的线性

问题

您希望组织由任意数量的开发人员组成的团队的工作。每个开发人员都将使用自己的存储库。他们将使用一个中央存储库来共享他们的工作。该设置与制作方法 10-2 相同。

这一次您想要定义一个工作流,它将保证所有存储库中主分支的线性结构。不允许合并提交,并且不应出现在任何存储库中。

为了保持历史的线性,所有团队成员都需要在更新的远程跟踪分支之上重新构建他们的工作。

解决办法

使用以下内容创建新目录:

$ cd git-recipes
$ mkdir 10-04
$ cd 10-04

然后用以下内容初始化一个项目:

# the command issued in git-recipes/10-04 directory
$ git init --bare shared-repo

现在,John 初始化了他的存储库,创建了一个初始提交,并将其推送到共享回购:

# commands issued in git-recipes/10-04 directory
$ git clone shared-repo johns-repo
$ cd johns-repo
$ git simple-commit i1 i2
$ git push -u origin master

然后下一个开发人员 Mark 加入了团队:

# commands issued in git-recipes/10-04 directory
$ git clone shared-repo marks-repo

现在johns-reposhared-repomarks-repo包含相同的提交i1i2$ git status -sb命令只打印当前分支的名称master。所有的存储库都是干净的,并且分支是同步的。所有储存库的状态如图 10-20 所示。

9781430261032_Fig10-20.jpg

图 10-20 。配方 10-4 中所有三个储存库的初始状态

约翰和马克平行工作

约翰和马克同时工作。约翰创建了三个提交j1j2j3,马克创建了两个提交m1m2:

# command issued in johns-repo
$ git simple-commit j1 j2 j3

# command issued in marks-repo
$ git simple-commit m1 m2

现在,在约翰的报告中执行的命令:

# command issued in johns-repo
$ git status -sb

打印以下信息:

## master...origin/master [ahead 3]

这意味着 John 的master分支包含三个修订,这三个修订没有包含在他的origin/master跟踪分支中。在 Mark 的存储库中执行的相同命令:

# command issued in marks-repo
$ git status -sb

产出:

## master...origin/master [ahead 2]

marks-repo中的master分支包含两个修订,这两个修订不包括在马克的origin/master跟踪分支中。

所有三个储存库的状态如图 10-21 所示。

9781430261032_Fig10-21.jpg

图 10-21 。约翰的主分支领先三个,马克的主分支领先两个的状态

约翰成功地将他的作品上传到共享存储库

现在开发人员想要分享他们的工作。John 是第一个将他的更改推送到中央存储库的人:

# command issued in johns-repo
$ git push origin master

约翰的命令成功了。当 John 再次运行$ git status -sb时,输出不再包含[ahead: 3]。他的master分支现在与他的origin/master分支完全同步。

Mark 解决了分叉的问题

共享存储库已经更改,因为 John 已经上传了他的修订。但是马克不知道这件事。他的命令$ git status -sb返回与之前完全相同的信息:[ahead 2]。请记住,此信息仅涉及 Mark 的本地分支机构。他们还没有更新。Mark 的存储库和共享存储库现在看起来像图 10-22 。马克的知识库中没有约翰的修订版j1j2j3

9781430261032_Fig10-22.jpg

图 10-22 。John 已成功将其 j1、j2 和 j3 版本推送到共享回购,因此 Mark 无法推送到 m1 和 m2 版本

马克想用以下方式推进他的工作:

# command issued in marks-repo
$ git push -u origin master

但是这个操作被拒绝,因为推送不是快进。

马克需要更新他的master分支。他首先从共享存储库中获取最新的修订:

# command issued in marks-repo
$ git fetch origin

为了检查他的存储库的状态,Mark 运行了$ git status -sb命令。它打印:

## master...origin/master [ahead 2, behind 3]

现在马克知道他的masterorigin/master主分支已经分叉。[ahead 2]通知他,Marks 的master分支包含两个不在他的origin/master分支中的修订。打印出[behind 3]是因为 Mark 的master分支遗漏了他的origin/master分支中包含的三个修订。

使用$ git log --graph --all --oneline --decorate马克可以想象他的master分支和他的origin/master分支分叉。图 10-23 中的显示了 Mark 的存储库和共享存储库的状态。

9781430261032_Fig10-23.jpg

图 10-23 。$ git fetch origin 后 Mark 的回购状态

为了保持历史的线性,Mark 将他的master分支放在从共享存储库中获取的origin/master分支之上:

# command issued in marks-repo
$ git rebase origin/master master

现在马克的master分支的历史是线性的,他的基础修订m1'm2'm3'在顶部。马克想确保万无一失,所以他运行了$ git log --graph --oneline --decorate$ gitk --all &命令。储存库的状态如图 10-24 中所示。

9781430261032_Fig10-24.jpg

图 10-24 。$ git rebase origin/master master 后的马克回购

marks-repo中执行的命令$ git status -sb如图图 10-24 打印:

## master...origin/master [ahead 2]

后面的消息消失了,因为 Mark 的master分支现在包含了来自他的origin/master分支的所有修订。

当处于[ahead 3]状态时,Mark 可以将其工作推送到共享存储库:

# command issued in marks-repo
$ git push -u origin master

这次接受推送。它转换共享存储库,如图图 10-25 所示。

9781430261032_Fig10-25.jpg

图 10-25 。Mark 成功执行$ git push -u origin master 后的 Mark-repo 和 shared-repo

马克的工作完成了。$ git status -sb的输出中没有超前或滞后信息。这意味着马克回购中的masterorigin/master分支是同步的。

约翰下载马克的作品

现在约翰的回购和共享回购看起来像图 10-26 。

9781430261032_Fig10-26.jpg

图 10-26 。在 John 的$ git fetch 命令之前的 Shared-repo 和 johns-repo

约翰可以用下面的命令获取马克的工作:

# command issued in johns-repo
$ git fetch

该命令将把图 10-26 中所示的存储库转换成图 10-27 中所示的形式。

9781430261032_Fig10-27.jpg

图 10-27 。John 的$ git fetch 命令后的 repo

当约翰的存储库处于图 10-27 中所示的状态时,那么$ git status -sb命令打印:

## master...origin/master [behind 2]

因此,John 的master分支遗漏了他的origin/master分支中包含的两个修订。John 可以使用以下命令快进他的分支:

# command issued in johns-repo
$ git rebase

图 10-27 中所示的存储库随着 John 的$ git rebase命令转换成图 10-28 中所示的状态。所有存储库的历史都是线性的。

9781430261032_Fig10-28.jpg

图 10-28 。约翰的回购后,他的$ git rebase 命令

它是如何工作的

Git 获取命令执行两个操作:

  • 它将对象从远程数据库复制到本地数据库。
  • 它更新远程跟踪分支。

但是,它不会更新本地跟踪分支。有两个有趣的例子:

  • 当命令$ git fetch运行后,可以快进的方式更新本地分支。
  • 当命令$ git fetch运行后,本地分支分叉。

在图 10-26 中所示的库johns-repo中执行时,$ git fetch命令会产生图 10-27 中所示的效果。这是一个快进的案例。在这种情况下,$ git status -sb命令打印:

## master...origin/master [behind 2]

这意味着本地跟踪分支master遗漏了包含在远程跟踪分支origin/master中的两个修订。

分支分叉的情况如图图 10-23 所示。图 10-22 中marks-repo执行的命令$ git fetch产生图 10-23 中所示的结果。这一次$ git status -sb的输出将是:

## master...origin/master [ahead 2, behind 3]

因此,本地跟踪分支比远程跟踪分支提前两次修订,落后三次修订。

将本地跟踪分支重置为远程跟踪分支

远程跟踪分支,比如origin/master,就是本地分支。它们可以像普通的本地分支一样使用。因此,为了保持历史线性,我们可以使用$ git rebase命令,如配方 7-1 所述。图 10-23 中marks-repo发出的命令$ git rebase origin/master master会产生图 10-24 中所示的效果。

如果设置主分支的跟踪,那么命令$ git rebase origin/master master可以简化为:

$ git rebase

它在其远程跟踪分支之上执行当前本地跟踪分支的重置。

你也可以使用$ git pull命令来达到同样的效果。默认情况下,命令$ git pull被执行为$ git fetch,后跟$ git merge。虽然改变这种行为,你可以使用-r标志或配置设置。命令$ git pull -r相当于$ git fetch后跟$ git rebase

记住,$ git fetch命令更新您的本地数据库和远程跟踪分支。它不会影响您的本地跟踪分支机构或您的普通本地分支机构。因此,您可以随时安全运行$ git fetch。它不会给你带来任何麻烦。

消息[前方 x]和[后方 y]

正如您已经看到的,可以使用$ git status -sb命令检查本地跟踪分支和远程跟踪分支之间的关系。在本地跟踪分支和远程跟踪分支指向不同修订的情况下,该命令的输出包含部分[ahead N, behind M],如:

## master...origin/master [ahead 2]
## master...origin/master [behind 2]
## master...origin/master [ahead 2, behind 1]

[ahead 2]消息表明本地跟踪分支比远程跟踪分支领先两个版本。这是您在本地跟踪分支中提交后将获得的状态。

[behind 2]消息表明您的本地跟踪分支在远程跟踪分支之后。您的本地跟踪分支缺少远程跟踪分支中可用的两个修订。当您团队中的某个成员将他或她的提交推送到共享存储库,并且您使用$ git fetch命令将它们下载到您的存储库时,您就处于这种状态。

最后的消息[ahead 2, behind 1]意味着本地跟踪分支和远程跟踪分支已经分离。本地跟踪分支包含两个不在远程跟踪分支中的修订,同时它丢失了远程跟踪分支中包含的一个修订。在执行了$ git commit$ git fetch命令之后,您获得了这个状态,假设有人推送到共享存储库。

访问远程分支

请记住,$ git status -sb$ git branch -a -vv命令只适用于您的本地分支。它们是:普通本地分支、本地跟踪分支和远程跟踪分支。命令$ git status$ git branch不访问远程存储库中的远程分支。远程分支仅在$ git fetch期间转移到您的存储库。因此,如果您想检查远端,您需要运行$ git fetch,然后运行$ git status -sb$ git branch -a -vv。但是无论您的速度有多快,$ git status -sb$ git branch -a -vv的结果可能已经过时,因为有人可能已经在您获取之后和您执行$ git status -sb$ git branch -a -vv之前将其推送到远程存储库。

为什么跟踪分支很重要

你应该注意到了$ git status -sb 总是比较本地跟踪分支和远程跟踪分支。这种比较是为您当前的分支执行的。为了获得$ git status -sb[ahead N, behind M]输出,您需要为您当前的分支定义跟踪。如果您的配置文件.git/config中没有[branch"..."]条目,例如:

[branch "master"]
    remote = origin
    merge = refs/heads/master

然后 git 就不知道比较哪个分支了。$ git status -sb的输出将不包含[ahead N, behind M]信息。

当您克隆一个存储库时,git 会自动为您的master分支配置跟踪。Git 还会在您向 checkout 命令传递远程分支的名称时设置跟踪—如配方 5-2 中的$ git checkout doc 。否则,您必须手动设置跟踪。以下是一些不同的解决方案:

  • $ git branch --set-upstream-to=origin/master命令(如配方 10-1 中的提示所示)
  • $ git push -u origin master命令(如配方 10-2 中的提示所示)
  • $ git config branch命令(如配方 10-1 中的提示所示)
  • 手动编辑.git/config文件

一旦设置,跟踪信息可用于简化许多命令。如果您当前在被设置为跟踪origin/master分支的master分支上,那么三个命令$ git rebase$ git rebase origin/master$ git rebase origin/master master 是等效的。

基本上,定义跟踪分支有两个原因:

  • $ git status -sb命令可以与$ git fetch一起使用,以确定您的分支是否与远程分支同步。
  • 许多命令可以简化为缺少参数——如在$ git rebase中——以默认跟踪分支。

10-5.没有中央存储库的协同工作

问题

您想要模拟两个开发人员 John 和 Sarah 在没有中央存储库的情况下的合作。在这种情况下,协同工作将由两个存储库组成:

  • 第一个开发者的非裸存储库
  • 10-05/sarahs-repo—第二个开发者的非裸库

两个开发人员都将在他们的master分支内提交。约翰将使用sarah分支获取莎拉的工作,莎拉将使用john分支获取约翰的工作。

提示这个秘籍强调了 git 的分布式本质,每个人都可以相互合作。

解决办法

使用以下内容创建新目录:

$ cd git-recipes
$ mkdir 10-05
$ cd 10-05

这项工作是由一个开发人员初始化的。让它成为约翰。他初始化了他的知识库:

# john's commands in git-recipes/10-05 directory
$ git init johns-repo

然后他设置他的个人数据:

# john's commands
$ cd johns-repo
$ git config --local user.name john
$ git config --local user.email john@example.net

并提交:

# john's command
$ git simple-commit j1 j2 j3

现在莎拉进入了这个项目。她克隆了johns-repo并配置了她的个人信息:

# sarah's commands
# executed in git-recipes/10-05
$ git clone johns-repo sarahs-repo
$ cd sarahs-repo
$ git config --local user.name sarah
$ git config --local user.email sarah@example.net

接下来,Sarah 对项目进行了两次修订s1s2:

# sarah's command
$ git simple-commit s1 s2

当提交准备好被获取时,Sarah 给 John 发电子邮件告知她的s1 s2修订。为了获得它们,John 需要建立远程存储库和本地跟踪分支。约翰跑了:

# john's command
$ git remote add origin ../sarahs-repo

然后,他用$ git fetch从 Sarah 的存储库中取出远程分支,作为到他的存储库中的远程跟踪分支。该命令创建了origin/master远程跟踪分支,该分支与 John 存储库中的本地master分支没有任何关系。可以用$ git branch -a -vv查一下。关于他的master分行的行不包含[origin/master]部分。看起来像是:

* master                abc123f s2

它证明它仍然是一个普通分支,因为本地跟踪分支包含[origin/master],如:

* master                abc123f [origin/master] s2

接下来,John 创建了一个名为sarah的普通本地分支:

# john's command run in 10-05/johns-repo
$ git branch sarah

并且他将他的sarah分支配置为origin/master分支的本地跟踪分支:

# john's command
$ git branch --set-upstream-to=origin/master sarah

为了检查莎拉在s1s2版本中编写的代码,约翰带着:

# john's command
$ git checkout sarah

该分支还不包含s1s2修订,证明如下:

# john's command
$ git status -sb

输出通知您,他的当前分支(sarah)比它的远程跟踪分支(origin/master)晚两次提交。John 更新了他的sarah分支:

# john's command
$ git rebase

现在约翰的存储库中的sarah分支包含了s1s2修订。约翰可以分析莎拉的贡献。当他认为这些修改没问题时,他可以将它们合并到自己的工作中:

# john's commands
$ git checkout master
$ git rebase sarah

现在是约翰进行更多修改的时候了。他跑$ git simple-commit j4 j5 j6。然后他给莎拉发电子邮件询问他的工作。

轮到莎拉下载约翰在修订版j4j5j6中贡献的代码。她用$ git clone命令开始工作,因此她的存储库中已经包含了.git/config条目,该条目将她的master分支设置为origin/master远程跟踪分支的本地跟踪分支。可以用$ git branch -a -vv命令检查。它输出:

* master                40695ac [origin/master] s2
  remotes/origin/master 604549f j3

片段[origin/master]表示master分支是origin/master远程跟踪分支的本地跟踪分支。要删除此关系,Sarah 运行:

# sarah's command
$ git config --unset branch.master.remote
$ git config --unset branch.master.merge

上述命令后,$ git branch -a -vv输出:

* master                40695ac s2
  remotes/origin/master 604549f j3

尽管她的存储库仍然包含名为origin/master的远程跟踪分支,但是她的本地master分支没有与之连接。这两个$ git config --unset命令将她的本地跟踪分支master变成了一个普通的本地分支。很好。

现在,Sarah 想要创建名为john的本地跟踪分支,以便与 John 的master分支相连接。她跑了:

# sarah's command
$ git branch john
$ git branch --set-upstream-to=origin/master john

然后她用$ git fetch从约翰的仓库里取东西。该命令从约翰的回购协议中的master分支以及一个新的远程跟踪分支remotes/origin/sarah获取所有丢失的修订。莎拉不需要约翰的origin/sarah分支,但是没有办法避开。

接下来,Sarah 切换到john分支并检查其状态:

# sarah's command
$ git checkout john
$ git status -sb

输出表明当前分支(john)比远程跟踪分支(origin/master)晚三次提交。她更新了她的john分支:

$ git rebase

她检查约翰的修改j4j5j6。然后,她将约翰的工作并入她的master分支:

$ git rebase john master

约翰如何更新他的项目?

为了下载并合并 Sarah 的所有代码,John 运行以下命令:

$ git fetch
$ git checkout sarah
$ git rebase
$ git rebase sarah master

开发人员如何对项目做出贡献?

每个开发人员通过在他或她的master分支中承诺:

$ git checkout master
$ git simple-commit x y z

莎拉如何下载约翰的稿件?

她遵循程序约翰如何更新他的项目?使用john分支名称代替sarah:

$ git fetch
$ git checkout john
$ git rebase
$ git rebase john master

它是如何工作的

这个秘籍强调了 git 的分布式本质。如您所见,每个存储库都可以用作修订源。您可以从您有权访问的每个存储库中获取数据。Git 不限制对一些特殊的中央存储库的获取,就像 Recipe 10-2 中的shared-repo一样,存储在服务器上。默认情况下,推送操作仅限于裸存储库,但是我们也可以避开这一限制。这将在配方 10-10 中完成。

如果您仔细遵循这个方法,您会注意到$ git fetch操作为远程存储库中的所有分支创建了远程跟踪分支。当您完成菜谱时,John 的存储库包含以下分支(由$ git branch -a -vv返回):

* master                542d21a z
  sarah                 bacddd0 [origin/master] j6
  remotes/origin/john   bacddd0 j6
  remotes/origin/master bacddd0 j6

有两个远程分支origin/masterorigin/john。第二个是 Sarah 在sarahs-repo中创建的远程john分支的远程跟踪分支。约翰不需要也不使用它,但它还是被创造出来了。最后,记住$ git fetch获取所有的远程分支,并将它们存储为远程跟踪分支。

这个过程还向您展示了您可以使用以下命令将名为some-branch的本地跟踪分支转换成普通的本地分支:

$ git config --unset branch.some-branch.remote
$ git config --unset branch.some-branch.merge

10-6.使用远程分支

问题

您和您的同事想要使用一个包含许多分支 的共享存储库,而不仅仅是一个主分支。这将让你有机会将团队重组为从事不同功能的小组。要处理项目中一个名为 foo 的特定特性,您需要创建一个名为 foo 的远程分支,存储在共享存储库中。使用 foo 特性的开发人员应该使用 foo 远程分支来共享他们的工作。

这个菜谱为您提供了操作远程分支所需的所有命令。您将学习如何:

  • 创建一个与本地分支同名的远程分支
  • 使用不同于本地分支的名称创建远程分支
  • 删除远程分支
  • 更新您的存储库以反映远程分支中的更改

解决办法

创建一个新目录并初始化和一个共享存储库:

$ cd git-recipes
$ mkdir 10-06
$ cd 10-06
$ git init --bare shared-repo

接下来为 John 创建一个存储库,创建一个初始版本 ,并将其发送给shared-repo:

$ git clone shared-repo johns-repo
$ cd johns-repo
$ git simple-commit "Initial commit"
$ git push -u origin master

然后为 Sarah 创建一个存储库:

$ git clone shared-repo sarahs-repo

让我们假设约翰是领导。他负责为所有团队成员设定任务。他决定团队的一些成员(包括 Sarah)将从事文档工作。这项工作将在一个名为doc的分支中完成。为此,John 在他的存储库中创建了一个新的普通本地分支,名为doc :

# john's command
$ git branch doc

然后,John 将该分支提供给其他开发人员。他将他的 doc 分支推入shared-repo中:

# john's command
$ git push -u origin doc

上述命令在johns-repo中为doc分支设置跟踪。在上面的命令之后,shared-repo包含了doc分支。您可以验证它正在运行:

# shared-repo's command
$ git branch

接下来,John 想要开始一个特定的单元测试工作。他创建了一个名为test的普通本地分支:

# john's command
$ git branch test

他意识到这个名字可能已经被许多团队成员用于他们自己的私人工作,这些工作与单元测试没有任何关系。因此,约翰决定将远程分支命名为special-unit-tests 。他用下面的命令将他的本地test分支推到special-unit-tests名下:

# john's command
$ git push -u origin test:special-unit-test

上面的命令在别名为origin的远程存储库中创建了一个名为special-unit-test的远程分支。用以下方式检查:

# shared-repo's command
$ git branch

让我们假设 Sarah 被分配从事文档和测试工作。她从遥控器里取出:

# sarah's command
$ git fetch

该命令在sarahs-repo中创建远程跟踪分支origin/docorigin/special-unit-tests

一段时间过去了,docspecial-unit-tests异地分公司的工作顺利通过。成员们使用配方 10-4 来同步他们的工作。

约翰从doc分公司下载了最新版本。他决定小组工作结束。远程doc分支应该被删除。约翰删除远程doc分支:

# john's command
$ git push origin :doc

上述命令执行了以下操作:

  • 删除了shared-repo.中的doc分支
  • 它删除了远程跟踪分支johns-repo.中的origin/doc
  • 然而,它没有将johns-repo中的本地跟踪分支doc转换成普通的本地分支。johns-repo中的doc分公司还在跟踪一个不存在的origin/doc远程跟踪分公司。您可以使用$ git branch -avv命令验证这一点。

image 提示本地跟踪分公司和普通本地分公司有什么区别?当然,你可以在这两种情况下都做出承诺。但是本地跟踪分支连接到远程跟踪分支;因此,您可以使用$ git status -sb来检查本地跟踪分支是在相应的远程跟踪分支之前还是之后。

因此,shared-repo不再包含doc分支。在shared-repo中执行的$ git branch 命令打印两个分支:masterspecial-unit-tests。然而,在 John 的存储库中,分支doc仍然存在——它是一个普通的本地分支。

Sarah 更新了她的远程分支机构:

# sarah's command
$ git fetch

然而,上面的命令不会删除shared-repo中不存在的远程分支doc的远程跟踪分支origin/doc。如果 Sarah 想要更新她的存储库,以反映已删除的远程分支,她需要以下命令:

# sarah's command
$ git remote prune origin

上面的命令删除了 Sarah 存储库中的远程跟踪分支origin/doc。该操作可在提取过程中通过附加的-p标志自动执行:

# sarah's command
$ git fetch -p

现在doc分支的工作已经完成。在共享存储库中或者在除 John 之外的任何其他成员的存储库中没有远程分支doc。约翰是领导,他负责将doc分公司整合到master分公司。他可以使用第六章和第七章中讨论的任何方法。例如,他可以将doc分支合并到master分支中,从而形成一个灯泡。配方 7-6 中给出了确切的程序。一旦 John 将doc分支集成到master分支中,他就会将包含doc分支的主分支推送到共享存储库中。这完全按照配方 10-4 来做,也许(如果约翰需要给灯泡换底)按照配方 7-9 来做。

约翰可以使用 gitolite 限制对远程分支机构的访问,如配方 11-10 中所述。

它是如何工作的

命令:

$ git push [remote-name] [branch-name]

在别名为remote-name的存储库中创建名为branch-name 的远程分支。为了成功,分支branch-name必须存在于您正在工作的存储库中。然而,它不一定是你现在的分行。如果您想推动当前分支,您可以使用:

$ git push [remote-name] HEAD

用于一个没有-u 的普通本地分支,如:

$ git push origin doc

该命令执行三个操作:

  • 它在本地存储库中创建一个远程跟踪分支origin/doc
  • 它在远程存储库中创建一个远程分支doc
  • 它将所需的修订从本地数据库发送到远程数据库。

如果您使用-u标志,那么普通本地分支doc将被转换为远程跟踪分支origin/doc的本地跟踪分支。

这就是如何创建一个与本地分支同名的远程分支。如果要创建不同名称的远程分支,则本地分支使用以下语法:

$ git push [remote-name] [local-branch-name]:[remote-branch-name]

命令:

$ git push origin foo:bar

发送名为foo的本地分支,并将其存储在远端的bar名下。上面的命令:

  • 在远程存储库中创建远程分支bar
  • 在本地存储库中创建远程跟踪分支origin/foo
  • 将本地分支foo设置为远程跟踪分支origin/foo的本地跟踪分支

如果要删除远程分支,请使用:

$ git push [remote-name] :[remote-branch-to-remove]

就像:

$ git push origin :foo

上面的命令:

  • 删除origin中的远程分支foo
  • 删除本地存储库中的远程跟踪分支origin/foo
  • 它不会删除您的本地跟踪分支foo.
  • 它不会将本地跟踪分支foo转换成普通的本地分支;您当地的foo分支仍然跟踪一个不存在的origin/foo分支。

您可以通过以下方式删除本地分支:

$ git branch -d foo

或者您可以将其更改为普通的本地分支机构,方法是:

$ git config --unset branch.foo.remote
$ git config --unset branch.foo.merge

每次运行$ git fetch 时,所有的远程分支都被复制到您的存储库中作为远程跟踪分支。但是如果您删除了一个远程分支,那么默认情况下,这个变更不会在其他开发人员中传播。每个开发人员都可以通过以下方式删除过时的远程跟踪分支:

$ git remote prune origin

以上命令删除不存在的远程分支的所有远程跟踪分支。在提取过程中,可以使用以下方法完成同样的操作:

$ git fetch -p

上述命令执行两项操作:

$ git fetch
$ git remote prune

10-7.使用远程分支机构捐款

问题

你是一个大型项目的开发人员之一。为了同步工作,整个团队使用带有远程分支的共享存储库。在本秘籍中,我们将使用以下设置:

  • 10-07/leaders-repo——一个项目领导者的非裸露知识库
  • 10-07/johns-repo—您的非裸存储库
  • 10-07/shared-repo—用于同步的空存储库

你负责新网页界面的工作。您计划使用名为new-web-interface的分支与团队共享您的工作。特别是,您希望:

  • 创建一个名为new-web-interface的本地分支
  • 在您的本地分支机构提交new-web-interface
  • 将您的本地new-web-interface推送到一个共享存储库中,由项目负责人进行审查

解决办法

创建新目录并初始化领导者的存储库:

$ cd git-recipes
$ mkdir 10-07
$ cd 10-07
$ git init leaders-repo
$ cd leaders-repo
$ git simple-commit "Initial commit"

接下来创建一个共享存储库:

# command issued in git-recipes/10-07 directory
$ git clone --bare leaders-repo shared-repo

领导者需要在他或她的存储库中添加源别名:

# command issued by leader in 10-07/leaders-repo directory
$ git remote add origin ../shared-repo

此时,您加入了团队:

# command issued in git-recipes/10-07 directory
$ git clone shared-repo johns-repo

因为我们将复制在这个菜谱中创建的三个存储库,John 需要重新定义他的原点以使用相对路径:

# command issued in 10-07/johns-repo directory
$ git remote rm origin
$ git remote add origin ../shared-repo

为了对项目有所贡献,请遵循以下程序(所有命令都应在10-07/johns-repo中发出):

  1. 为您的贡献创建分支:$ git checkout -b new-web-interface
  2. 在您的new-web-interface分支中提交:$ git simple-commit a b c
  3. 将分支发送到共享存储库:$ git push -u origin new-web-interface

现在,您的贡献被存储在远程存储库shared-repo的远程分支new-web-interface中。

它是如何工作的

这个方法为组织项目成员的合作提供了一个更加方便的解决方案。通过为任务使用专用的远程分支,您可以在团队内部设置组时获得更大的灵活性。您还可以在将代码合并到主分支之前检查它。

10-8.接受捐款

问题

你是一个项目的领导者。您团队中的一名成员使用new-web-interface分支将一些代码推送到共享存储库中。你要检查贡献的代码。在这个秘籍中,我们假设代码是正确的,你(记住,你是领导者)接受它。

在这个秘籍中,我们使用了秘籍 10-7 中的场景。你在leaders-repo担任领导工作。

解决办法

复制配方 10-7 中的所有储存库:

$ cd git-recipes
$ cp -R 10-07 10-08
$ cd 10-08

现在你是一个领导者,你检查new-web-interface分支中的贡献(所有的命令都应该在10-08/leaders-repo中发出):

  1. 您获取投稿:$ git fetch
  2. 您签出远程分支$ git checkout new-web-interface
  3. 你用任意的命令和工具检查文件,例如,$ ls$ cat a.txtvi b.txt
  4. 您可以使用任意命令检查修订,例如,$ git log --oneline$ git log --oneline --name-only HEAD∼3..HEAD
  5. 你决定代码是正确的,应该合并到master分支。
  6. 你检查master分支:$ git checkout master
  7. 你把工作合并到master$ git merge new-web-interface
  8. 您将作品发布给所有其他团队成员:$ git push origin master

它是如何工作的

new-web-interface分支整合到主分支的主开发线包括两个步骤。首先,领导者必须获取在new-web-interface中完成的工作。这是通过$ git fetch$ git checkout new-web-interface命令完成的。在这两个命令之后,领导者有了一个名为new-web-interface的地方分支。

因为new-web-interface是一个本地分支,所以可以用第六章和第七章中讨论的任意方法进行整合。这里我们使用了简单的$ git merge命令。也可以用$ git merge --no-ff$ git rebase来完成。

一旦new-web-interface分支被集成到master中,它就可以被公开。为此,领导者将master分支推入shared-repo

10-9.向远程分支追加提交

问题

你是一个已经将自己的工作推到共享存储库中的new-web-interface分支的开发人员。领导让你做一些改进。您将在new-web-interface分支中进行一些新的提交。

在这个秘籍中,我们使用了秘籍 10-7 中的场景。你作为一名开发人员在johns-repo工作。

解决办法

复制配方 10-7 中的所有储存库:

$ cd git-recipes
$ cp -R 10-07 10-09
$ cd 10-09

领导怎么下载你作品的第一版?

为了下载你的工作的第一个版本,领导者更新他的存储库(命令应该在10-09/leaders-repo中运行):

  • 他取你的修改:$ git fetch.
  • 他去了new-web-interface分支:$ git checkout new-web-interface

开发人员如何向远程分支追加提交?

要添加新的提交,请遵循以下步骤(命令应该在10-09/johns-repo中运行):

  1. 转到new-web-interface分支:$ git checkout new-web-interface
  2. 创建新的提交:$ git simple-commit n1 n2 n3 n4 n5
  3. 发布您的作品:$ git push origin new-web-interface

领导如何从远程分支机构下载最新版本?

领导者更新他的存储库(命令应该在10-09/leaders-repo中运行):

  1. 他取你的修改:$ git fetch
  2. 他去了new-web-interface分支:$ git checkout new-web-interface
  3. 他更新了new-web-interface分支:$ git rebase origin/new-web-interface

现在他可以检查你的新修改并接受它们(如秘籍 10-8)或要求新的改进(如秘籍 10-9)。因为领导者用$ git checkout new-web-interface命令创建了他的本地new-web-interface分支,所以为该分支设置了跟踪。因此他可以在new-web-interface上使用$ git rebase来更新这个分支。

它是如何工作的

您用于投稿的分支可以由您和您团队的其他成员在更长的时间内使用。您可以迭代地提交并请求代码评审。这样可以重复很多次。菜谱解释了领导者如何用new-web-interface分支中的最新变化来更新他的存储库。

当然这些相同的程序:

  • 向远程分支追加新提交
  • 从远程分支下载最新提交

can be performed by every member. Thus you can use the new-web-interface branch as a way to collaborate with others while working on a given feature.

10-10.用$ git push -f 重写历史

问题

你是团队的一员。你把工作推到了一个叫new-web-interface的远程分支。你的作品被拒绝了很多次。你被一次又一次地要求改正。因此,远程分支new-web-interface包含大量提交。你负责new-web-interface远程分部。领导者要求您将该分支中的所有提交压缩成一个提交,然后他才能最终合并它。

在这个秘籍中,我们使用了秘籍 10-7 中的场景。你作为一名开发人员在johns-repo工作。

解决办法

复制配方 10-7 中的所有储存库:

$ cd git-recipes
$ cp -R 10-07 10-10
$ cd 10-10

现在你是一名在10-10/johns-reponew-web-interface工作的开发者:

您的本地new-web-interface分支包含三个版本abc。您希望压缩它们并更新远程分支。

以下是您必须遵循的程序(所有命令都将在10-10/johns-repo中执行):

  1. 转到new-web-interface分支:$ git checkout new-web-interface

  2. 您的new-web-interface分支包含三个版本abc。你可以和$ git log --oneline核实一下。

  3. 修订abc还没有合并到master分支。你可以用$ git log --oneline master..new-web-interface查一下

  4. Squash your three commits with: $ git rebase -i HEAD∼3. Use the following interactive rebasing subcommands:

    reword XXXXXXX a
    fixup  XXXXXXX b
    fixup  XXXXXXX c
    

    将新版本的注释设置为abc。交互式重置基础的细节在配方 8-3 中描述。

  5. 您的new-web-interface分支包含一个新的修订abc。你可以和$ git log --oneline核实一下。

  6. 版本abc还没有合并到master分支中。你可以用$ git log --oneline master..new-web-interface查一下

  7. 使用$ git push -f origin new-web-interface重新发布您的作品

它是如何工作的

命令$ git push -f origin new-web-interface强制 git 更新远程分支new-web-interface,即使这会导致历史被重写。默认情况下,$ git push只对快速更新成功。如果你知道你在做什么,你可以使用-f标志来强制转移。

Git 允许您配置一个拒绝所有使用$ git push的非快进更新的存储库,即使使用了-f标志。您可以通过将receive.denyNonFastForwards设置为true来实现这一点。如果你跑步:

$ git config receive.denyNonFastForwards true

shared-repo中,你将禁止所有改变历史的推送。

image 提示其他关于推送的选项可以在$ git config --help手册中找到。很多都是以receive开头的前缀。

10-11.完成远程分支的工作

问题

你是团队的一员。你把你的工作推到了名为new-web-interface的远程分支。该分支被整合到master分支中,不再使用。领导要求您移除 远程分支new-web-interface。您还想删除您的本地分支。

在这个秘籍中,我们使用了秘籍 10-8 中的场景。你作为一名开发人员在johns-repo工作。

解决办法

复制配方 10-8 中的所有储存库:

$ cd git-recipes
$ cp -R 10-08 10-11
$ cd 10-11

现在你是约翰。所有命令都应该在10-11/johns-repo中运行:

  1. 您用$ git fetch更新您的项目。
  2. 转到master分支:$ git checkout master
  3. $ git rebase origin/master更新您的master分支
  4. 检查可以安全移除的分支:$ git branch --merged。输出应包括—除其他外— new-web-interface分支。这意味着可以安全地删除new-web-interface分支。命令$ git branch --merged是一个安全检查:如果分支new-web-interface没有被打印,那么删除分支是不安全的。
  5. 移除shared-repo中的远程分支new-web-interface以及带有$ git push origin :new-web-interface的本地跟踪分支origin/new-web-interface
  6. 最后用$ git branch -d new-web-interface删除你的new-web-interface分支

它是如何工作的

秘籍中发生的一件奇怪的事情在秘籍 10-6 中已经提到过了。在步骤 5 中使用以下命令删除远程分支后:

$ git push origin :new-web-interface

$ git branch -a -vv命令打印:

* master                59de3b0 [origin/master] z
  new-web-interface            59de3b0 [origin/new-web-interface] z
  remotes/origin/HEAD   -> origin/master
  remotes/origin/master 59de3b0 z

这意味着new-web-interface分支仍然是本地跟踪分支。它跟踪不再存在的origin/new-web-interface分支。我们通过在步骤 6 中完全删除new-web-interface分支来解决这个矛盾。

10-12.推送到非空存储库

问题

您在一个从非裸存储库克隆而来的存储库中工作。即使原始存储库不是一个空存储库,您也希望将其推送到原始存储库。在这个秘籍中,我们将使用两个储存库:

  • johns-repo—您提交的非裸存储库
  • public-repo—一个你推送到的非裸库

解决办法

使用以下内容创建新目录:

$ cd git-recipes
$ mkdir 10-12
$ cd 10-12

用以下内容初始化 johns-repository:

# commands issued in git-recipes/10-12 directory
$ git init johns-repo
$ cd johns-repo
$ git simple-commit "Initial commit"

接下来,克隆johns-repo以获得public-repo:

# command issued in git-recipes/10-12 directory
$ git clone johns-repo public-repo

要允许推入到非裸存储库public-repo中,请使用以下命令更改其配置:

# command issued in public-repo directory
$ git config receive.denyCurrentBranch ignore
$ git config core.worktree ../

然后将文件public-repo/.git/hooks/post-update.sample重命名为public-repo/.git/hooks/post-update。您可以通过以下方式实现这一点:

# command issued in public-repo directory
$ mv .git/hooks/post-update.sample .git/hooks/post-update

更改public-repo/.git/hooks/post-update的内容,如清单 10-4 所示。

清单 10-4。 公开-回购/的内容。git/hooks/更新后

#!/bin/sh
exec git reset --hard

public-repo的配置完成。现在转到johns-repo并添加遥控器:

# command issued in johns-repo
$ git remote add origin ../public-repo

johns-repo中创建三个提交,使用:

# command issued in johns-repo
$ git simple-commit one two three

并用$ git push origin master将它们推向公开回购。

如果你现在用$ ls列出public-repo中的文件,你会注意到它的工作目录包含了one.txttwo.txtthree.txt文件。这证明了推送操作将johns-repo的最新状态转移到了public-repo

它是如何工作的

由于工作目录的原因,推送到非空的远程存储库会导致问题。假设您和您的同事在主分支机构工作,并且你们都创建了一个名为lorem.txt的文件。如果您提交文件并将其推送到同事的存储库,那么他的工作目录会发生什么变化?是否应该执行检验?如果是这样,你的朋友可能会丢失他在lorem.txt中完成的工作。

第一步是允许以更新远程分支的方式进行推送。它是通过以下方式完成的:

$ git config receive.denyCurrentBranch ignore

此命令允许您推送至远程存储库。推送会将必要的对象从您的存储库上传到远程数据库,然后更新您正在推送的远程分支。远程存储库的工作目录不会受到影响。

要在远程存储库中执行签出,我们必须配置工作目录的路径。它是通过以下方式完成的:

$ git config core.worktree ../

最后一步是当有人推到public-repo时强制结账。这是用post-update钩子完成的。要使用钩子,你必须创建一个名为public-repo/.git/hooks/post-update的 shell 脚本。该脚本应该包含一个单独的$ git reset --hard命令,如清单 10-4 所示。

image 提示这个配方可以当作一个部署工具。public-repo是无人工作的仓库。这是一个可通过 HTTP 协议访问的只读存储库。秘籍展示了如何通过简单的$ git push命令在网络上发布你的作品。

摘要

当我们讨论同步时,我们总是考虑两个存储库:本地(发出命令的那个)和远程(通过 URL 可用的那个)。为了避免反复输入 URL 的麻烦,git 可以将它存储在本地的.git/config文件中。使用$ git remote命令管理远程 URL。

git 存储库的同步是在修订图的基础上实现的。$ git push命令将修订从本地存储库复制到远端。当您获取时,修订会从远程存储库复制到本地存储库。在这两种情况下,数据库条目在传输过程中不会更改,它们的 SHA-1 保持不变。您可以将一组 git 存储库视为分布式数据库,其中 SHA-1 充当主键。因为 SHA-1 散列是唯一的,所以我们可以在任意存储库之间复制项目,而没有密钥冲突的风险。如果该键存在于目标数据库中,它总是被视为同一个对象。

为了解释这一点,我在我的一个存储库中创建了以下版本:

6c69fa3372f7099836176c8d0f123895adea58f1 Unique commit by gajdaw

该修订版的名称是:

6c69fa3372f7099836176c8d0f123895adea58f1

从 git 的角度来看这个名字在整个宇宙中是独一无二的——在所有已知的 git 仓库中。这是一个非常强有力的假设,使得存储库的同步变得容易。每个想要同步他或她的工作和我的工作的人都需要这个提交的副本。当我按下时,远端将接收到以下对象:

6c69fa3372f7099836176c8d0f123895adea58f1 Unique commit by gajdaw

这将是我的修订版的精确副本,有一个相同的 SHA-1 名字。

我们可以用另一种方式来表达同样的事实。每当您分析任何存储库中的历史时,都要查找这个:6c69fa3372f7099836176c8d0f123895adea58f1 name。一旦你找到它,你总是可以说:哦,我的存储库中有沃齐米耶兹·加伊达在 2013 年 9 月 6 日提交的修订,为了他的书第十章的“总结”。没有其他同名的修订版。

一旦您理解了如何添加和删除遥控器以及如何复制修订,掌握组工作的下一步就是分支。直到现在,我们都集中在普通的地方分行。这些是你工作中使用的个人本地分支。没人知道他们。你不必向任何人咨询你在这些部门的工作。您可以创建、修改和销毁它们。

同样的规则适用于所有的存储库——不仅仅是您的。因此,我们需要既允许独立又允许合作的规则。这些规则非常简单:您的本地分支存储在以远程名称命名的单独目录中的其他存储库中。

当处于松散格式时,您的普通本地分支ab存储在:

.git/refs/heads/a
.git/refs/heads/b

如果有人将您的存储库命名为foo,并带有:

$ git remote add foo [URL]

并使用$ git fetch foo从您的存储库中获取数据,那么您的本地分支ab将被存储在他或她的存储库中:

.git/refs/remotes/foo/a
.git/refs/remotes/foo/b

它们不会与存储在.git/refs/heads中的本地分支冲突。这就是全部的诀窍。

存储在.git/refs/remotes中的遥控跟踪分支,如.git/refs/remotes/foo/a,可以简称为foo/a。您可以将它们用作普通的修订指针。每当你需要一个修订版的 SHA-1 时,你可以使用foo/a,就像你使用任何其他方法来引用提交、HEADmaster∼5doc²等等。

当您考虑同步时,三种重要的分支类型是:

  • 远程分支机构
  • 本地跟踪分支
  • 远程跟踪分支

它们之间的关系以及$ git commit$ git fetch$ git push对它们的影响方式如图 10-29 所示。

9781430261032_Fig10-29.jpg

图 10-29 。$ git commit、$ git fetch 和$ git push 对三种类型的分支的影响

下面是图 10-29 的总结:

  • 当您使用$ git commit提交时,您将本地跟踪分支向前移动。
  • 当你用$ git fetch抓取时,你向前移动你的远程追踪分支。
  • 当您按下$ git push时,您向前移动远程分支和远程跟踪分支。

始终可以使用$ git branch -a -vv命令检查跟踪。输出列表:

  • 普通地方分行为:

    lorem    a1b2c3f Some commend
    
  • 本地跟踪分支机构为:

    ipsum    a1b2c3f [origin/ipsum] Some comment
    
  • 远程跟踪分支为:

    remotes/origin/dolor    a1b2c3f Some comment
    

$ git branch -a -vv命令没有列出远程分支。要列出远程分支,您必须首先用$ git fetch获取它们。我们可以说$ git branch是一个本地命令——它不执行本地和远程存储库之间的网络传输。

你将在配方 10-4 和 10-5 中找到设置和取消跟踪的具体步骤。如果你理解了每一类分支的作用,它们就非常简单。

git 的分布式角色在有和没有中央存储库的情况下展示团队工作的方法中得到了强调。为了更好地理解这个概念,请尝试在任一存储库中运行以下命令:

$ git fetch --no-tags https://github.com/github/GitPad master:refs/remotes/xyz/pqr

该命令从库https://github.com/github/GitPad中获取master分支,并将其存储在.git/refs/remotes/xyz/pqr文件中。该操作从 GitPad 存储库中复制修订,并将它们存储在您的.git/objects数据库中。如果您不想为远程存储库定义别名,您不必这样做。Git 不需要。即使没有定义远程的别名,git 也能够从一些 URL 可以访问的任何存储库中下载修订版到您的存储库中。你的仓库和 Github 上 GitPad 的仓库之间没有任何联系。远程分支主机(在 GitPad 的存储库中)和远程跟踪分支xyz/pqr(在您的存储库中)之间的映射由 refspec 设置:

master:refs/remotes/xyz/pqr

冒号前的部分是远程分支的名称,冒号后的部分是远程跟踪分支的名称。通过使用 URL 和 refspec,您可以从任何想要的存储库中获取任意分支。

有时候有人问我,为什么有时我们写origin master用空格分开,而有时我们用/作为分隔符。就像:

$ git pull origin master
$ git rebase origin/master

在第一个命令中,origin是遥控器的名称,master是分支的名称。这是一个普通的本地分支或本地跟踪分支。

在第二个命令中origin/master是远程跟踪分支的名称。上述命令的语法可以描述为:

$ git pull [remote] [branch]
$ git rebase [branch]

在第二个命令中,我们使用远程跟踪分支origin/master作为[branch]参数。

十一、托管 Git 存储库

一旦你和你的同事们学会了如何提交、使用分支和远程操作,你就会想把 git 作为一个团队来使用。在这一章中,我将展示如何建立一个虚拟主机来与他人共享 git 库。

Git 可以使用 ssh、http、https 和 git 网络协议。要选择满足您需求的最佳解决方案,您必须回答两个问题:

  • 您是否希望托管仅对经过身份验证的用户具有读/写访问权限(即没有匿名公共访问权限)的存储库?
  • 是否希望托管具有匿名公共只读访问权限的存储库?

如果您只想为经过身份验证的用户托管存储库,那么 ssh 是最佳选择。如果你想允许匿名只读访问,你可以使用本地 git 协议或 http。如果您需要匿名和带身份验证的两种类型的访问,您可以组合两种或更多的协议。例如,您可以使用 ssh 来认证用户,使用 git 协议进行匿名访问。表 11-1 展示了 ssh、http、https 和 git 协议的基本属性。

表 11-1 。ssh、http、https 和 git 协议的属性

|

草案

|

只读访问:

不需要认证

|

读/写权限:

仅授权用户

|
| --- | --- | --- |
| 嘘 | 不 | 是 |
| 超文本传送协议(Hyper Text Transport Protocol 的缩写) | 是 | 不 |
| 安全超文本传输协议 | 是 | 是 |
| 饭桶 | 是 | 不 |

本章中的菜谱包含了详细的教程,展示了如何通过 ssh、http 和 git 托管存储库。因为我真的认为 ssh 是最重要的解决方案,所以我们将深入研究关于这个协议的细节。我不仅会向您提供 ssh 如何认证用户的一般信息,还会解释以下内容:

  • 生成 RSA 密钥
  • 使用授权密钥文件
  • 用∽/配置 ssh。ssh/配置文件

虽然这些知识和 git 没有严格的关系,但是和 ssh 有关系,我真的相信你会需要的。正如我已经解释过的,ssh 是私有存储库的最佳解决方案。你对宋承宪越有信心越好。

对于那些需要匿名访问存储库的人,我们将同时使用 http 和 git 协议。如果效率是您关心的问题之一,那么您应该使用 git 本地协议。

短语“通过 http 托管 git 存储库”指的是允许通过 http 连接执行 git 获取和 git 克隆操作。为这样的存储库使用 web 浏览器是没有用的:您将获得工作目录和。git 存储库。如果您想使用 web 浏览器检查存储库,您需要 web 应用。我将向您展示两个最流行的解决方案:gitweb 和 cgit。 Gitweb 是由 git 创建者开发的 Perl 脚本,包含在 git 源代码中。Cgit 是用 C 写的,被认为是 git 最快的 CGI 应用。cgit 的另一个优点是它允许您添加一个 URL 来克隆一个存储库。

最后两个方案涉及特权问题。当您通过 ssh 托管 git 存储库时,经过身份验证的用户将获得对所有存储库的完全访问权。git 或 ssh 中都没有内置的支持来限制访问。如果要授予对以下内容的访问权限:

  • 基于每个用户
  • 基于每个存储库
  • 基于每个分支机构(一般:每个参考)

你需要吉托利特。Gitolite 是 git 之上的一个附加层,由 ssh 托管。这一层允许您授予或撤销三种类型的权限:读、写和强制写。可以为任意用户分配任意存储库和任意分支的所有权限。

我计划这本书是一个动手实践的介绍。关于托管 git 存储库的方法可能会导致许多令人头疼的问题。哪一个是合适的工作平台?如何准备在不同系统上没有任何缺陷的秘籍?如何避免弄乱你目前工作的系统?如果您没有任何机器的 root 访问权限,该怎么办?为了避免这些问题,我决定使用虚拟系统。在我看来,这是实践操作系统管理的最佳方式。因此,你将能够完全按照秘籍所写的去做,如果出了问题,你的系统也不会受到影响。我认为仅仅这两个优点就足以成为使用虚拟系统的理由。

设置虚拟机最简单的方法是使用 VirtualBox 和 vagger。本章的前两个秘籍给你必要的介绍。

11-1.安装 VirtualBox 和流浪者

问题

你想在你的机器上安装 VirtualBox 和 vagger。

解决办法

  1. 启动您的网络浏览器,访问https://www.virtualbox.org/wiki/Downloads并下载适用于您平台的 VirtualBox 4.2.18。

  2. 在您的计算机上安装 VirtualBox 4.2.18 软件包。您可以保留所有选项的默认值。

  3. 访问http://downloads.vagrantup.com并为您的平台下载流浪者 1.3.1。

  4. 使用默认设置在您的计算机上安装 vagger 1 . 3 . 1。

  5. 启动命令行并执行

    $ vagrant --version
    

如果安装了 vagger 并准备运行,这个命令将打印它的版本号。

它是如何工作的

VirtualBox 和 vagger 都提供了易于理解的安装程序。在撰写本文时,最新的可用版本是 VirtualBox 的 4.2.18 和 Vagrant 的 1.3.1。你可以在阅读本章时尝试最新版本的秘籍。如果 vagger 的配置格式发生了变化,您可以随时切换回我使用的版本。

11-2.运行虚拟 Linux

问题

你想在你的电脑上运行虚拟的 Ubuntu Linux。

解决办法

创建一个新目录 ,包含:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-02
$ cd 11-02

现在,在当前文件夹中初始化新的虚拟机配置:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant init precise32 http://files.vagrantup.com/precise32.box

虚拟系统已经准备好启动。使用以下命令启动虚拟 Linux:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant up

首次运行此命令时,需要一段时间才能完成。那是因为流浪汉会下载差不多用了 300 MB 的镜像文件。该文件将存储在:

# Unix-like systems
∼/.vagrant.d/boxes/precise32

# Windows
C:\Users\[username]\.vagrant.d\boxes\precise32

该操作在虚拟系统第一次启动时只执行一次。

$ vagrant up 完成后,你将拥有一个完全成熟的 Linux 系统作为应用之一运行在你的机器上。命令:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant status

将虚拟机的状态输出为:

default                   running (virtualbox)

虚拟系统不提供用户界面。您可以使用 ssh 会话来访问该系统并在其中工作,就像您访问任何其他 Linux 主机一样。要打开虚拟机的 ssh 会话,请运行以下命令:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant ssh

$ vagrant ssh命令成功时,您将拥有 shell 访问主机存储库:运行虚拟 Linux :shell 访问虚拟系统。例如,您可以使用以下命令检查系统版本:

# Guest OS (Ubuntu 12.04)
$ uname -a

或者列出已登录的用户:

# Guest OS (Ubuntu 12.04)
$ who

之后,您可以使用以下命令关闭 ssh 会话:

# Guest OS (Ubuntu 12.04)
$ exit

并使用以下命令检查虚拟机的状态:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant status

如您所见,虚拟系统仍在运行。关闭 ssh 会话不会影响虚拟系统的状态。

最后,使用以下命令停止虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant halt

这个命令关闭系统。当系统关闭时,命令:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant status

印刷品:

default                   poweroff (virtualbox)

您可以使用$ vagrant up命令再次启动它。

在继续下一个配方之前,你必须记住暂停当前配方中的虚拟系统。如果您不记得当前运行的虚拟机所在的文件夹,您可以使用 VirtualBox。图 11-1 展示了 VirtualBox 的主窗口。使用Machine/Close/Power off主菜单选项,您可以关闭所有可用的虚拟机。

9781430261032_Fig11-01.jpg

图 11-1 。VirtualBox 的主窗口列出了 所有可用的虚拟机

image 注意在你开始下一个配方之前,我建议你关闭当前配方的虚拟机。您可以使用$ vagrant halt命令。原因在“如果启动两个虚拟机会发生什么?”一节的方法 11-6 中有解释

它是如何工作的

命令:

$ vagrant init precise32 http://files.vagrantup.com/precise32.box

创建名为Vagrantfile的配置文件,如清单 11-1 中的所示。

清单 11-1。 默认流浪文件的内容 文件无注释

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = " http://files.vagrantup.com/precise32.box "
end

image 注意$ vagrant init命令创建的原始文件包含大量注释。他们以#开头。清单 11-1 显示了Vagrantfile的内容,去掉了所有的注释。

条目:

config.vm.box_url = " http://files.vagrantup.com/precise32.box "

将虚拟系统的基本框设置为http://files.vagrantup.com/precise32.box。precise32.box 文件包含 Ubuntu 12.04 LTS 32 位版本。运行后:

$ vagrant up

您的机器上将运行 Ubuntu 12.04 虚拟机。

image 提示http://www.vagrantbox.es可以获得大量的流浪者箱子。在那里你会发现 CentOS,Debian,Gentoo,OpenBSD,等等。通过更改基本框,您将更改虚拟机运行的操作系统。

因为虚拟系统不提供用户界面,所以我们将使用安全外壳(SSH) 来访问虚拟机。

现在,你的电脑运行两个操作系统。原系统称为主机系统**,虚拟系统称为客户系统* 。例如,如果您在 Windows 下工作:*

** Windows 是你的主机系统。

  • Ubuntu Linux 12.4 是你的访客系统。

前几章中发出的所有命令都是由您的主机操作系统的外壳执行的。从菜谱 11-2 开始,我们将使用两个命令行:一个用于主机操作系统,另一个用于客户操作系统。为了使说明更加清晰,我使用了以下注释:

# Host OS (e.g., Windows, Linux, OS X)
$ command ...

# Guest OS (Ubuntu 12.04)
$ command ...

它们解释了每个命令应该使用哪个命令行。

启动、停止和销毁 虚拟 Linux

vagger 提供了以下命令来控制虚拟机的状态:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant status
$ vagrant up
$ vagrant suspend
$ vagrant resume
$ vagrant halt
$ vagrant destroy

第一个命令$ vagrant status,返回关于虚拟机的信息。

$ vagrant up命令引导虚拟机。

下一个命令$ vagrant suspend,将系统以当前状态保存在硬盘上。当您挂起虚拟系统时,它不会消耗 RAM 或处理器。唤醒暂停系统的过程是通过$ vagrant resume$ vagrant up命令完成的。

$ vagrant halt命令关闭虚拟机。下次用$ vagrant up启动虚拟机时,系统将重新启动。

$ vagrant destroy命令永久删除虚拟机。下次运行$ vagrant up时,将从头开始创建虚拟机。这意味着您在之前的 ssh 会话中创建的所有文件都将被删除。

如果您目前已经完成了虚拟系统,但是您认为将来可能需要它,那么您应该使用$ vagrant suspend命令。在执行了$ vagrant suspend命令之后,虚拟系统不会消耗你的计算机资源,比如 RAM 或者处理器。系统存储在文件系统中,直到下一个$ vagrant resume$ vagrant up命令。请记住,唤醒被$ vagrant suspend挂起的虚拟系统的过程是恢复虚拟系统的最快方法。

如果你想重新开始,那么使用$ vagrant destroy命令。

虚拟机的状态

在任何给定时间,虚拟机都可能处于以下状态之一:

  • 未创建
  • 运转
  • 获救的
  • 关闭电源

第一个状态由$ vagrant status命令返回:

default                   not created (virtualbox)

机器处于以下状态:

  • 就在$ vagrant init之后和$ vagrant up之前
  • $ vagrant destroy之后

第二种状态由$ vagrant status命令描述为:

default                   running (virtualbox)

这是成功执行$ vagrant up命令后机器的状态。

下一个状态——表示为已保存——由$ vagrant status命令返回:

default                   saved (virtualbox)

这是执行$ vagrant suspend命令后的状态。

最后一个状态——电源关闭——由$ vagrant status命令返回,如下所示:

default                   poweroff (virtualbox)

这是虚拟机在$ vagrant halt命令后进入的状态。

VirtualBox 显示的图标清楚地显示了每个虚拟机的状态。图 11-1 中可见的虚拟机用标有running的绿色箭头表示。

image 提示不要使用$ sudo shutdown -h now或任何其他类似的命令来关闭虚拟系统。该命令将系统从运行状态转换为未创建状态。$ sudo shutdown -h now的结果与$ vagrant destroy相同。

打开到虚拟主机 的 SSH 会话

您可以使用 SSH 通过以下方式访问虚拟系统:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant ssh

此命令使用下列设置运行 ssh 客户端:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant ssh-config

您还可以使用任何其他 ssh 客户机来访问虚拟系统。它可以是您的 shell 中提供的一个典型的 ssh 客户端。您可以运行:

# Host OS (e.g., Windows, Linux, OS X)
$ ssh -p2222 vagrant@127.0.0.1

并使用以下凭据:

username: vagrant
password: vagrant

现在,你需要使用密码。稍后,在秘籍 11-5 中,我将告诉你如何避免使用 RSA 密钥输入密码。在那里,您还将学习如何配置您的 SSH 客户端。

访问 root 用户的帐户

默认情况下,虚拟机中的 root 帐户是锁定的。要访问它,你需要运行$ sudo su命令。您也可以通过以下步骤解锁帐户:

  1. 使用

    # Guest OS (Ubuntu 12.04)
    $ sudo passwd root
    

    为 root 用户设置新密码

  2. 使用

    # Guest OS (Ubuntu 12.04)
    $ sudo passwd -u root
    

    解锁 root 用户的帐户

  3. 使用

    # Guest OS (Ubuntu 12.04)
    $ su
    

    切换到 root 用户的帐户

您必须在 ssh 会话中运行以上所有命令,如# Guest OS (Ubuntu 12.04)注释所示。

同步文件夹

主机操作系统中带有Vagrantfile的文件夹(在菜谱 11-2 中是git-recipes/11-02)在客户操作系统中以/vagrant的形式存在。这意味着您可以在两个系统之间共享文件。在主机操作系统中创建的文件:

# Host OS (e.g., Windows, Linux, OS X)
git-recipes/11-02/lorem.txt

在客户操作系统中以下列名称提供:

# Guest OS (Ubuntu 12.04)
/vagrant/lorem.txt

您可以通过以下步骤进行验证:

  1. 使用

    # Host OS (e.g., Windows, Linux, OS X)
    $ vagrant up
    

    启动虚拟机

  2. # Host OS (e.g., Windows, Linux, OS X)
    $ echo lorem > lorem.txt
    

    在主机系统中创建文件

  3. 使用

    # Host OS (e.g., Windows, Linux, OS X)
    $ vagrant ssh
    

    打开 SSH 会话

  4. List the contents of /vagrant directory with:

    # Guest OS (Ubuntu 12.04)
    $ ls /vagrant
    

    如您所见,文件/vagrant/lorem.txt在 ssh 会话中是可用的。

  5. 接下来,用

    # Guest OS (Ubuntu 12.04)
    $ cat /vagrant/lorem.txt
    

    检查/vagrant/lorem.txt文件的内容

  6. 使用:

    # Guest OS (Ubuntu 12.04)
    $ echo ipsum > /vagrant/ipsum.txt
    

    在 ssh 会话中创建一个文件

  7. 使用

    # Guest OS (Ubuntu 12.04)
    $ exit
    

    关闭 ssh 会话

  8. List the files in the git-recipes/11-02 directory with:

    # Host OS (e.g., Windows, Linux, OS X)
    $ ls
    

    文件ipsum.txt在您的主机操作系统中可用。

  9. # Host OS (e.g., Windows, Linux, OS X)
    $ cat ipsum.txt
    

    检查git-recipes/11-02/ipsum.txt文件的内容

  10. 使用

```
# Host OS (e.g., Windows, Linux, OS X)
$ vagrant halt
```

停止虚拟系统

正如你所看到的,这两个文件lorem.txtipsum.txt在你的主机操作系统(比如 Windows)和客户操作系统(比如 Ubuntu 12.04)中都是可用的。这是在两个系统之间共享文件的最简单的方法。稍后我们还将使用$ scp命令在两个系统之间复制文件。

如果您想同步不同的文件夹,您可以在Vagrantfile中使用以下配置:

config.vm.synced_folder [HOST-PATH], [GUEST-PATH]

第一个目录是主机操作系统上的路径。它可以是绝对的,也可以是相对于带有Vagrantfile的目录,但它必须在启动时存在。第二条路径是在客户操作系统上。必须是绝对的。如果在启动过程中来宾路径不存在,将会创建一个。

以下是 Windows 的示例:

config.vm.synced_folder "c:\\some-dir-on\\windows", "/some-dir-on/virtual/ubuntu"

对于 Linux:

config.vm.synced_folder "/some-dir-on/Linux", "/some-dir-on/virtual/ubuntu"

11-3.在虚拟机上编译 git

问题

您希望在虚拟 Ubuntu Linux 上编译并安装最新版本的 git。编译 git 源代码的能力在以下情况下非常有用:

  • 使用最新的 git 版本工作
  • 编译依赖 git 的外部应用,如 cgit
  • 为 git 项目做贡献

解决办法

  1. 启动虚拟机:

    # Host OS (e.g., Windows, Linux, OS X)
    $ cd git-recipes
    $ mkdir 11-03
    $ cd 11-03
    $ vagrant init precise32 http://files.vagrantup.com/precise32.box
    $ vagrant up
    $ vagrant ssh
    
  2. 从 Ubuntu 可用的二进制包安装 git:

    # Guest OS (Ubuntu 12.04)
    $ git --version
    $ sudo apt-get install -y git
    $ git --version
    
  3. 编译并安装来自源代码的 git:

    # Guest OS (Ubuntu 12.04)
    $ sudo apt-get update -y
    $ sudo apt-get install -y make libssl-dev libz-dev gettext libexpat1-dev libcurl4-openssl-dev
    $ git clone --depth 1 git://git.kernel.org/pub/scm/git/git.git
    $ cd git
    $ make prefix=/usr all
    $ sudo make prefix=/usr install
    $ git --version
    
  4. 完成秘籍:

    # Guest OS (Ubuntu 12.04)
    $ exit
    
    # Host OS (e.g., Windows, Linux, OS X)
    $ vagrant halt
    

它是如何工作的

为了编译 git,我们需要下载它的源代码,您可能会猜到,可以在 git 存储库中找到。因此,我们需要一个 git 客户端。

默认情况下,git 不使用 precise32 框安装在虚拟机上。您可以使用$ git --version命令来验证它。要安装它,你必须启动虚拟机并运行$ sudo apt-get install -y git命令。这个命令从 Ubuntu 可用的二进制包中安装 git。当发出不带-y的命令时,$ sudo apt-get install命令通常会在需要安装额外的软件包时要求确认。-y选项强迫对所有这些类型的问题给出肯定的答案。

要编译最新版本的 git,您必须:

  • 更新系统依赖关系。这是通过

    $ sudo apt-get update -y
    

    完成的

  • 安装 git 所需的所有必要的库和工具。执行此操作的命令是:

    $ sudo apt-get install -y make libssl-dev libz-dev gettext libexpat1-dev libcurl4-openssl-dev
    
  • 使用 git 克隆最新版本的 git 源代码:

    $ git clone --depth 1 git://git.kernel.org/pub/scm/git/git.git
    
  • 并执行编译源代码和安装 git 的命令:

    $ cd git
    $ make prefix=/usr all
    $ sudo make prefix=/usr install
    

完成后,您将在虚拟机上安装最新版本的 git。您可以通过以下方式验证这一点:

$ git --version

image 提示如果你愿意,你也可以为刚刚安装的版本生成文档。您可以使用以下两个命令来实现这一点:$ sudo apt-get install -y asciidoc$ sudo make prefix=/usr install-doc。然而,安装 asciidoc 将花费大量时间,因为它会安装 TeX 和 LaTeX 等。

Git 源托管在http://git.kernel.org/cgit/git/git.git。你会发现这个网站的很多镜像,比如https://github.com/git/git。但是最新的版本总是可以在 kernel.org 和其他网站上找到。

我使用了--depth 1参数进行克隆:

$ git clone --depth 1 git://git.kernel.org/pub/scm/git/git.git

上面的命令创建了一个所谓的浅知识库。存储库的历史被截断为给定数量的最新修订。如果您只需要签出最新的修订版,浅层克隆就可以了。你仍然可以在这个库中提交,但是你必须像我们在方法 7-2 中所做的那样把你的提交作为补丁发送。您不能使用浅层存储库进行克隆、提取或推送。使用浅层存储库的主要优点是克隆速度更快。

image 提示请注意,http://git.kernel.org/cgit/git/git.git站点由 cgit 提供支持。我将在菜谱 11-9 中向您展示如何使用 cgit 来托管存储库。

当你完成秘籍时,不要破坏虚拟机。如果您销毁虚拟机,您将不得不重复完整的过程,包括安装所有软件包和克隆 git 源。如果你用$ vagrant suspend暂停虚拟机或者用$ vagrant halt关机,那么下一次你用$ vagrant up启动机器时,这个程序中安装的所有软件都将可用。

11-4.通过 ssh 托管 git 存储库

问题

您想要设置一个主机,用于在您的公司内部共享 git 存储库。你的任务是:

  • 设置一个虚拟机来托管 git 存储库
  • 在虚拟主机上安装在方法 3-1 中创建的存储库,以便每个开发人员都可以使用该存储库进行克隆、获取和推送
  • 通过提交和推送存储在虚拟系统中的远程存储库来检查一切是否按预期工作

image 提示有了这个方法,即使你不能访问 root 帐户,你也可以托管私有的 git 库。例如,您可以将这种方法用于虚拟共享主机。

解决办法

启动虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-04
$ cd 11-04
$ vagrant init precise32 http://files.vagrantup.com/precise32.box
$ vagrant up
$ vagrant ssh

在虚拟机上安装 git:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get install -y git

虚拟机已准备就绪。您可以关闭 ssh 会话:

# Guest OS (Ubuntu 12.04)
$ exit

使用 scp 复制空存储库

创建您想要共享的空存储库。我们将使用在配方 3-1 中创建的存储库。

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ git clone --bare 03-01 03-01.git

空存储库位于03-01.git目录中。使用以下命令将空存储库复制到您的虚拟机上:

# Host OS (e.g., Windows, Linux, OS X)
$ scp -P 2222 -r 03-01.git vagrant@127.0.0.1:03-01.git

使用以下凭据:

username: vagrant
password: vagrant

如果您想检查是否真的复制了空存储库,请打开 ssh 会话:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ cd 11-04

$ vagrant ssh

并列出文件,包括:

# Guest OS (Ubuntu 12.04)
$ ls

您将看到目录/home/vagrant现在包含了03-01.git目录。您可以进入该存储库并使用以下命令检查日志:

# Guest OS (Ubuntu 12.04)
$ cd 03-01.git
$ git log --oneline

$ git log命令将打印我们在配方 3-1 中创建的三个修订。

虚拟系统上的存储库/vagrant/home/03-01.git可从以下 URL 获得:

ssh://vagrant@127.0.0.1:2222/home/vagrant/03-01.git

使用托管在虚拟机上的存储库

现在你是开发者之一。让我们假设他的名字是保罗。将存储库从虚拟系统克隆到本地 Peter 的驱动器:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes

$ mkdir 11-04-pauls-machine
$ cd 11-04-pauls-machine
$ git clone ssh://vagrant@127.0.0.1:2222/home/vagrant/03-01.git

使用凭据:

username: vagrant
password: vagrant

现在保罗提交到他的本地存储库中:

# the commands issued in 11-04-pauls-machine/ directory
$ cd 03-01
$ echo one > one.txt
$ git add -A
$ git commit -m "One"

最后,Paul 将其工作推送到存储在虚拟系统上的远程存储库:

$ git push origin master

他使用的凭证与之前使用的相同:

username: vagrant
password: vagrant

image 提示如果您想通过网络访问虚拟系统,您需要配置端口转发,将下面一行添加到流浪者文件:config.vm.network :forwarded_port, guest: 22, host: 3333

将这一行添加到浮动文件后,用:$ vagrant reload重新加载虚拟机

如果运行虚拟系统的机器的 IP 地址是 192.168.10.225,那么您的本地网络上的每个人都可以使用$ ssh -p3333 vagrant@192.168.10.225访问虚拟系统

以下命令可用于克隆存储库:$ git clone ssh://vagrant@192.168.10.225:3333/home/vagrant/03-01.git

检查存储在虚拟系统上的存储库日志

您想要检查虚拟系统中托管的空存储库的历史。打开 ssh 会话:

# Host OS (e.g., Windows, Linux, OS X)
# the command should be executed in 11-04 directory
$ vagrant ssh

进入03-01.git目录:

# Guest OS (Ubuntu 12.04)
$ cd 03-01.git

列出历史记录:

# Guest OS (Ubuntu 12.04)
$ git log --oneline

输出将包括由 Paul 创建的三个提交一、二、三。

秘籍完成了。关闭 ssh 会话:

# Guest OS (Ubuntu 12.04)
$ exit

并停止虚拟系统:

# Host OS (e.g., Windows, Linux, OS X)

$流浪者停止:它是如何工作的

如果您想要托管 git 存储库,您需要的只是一个主机:

  • 安装 git 客户端后
  • 通过 ssh 访问

虚拟框 precise32 不包含 git 客户端。为了安装 git,我们在 ssh 会话中发出了以下命令:

$ sudo apt-get install -y git

如果你现在用$ vagrant suspend暂停系统,或者用$ vagrant halt暂停系统,然后用$ vagrant up重新启动,git 客户端将可用。另一方面,如果您用$ vagrant destroy销毁虚拟机,您将不得不再次安装 git。

正如你在菜谱 2-4 中所学的,git 库可以用标准命令复制,比如cpscprsync。要建立一个中央共享存储库,您只需将一个空存储库复制到虚拟系统中。我们使用流浪者提供的默认帐户访问虚拟系统:

username: vagrant
password: vagrant

vagrant用户的主目录是/home/vagrant。以下命令:

$ scp -P 2222 -r 03-01.git vagrant@127.0.0.1:03-01.git

将目录03-01.git从本地文件系统(即您的主机操作系统)复制到虚拟系统(即客户操作系统)上。由于有了-r选项,操作会递归所有子目录。虚拟系统上的存储库/vagrant/home/03-01.git可以从以下 URL 获得:

ssh://vagrant@127.0.0.1:2222/home/vagrant/03-01.git

如果您想让它通过网络可用,请记住使用以下命令在流浪者文件中配置端口转发:

config.vm.network :forwarded_port, guest: 22, host: 22

然后,知道vagrant帐户密码的每个人都可以使用这个简化的 URL 克隆、获取和推送这个存储库:

ssh://vagrant@x.x.x.x/home/vagrant/03-01.git

其中x.x.x.x是您的主机的 IP 地址。

使用这种方法:

  • 每个开发者都需要知道流浪儿账号的密码。
  • 每个开发人员在每次克隆、获取或推送时都必须输入流浪儿帐户的密码。
  • 每个开发人员都有对流浪者帐户的完全 ssh 访问权限。
  • 此存储库没有公共只读访问权限。
  • 每个开发人员不仅可以对一个存储库进行克隆、获取、推送访问,还可以对这个流浪者帐户下托管的所有其他存储库进行访问。

前三个问题将在配方 11-5 中解决。我们将使用 git 帐户和 RSA 密钥来完成这项工作。

第四个问题将在方法 11-6 和 11-7 中解决,其中我们将使用 git 和 http 协议来托管 git 存储库。

最后一个问题将在配方 11-10 中用吉托利特解决。

11-5.用 authorized_keys 简化 ssh 授权

问题

你想从以下方面改进配方 11-4 的解决方案:

  • 开发者不再需要流浪者账号的密码了
  • 开发人员在克隆、获取或推送时不必键入密码
  • 没有人可以通过共享存储库获得对主机的 ssh 访问

解决办法

初始化并引导新的虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-05
$ cd 11-05
$ vagrant init precise32 http://files.vagrantup.com/precise32.box
$ vagrant up
$ vagrant ssh

在虚拟机上安装 git:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get install -y git

创建 git 用户:

# Guest OS (Ubuntu 12.04)
$ sudo adduser --disabled-password --shell /usr/bin/git-shell --gecos Git git

初始化新的储存库

为了创建一个所有团队成员都可以访问的新存储库,管理员(即,自由用户)执行以下命令:

# Guest OS (Ubuntu 12.04)
# Command executed by vagrant user
$ sudo git init --bare /home/git/lorem.git
$ sudo chown -R git:git /home/git/lorem.git

每当您想要创建一个由您的团队共享的新存储库时,使用上面的两个命令。

创建新账户

在这个秘籍中,你是开发者之一。让他的名字叫彼得。首先,我们需要一个彼得的账户。这是由管理员使用以下命令完成的:

# Guest OS (Ubuntu 12.04)
# Command executed by vagrant user
$ sudo adduser --disabled-password peter --gecos Peter

登录彼得的账户

使用以下命令登录 Peter 的帐户:

# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo su - peter

当我说这个命令应该由 Peter 执行时:

# Guest OS (Ubuntu 12.04)
# Commands executed by peter user
$ command

这三个命令:

$ whoami
$ pwd
$ hostname

应打印:

Who am i?             peter
Current directory:  /home/peter
Hostname:             precise32

生成密钥

你的组织中的每个成员都必须重复这一部分。在这里,我们模仿彼得的作品。使用登录 Peter 的帐户(如果到目前为止您还没有这样做的话):

# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo su - peter

为 Peter 生成 RSA 密钥对,并将其保存在.ssh目录中:

# Guest OS (Ubuntu 12.04)
# Commands executed by peter user
$ mkdir .ssh
$ chmod 700 .ssh
$ ssh-keygen -t rsa -C peter@example.net -N "" -f .ssh/id_rsa

image 提示不用这三个命令:

$ mkdir .ssh
$ chmod 700 .ssh
$ ssh-keygen -t rsa -C peter@example.net -N "" -f .ssh/id_rsa

您可以使用单个命令:

$ ssh-keygen -t rsa -C peter@example.net

但是你必须回答一些问题。如果您这样做,请记住对所有选项使用默认值(只需按 ENTER 键,直到命令完成)。所有选项的含义在下面的“它如何工作”一节中给出

$ ssh-keygen命令生成两个文件,并通知您它们的存储位置:

Your identification has been saved in /home/peter/.ssh/id_rsa.
Your public key has been saved in /home/peter/.ssh/id_rsa.pub.

第一个文件包含您的私钥,第二个文件包含您的公钥。如果一切正常,命令$ ls .ssh/id_rsa*应该打印两个文件的名称:.ssh/id_rsa.ssh/id_rsa.pub

image 注意我经常创建虚拟系统只是为了测试、检查或验证。这个系统只使用很短的时间,然后就被破坏了。对主机的远程访问被阻止,因为我没有打开端口转发。在这种情况下,我通常对虚拟用户(如 Peter)使用空密码短语。

向管理员发送公钥

Peter 必须将他的公钥发送给管理员。因为他和管理员在同一个主机上工作,所以他可以使用一个简单的$ cp命令:

# Guest OS (Ubuntu 12.04)
# Commands executed by peter user
$ cp /home/peter/.ssh/id_rsa.pub /var/tmp/peter.pub

如果 Peter 在其他机器上工作,他必须找到一种方法将他的公钥发送给管理员。他可以使用电子邮件、ftp、可移动介质(如闪存)等等。

彼得的工作暂时结束了。您可以注销他:

# Guest OS (Ubuntu 12.04)
# Command executed by peter user
$ exit

使用 authorized_keys 文件授予 SSH 访问权限

我们希望允许 Peter 通过 SSH 访问 git 帐户。这是通过公钥文件完成的。我将假设 Peter 的公钥存储在/var/tmp/peter.pub中。

我们想授予 SSH 对名为 git 的帐户的访问权限。定义谁可以打开这个帐户的 ssh 连接的配置文件被命名为/home/git/.ssh/authorized_keys

首先,您必须使用以下命令创建一个/home/git/.ssh/authorized_keys文件:

# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo su
# mkdir /home/git/.ssh
# touch /home/git/.ssh/authorized_keys
# chown -R git:git /home/git/.ssh
# chmod 700 /home/git/.ssh
# exit

要授予访问权限,您必须将 Peter 的密钥附加到 git 帐户的/home/git/.ssh/authorized_keys文件中。管理员可以使用以下命令来实现这一点:

# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo sh -c 'cat /var/tmp/peter.pub >> /home/git/.ssh/authorized_keys'

image 提示代替一号命令:

$ sudo sh -c 'cat /var/tmp/peter.pub >> /home/git/.ssh/authorized_keys'

您也可以使用两个命令:

$ sudo su
# cat /var/tmp/peter.pub >> /home/git/.ssh/authorized_keys

要撤销 ssh 访问,管理员必须从 git 帐户的/home/git/.ssh/authorized_keys文件中删除带有 Peter 公钥的行。

使用远程存储库

Peter 希望在管理员创建的lorem存储库中工作。他想:

  • 克隆存储库
  • 创建一些修订
  • 将他的工作推到远程存储库

首先,Peter 必须登录他的帐户:

# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo su - peter

然后他配置 git:

# Guest OS (Ubuntu 12.04)
# Commands executed by peter user
$ git config --global user.name Peter
$ git config --global user.email peter@example.net

现在,Peter 可以克隆存储库了:

# Guest OS (Ubuntu 12.04)
# Command executed by peter user
$ git clone ssh://git@localhost/home/git/lorem.git

继续他的工作

# Guest OS (Ubuntu 12.04)
# Command executed by peter user
$ cd lorem
$ echo a > a.txt
$ git add -A
$ git commit -m "The first revision by Peter"

当工作完成时,Peter 将他的修改推送到服务器上:

# Guest OS (Ubuntu 12.04)
# Command executed by peter user
$ git push -u origin master

彼得的工作完成了。他结束了他的会议:

# Guest OS (Ubuntu 12.04)
# Command executed by peter user
$ exit

管理员检查存储库的日志

管理员想要检查存储在服务器上的空的lorem.git存储库的日志。他可以通过以下方式做到这一点:

# Guest OS (Ubuntu 12.04)
# Command executed by vagrant user
$ cd /home/git/lorem.git
$ git log

这个命令应该打印 Peter 的第一个修订版。

它是如何工作的

在这个秘籍中,你将使用三个不同的账户:vagrantgitpeter

第一个帐户vagrant,是您主机的管理员。它初始化所有的存储库并设置授权规则。

第二个用户git是一个虚拟用户。它不允许您打开 ssh 连接。它唯一的目的是允许其他开发人员使用 git clone、fetch 和 push 命令连接 git 存储库。当一个开发人员,比如 Peter,发出一个 git 命令——比如$ git fetch—那么 Peter 的 git 客户端将使用这个虚拟 git 帐户连接到服务器,然后使用 git 帐户权限执行一些命令。要做到这一点,管理员(我们示例中的流浪者用户)必须允许 Peter 使用服务器上的 git 帐户。这将通过 git 账户上的authorized_keys文件来完成。

最后一个账号peter,是你用来模仿别人作品的账号。我们将在虚拟系统上使用该帐户,但您也可以在您组织中的任何其他计算机上使用任何其他帐户。

默认情况下,第一个帐户vagrant可用。另外两个账户,gitpeter,是用$ sudo adduser命令手动创建的。

git 是伪账号是什么意思?多亏了以下的--shell /usr/bin/git-shell选项:

$ sudo adduser --shell /usr/bin/git-shell --gecos Git git

git 帐户将使用git-shell。如果您尝试使用以下命令打开 git 帐户的 ssh 会话:

# Host OS (e.g., Windows, Linux, OS X)
$ ssh -p2222 git@localhost

您将看到错误消息:

fatal: Interactive git shell is not enabled.

这就是我们如何限制对 git 帐户的访问。因为该帐户使用解释器/usr/bin/git/git-shell,所以没有人可以使用 ssh 或$ sudo su git命令登录该帐户。

image 提示你可以通过创建一个目录∼/git-shell-commands来削弱git-shell所施加的限制。如果该目录包含 shell 脚本,那么您将被允许打开到该帐户的 ssh 连接并执行该脚本。目录和 shell 脚本都应该拥有rx权限。

彼得的账户是普通用户账户。它被其中一个开发人员用于他或她的日常工作。您可以使用两种方法登录该帐户:

# SSH connection to peter's account - first method
# Guest OS (Ubuntu 12.04)
# Commands executed by vagrant user
$ sudo su peter
$ cd

或者:

# SSH connection to peter's account - second method
# Host OS (e.g., Windows, Linux, OS X)
$ ssh -p2222 peter@localhost

你选择哪种方法并不重要。

每次有新的开发人员想要加入团队时,都应该完成以下任务:

  • 开发者必须生成他或她的 RSA 密钥。
  • 开发者必须将他的公钥发送给管理员。
  • 管理员必须将开发者的公钥附加到/home/git/.ssh/authorized_keys文件中。

使用这种方法,每个开发人员都可以完全访问 git 用户可以使用的所有存储库。您不能基于每个用户限制访问或定义权限。

任何存储库都没有公共只读访问权限。

RSA 密钥

命令:

$ ssh-keygen -t rsa -C peter@example.net

为 RSA 算法生成私钥/公钥对。您将被问到两个重要的问题:路径和密码。两者都很重要。

如果您已经有一对名为∼/.ssh/id_rsa∼/.ssh/id_rsa.pub的键,那么您不应该覆盖它们。它们对你来说非常重要。如果不确定,请不要删除或覆盖这些文件。使用您已经拥有的密钥。

假设您的∼/.ssh/目录不包含id_rsaid_rsa.pub,您可以对ssh-keygen命令使用-f参数:

$ ssh-keygen -t rsa -C peter@example.net -f .ssh/id_rsa

通过使用这个命令,您将避免关于路径的问题。您也可以为密钥使用不同的文件名,如下所示:

$ ssh-keygen -t rsa -C peter@example.net -f peter

该命令将在当前目录下生成两个文件:peterpeter.pub。要使用它们,您将需要∼/.ssh/config文件,如“SSH 配置”一节所述。

ssh-keygen命令询问的第二个问题是一个密码短语。这是保护您的私钥的密码。每次运行 git push 或 fetch 命令时,您都必须键入它。

SSH 和授权密钥

ssh 协议允许您使用两种不同的授权方法通过 SSH 客户端访问您的远程帐户。您可以使用您的帐户密码或公钥/私钥。

配方 11-1 中讨论的命令:

$ ssh -p2222 vagrant@127.0.0.1

使用第一种方法。如果您提供了该帐户的有效密码,则授予访问权限。这种方法很简单,但是不方便,因为每当你推或者取的时候 git 都会询问你的密码。

第二种授权方法依赖于 RSA 密钥。有两种:私有和公有。让我们假设您的当前帐户是person@local,您的私钥名为id_rsa,您的公钥名为id_rsa.pub。要允许从账户person@local访问账户foreign@remote,您必须:

  • 使用密码登录foreign@remote账户
  • foreign@remote帐户创建一个名为∼/.ssh/authorized_keys的文件
  • 将来自person@local的公钥id_rsa.pub的内容附加到上一步创建的文件中

authorized_keys文件是允许访问 SSH 帐户的一种非常简单有效的方式。该文件可以包含任意数量的公钥,每个公钥位于单独的行中。

掌握 SSH 授权的第一步是在远程主机上使用默认的∼/.ssh/id_rsa∼/.ssh/id_rsa.pub文件以及authorized_keys。迟早你会奇怪为什么这些键必须被称为∼/.ssh/id_rsa∼/.ssh/id_rsa.pub。如果我想使用两对或多对密钥怎么办?在这种情况下,您必须更改 ssh 客户端的配置。

image 提示您可以将∼/.ssh/authorized_keys文件视为允许打开 ssh 会话而无需给出密码的个人列表。

SSH 配置

如果您想将 git 与 SSH 协议结合使用,您需要了解如何配置 SSH 客户端的基本知识。原因很简单:git 命令不知道如何解析 SSH 选项。因此,不能将 SSH 选项传递给 git 命令,如下所示:

$ git clone --ssh-option ssh://user@host

当您运行以下命令时:

$ git clone ssh://git@localhost/home/git/lorem.git

git 使用默认的 SSH 配置。如果您想使用特定的 SSH 选项,比如--ssh-option,您必须将它应用到您的默认配置中。

image 提示有些选项,比如端口号,可以通过$ git命令来处理。这不需要选项,因为端口号可以嵌入 URL: $ git clone ssh://user@host:2222

一般来说,没有办法将 SSH 选项直接传递给 git 命令。

您需要的最典型的 SSH 选项是带有您的私钥的文件名。您可以将它传递给 ssh 客户端,如下所示:

$ ssh -i /some/path/my-private-key.rsa_id user@host

但是 git 命令不接受。以下解决方案将不起作用:

$ git clone -i /some/path/my-private-key.rsa_id ssh://user@host

SSH 的配置存储在∼/.ssh/config文件中。如果这个文件不存在,那么所有选项都来自名为/etc/ssh/ssh_config的系统配置文件。你的默认键被命名为∼/.ssh/id_rsa∼/.ssh/id_rsa.pub

如果文件∼/.ssh/config存在,那么它的内容会覆盖 ssh 客户端使用的选项。最常用的选项显示在清单 11-2 中。事实上,清单 11-2 展示了$ vagrant ssh命令使用的选项。你可以用$ vagrant ssh-config命令来检查它们。这就是为什么您只需很少的努力就可以连接到虚拟机(无需键入端口号或密码!).

清单 11-2。∽/的例句内容。ssh/配置文件

Host localhost
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /somewhere/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

image 提示要创建如清单 11-2 所示的文件,可以使用$ vagrant ssh-config > ∼/.ssh/config命令。如果这样做,记得调整设置。

如果你创建了清单 11-2 中的所示的文件∼/.ssh/config,那么命令:

$ ssh localhost

使用来自insecure_private_key文件的私钥和来自清单 11-2 的所有其他选项打开到端口 2222 上的vagrant@127.0.0.1的连接。虚拟机上的vagrant账户有其∼/.ssh/authorized_keys文件,该文件带有insecure_private_key的公钥。这就是为什么您可以使用 ssh 登录虚拟机,而无需提供密码!

使用这些设置,命令:

$ Host OS (Windows, Linux, OS X)
$ git clone localhost:/home/git/lorem.git

克隆一个库vagrant@127.0.0.1:/home/vagrant/lorem.git。如您所见,简单的文本文件∼/.ssh/config允许您指定 git 应该使用的所有选项。可以在每个主机的基础上设置.ssh/config文件中的选项。

总之,SSH 授权使用:

  • 在客户端:

  • 私钥:∼/.ssh/id_rsa

  • 公钥:∼/.ssh/id_rsa.pub

  • 客户端配置:∼/.ssh/config

  • 在远程端:

  • 可信用户列表:∼/.ssh/authorized_keys

11-6.用 git 守护进程托管 git 存储库

问题

您希望为 git-recipes 目录下的所有存储库(即,您按照本书中的方法创建的所有存储库)设置具有公共只读访问权限的托管。通过公共只读访问,我们的意思是每个人都可以从您的存储库中克隆和获取。您希望对这个秘籍使用 git 守护进程。

解决办法

初始化新的虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-06
$ cd 11-06

创建适合你的系统的浮动文件:如果你在 Windows 上工作,那么修改浮动文件,如清单 11-3 所示。如果你在一个类似 Unix 的系统上工作,那么使用清单 11-4 中的所示的浮动文件。

清单 11-3。Windows 用配方 11-6 中的流浪文件

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = " http://files.vagrantup.com/precise32.box "
  config.vm.network :forwarded_port, guest: 9418, host: 9418
  config.vm.synced_folder "c:\\some\\where\\git-recipes", "/pub/git"
end

清单 11-4。 用于类 Unix 系统的配方 11-6 中的浮动文件

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = " http://files.vagrantup.com/precise32.box "
  config.vm.network :forwarded_port, guest: 9418, host: 9418
  config.vm.synced_folder "/some/where/git-recipes", "/pub/git"
end

image 提示在这个秘籍中,我们不需要发出$ vagrant init,因为我们手动创建了完整的流浪文件。

引导虚拟机并打开 ssh 会话:

$ vagrant up
$ vagrant ssh

更新系统依赖项并安装 git 和 git-daemon-run 包:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get update -y
$ sudo apt-get install -y git git-daemon-run

负责启动和停止 git 守护进程的脚本被保存为/etc/sv/git-daemon/run。打开编辑器:

$ sudo vi /etc/sv/git-daemon/run

并修改这个脚本,如清单 11-5 所示。你需要调整两条路径/pub/pub/git并添加--export-all选项。参数--export-all允许对所有存储库进行只读访问。

清单 11-5。 修改脚本/etc/sv/git-daemon/run

#!/bin/sh
exec 2>&1
echo 'git-daemon starting.'
exec chpst -ugitdaemon \
  "$(git --exec-path)"/git-daemon --verbose --reuseaddr \
    --export-all \
    --base-path=/pub /pub/git

现在,您可以使用以下命令重新启动守护程序

# Guest OS (Ubuntu 12.04)
$ sudo sv restart git-daemon

虚拟机已准备就绪。您可以关闭 ssh 会话:

# Guest OS (Ubuntu 12.04)
$ exit

git 守护进程托管的克隆库

现在 git 守护进程正在运行,每个人都可以克隆存储在机器上 git-recipes 目录中的任意存储库。

要克隆非裸 03-06 存储库,请运行:

# Host OS (e.g., Windows, Linux, OS X)
$ git clone git://localhost/git/03-06

您可以用几乎相同的方式克隆一个空存储库:

# Host OS (e.g., Windows, Linux, OS X)
$ git clone git://localhost/git/03-01.git

或者甚至是非空存储库的. git 目录:

# Host OS (e.g., Windows, Linux, OS X)
$ git clone git://localhost/git/03-05/.git

如果您的计算机是 192.168.10.225,则网络上的每个人都可以使用以下方式进行克隆:

$ git clone git://192.168.10.225/git/03-05

它是如何工作的

我们希望让存储在主机系统上 git-recipes 目录中的所有存储库在客户机系统上可用。这是通过Vagrantfileconfig.vm.synced_folder配置条目实现的。这已经在配方 11-2 中讨论过了。第二个配置选项:

config.vm.network :forwarded_port, guest: 9418, host: 9418

涉及端口转发。

端口转发

正如您已经知道的,当您启动虚拟机时,您的计算机运行两个操作系统:主机操作系统和客户操作系统。我假设你的主机操作系统是 Windows,你的客户操作系统是 Ubuntu 12.04。虽然下面的解释是正确的,但不管你的操作系统是什么,这些假设将帮助我澄清问题。

你必须明白的要点是每个系统都使用自己的 TCP 端口

假设您在 Windows(即主机操作系统)上工作。你启动网络浏览器,输入地址http://localhost:8080。浏览器将尝试连接到您的主机操作系统(即 Windows)上的端口 8080。如果使用 8080 端口在 Windows 机器上运行 apache 服务器,连接可能会成功。您的浏览器接收由您的主机提供的网页。

让我们考虑一下客户操作系统 Ubuntu。假设您打开到虚拟机的 ssh 连接,并在 ssh 会话中运行:

# Guest OS (Ubuntu 12.04)
$ ftp localhost:8080

ftp 将尝试在您的客户机操作系统 Ubuntu 12.04 上打开到端口 8080 的 TCP 连接。如果 Ubuntu 使用 8080 端口运行 ftp 守护程序,您的 ftp 客户端将会收到一些 ftp 输出。

在这两种情况下,都使用了相同的端口号 8080。但是正如您所看到的,相同的端口号可以分配给不同的 TCP/IP 服务。如果您的客户端运行在主机上,您将连接到主机上的守护进程。如果您的客户机运行在客户机上,您将连接到客户机上的守护进程。

我们可以说这两个系统是分离的。

从外部世界来看,您的客户操作系统是完全不可访问的。如果你的 IP 地址是 192.168.10.225,而你的网络上有人试图访问http://192.168.10.225:8080,那么他的请求会被发送到主机操作系统(即 Windows)。

我们如何连接主机和客户操作系统上的端口?这可以通过浮动文件中的config.vm.network配置选项来完成。选项:

config.vm.network :forwarded_port, guest: N, host: M

以这样一种方式设置转发,即在端口 M 上到您的主机的连接将由在来宾系统上工作的端口 N 上的守护进程提供服务。当您的系统启动时,会打印关于转发的信息:

[default] Forwarding ports...
[default] -- N => M (adapter 1)

如果您想通过运行在客户机上的 apache 来提供网页,您可以使用以下设置:

config.vm.network :forwarded_port, guest: 80, host: 8080

通过以上设置,您在主机操作系统中使用的地址http://localhost:8080将由运行在客户操作系统端口 80 上的守护进程提供服务。如果你的机器是 example.net,那么每个人都可以使用http://example.net:8080。这些请求将被转发到运行端口 80 的客户机守护程序。关于将主机上的 8080 转发到客户机上的 80 的信息在引导期间以如下方式显示:

[default] Forwarding ports...
[default] -- 80 => 8080 (adapter 1)

默认情况下,流浪者使用以下规则:

config.vm.network :forwarded_port, guest: 22, host: 2222

呈现为:

[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)

这就是为什么您可以使用$ vagrant ssh或标准 ssh 客户端连接到客户操作系统。当您引导虚拟系统时,端口 2222 被转发到运行在标准端口 22 上的客户机操作系统上的 ssh 守护进程。默认情况下,客户机操作系统上端口 22 的连接仅限于 IP 地址 127.0.0.1。这就是为什么如果您想通过网络对您的客户操作系统使用 ssh,您必须使用以下配置设置:

config.vm.network :forwarded_port, guest: 22, host: 22

上面的规则打开了端口转发:到主机上端口 22 的连接将由客户机上的服务 22 提供服务。IP 地址不受限制,因此网络上的每个人都可以使用 ssh 连接到您的虚拟机。

本配方中使用的配置:

config.vm.network :forwarded_port, guest: 9418, host: 9418

打开端口 9418 的转发。这是 git 守护进程使用的标准端口。对主机端口 9418 的所有请求都由运行在同一端口上的客户机操作系统上的守护程序处理。

image 提示git 守护进程的文档可以从$ git daemon --help获得

如果启动两个虚拟机会怎么样?

当你启动两个虚拟机时,它们不能使用相同的端口。因此,端口 2222 只能转发给其中一个。您启动的第一个虚拟机将获得其 ssh 守护进程的端口号 2222:

# Booting the first virtual machine
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)

第二台虚拟机将获得 2200 端口:

# Booting the second virtual machine
[default] Forwarding ports...
[default] -- 22 => 2200 (adapter 1)

很容易忘记哪个端口用于哪个机器。因此,我建议您在任何给定时间只运行一台虚拟机。

命令:

$ vagrant ssh

不受端口号变化的影响。它将始终将您与从当前服务器启动的机器连接起来。您可以使用以下命令检查 ssh 设置:

$ vagrant ssh-config

但是其他一些命令,比如 scp,需要调整。

11-7.通过 http 托管 git 存储库

问题

您希望将托管设置为对存储库的公共只读访问。对于公共只读访问,我们的意思是每个人都可以从存储库中克隆和获取。你想使用 http 协议。

解决办法

初始化新的虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-07
$ cd 11-07

创建清单 11-6 中的所示的流浪者文件。git-recipes 的路径需要调整以适应您的系统。

清单 11-6。 流浪记 11-6

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = " http://files.vagrantup.com/precise32.box "
  config.vm.synced_folder "c:\\some\\where\\git-recipes", "/pub/git"
end

引导虚拟机并打开 ssh 会话:

$ vagrant up
$ vagrant ssh

更新系统依赖项并安装 git 和 apache2 包:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get update -y
$ sudo apt-get install -y git apache2

安装要共享的存储库。

# Guest OS (Ubuntu 12.04)
$ cd /var/www
$ sudo git clone --bare /pub/git/03-01 03-01.git
$ sudo chown -R vagrant:www-data /var/www/03-01.git

创建挂钩:

# Guest OS (Ubuntu 12.04)
$ cd /var/www/03-01.git/hooks
$ mv post-update.sample post-update

首次运行发布更新:

# Guest OS (Ubuntu 12.04)
$ cd /var/www/03-01.git/
$ git update-server-info

该存储库现在可以通过以下 URL 向公众开放:http://localhost/03-01.git。用以下内容克隆它:

# Guest OS (Ubuntu 12.04)
$ cd
$ git clone http://localhost/03-01.git

关闭 ssh:

# Guest OS (Ubuntu 12.04)
$ exit

停止虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant halt

它是如何工作的

这个方法中描述的托管方法提供了对存储库/var/www/03-01.git的只读匿名访问。任何可以使用 HTTP 协议访问主机的人都可以克隆这个存储库。但是使用 HTTP 创建的克隆,如:

$ git clone http://localhost/03-01.git

不能用于推送到原始存储库。HTTP 允许只读访问。

如果您希望以只读匿名模式通过 HTTP 协议托管存储库,您必须:

  • 通过 HTTP 协议使包含存储库的目录可用
  • 更新关于分支机构的信息
  • 创建一个钩子,它将在每次推送时更新关于分支的信息

命令$ git update-server-info创建一个包含所有分支和引用的文本文件.git/info/refs。当您使用 HTTP 协议访问存储库时,如在$ git clone?? 中,那么 git 首先下载.git/info/refs。这就是客户端了解克隆的存储库中的引用的方式。

文件.git/info/refs必须在每次推送后更新。否则,客户端会得到过时的引用。这是通过将吊钩存放在.git/hooks/post-update中实现的。这个 shell 脚本执行一个$ git update-server-info命令。

image 提示如果您想推送至/var/www/03-01.git,您必须使用配方 11-4 中描述的 SSH 协议或配方 10-2 中描述的本地协议。

11-8.使用 Gitweb CGI 应用

问题

您希望安装并运行名为 Gitweb 的 web 应用。这是一个用 Perl 编写的 CGI 脚本,它为 git 库提供了一个直观的 web 界面。您希望允许浏览git-recipes目录下的所有存储库。

解决办法

初始化新的虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-08
$ cd 11-08

创建清单 11-7 中的所示的流浪汉文件。如果您在类似 Unix 的系统上工作,记得将路径更改为一个git-recipes目录。

清单 11-7。 来自 Windows 的配方 11-8 的流荡文件

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = " http://files.vagrantup.com/precise32.box "
  config.vm.network :forwarded_port, guest: 80, host: 8080
  config.vm.synced_folder "c:\\some\\where\\git-recipes", "/pub/git"
end

引导虚拟机并打开 ssh 会话:

$ vagrant up
$ vagrant ssh

更新系统依赖项并安装 git 和 gitweb 包:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get update -y
$ sudo apt-get install -y git gitweb

接下来修改 Gitweb 的配置。用$ sudo vi /etc/gitweb.conf打开/etc/gitweb.conf文件,更改行:

$projectroot = "/var/cache/git";

致:

$projectroot = "/pub/git";

最后,打开您的 web 浏览器(在主机上)并访问以下地址:

http://localhost:8080/gitweb

您应该会看到图 11-2 中的网页。每个存储库的名称都是超链接。如果您跟随它,您将看到呈现关于存储库的详细信息的页面。图 11-3 显示了 03-01 号仓库的详细信息。描述:

Unnamed repository; edit this file 'description' to name the repository.

这在两个图中都是可见的。它来自于.git/description文件。

9781430261032_Fig11-02.jpg

图 11-2 。Gitweb 应用的主页列出了所有可用的存储库

9781430261032_Fig11-03.jpg

图 11-3 。Gitweb 展示的 03-10 知识库主页

image 提示好好看看图 11-3 。Gitweb 不提供 URL 来克隆存储库。你只能通过网络界面浏览它们。

它是如何工作的

您希望由客户机上的 apache 守护进程提供 web 页面。为此,请为端口 8080 配置转发:

config.vm.network :forwarded_port, guest: 80, host: 8080

Vagrantfile中的第二个选项,即:

config.vm.synced_folder "c:\\some\\where\\git-recipes", "/pub/git"

打开主机(即您的 Windows 系统)上的git-recipes目录与客户机(即虚拟 Ubuntu 系统)上的/pub/git的同步。

默认情况下,Gitweb 扫描/var/cache/git目录中的存储库。这就是为什么您需要在/etc/gitweb.conf文件中更改项目根。我们将它设置到主机操作系统上的目录git-recipes。这就是为什么我们需要将客户机上的/pub/git与主机上的git-recipes同步。

apache 的默认配置存储在/etc/apache2/sites-available/default中。这就是你应该改变 web 服务器配置的地方。您可能需要它来配置 url 重写规则。

11-9.使用 cgit CGI 应用

问题

您希望安装并运行 web 应用 cgit。这是一个用 c 写的 CGI 脚本。它是 Gitweb 的一个替代品。您希望允许浏览git-recipes目录下的所有存储库。

解决办法

初始化虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-09
$ cd 11-09

创建清单 11-7 中的所示的流浪者文件。请记住,您可能需要调整文件夹。然后启动虚拟机并打开 ssh 会话:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant up
$ vagrant ssh

更新系统包并安装 git、apache2 以及编译 cgit 所需的所有工具和库:

# Guest OS (Ubuntu 12.04)
$ sudo apt-get update -y
$ sudo apt-get install -y git
$ sudo apt-get install -y apache2
$ sudo apt-get install -y make libssl-dev libz-dev gettext libexpat1-dev libcurl4-openssl-dev

下载 cgit 资源:

# Guest OS (Ubuntu 12.04)
$ git clone --recurse-submodules --depth 1 git://git.zx2c4.com/cgit

使用以下命令编译并安装 cgit:

# Guest OS (Ubuntu 12.04)
$ cd cgit
$ make
$ sudo make install CGIT_SCRIPT_PATH="/var/www"

调整 cgit 的配置以满足您的需求:

# Guest OS (Ubuntu 12.04)
$ sudo mv /var/www/cgit.cgi /usr/lib/cgi-bin
$ sudo touch /etc/cgirc
$ sudo sh -c "echo scan-path=/pub/git >> /etc/cgitrc"
$ sudo sh -c "echo clone-prefix=ssh://vagrant@localhost:2222/pub/git >> /etc/cgitrc"

最后,运行您的 web 浏览器并打开以下 URL:

http://localhost:8080/cgi-bin/cgit.cgi

你会看到图 11-4 中所示的网页。图 11-5 显示了关于仓库 03-10 的详细信息。正如您所看到的,这次页面包含一个 URL,允许您克隆存储库。

9781430261032_Fig11-04.jpg

图 11-4 。cgit 主页

9781430261032_Fig11-05.jpg

图 11-5 。cgit 03-10 主页

它是如何工作的

cgit 的最新版本可从以下网址获得:

http://git.zx2c4.com/cgit/

cgit 的源代码包括 git 的源代码。这是通过子模块完成的。开关--recurse-submodules克隆存储库和所有子模块。不幸的是,--depth 1选项不用于子模块。这个配方中 git 源代码的克隆将比配方 11-3 中持续更长时间。

配置文件/etc/gitrc允许轻松配置用于克隆的 URL。这种类型的 URL 可见于图 11-3 的下部。如果您想将 cgit 与 ssh 和 git 守护进程一起使用,请尝试以下配置:

clone-prefix=ssh://vagrant@localhost:2222/pub/git git ://localhost/git

11-10.与 gitolite 合作

问题

您希望使用 ssh 和 gitolite 为您的组织设置 git 托管。这将使您能够使用三种不同的权限授予访问权限:

  • 阅读
  • 强制写入

这些权限中的每一项都可以基于每个用户、每个存储库和每个分支进行分配。

在这个菜谱中,您的任务是设置一个虚拟机,通过 ssh 使用 gitolite 托管 git 存储库。您想要创建两个 gitolite 帐户:admin 和 peter 以及一个存储库 abc。管理员将是管理员,彼得将是普通用户。使用管理员帐户,您将为 peter 设置以下权限:

  • 分支a—强制推送的读/写访问
  • 分支b—无强制推送的读/写访问
  • 分支c—只读访问

此配方中的存储库 abc 将是配方 6-7 中存储库的克隆。

解决办法

使用以下内容初始化新的虚拟机:

# Host OS (e.g., Windows, Linux, OS X)
$ cd git-recipes
$ mkdir 11-10
$ cd 11-10
$ vagrant init precise32 http://files.vagrantup.com/precise32.box

添加负责同步 git-recipes 目录的配置选项:

config.vm.synced_folder "c:\\some\\where\\git-recipes", "/pub/git"

引导虚拟机并打开 ssh 会话:

# Host OS (e.g., Windows, Linux, OS X)
$ vagrant up
$ vagrant ssh

gitolite 包的安装

升级依赖项并安装 gitolite:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo apt-get update -y
$ sudo apt-get install -y gitolite

创建 git 和 peter 帐户

使用以下内容创建新用户:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo adduser --gecos Git git
$ sudo adduser --gecos Peter peter

为流浪用户配置 git 和 RSA 密钥

为流浪用户配置 git 设置:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ git config --global user.name vagrant
$ git config --global user.email vagrant@localhost

在您的客户机上为流浪用户生成 ssh 密钥

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ ssh-keygen -t rsa -C vagrant@localhost -N "" -f .ssh/id_rsa

将流浪者的公钥复制到 git 帐户的主目录:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ scp ∼/.ssh/id_rsa.pub git@127.0.0.1:admin.pub

使用 git 帐户的密码来完成上面的命令。

为 peter 用户配置 git 和 RSA 密钥

切换到 peter 的帐户:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo su - peter

为 peter 用户配置 git 设置:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git config --global user.name peter
$ git config --global user.email peter@localhost

在您的客户机上为 peter 用户生成 ssh 密钥

# Guest OS (Ubuntu 12.04)
# as peter user
$ mkdir .ssh
$ ssh-keygen -t rsa -C vagrant@localhost -N "" -f .ssh/id_rsa

将 peter 的公钥复制到流浪者帐户的主目录中:

# Guest OS (Ubuntu 12.04)
# as peter user
$ scp ∼/.ssh/id_rsa.pub vagrant@127.0.0.1:peter.pub

使用流浪者帐户的密码完成上述命令。

关闭彼得的外壳:

# Guest OS (Ubuntu 12.04)
# as peter user
$ exit

为 git 帐户配置 gitolite

以 git 用户身份登录:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo su - git

现在,$ ls命令:

# Guest OS (Ubuntu 12.04)
# as git user
$ ls

应该打印一个文件:admin.pub。为 git 帐户设置 gitolite:

# Guest OS (Ubuntu 12.04)
# as git user
$ gl-setup admin.pub

上面的命令只执行一次——当您在git@localhost帐户上初始化 gitolite 的工作时。它打开带有 gitolite 配置的vi编辑器。保存文件并关闭 git 帐户的 ssh 会话:

# Guest OS (Ubuntu 12.04)
# as git user
$ exit

创建存储库

要创建新的存储库,请打开 git 帐户的 ssh 会话:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo su - git

从配方 6-7 克隆存储库:

# Guest OS (Ubuntu 12.04)
# as git user
$ git clone --bare file:///pub/git/06-07 /home/git/repositories/abc.git

从 git 帐户注销:

# Guest OS (Ubuntu 12.04)
# as git user
$ exit

设置权限

作为一个流浪用户克隆 gitolite-admin 存储库:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ git clone git@localhost:gitolite-admin.git

peter.pub密钥从流浪者的主目录复制到gitolite-admin/keydir目录:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ mv /home/vagrant/peter.pub /home/vagrant/gitolite-admin/keydir

作为一个流浪用户,编辑/home/vagrant/gitolite-admin/conf/gitolite.conf文件并插入清单 11-8 中的内容。

清单 11-8。/home/vagger/gitolite-admin/conf/gitolite . conf 文件的内容

repo    gitolite-admin
        RW+     =   admin

repo    testing
        RW+     =   @all

repo    abc
        RW+    a    =    peter
        RW     b    =    peter
        R      c    =    peter

提交gitolite-admin存储库中的更改,并将其推送到服务器:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ cd /home/vagrant/gitolite-admin
$ git add -A
$ git commit -m "Peter's priviledges for abc repo"
$ git push origin master

在 gitolite 控制的储存库内工作

现在你在假冒彼得的作品。登录 Peter 的帐户:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ sudo su - peter

克隆abc存储库:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git clone git@localhost:abc

克隆会成功只是因为彼得对某些引用拥有R权利。如果您尝试使用 peter 的帐户克隆gitolite-admin存储库:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git clone git@localhost:gitolite-admin

该操作将被拒绝,因为 Peter 对gitolite-admin存储库没有权限。

现在,使用cd命令进入abc存储库,切换到分支a,删除最后一次提交,并执行强制推送:

# Guest OS (Ubuntu 12.04)
# as peter user
$ cd abc
$ git checkout a
$ git reset --hard HEAD∼
$ git push -f origin a

上述推送将被接受,因为 Peter 拥有对分支aRW+访问权。

接下来,切换到分支b,尝试删除最后一次提交,并对分支b执行强制推送:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git checkout b
$ git reset --hard HEAD∼
$ git push -f origin b

这一次强制推送将被拒绝,因为 Peter 只有分支bRW特权。

将您的本地分支b更新到状态origin/b,然后创建一个新的修订版本并推它:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git rebase origin/b
$ echo x > x.txt
$ git add -A
$ git commit -m x
$ git push origin b

以上操作是快进推送。彼得有权利,因此快进被接受。

切换到分支c并尝试执行快进推送:

# Guest OS (Ubuntu 12.04)
# as peter user
$ git checkout c
$ echo y > y.txt
$ git add -A
$ git commit -m y
$ git push origin c

这一次推送即使是快进也被拒绝了。这是因为 Peter 对分支c只有R权限。

它是如何工作的

为团队的私有项目托管 git 存储库的最佳解决方案是使用 SSH 和 git 帐户和公钥,如配方 11-5 所示。这种方法的问题是,其公钥被附加到 git 用户的authorized_keys文件中的每个用户都具有完全的读/写访问权限,并对存储在 git 用户帐户中的每个存储库进行强制推送。换句话说,每个通过 SSH 验证成功访问 git 帐户的用户都获得了对每个 git 帐户的完全访问权。使用内置 git 特性可以实施的唯一限制是禁用 shell 访问,就像我们在配方 11-5 中所做的那样,将 git 用户的 shell 改为 git-shell。

Gitolite 就是规避这些缺点的工具。它安装和使用都非常简单。

image 提示完整的 gitolite 文档可在http://gitolite.com/gitolite/master-toc.html获得。

要开始使用 gitolite,您需要一个主机使用 git 帐户通过 ssh 提供 git 存储库服务。git 帐户不能用 git-shell 限制。它需要一个普通的 ssh shell。gitolite 本身会限制对 git 帐户外壳的访问。因此,我们需要的第一个帐户是git

我们将使用的第二个帐户是设置权限的管理员。他创建新的存储库,删除不再使用的存储库,添加新用户,删除过时用户,并为每个用户定义对每个存储库的适当权限。在这个角色中,我们将使用vagrant帐户。

要伪造一个普通用户的作品,我们需要另一个帐户。它将被命名为peter

概括地说,在这个配方中,我们使用三个帐户:

  • git—这是用于 ssh 访问的帐户
  • 他是管理员,负责分配访问权限
  • 他是一名普通的开发人员,在一个仓库中工作

gitolite 包由vagrant用户用$ sudo apt-get install命令安装在系统中。一旦软件包被安装,我们可以为任何帐户配置它。为帐户配置 gitolite 的命令是$ gl-setup。该命令唯一需要的参数是管理员公钥的文件名。我们想使用vagrant作为管理员,因此我们首先必须为流浪者生成 RSA 密钥:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ ssh-keygen -t rsa -C vagrant@localhost -N "" -f .ssh/id_rsa

必须将密钥复制到 git 帐户。为此,我们使用 scp 命令:

# Guest OS (Ubuntu 12.04)
# as vagrant user
$ scp ∼/.ssh/id_rsa.pub git@127.0.0.1:admin.pub

你应该注意到,流浪者的公钥∼/.ssh/id_rsa.pub将被复制到git@localhost 账户并存储在admin.pub文件中。文件从id_rsa.pub重命名为admin.pub。这一点非常重要。Gitolite 使用文件名作为用户名。如果您将公钥文件命名为sarah.pub,那么 gitolite 会将该密钥视为名为sarah的用户。

当文件admin.pub 被复制到git@localhost账户时,你可以配置 gitolite 在git@localhost账户运行。使用以下命令打开到 git 帐户的 shell 连接:

$ sudo su - git

然后运行以下命令:

$ gl-setup admin.pub

它将为git@localhost账户配置 gitolite。

管理用户和权限

所有与用户相关的管理任务都由可以通过admin.pub键认证的人执行。在该配方中,用户是vagrantadmin.pub键是传递给$ gl-setup命令的参数。

Gitolite 没有用户界面。管理是通过位于以下 URL 下的gitolite-admin存储库完成的:git@localhost:gitolite-admin。第一步是克隆这个存储库:

$ git clone git@localhost:gitolite-admin

只有当发出上述命令的用户的 RSA 密钥之一与admin.pub相同时,该命令才会成功。因此vagrant用户可以克隆存储库gitolite-admin。当 Peter 试图克隆它时,他只会看到权限不足的消息。

一旦vagrant克隆了gitolite-admin存储库,他就可以执行管理任务。为了添加用户,他将公钥复制到gitolite-admin/keydir目录中。为了分配权限,他编辑了gitolite-admin/conf/gitolite.conf文件。当管理员将更改推送到服务器时,这些更改将被应用:

# the command issued by administrator
# in his or her gitolite-admin repository
# applies the rights
$ git push origin master

gitolite.config文件中分配权限的最简单语法如下:

repo foo
    RW+         =   sarah
    RW          =   paul
    R           =   ann

它将RW+规则分配给sarah,将RW规则分配给paul,将R规则分配给ann。所有这些规则都是为foo库定义的。sarahpaulann的密钥应存储在:

gitolite-admin/keys/sarah.pub
gitolite-admin/keys/paul.pub
gitolite-admin/keys/ann.pub

请记住,他们帐户的用户名并不重要。莎拉可以使用账户rose@some-host,只要她的∼/.ssh/id_rsa.pub文件包含与gitolite-admin/keys/sarah.pub相同的密钥。

管理存储库

gitolite 管理的存储库存储在/home/git/repositories目录中。管理它们最简单的方法是使用git帐户。打开 ssh 会话后,使用以下命令进入git@localhost:

$ ssh git@localhost

您可以输入目录:

$ cd
$ cd repositories

并列出所有可用的存储库:

$ ls -la

如果要初始化新的存储库,请使用以下命令:

$ git init --bare /home/git/repositories/lorem.git

要克隆现有项目,请使用:

$ git clone --bare some-existing-repo /home/git/repositories/ipsum.git

您也可以通过以下方式删除存储库:

$ rm -rf /home/git/repositories/some-repo.git

命令$ ssh git@localhost只有在管理员发出时才会成功。如果任何其他用户试图使用 ssh 访问git@localhost,他或她将只能看到关于他或她当前权限的信息:

hello peter, this is gitolite 2.2-1 (Debian) running on git 1.7.9.5
the gitolite config gives you the following access:
     R   W      06-07
    @R_ @W_     testing

摘要

本章中的秘籍主要涉及通常由对您的主机具有 root 访问权限的人执行的任务。为了避免干扰您的组织用于日常工作的主机的风险,我决定使用虚拟机来解释管理任务。通过这种方式,您可以练习如何在没有丝毫风险的情况下安装和删除系统中的软件包。

我准备了这些秘籍给没有使用过流浪者或宋承宪的初学者。我唯一的假设是,你应该熟悉上一章的所有秘籍。因此,我包括了所有必要的背景。而且,每个秘籍都是完整的。这可能会使秘籍运行时间稍长,但我相信它们会更容易遵循。

依你的需求而定,在我看来,以下是你可以做出的最佳选择:

  • 如果您想要托管能够定义访问规则的私有存储库,请遵循方法 11-10。
  • 如果您希望托管私有存储库,但不能定义访问规则(每个用户对所有存储库都有完全访问权限),请遵循方法 11-5。
  • 如果您想要托管公共可用的存储库,请遵循方法 11-6。
  • 如果您的存储库需要 web 界面,请使用 cgit——遵循方法 11-9。

git 使用的协议

除了在表 11-1 中给出的四种网络协议,git 还可以处理本地 URL。您已经非常熟悉以下命令:

$ git clone a/local/path/repo.git

该命令克隆本地目录a/local/path/repo.git中可用的存储库。还可以使用以下内容创建本地克隆:

$ git clone file:///a/local/path/repo.git

两个 URL 的区别:

a/local/path/repo.git
file:///a/local/path/repo.git

第一个允许 git 使用标准的文件系统操作(比如复制)和硬链接。第二个 URL 不允许 git 创建硬链接,数据传输是以网络方式执行的,而不是用cp命令。这当然效率较低,但是创建了一个新的副本,没有任何硬链接。

当我们将来自表 11-1 的四个网络协议与两个本地协议结合起来时,我们得到了 git 使用的协议的完整列表。用于存储库的协议编码在 URL 中。表 11-2 给出了 git 使用的所有协议的 URL 示例。如果您知道存储库的 URL,您可以很容易地猜到 git 将使用的协议。

表 11-2 。git 使用的协议可以从存储库的 URL 中猜测出来

|

草案

|

用于克隆的 URL

|
| --- | --- |
| 当地的 | $ git clone some/local/path/dir/repo.git |
| 文件 | $git clone file:///pub/git/dir/repo.git $git clone file:///c/dir/repo.git |
| 嘘 | $git clone ssh://user@host:dir/repo.git $git clone user@host:dir/repo.git $git clone host:/dir/repo.git |
| 超文本传送协议(Hyper Text Transport Protocol 的缩写) | $git 克隆 http://host/dir/repo.git |
| 安全超文本传输协议 | $git 克隆 https://host/dir/repo.git |
| 饭桶 | $git 克隆 git://host/dir/repo.git |*

十二、使用 Github

在这一章中,我将讨论使用 Github 来托管存储库。目前这是一个流行的开源项目托管平台。

我们首先创建一个 Github 帐户并配置 SSH 密钥。完成后,您将学会如何:

  • 从公共 Github 库克隆
  • 克隆并推送至您的存储库

稍后,我将向您展示如何从头开始一个新的 Github 托管的项目,以及如何导入一个现有的项目。然后,我们将继续进行拉请求。为了同时扮演贡献者和管理员的角色,我们将使用两个 Github 帐户:您的个人帐户和组织帐户。通过这种方式,你可以将你的组织拥有的项目转移到你的个人账户中。

12-1.创建 Github 帐户

问题

您想使用Github.com有两个原因:

  • 托管您自己的 git 存储库
  • 为一些开源项目做贡献

除非您是注册的Github.com用户,否则您不能这样做。因此,您的首要任务是创建一个新的 Github 帐户。

解决办法

启动网络浏览器并访问Github.com 。点击链接“注册 GitHub”。填写登记表并提交。当您使用新创建帐户完成登录时。

如果你想为你的 Github 帐户使用头像,请访问http://gravatar.com网站。在Gravatar.com中创建一个账号,上传你的头像,并与你用于 Github 账号的邮箱关联。

当您在Gravatar.com上完成头像配置后,请转到Github.com,注销,然后再次登录您的帐户。你的帐户现在应该使用新的头像。

它是如何工作的

注册过程很简单,应该很容易使用。这个配方的重点是强调我们在配方 2-1 中提到的事实:Github 不允许使用 SSH 协议,除非你已经配置了自己的 SSH 密钥。命令:

$ git clone git@github.com:jquery/jquery.git

失败,产生以下消息:

Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

产生此错误的原因是:

  • 如果您没有 Github 帐户。
  • 如果您的帐户没有配置为使用 SSH 密钥。

上面$ git clone 命令中的 URL:

git@github.com:jquery/jquery.git

使用 SSH 协议。你可以在表 11-2 中找到 git 使用的协议的完整参考。

12-2.用 SSH 密钥配置 Github 帐户

问题

您希望以 git 允许您使用 SSH URLs 克隆、获取和推送的方式配置 Github 帐户。

解决办法

要完成这个秘籍,你需要一个 SSH 密钥对。检查您的∼/.ssh/目录是否包含它们。打开一个 bash 窗口,列出。ssh 目录:

$ ls ∼/.ssh

如果这个目录不包含id_rsaid_rsa.pub文件,那么您必须生成它们。您需要的命令是:

$ ssh-keygen -t rsa -C your.email@example.net

该命令将询问您两个问题,这两个问题已在 RSA 密钥部分的方法 11-5 中解释过。第一个问题用键设置两个文件的名称;第二个设置使用密码来保护您的密钥。如果您按下输入,文件将被存储在默认位置,密码将不会被使用。

id_rsaid_rsa.pub文件准备好后,启动你的网络浏览器并访问 Github.com。点击标题为“注册 GitHub 完成后,使用您新创建的帐户登录。

要使用基于密钥的认证,您必须将您的公钥上传到您的帐户。将存储在∼/.ssh/id_rsa.pub中的公钥内容复制到剪贴板上。如果您在 Windows 上工作,可以使用以下命令:

$ clip < ∼/.ssh/id_rsa.pub

在其他系统上工作的读者可以用他们喜欢的文本编辑器打开文件,并使用编辑/复制命令。

进入你的 Github 账户设置,打开 SSH 密钥菜单选项,然后按下添加 SSH 密钥按钮。当您看到标题为Add a SSH Key的表单时,将剪贴板的内容粘贴到表单中。然后按下添加键按钮。添加 SSH 密钥的过程如图 12-1 所示。按照标记为 A、B、C、D 和 e 的箭头进行操作。完成后,上传的密钥将列在 SSH 密钥下,如图 12-2 所示。

9781430261032_Fig12-01.jpg

图 12-1 。在 Github 帐户上添加新的 SSH 密钥

9781430261032_Fig12-02.jpg

图 12-2 。成功上传的密钥列在 SSH 密钥下

配置完成。您可以通过使用 SSH 协议克隆任意的公共存储库来验证这一点。如果命令:

$ git clone git@github.com:jquery/jquery.git

成功,那么您的密钥是正确的。您可以使用 SSH 协议来访问 Github 上的存储库。

它是如何工作的

SSH 协议不允许匿名访问。要使用它,您需要一个服务器上的帐户。Github 对所有用户使用一个名为 git 的帐户。这就是为什么所有克隆命令都包含 git@github,如:

$ git clone git@github.com:jquery/jquery.git

上面的命令意味着您正在主机 github.com 上访问一个 git 帐户。只有在基于密钥的认证成功的情况下,在 github.com 上工作的 SSH 守护进程才会授予对 git 帐户的访问权限——如果您的公共 SSH 密钥是为您的 git@github.com 帐户配置的,就会发生这种情况。否则,您将无法访问任何资源。这种托管解决方案在配方 11-5 和 11-10 中进行了解释。在图 12-1 和图 12-2 中呈现的 web 界面只是简化了管理你的公共 SSH 密钥的任务。

12-3.为新项目创建 Github 托管的存储库

问题

你想开始一个新项目,并把它放在 Github 上。

解决办法

启动您的网络浏览器并登录到 Github.com。然后点击创建新的回购按钮。按钮创建新的回购在 Github 网页右上角显示的菜单上(参见图 12-3 中的 A)。您将在图 12-1 和图 12-2 中找到相同的按钮。

9781430261032_Fig12-03.jpg

图 12-3 。在Github.com创建新的存储库

当您按下创建一个新的回购按钮时,您将看到图 12-3 中显示的表单。在储存库名称编辑框中输入 12-03,然后按下创建储存库按钮(参见图 12-3 中的 B 和 C)。

此后,您将看到图 12-4 所示的页面,其中显示了该存储库的 URL 以及您可以用来开始工作的两个程序。假设您的 Github 帐户名为 john-doe,那么您的存储库的 URL 将如下所示:

git@github.com:john-doe/12-03.gi

9781430261032_Fig12-04.jpg

图 12-4 。新创建存储库的网页

你会在图 12-4 所示的编辑框中找到。

Github 的工作目前已经完成。打开 bash 命令行并执行以下命令:

# the commands should be executed in git-recipes directory
$ cd git-recipes
$ git clone git@github.com:john-doe/12-03.git
$ cd 12-03
$ git simple-commit a b c
$ git push -u origin master

现在回到 Github.com,跟随图 12-5 中的链接,你会看到存储在库中的文件列表。该列表将包括三个文件:a.txtb.txtc.txt,如图 12-5 中的 B 箭头所示。

9781430261032_Fig12-05.jpg

图 12-5 。$ git push -u origin 主命令将主分支从您的驱动器发送到您的由 Github 托管的存储库

它是如何工作的

要在 Github 上创建一个新的存储库,你应该使用 web 界面。创建新存储库后,您可以克隆它。使用本地克隆创建新的提交,当您想要在 Github 上发送更新的存储库时,使用$ git push命令。使用图 12-5 中 C 箭头所指的按钮,您可以显示储存在库中的修订列表。列表如图图 12-6 所示。

9781430261032_Fig12-06.jpg

图 12-6 。Github 提供的修订列表

正如你在配方 8-1 中所记得的,每个版本都存储了两个特殊的属性来分配作者。分别是AuthorCommitAuthor是创造代码的人的名字。Commit是版本创建者的名字。对于图 12-6 中出现的修改,AuthorCommit都设置为同一个人,Włodzimierz Gajda和 email gajdaw@gajdaw.pl。Github 用这封邮件来猜测账户的名字。因此,我的提交被标注为由gajdaw创作。因为AuthorCommit数据相同,所以作者简化为简单的gajdaw。这意味着gajdaw创建了代码和提交。

如果提交包含不同名称的Author和不同名称的Commit会发生什么?Github 显示这些信息,如图图 12-7 所示。在图 12-7 中展示的修订版是由 John Doe 编写的,并由 gajdaw 重新修改。Github 显示这些消息:

John Doe authored...
Gajdaw committed...

9781430261032_Fig12-07.jpg

图 12-7 。对于不同作者和提交数据的修订,Github 会同时显示两者

image 提示如何创建如图图 12-7 所示的版本?您可以使用配方 8-1 中描述的$ git commit --amend。您可以以类似的方式使用$ git cherry-pick$ git am$ git rebase命令。

12-4.为现有项目创建 Github 托管的存储库

问题

你已经为你的项目工作了一段时间,现在你想在 Github 上发布它。

解决办法

使用以下命令创建一个项目:

$ cd git-recipes
$ git init 12-04
$ cd 12-04
$ git simple-commit a b c d

启动您的 web 浏览器,登录到 Github.com,并使用图 12-3 所示的过程创建一个名为 12-04 的新的空存储库。

当你到达图 12-4 所示的页面时,你会看到上传的说明:如何将一个现有的项目上传到这个新创建的存储库中。转到 bash 命令行,执行 Github 提示的两个命令:

# the commands should be executed in git-recipes/12-04 directory
$ git remote add origin git@github.com:your-github-username/12-04.git
$ git push -u origin master

当执行这些命令时,您的 Github 存储库应该包含来自您的项目的所有修订:abcd

它是如何工作的

如果您想使用 Github 发布一个现有的项目,您必须创建一个新的空存储库,然后执行两个命令:

$ git remote add origin git@github.com:your-github-username/12-04.git
$ git push -u origin master

12-5.在 Github 上创建组织账户

问题

Github 接口包含一个叫做 pull 请求的特殊操作。它简化了开源项目的工作流程。如果您想练习如何处理拉取请求:

  • 一个发送他的贡献的开发者

  • 接受贡献代码的项目所有者需要两个 Github 角色。在这个秘籍中,你要在 Github 上创建你自己的组织。这将让你有机会在两种不同的性格下工作:

  • 作为普通用户

  • 作为组织的所有者

在接下来的课程中,我们将使用这两个帐户来练习拉取请求。

解决办法

要创建您自己的组织,请访问您的 Github 帐户并进入您帐户的设置,然后进入组织并按下创建新组织按钮。按照图 12-8 中的按钮操作。

9781430261032_Fig12-08.jpg

图 12-8 。创建新组织

新建组织按钮将打开如图图 12-9 所示的对话框。输入您的组织名称和电子邮件地址。我在 Github 上的用户名是 gajdaw,我使用 gajdaw-learning-git 作为我的组织的名称,但是你可以选择任何你喜欢的名称。然后点击页面底部的创建组织

9781430261032_Fig12-09.jpg

图 12-9 。新组织的属性

创建组织后,您可以使用以下 URL 访问其页面:

https://github.com/gajdaw-learning-git

不要使用 gajdaw-learning-git,而是使用您在图 12-9 所示的对话框中输入的名称。我的组织 gajdaw-learning-git 的主页如图图 12-10 所示。

9781430261032_Fig12-10.jpg

图 12-10 。gajdaw-learning-git 组织的主页位于https://github.com/gajdaw-learning-git

它是如何工作的

Github 界面允许你创建组织。因为每个 Github 都可以拥有自己的库,所以您可以使用您的组织来模拟 pull 请求的工作。

注意 Github 数据库许可证严格禁止一个人创建多个帐户。因此,您不能创建两个不同的用户帐户来处理拉请求。

12-6.创建由组织托管的新项目

问题

您想要启动一个由您的组织托管的新项目。

解决办法

访问您的 Github 帐户并:

  1. 按下创建新的回购按钮
  2. 所有者下拉列表中选择您的组织名称
  3. 将新存储库的名称填写为 12-06
  4. 点击页面下部的创建存储库按钮

所有步骤如图 12-11 所示。

9781430261032_Fig12-11.jpg

图 12-11 。创建由您的组织拥有的新存储库

现在打开一个 bash 命令行并执行以下命令:

$ cd git-recipes
$ git clone git@github.com:your-github-organization/12-06.git
$ cd 12-06
$ git config --local user.name admin
$ git config --local user.email admin@example.net
$ git simple-commit a b c
$ git push -u origin master

您必须将您的名称-github-organization 替换为您在图 12-9 中使用的名称。当您运行上述命令时,您在 Github 上托管的存储库现在应该包含由 admin 编写的修订版 a、b、c。

它是如何工作的

对您的组织所拥有的存储库的处理与对您所拥有的存储库的处理是相同的。秘籍 12-3 和 12-6 的唯一区别是,在后者中,您将所有者从您的个人帐户更改为您的组织。

在存储于git-recipes/12-06的存储库中,您将使用管理员身份工作。

12-7.发送拉取请求

问题

Github 主持了一个名为 12-06 的项目。该项目可从以下 URL 获得:

https://github.com/your-github-organization/12-06

您希望为这个项目做出贡献,发送一个包含三个提交的 pull 请求。

解决办法

启动 web 浏览器,登录到您的 Github 帐户,然后转到以下 URL:

https://github.com/your-github-organization/12-06

您将看到图 12-12 中的所示的网页。确保您正在访问由 a 标识的项目 your-github-organization/12-06。

9781430261032_Fig12-12.jpg

图 12-12 。您希望贡献的 your-organization/12-06 知识库的主页

使用标识为 B 的 Fork 按钮来创建您个人的库副本。当您按下按钮时,将会询问您“我们应该在哪里分叉这个存储库?”选择您的个人帐户(不是您的组织)来回答问题。这将重定向到如图图 12-13 所示的网页。您应该注意到,这一次存储库在您的用户名下可用,如下所示:

your-github-username/12-06

9781430261032_Fig12-13.jpg

图 12-13 。您的组织拥有的原始存储库的私有分支

在图 12-13 中显示了可以验证存储库名称的地方。

在这个菜谱中,我们将使用两个存储库。它们的网址是:

  • 原始存储库:git@github.com:your-organization/12-06.git
  • 你的叉子:git@github.com:your-github-username/12-06.git

现在是时候在你的硬盘上创建一个克隆了。打开 bash 命令行,键入以下命令:

$ cd git-recipes
$ mkdir 12-07
$ cd 12-07
$ git clone git@github.com:your-organization/12-06.git.

好好看看上面的命令。这应该是原始存储库的 URL。

现在添加我使用的远程 URL 别名,并指向您的分支:

$ git remote add my git@github.com:your-github-username/12-06.git

保存在 git-recipes/12-07 中的本地存储库包含两个远程别名。命令$ git remote -v打印以下结果:

$ git remote -v
my      git@github.com:your-github-account/12-06.git (fetch)
my      git@github.com: your-github-account/12-06.git (push)
origin  git@github.com:your-organization/12-06.git (fetch)
origin  git@github.com: your-organization/12-06.git (push)

使用以下两个命令检查您的身份:

$ git config user.name
$ git config user.email

上述命令的输出应该包括您的真实姓名和真实电子邮件地址。

您已经准备好为原始存储库做贡献了。为此,创建一个新分支:

$ git checkout -b numbers

创建三个版本一、二、三,使用:

$ git simple-commit one two three

将名为numbers的分支推到 Github 上的 fork,方法是:

$ git push -u my HEAD

转到 web 浏览器,检查您的 fork 是否包含新推送的分支。使用图 12-14 中所示的按钮。

9781430261032_Fig12-14.jpg

图 12-14 。你推的分支在你的叉子里有。您可以使用箭头所指的按钮来查看它

现在,您已经准备好将您的提交发送到原始存储库。按比较&拉动要求如图图 12-15 所示。

9781430261032_Fig12-15.jpg

图 12-15 。按钮比较&拉动请求

图 12-15 中的按钮将打开图 12-16 中所示的对话框。字母 A 显示了关于以下内容的准确信息:

  • 将接收拉取请求的分支

    your-organization:master
    
  • 将被推送的分支

    your-github-username:numbers
    

9781430261032_Fig12-16.jpg

图 12-16 。发送拉请求的对话框

图 12-16 中的字母 B 包含您需要键入的标题。我用 foo bar 作为标题。C 指向您的拉取请求背后的目的的完整描述。d 列出将被推送的提交。要继续请求拉动,点击发送拉动请求

点击发送拉取请求后,你将被重定向到图 12-17 所示的网页。这是您派生的原始存储库。

9781430261032_Fig12-17.jpg

图 12-17 。提取请求已成功发送,并且在贵组织拥有的原始存储库中可用

你的工作结束了。提取请求已发送到原始存储库。这取决于该存储库的所有者是否接受您的“拉”请求。

它是如何工作的

拉请求的工作包括三个存储库,如下所示:

  • Github 上托管的原始存储库
  • 你的 fork 托管在 github
  • 存储在机器上的本地存储库

您负责其中的两个:您的 fork 和您的本地存储库。第一个是在图 12-8 所示的 Githhub 界面中点击 Fork 创建的。本地存储库是作为原始存储库的克隆创建的。三个仓库之间的关系如图图 12-18 所示。

9781430261032_Fig12-18.jpg

图 12-18 。处理拉请求所需的三个存储库之间的关系

使用拉取请求时,工作流组织如下:

  • 您只能在您的本地存储库中提交、合并和重设基础。
  • 您将更改从本地存储库推送到您的分支。
  • 您将 pull 请求从 fork 发送到原始存储库。
  • 您从原始存储库获取最新的更新到您的本地存储库。

命令流程如图 12-19 中的所示。

9781430261032_Fig12-19.jpg

图 12-19 。处理拉请求时的命令流

因为您的本地存储库是一个克隆的原始存储库,origin别名指向组织拥有的存储库。您可以通过以下方式获取其他开发人员所做的最新更新:

$ git fetch origin

如你所知,fetch 不会移动你的本地跟踪分支。要合并本地主分支中的最新修订,您必须执行 merge 或 rebase,例如:

$ git rebase origin/master master

为了使推送到 fork 更容易,您需要为这个存储库设置一个别名。命令:

$ git remote add my git@github.com:your-github-username/writers.git

定义一个名为my的别名。因此,如果您想使用 fork,可以使用以下命令:

$ git push my branch-name
$ git push my HEAD

第一个命令推送具有给定名称的分支,第二个命令推送当前分支。

当您的分支和您的贡献在分叉的存储库中可用时,您可以发送一个 pull 请求。

我更喜欢通过克隆由 organization 托管的原件来创建存储在我的机器上的本地存储库。然后,别名 original 用于获取其他人最近做出的贡献,而我的别名用于将我的新贡献推送到分叉存储库中。但是您也可以使用其他设置。您可以克隆您的 fork,并为 organization 托管的原始存储库定义一个名为 upstream 的新远程。然后,您将使用 origin alias 将您的贡献推送到 fork,并使用 upstream alias 下载最新的贡献。除了重命名的遥控器,这两种解决方案实际上没有区别。

12-8.返工您的拉动式请求

问题

您参与了一个发送拉请求的开源项目。但是你的贡献没有被接受。你被要求做一些改进。您希望在您的拉请求中添加两个新的提交。

解决办法

转到您的本地存储库,您在其中创建了作为拉请求发送的修订。它位于 git-recipes/12-07 目录中:

$ cd git-recipes/12-07

我假设您发送了一个 pull 请求,将本地 numbers 分支的修订合并到原始存储库中的主分支。为了获得清晰的线性历史记录,您应该使用远程主分支中的最新版本更新您的本地分支。执行以下命令:

$ git fetch origin
$ git rebase origin/master numbers

这些命令会将你的修改移动到origin/master分支的顶部。现在,您可以向 pull 请求添加一些提交。你目前的分行在重定基数后是数字。在其中创建两个新版本:

$ git simple-commit red green

把它推到你的叉子上:

$ git push -f my HEAD

您的拉动请求现在应该包含红色和绿色两个新版本。你可以用下面的方法来检查。转到原始库,按照图 12-20 所示的拉取请求按钮。

9781430261032_Fig12-20.jpg

图 12-20 。“拉请求”按钮提供了所有拉请求的列表

现在从列表中检查您的拉取请求。你会在窗口的下部看到你的两个修改,如图图 12-21 所示。

9781430261032_Fig12-21.jpg

图 12-21 。推送到分叉存储库中适当分支的新修订会自动添加到拉请求中

它是如何工作的

分叉存储库中的分支可用于向拉请求添加更多的提交。只要拉取请求未被接受,您就可以推送到该分支。

原始存储库中的主分支可能会向前移动。如果是这种情况,您应该使用以下内容更新您的本地存储库:

$ git fetch origin
$ git rebase origin/master numbers

当新的提交准备好时,您可以用一个命令将它们附加到现有的拉请求中:

$ git push -f my numbers

如果在更新阶段你的修改被重新设定了基数,标志-f是必要的。如果原始主分支中没有新的修订,那么您可以使用不带-f标志的命令:

$ git push my numbers

请记住,您当地的分支机构,以及您 fork 中的分支机构,在被接受之前,都被视为您的私人工作。这意味着您可以使用第八章中描述的任意方法来调整历史记录。您可以添加新的提交,可以对提交进行重新排序,可以删除提交,最后,您可以将所有提交压缩成一个提交。

请记住,每当您想要为 Github 上的项目做贡献时,请使用分支。不要为此在你的总分行工作。如果您在单独的分支中准备您的拉取请求,这不会使您的工作变得复杂。当拉取请求被拒绝时,您只需删除该分支。否则,您需要使用$ git reset --hard HEAD∼n来调整主分支,以删除被拒绝的修订。

12-9.接受拉取请求

问题

你拥有一个流行的知识库。开发人员通常会为您的项目做出贡献。您希望接受刚刚收到的拉请求。

解决办法

在这个秘籍中,你将只在 Github 提供的网络界面中工作。转到您的组织,然后选择存储库 12-06 并打开可用的“拉”请求列表。接下来,打开包含有关拉取请求的详细信息的页面。您可以按照图 12-22 中的所示的链接进行操作。

9781430261032_Fig12-22.jpg

图 12-22 。由您的组织托管的存储库中所有提取请求的列表

拉动请求的细节如图 12-23 中的所示。您可以分析所有贡献的代码。当你确定代码是正确的,你可以点击图 12-23 所示的按钮进行合并。

9781430261032_Fig12-23.jpg

图 12-23 。用于合并拉取请求的按钮

如图 12-24 所示,当拉动请求被合并时,它将包含在项目历史中。

9781430261032_Fig12-24.jpg

图 12-24 。合并的拉取请求在项目历史记录中可用

它是如何工作的

发送到存储库的所有拉请求都列在图 12-22 所示的页面上。你可以彻底检查他们每一个人。特别是,Github 允许您:

  • 列出包含在拉请求中的提交
  • 列出拉请求修改的文件
  • 要列出修改的行
  • 与其他开发人员讨论拉取请求

当您确定拉动请求正确时,您可以使用图 12-23 中的所示的合并拉动请求按钮来接受它。

摘要

在这一章中,我向你提供了使用 Github 作为项目托管平台的基本信息,以及为他人所有的项目做贡献的程序。

如果您想使用 Github 托管一个开源项目,请遵循秘籍 12-3、12-4 和 12-6。每当你收到一个拉动请求,你可以按照秘籍 12-9 接受它。

也许你想为一个开源项目做贡献?你可以用秘籍 12-7 和 12-8 中解释的方法来做这件事。

如果你主持一个由你信任的开发团队开发的项目,你可以避免你和你的朋友使用拉请求。设置适当的权限,你可以允许其他 Github 用户直接推送到原始存储库。在图 12-25 中所示的团队管理链接下有一个对话框。

9781430261032_Fig12-25.jpg

图 12-25 。“团队管理”链接会将您带到可以为其他开发人员分配权限的页面

如果您需要更多的参数来尝试 Github,请考虑它包含一个嵌入式问题跟踪器。所有参与该项目的贡献者都可以用它来讨论提交的代码。所有这些特性使 Github 成为一个真正伟大的代码共享平台。

十三、更多秘籍

在这最后一章,我将讨论一些尚未涉及的细节,这些细节迟早会成为你不可或缺的。您将了解到:

  • 如何使用命令$ git diff比较不同版本的文件?
  • 如何克服关于行尾的问题?
  • 配置忽略文件的三种不同方法
  • 使用标签
  • 命令$ git archive生成包含项目的压缩包

13-1.使用$ git diff 命令

问题

您想学习如何使用$ git diff命令来分析同一文件的两个版本之间的差异。

解决办法

创建新的存储库:

$ cd git-recipes
$ mkdir 13-01
$ cd 13-01
$ git init

然后创建名为numbers.txt的文件,其内容如清单 13-1 所示。

清单 13-1。 第一版文件编号. txt

one
two
three
four
five
six
seven
eight
nine
ten

使用以下命令提交文件:

$ git add -A
$ git commit -m "Numbers: en"

现在,存储库是干净的,三个快照——第一个存储在HEAD中,第二个存储在暂存区中,第三个存储在工作目录中——都包含相同版本的numbers.txt文件。

接下来,更改文件的内容。将四个单词fourfivesixseven替换为包含单词foobar的两行。您应该获得的文件如清单 13-2 中的所示。

清单 13-2。 第二版文件编号. txt

one
two
three
foo
bar
eight
nine
ten

命令$ git status -sb现在将文件打印为:

_M numbers.txt

文件已修改,但尚未暂存。

命令$ git diff现在将产生如清单 13-3 所示的输出。当不带任何参数执行时,命令$ git diff比较工作目录和暂存区。

清单 13-3。$ git diff 命令的输出

index f5ef170..a769e64 100644
--- a/numbers.txt
+++ b/numbers.txt
@@ -1,10 +1,8 @@
 one
 two
 three
-four
-five
-six
-seven
+foo
+bar
 eight
 nine
 ten

将文件numbers.txt暂存为:

$ git add -A

该命令之后,文件处于M_状态。命令$ git status -sb将会显示:

M_ number.txt

现在,命令$ git diff打印空的结果。这意味着临时区域中的文件与工作目录中的文件相同。如果您想比较暂存区中的文件和存储在HEAD中的文件,您可以使用附加参数--staged:

$ git diff --staged

上述命令比较存储在HEAD中的文件和暂存区中的文件。结果将与清单 13-3 中的完全相同。

image 提示命令$ git diff将工作目录与暂存区进行比较。命令$ git diff --staged将暂存区与 HEAD 指向的版本中存储的版本进行比较。

现在,使用以下命令提交阶段性更改:

$ git commit -m "Numbers: foo bar"

命令$ git status -sb证明您的存储库是干净的。所有三个快照,HEAD、staging area 和工作目录,包含完全相同版本的文件numbers.txt。因此,这两个命令:

$ git diff
$ git diff --staged

打印空结果。

使用以下命令,通过比较下一个到最后一个版本HEAD∼和最后一个版本HEAD来完成配方:

$ git diff --unified=1 HEAD∼ HEAD

上述命令的输出如清单 13-4 所示。

清单 13-4。$ git diff-unified = 1 头的输出∾头命令

diff --git a/numbers.txt b/numbers.txt
index f5ef170..a769e64 100644
--- a/numbers.txt
+++ b/numbers.txt
@@ -3,6 +3,4 @@ two
 three
-four
-five
-six
-seven
+foo
+bar
 Eight

附加参数--unified=1更改了更改内容前后的行数。

它是如何工作的

命令$ git diff使用 GNU diffutils 工具定义的格式,可从以下网址获得:

http://www.gnu.org/software/diffutils/

$ git diff命令产生符合以下格式的输出:

--- a/some-file-name
+++ b/some-file-name
@@ -a,b +c,d @@
xxx
+yyy
-zzz
qqq

上面的描述明确地定义了文件的两个版本:应用更改之前的版本和应用更改之后的版本。使用上面的输出,我们可以构建两种状态的文件:更改前和更改后。

前两行告诉您输出描述了名为some-file-name的文件的变化。通过删除以+开头的行并写入以-开头的行(不带前导破折号),可以检索到更改前的版本。上面的输出描述了包含更改之前的文件:

xxx
zzz
qqq

更改后的版本可以通过删除以破折号开头的行并写入以+开头且不带前导加号的行来检索。更改后的文件如下所示:

xxx
yyy
qqq

专线:

@@ -a,b +c,d @@

定义了两个线条范围:a,bc,d。第一个范围a,b描述了文件的第一个版本。第二个范围,c,d描述了文件的第二个版本。

范围a,b表示该版本在更改前:

xxx
zzz
qqq

从第a行开始,包含第b行。

第二个范围c,d表示修改后的这个版本:

xxx
yyy
qqq

从第c行开始,延续到第d行。

$ git diff命令允许您更改输出中使用的行数。命令:

$ git diff --unified=4

以这样一种方式改变$ git diff的行为,即每一个改变都被四条未修改的线包围,如:

--- a/some-file-name
+++ b/some-file-name
@@ -a,b +c,d @@
xxx
xxx
xxx
xxx
+yyy
-zzz
qqq
qqq
qqq
qqq

清单 13-4 中的显示的输出是用--unified=1生成的,因此描述被单线包围(它们包含单词threeeight):

@@ -3,6 +3,4 @@ two
 three        <- the first surrounding line
-four
-five
-six
-seven
+foo
+bar
 eight        <- the last surrounding line

感谢3,6,我们知道文件的第一个版本从第3行开始,由6行组成:

three
four
five
six
seven
eight

3,4告诉我们文件的第二个版本从第3行开始,由4行组成:

three
foo
bar
eight

为了产生如清单 13-4 所示的输出,我们传递了两个标识符HEAD∼HEAD来比较不同的修订。同样,您可以比较不同的分支:

$ git diff master dev

以及存储在不同分支中的文件:

$ git diff master dev -- some-file

默认情况下,$ git diff比较行。您还可以更改其行为来搜索已更改的单词。这可以通过以下方式实现:

$ git diff --word-diff

如果你想找到给定文件some-file被修改的版本,你可以使用两个:

$ git diff --name-only master dev -- some-file

$ git log --name-only master dev -- some-file

image 提示命令$ git diff有一个非常有用的参数--check,可以用来验证提交没有引入只影响白色字符的更改。命令$ git diff --check 报告处理白色字符的问题。

13-2.提交文件而不进行行尾转换

问题

您想要启动一个新的存储库,其中包含具有不同行尾类型的文本文件。有些使用类似 Linux 的由单个LF字符组成的行尾,有些使用类似 Windows 的由两个字符CRLF组成的行尾。您的存储库甚至包含使用两种类型的文件:混合在一个文件中的LFCRLF。您希望提交所有文件,而不对行尾字符进行任何转换。

image 提示首先,您可能会认为同时使用了LFCRLF的文件已损坏。但无论如何你都可能需要它们。当我在一个库上工作来处理由外部工具产生的文本文件时,我发现它们作为测试的静态附件非常有用。原来,我使用的应用生成了损坏的文件,不仅包含LFCRLF,还包含CR作为行尾。三个都混在一个文件里!

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 13-02
$ cd 13-02
$ git init

创建存储在不同目录中的三个文件:

$ mkdir linux
$ mkdir mixed
$ mkdir windows

$ printf "linux \n a \n b \n c \n d" > linux/abcd.txt
$ printf "mixed \n a \r\n b \n c \r\n d" > mixed/abcd.txt
$ printf "windows \r\n a \r\n b \r\n c \r\n d" > windows/abcd.txt

第一个名为linux/abcd.txt的使用了LF行尾(它们通常在字符串中被编码为\n)。名为mixed/abcd.txt的第二个文件包含了LFCRLF行尾。最后一个名为windows/abcd.txt的文件使用了CRLF行尾。这些在嵌入字符串时,写成\r\n。您可以使用以下命令验证行尾:

$ hexdump -c linux/abcd.txt
$ hexdump -c mixed/abcd.txt
$ hexdump -c windows/abcd.txt

要提交文件而不转换新行,请关闭core.autocrlf设置:

$ git config --local core.autocrlf false

提交文件:

$ git add -A
$ git commit -m "Three files: linux, windows and mixed line endings"

现在,存储在数据库.git/objects中的最近提交包含以下行尾:

  • linux/abcd.txt用途LF
  • mixed/abcd.txt同时使用LFCRLF
  • windows/abcd.txt用途CRLF

工作目录和临时区域中的文件完全相同。

它是如何工作的

Git 配置包含一个选项core.autocrlf。该选项控制 git 处理行尾转换的方式。它可以取三个不同的值:trueinputfalse。因为行尾的转换可以在签出或提交文件时执行,所以我们必须分析这两种情况下每个值的含义。

第一个值true,影响签出和签入。在结帐过程中,git 将LF字符转换成CRLF。提交时,执行反向转换:CRLF行尾转换为LF

第二个值input,在检入操作期间开启从CRLFLF的转换。使用此设置执行签出时,没有转换。

最后一个值false,关闭所有转换。存储在对象数据库中的文件与工作目录中的文件具有相同的行尾。

core.autocrlf的三个值的含义在表 13-1 中进行了总结。

表 13-1 。core.autocrlf 选项的所有值及其对签出和提交的影响

|

价值

|

检验

|

犯罪

|
| --- | --- | --- |
| true | LF => CRLF | CRLF => LF |
| input | 没有人 | CRLF => LF |
| false | 没有人 | 没有人 |

正如你在配方 8-4 和表 8-1 中所记得的,你的存储库由三个快照组成。我们可以将它们表示为HEAD、暂存区和工作目录。在资源库 13-02 中,这三个区域包含表 13-2 中显示的行尾。

表 13-2 。存储库 13-02 中三个快照的行尾

image

所有三个快照都使用完全相同的行尾。

13-3.在不进行行尾转换的情况下签出文件

问题

您的 git 配置包含设置为truecore.autocrlf选项。因此,当您克隆一个存储库时,工作目录中的行尾会被转换成CRLF

您刚刚克隆了存储库,并将core.autocrlf设置为true。您的意图是使工作目录中的文件与数据库中的文件完全相同。因为core.autocrlf被设置为true,你已经创建了一个你认为损坏的克隆体。你想改正你的错误。

你的任务是再次签出所有文件。这一次你要避免任何行尾的转换。您希望工作目录中的行尾与数据库中 HEAD revision 中存储的行尾相匹配。

解决办法

为了理解解决方案,您首先需要创建一个包含损坏文件的存储库。

通过以下方式将core.autocrlf设置为true:

$ git config --global core.autocrlf true

然后从以前的配方中克隆存储库:

$ cd git-recipes
$ git clone 13-02 13-03

core.autocrlf设置为true导致了linux/abcd.txt文件的转换。它现在包含了CRLF行尾。带有混合行尾的文件,即mixed/abcd.txt,未被转换。在core.autocrlf设置为true的克隆后,您得到的行尾在表 13-3 中总结。

表 13-3 。core.autocrlf 设置为 true 的$ git clone 命令之后,存储库 13-03 中三个快照的行尾

image

现在,工作目录包含存储在 HEAD revision 中的不同行尾。因此,您可能认为您的存储库很脏;然而,事实并非如此。如果您使用$ git status命令,您将看到存储库是干净的。这是因为 git 会为您处理行尾的转换。这可能导致另一个问题:如何提交带有CRLF行尾的文件?我们将在秘籍 13-4 中讨论这个问题。

现在,您认为您的工作目录已经损坏。您的意图是让工作目录中的文件具有与 HEAD 中相同的行尾。要再次执行签出操作,这次不对行尾进行任何转换,请按照以下步骤操作:

  1. 删除所有被跟踪的文件:$ git ls-files | xargs rm
  2. 移除暂存区:$ rm .git/index
  3. 关闭新行转换:$ git config --local core.autocrlf false
  4. 重新创建工作目录和暂存区:$ git reset --hard

现在暂存区和工作目录包含的文件与它们在HEAD版本中存储的完全一样。存储库中使用的行尾与表 13-2 中的相同。

它是如何工作的

如果您将选项core.autocrlf设置为true,然后克隆存储库,使用LF行尾的文件将被转换为使用CRLF行尾。因此,紧接着:

$ git clone 13-02 13-03

文件linux/abcd.txt现在包含了CRLF行尾。包含LFCRLF的文件,如mixed/abcd.txt,不会被转换。

要执行在工作目录和临时区域中重新创建所有文件的签出,您必须:

  • 删除所有跟踪的文件
  • 移除临时区域
  • 使用带有--hard选项的$ git reset命令

所有被跟踪文件的列表由$ git ls-files命令返回。如果用xargs将结果列表传递给$ rm命令:

$ git ls-files | xargs rm

所有被跟踪的文件都将从您的工作目录中删除。

暂存区存储在.git/index文件中。您可以用$ rm .git/index命令删除这个文件。

执行上述命令后,暂存区和工作目录不再包含存储在HEAD中的文件。正如您已经知道的,您可以用$ git reset --hard命令重新创建工作目录和暂存区。该命令使用存储在HEAD中的快照重新创建工作目录和暂存区。如果在core.autocrlf设置为false的情况下执行操作,暂存区和工作目录将使用原始行尾(存储在HEAD快照中的行尾)填充文件。

当你完成配方 13-3 时,储存库将包含表 13-2 中显示的行尾。

13-4.在签出和提交更改期间,将行尾转换为工作目录中的 CRLF

问题

您在包含使用不同行尾的文本文件的存储库中工作。你想:

  • 将工作目录中的所有文件转换为使用CRLF行尾
  • 将带有CRLF行尾的文件提交到存储库中

这个版本应该在内部(即在 git 数据库中)使用CRLF编码。如果有人在没有任何新行转换的情况下克隆了这个存储库(例如,将autocrlf设置为false,他们应该使用CRLF获得包含所有文本文件的工作目录。

解决办法

从配方 13-1 克隆存储库:

$ cd git-recipes
$ git clone 13-02 13-04
$ cd 13-04

现在将工作目录中的行尾转换为CRLF:

  1. $ git config --local core.autocrlf truecore.autocrlf选项设置为true
  2. $ git ls-files | xargs rm删除所有被跟踪的文件
  3. $ git reset --hard恢复所有被跟踪的文件

存储在工作目录中的文件linux/abcd.txt现在使用CRLF行尾。您希望将该文件提交到存储库中,以使存储在数据库中的对象使用CRLF行尾。但是命令$ git status -sb打印出工作目录干净的信息。因此你不能用简单的$ git add$ git commit命令提交这个文件。

要提交以CRLF行结尾的linux/abcd.txt文件,您必须更新暂存区。遵循以下程序:

4.用$ git config --local core.autocrlf falsecore.autocrlf选项设置为false

5.用$ rm .git/index移除暂存区

6.用$ git reset重新创建.git/index文件

$ git status -sb检查所有文件的状态。如您所见,文件linux/abcd.txt被列为已修改。命令:

$ git diff

产出:

-linux
- a
- b
- c
+linux ^M
+ a ^M
+ b ^M
+ c ^M

在上面的输出中,字符^M代表CR。我们可以说文件linux/abcd.txt被改变了,每一行都包含了一个新的CR字符。

因为$ git status -sb打印了存储库是脏的信息,所以您可以创建一个新的提交。使用以下命令提交所有已更改的文件,完成配方

$ git add -A
$ git commit -m " Standardization: committing line endings changed to CRLF"

现在,储存在 13-04 储存库数据库中的最新版本包含了表 13-4 中的行尾。

表 13-4 。存储库 13-04 的最终状态中的三个快照中的行结束

image

它是如何工作的

如果您:

  • $ git config --local core.autocrlf true打开行尾转换
  • 移除跟踪的文件
  • 签出跟踪的文件

以前使用LF的文件,如linux/abcd.txt,将使用CRLF。文件的内容改变了——原来是LF,现在是CRLF,但是$ git status报告工作目录是干净的。这就导致了以下问题:如何提交行尾改变的文件?

为此,您必须使用工作目录中使用的行尾重新创建临时区域:

  • 关闭行尾转换$ git config --local core.autocrlf false
  • $ rm .git/index移除暂存区
  • $ git reset重新创建.git/index文件

现在$ git status打印工作目录脏的信息。您可以将带有CRLF行尾的linux/abcd.txt文件提交到数据库中。

13-5.将行尾转换为 LF 并提交更改

问题

您在一个包含不同新行编码的存储库中工作。你想:

  • 以使用LF行尾的方式转换工作目录中的文件
  • 将行尾转换为LF的文件作为新版本提交。

存储在 git 数据库中的对象应该包含LF行尾。

解决办法

从配方 13-1 克隆存储库:

$ cd git-recipes
$ git clone 13-02 13-05
$ cd 13-05

并遵循以下程序:

  1. 用一个规则* text=auto创建文件.gitattributes文件。你可以用$ echo "* text=auto" >>.gitattributes来做
  2. $ rm .git/index移除暂存区
  3. $ git reset重新创建.git/index文件

$ git status -sb检查所有文件的状态。如你所见,这次mixed/abcd.txtwindows/abcd.txt都被列为修改过的。使用以下命令完成提交所有已更改文件的配方:

$ git snapshot Standardization: line endings changed to LF.

所有三个快照HEAD、工作目录和暂存区现在都包含了LF行尾。配方 13-5 的结果总结在表 13-5 中。

表 13-5 。存储库 13-05 的最终状态中的三个快照中的行结束

image

注意,这个配方转换了文件mixed/abcd.txt

它是如何工作的

本配方中描述的程序使用以下.gitattributes条目:

* text=auto

由于上面的规则,当签入时,所有的文本文件都将被转换为使用LF

如果您用两个命令$ rm .git/index$ git reset重新创建暂存区,那么$ git status会通知您文件已经更改。下一次提交操作将在数据库中保存以LF行结尾的文件。

13-6.所有行尾的意外转换

问题

您希望了解如何避免在开源项目中无意转换所有行尾。为了更深入地了解这个问题,您希望重现这个失败。您的任务是在https://github.com/hakimel/reveal.js克隆一个托管在 github 上的reveal.js项目,然后以这样一种方式更改存储库配置,git 将认为所有文件都已更改。

解决办法

设置全局 git 配置,以便在签出时执行LF=>CRLF转换,在签入时执行CRLF=>LF转换:

$ git config --global core.autocrlf true

然后克隆reveal.js存储库:

$ cd git-recipes
$ git clone https://github.com/hakimel/reveal.js.git 13-06
$ cd 13-06

紧随克隆命令之后,13-06目录中的所有文本文件将使用CRLF行尾。无论您的操作系统是什么,都将使用这种编码。CRLF将用于 Windows、Linux 和 MacOS。存储库处于干净状态——您可以用$ git status -sb来验证它。

现在,使用以下命令关闭所有行尾转换:

$ git config --global core.autocrlf false

并使用以下内容重新创建临时区域:

$ rm .git/index
$ git reset

即使您的工作目录中的文件没有被改动,上面的更改也会让 git 感到困惑。命令$ git status -sb会通知你 git 认为所有的文本文件都被修改了。

您可以使用以下命令验证工作目录中的更改:

$ git diff --check

它将打印关于行尾变化的警告。

image 提示如果你用的是 Linux,可以跳过这两个命令:$ rm .git/index$ git reset在这个菜谱里;你会得到同样的结果。

它是如何工作的

项目reveal.js使用LF行尾。存储在对象数据库中的所有文本文件都使用行尾LF。当您在autocrlf设置为true的情况下执行克隆时,git 将会——在签出期间——执行LF=>CRLF转换;您的工作目录将包含带有CRLF的文件。但是,临时区域将使用原来的行尾,即LF

只要您打开了autocrlf, git 将在比较工作目录和临时区域时使用CRLF=>LF转换。因此,存储库保持干净。

如果使用$ git config --global core.autocrlf false关闭签入期间执行的转换,git 会将工作目录与临时区域进行比较,而不进行任何转换。因为存储在这两个位置的文件使用不同的行尾,所以$ git status命令报告在您的工作目录中有未暂存的更改。

这是你应该避免的情况。

想象现在在这种状态下你想为reveal.js做贡献。您更改了其中一个文件中的一行,然后用$ git add -A$ git commit命令提交更改。这个修订一旦被接受,将会引起项目领导和其他开发人员的头痛,因为它引入了数百个变更。除了一个以外,所有的都是不必要的,而且可能会被下一个使用不同行尾的投稿人恢复。

这个秘籍提供了一个你应该避免的模式。

13-7.为单个文件和目录定义行尾

问题

你开始一个新项目。您希望以这样的方式配置它:

  • 存储在linux/目录下的文本文件总是使用LF行尾。
  • 存储在windows/目录下的文本文件总是使用CRLF行尾。
  • 存储在mixed/目录下的文本文件永远不会被转换——它们总是保留原来的行尾。

解决办法

初始化新的存储库:

$ cd git-recipes
$ git init 13-07
$ cd 13-07

创建目录和文件:

$ mkdir linux
$ mkdir mixed
$ mkdir windows

$ printf "linux \n a \n b \n c \n d" > linux/abcd.txt
$ printf "mixed \n a \r\n b \n c \r\n d" > mixed/abcd.txt
$ printf "windows \r\n a \r\n b \r\n c \r\n d" > windows/abcd.txt

接下来创建包含以下内容的.gitattributes文件:

*           eol=lf
windows/*   eol=crlf
mixed/*     -text

最后,使用$ git snapshot Initial commit命令提交所有文件。

存储库现在包含了定义行尾转换的非常精确的规则。现在,如果有人克隆了这个库,那么不管他或她的设置是什么,克隆的库将包含我们在$ print命令中使用的完全相同的行尾。要验证这一点,用以下方法将core.autocrlf设置为true:

$ git config --global core.autocrlf true

然后克隆存储库:

$ cd ..
$ git clone 13-07 13-07-verification
$ cd 13-07-verification

命令:

$ hexdump -c linux/abcd.txt

打印带有LF行尾的文件内容。这证明即使core.autocrlf被设置为true,也没有执行转换。

它是如何工作的

规则* eol=lf强制 git 总是使用LF行尾签出所有文件。因此,默认情况下,所有文件都将使用LF编码。特别是存储在linux/目录下的文件。第二条规则是windows/* eol=crlf,定义了第一条规则的例外。当检出存储在windows/目录下的文件时,将使用CRLF。最后一个规则mixed/* -text,关闭所有保存在mixed/目录下的文件的行尾转换。

写在.gitattributes文件中的配置覆盖用$ git config命令定义的设置。因此,无论您的设置是什么,项目的工作目录将始终遵循预定义的假设:

  • 存储在windows/下的所有文本文件都将使用CRLF
  • 存储在mixed/下的所有文本文件将始终保留原始行尾
  • 所有其他文本文件将使用LF

image 提示这个解决方案用在一个 jQuery 项目中。由于存储在.gitattributes文件中的* eol=lf规则,所有文本文件总是使用LF作为行尾字符进行编码;无论您的平台和配置如何。

13-8.忽略自动生成的文件

问题

您开始了一个新项目,其中一些工具会生成临时文件。您不希望将它们提交到存储库中。项目中的临时文件符合以下规则:

  • 它们存储在/ tmp/目录中。
  • 它们的名称以扩展名.abc结尾。

因此,您希望忽略与以下两种模式匹配的文件:

/tmp/
*.abc

您希望与参与此项目的所有开发人员共享忽略文件的规则。

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 13-08
$ cd 13-08
$ git init

使用以下内容创建一个空的初始版本:

$ git commit --allow-empty -m "Initial commit"

用以下内容创建名为.gitignore的文件:

/tmp/
*.abc

您可以使用以下两个命令来完成此操作:

echo /tmp/ > .gitignore
echo "*.abc" >> .gitignore

使用以下命令将文件.gitignore提交到存储库中:

$ git add -A
$ git commit -m "Gitignore: new rules to ignore files"

存储库已准备就绪;可以分享给其他开发者。

要测试存储在/tmp/目录中的文件和扩展名为.abc的文件是否真的被忽略,请创建两个文件:

$ echo abc > some-file.abc
$ mkdir tmp
$ echo def > tmp/some-file.txt

并使用$ git status命令检查存储库的状态。$ git status不会报告与.gitignore文件中定义的模式匹配的文件。

它是如何工作的

如果您的项目包含一些自动生成的文件,您可能应该忽略它们。最好的方法是创建一个名为.gitignore的特殊文件。该文件应该包含 git 将忽略的模式。因此,如果您将文件提交到您的存储库中,您在同一个项目中工作的所有同事将共享这些规则。

存储在.gitignore中的规则如下。

  • 如果规则以斜杠/开始,它将只匹配存储在项目根目录中的条目。规则/foo只会匹配文件/foo,而不会匹配文件some/dir/foo
  • 如果规则以斜杠/结束,它将只匹配目录。因此,规则bar/将匹配目录bar/some/other/dir/bar/,但不匹配文件some/special/bar

您可以基于每个目录使用.gitignore文件。目录中存储的.gitignore文件会影响该目录及其子目录。

三种类型的设置

应该忽略的文件模式可以在三个不同的级别上定义:

  • .gitignore—该文件被提交到目录中;它只影响提交它的存储库。
  • 全局.gitignore—该文件驻留在您的主目录中。它会影响您所有的存储库。这是您的私有文件:您不需要将该文件提交到存储库中。
  • .git/info/exclude—该文件存储在。git 目录;这是你的私人文件,你不能与他人分享。exclude文件只影响一个存储库:包含该文件的存储库。

如何清理包含被忽略文件的项目?

如果存储库包含被忽略的文件,您可以使用以下命令删除所有被跟踪的文件:

$ git ls-files | xargs rm

如果您想删除所有未被跟踪的文件,请使用:

$ git clean -f

13-9.使用自定义项目。分发文件

问题

您想要启动一个新的互联网应用来发布博客。您计划将应用作为开放源代码发布。博客条目将存储在数据库中,访问数据库服务器的凭证将存储在一个文件中。

为了让计划使用您的应用的人的生活更轻松,您需要:

  • 定义忽略配置文件的规则
  • 创建配置文件的一般结构

.gitignore 文件和通用配置文件都应该与应用的代码一起提交。

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 13-09
$ cd 13-09
$ git init

使用以下内容创建一个空的初始版本:

$ git commit --allow-empty -m "Initial commit"

创建名为database.ini-dist的配置文件。文件的内容显示在清单 13-5 中。

清单 13-5。 配置文件 database.ini-dist

[parameters]
    database_host     = your.host.example.net
    database_name     = dbname
    database_user     = admin
    database_password = sEcrEtPaSSword

创建包含单个规则的.gitignore文件:

/database.ini

您可以使用以下命令生成该文件:

$ echo /database.ini > .gitignore

提交两个文件:

$ git add -A
$ git commit -m "Generic database configuration"

它是如何工作的

如果有人想使用你的应用,他必须克隆它并定制配置。用户必须将文件database.ini-dist重命名为database.ini,并根据自己的设置更改其内容。多亏了.gitignore文件,他的个人设置永远不会被提交到存储库中。

13-10.使用。git/info/exclude 文件

问题

您希望使用 NetBeans IDE 为开源项目http://github.com/symfony/symfony.git做出贡献。

解决办法

克隆您要参与的存储库:

$ cd git-recipes
$ mkdir 13-10
$ cd 13-10
$ git clone http://github.com/symfony/symfony.git .

当您使用 NetBeans 打开一个新项目时,IDE 会在项目的根目录下创建nbproject/目录。为了避免提交目录nbproject/,在.git/info/exclude文件中创建以下条目:

/nbproject/

您可以使用以下命令来完成此操作:

$ echo /nbproject/ > .git/info/exclude

现在启动 NetBeans 并打开您刚刚克隆的项目。IDE 将创建它的/nbproject/目录,但是由于模式/nbproject/存储在.git/info/exclude文件中,这个库保持干净。命令:

$ git status -sb

不报告/nbproject/目录内的更改。

它是如何工作的

许多现代的 ide 使用特殊的目录来存储每个项目的配置。NetBeans 将其配置存储在/nbproject/目录中,JetBrains 生产的 PhpStorm 和其他工具将配置存储在/.idea/目录中。因为每个开发人员可以使用不同的工具和编辑器,所以这些文件和目录通常不会在项目中提交。

因为默认情况下配置存储在存储库的工作目录中,git 将用$ git status 命令报告这些文件。为了避免这种情况,您可以使用.git/info/exclude或您的个人.gitignore文件忽略配置文件。

如果您选择忽略带有.git/info/exclude文件的配置,您将不得不在每个新项目中定义模式/nbproject/

如果您选择使用您的个人.gitignore文件,您可以定义将在您的所有项目中使用的模式。

13-11.使用标签

问题

您希望使用标签来标记项目的发布。

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 13-11
$ cd 13-11
$ git init

使用以下内容创建项目历史:

$ git simple-commit a b c d

现在您想用v1.2.3 标记项目的当前状态。您可以使用以下命令来完成此操作:

$ git tag -a v1.2.3 -m "Release 1.2.3"

使用以下内容创建更多提交:

$ git simple-commit e f g

该州尚未准备好下一个版本。然而,出于某种原因,您希望使用轻量级标签来保留对最近提交的引用。为此,执行以下命令:

$ git tag temp-version

您的存储库现在包含七个提交abcdfg和两个标记v1.2.3temp-version

它是如何工作的

Git 允许您用标签标记任意的修订。有两种类型的标签:

  • 带注释的
  • 轻量级选手

带注释的标签作为对象存储在您的存储库中。它们包含以下信息:

  • 作者
  • 标签的创建日期
  • 评论
  • 被标记的修订的 SHA-1

轻量级标签只包含它们所指向的版本的 SHA-1。

您可以使用以下命令列出所有标记,包括带注释的和轻量级的:

$ git tag

这两种类型的标签都存储在.git/refs/tags目录中。您的.git/refs/tags存储库现在包含两个文件。您可以通过以下方式进行检查:

$ ls .git/refs/tags

存储在.git/refs/tags中的文件包含 SHA-1 散列。在轻量级标签的情况下,这个散列指向修订。对于带注释的标签,散列指向存储在数据库中的标签对象。

命令:

$ git show -s tagname

打印标签的详细信息。对带注释的标签执行时:

$ git show -s v1.2.3

打印关于标签对象的详细信息:

  • 标记名(标记的 SHA-1 散列)
  • 标记者(创建标记的人)
  • 日期
  • 评论
  • 修订本

下面是示例输出:

tag v1.2.3
Tagger: Włodzimierz Gajda <gajdaw@gajdaw.pl>
Date:   Sun Nov 3 10:32:10 2013 +0100

Release 1.2.3

commit b2e1f624d8c7ce5e6a0917ed55d3bfc69bbefd9e

当用于轻量级标签时,该命令仅产生提交的数据:

commit e2833c1517a3873661a35f808349b473f56aff7c
Author: Włodzimierz Gajda <gajdaw@gajdaw.pl>
Date:   Sun Nov 3 10:33:32 2013 +0100

创建、删除和列出标签

要创建带注释的标签,请使用:

$ git tag -a tag-name -m "tag comment" [REVISION]

轻量级标签由以下各项创建:

$ git tag tag-name [REVISION]

要删除带注释的标签和轻量级标签,请使用:

$ git tag -d tag-name

您可以使用以下选项列出标签:

$ git tag

如果您想列出所有按日期排序的标签,请使用以下命令:

$ git log --tags --simplify-by-decoration --pretty="%ai %d"

下面是检查最新注释标签的命令:

$ git describe

发布标签

您可以通过以下方式发布您的标签:

$ git push --tags

每当执行该命令时:

$ git fetch

它将从服务器获取所有的标签。

如果出于任何原因您想要删除远程标签,您应该使用:

$ git push origin :refs/tags/tag-name

与分支类似,当远程存储库中的标签被删除时,您的本地存储库不会受到影响。命令$ git fetch 将获取任何新的标记,但不会删除远程存储库中已删除的任何标记。要将您的标签与远程标签同步,请使用:

$ git fetch --tags

使用标签

标签可以像任何其他版本的标识符一样使用。您可以将它们传递给$ git branch$ git checkout、和$ git reset、命令,例如:

$ git reset --hard v1.2.3
$ git checkout -b my-new-branch v1.2.3

image 提示你可以把标签当成不能动的树枝。

13-12.将存储库导出到压缩档案

问题

您从事的项目刚刚发布了一个稳定的版本。您希望生成一个压缩的归档文件,其中包含对希望使用您的项目的任何人都很重要的所有文件。您不希望在压缩的档案中包含仅对参与该项目的开发人员重要的文件。

解决办法

初始化新的存储库:

$ cd git-recipes
$ mkdir 13-12
$ cd 13-12
$ git init

使用以下内容创建一个空的初始版本:

$ git commit --allow-empty -m "Initial commit"

现在创建目录src/doc/,并在其中提交一些文件:

$ mkdir src
$ echo "/* code */" > src/main.c
$ mkdir doc
$ echo "<DOCTYPE html>" > doc/index.html
$ git add -A
$ git commit -m "Source code and documentation"

最后,在Test/目录中创建一些测试:

$ mkdir Tests
$ echo "/* tests */" > Tests/TestMain.c
$ git add -A
$ git commit -m "Tests"

现在您的项目包含三个目录:src/doc/Tests/。存储在src/doc/中的文件对您项目的用户来说很重要。存储在Tests/目录中的文件只对参与您的项目的开发人员重要。对于任何想使用你的项目的人来说,它们并不重要。

用以下内容创建名为.gitattributes的文件:

/Tests/ export-ignore
/.gitattributes export-ignore

您可以使用以下命令来完成此操作:

$ echo "/Tests/ export-ignore" > .gitattributes
$ echo "/.gitattributes export-ignore" >> .gitattributes

提交此文件:

$ git add .gitattributes
$ git commit -m "Gitattributes to exclude /Tests/ and /.gitattributes from ZIP"

该项目已经达到了历史上的一个稳定点。使用以下命令将其标记为 v2.3.4:

$ git tag -a v2.3.4 -m "Release 2.3.4"

最后,生成包含项目版本 2.3.4 的压缩归档文件:

$ git archive --format=zip --output=../project-v2.3.4.zip v2.3.4

该命令将创建文件git-recipes/project-v2.3.4.zip。该文件将包含您的项目,但没有Tests/目录和.gitattributes文件。要列出压缩文件的内容,可以使用以下命令:

$ unzip -l ../project-v2.3.4.zip

它是如何工作的

命令:

$ git archive --format=zip --output=filename.zip [REVISION]

将保存在[REVISION]中的版本的项目导出到名为filename.zip的文件中。该文件以 ZIP 格式存储。由于有了这个命令,您不必创建项目的压缩版本,例如:

project-v0.1.2.zip
project-v2.8.4.zip
project-v5.15.89.zip

将项目保留在特定版本中。您所要做的就是创建标签,例如:

$ git tag -a v0.1.2 -m "Release 0.1.2"
$ git tag -a v2.8.4 -m "Release 2.8.4"
$ git tag -a v5.15.89 -m "Release 5.15.89"

如果您想获得特定版本的压缩文件,您只需执行一个命令,例如:

$ git archive --format=zip --output=project-v0.1.2.zip v0.1.2
$ git archive --format=zip --output=project-v2.8.4.zip v2.8.4
$ git archive --format=zip --output=project-v5.15.89.zip v5.15.89

所有版本都存储在同一个存储库中,可以使用$ git archive命令按需生成。不需要备份project-v2.8.4.zip等文件。如果您备份了您的存储库,您将始终能够生成所有被标记的特定版本。

Gitattribute file 允许您从自动生成的归档文件中排除一些文件。当.gitattributes文件包含以下规则时:

/Tests/ export-ignore

那么生成的档案将包含除了存储在/Tests/目录中的文件之外的所有文件。

摘要

本章介绍的第一个命令$ git diff,将帮助您检查项目的状态。它使用 GNU diffutils 工具定义的格式报告变化。默认情况下,在没有参数的情况下调用时:

$ git diff

将工作目录与临时区域进行比较。使用--staged参数,您可以将暂存区与HEAD进行比较:

$ git diff --staged

使用两个修订版调用 git diff,比较这些修订版中的文件:

$ git diff master beta

附加参数--unified可用于指定将要打印的未更改行数。

配方 13-2 至 13-7 提出了各种有关行尾的问题。因为 git 是一个同步一组开发人员工作的工具,并且因为每个开发人员可以使用不同的平台,所以您必须意识到可能会使团队工作复杂化的问题。

讨论的第一个项目是如何提交以及如何在 git 不执行任何转换的情况下,完全按照文件的原样签出文件。这些在秘籍 13-2 和 13-3 中提到的事情,在你对你的行尾有问题并想摆脱它们的时候是必不可少的。请记住,要获得干净的签出,您可以删除跟踪的文件:

$ git ls-files | xargs rm

和集结地:

$ rm .git/index

命令$ git reset --hard使用存储在HEAD中的快照重新创建工作目录和暂存区。

秘籍 13-4 和 13-5 更详细地解释了如果你想提交行尾分别转换成 CRLF 和 LF 的文件,你应该遵循的步骤。

秘籍 13-6 提出了一个反模式,应该让你相信行尾是多么重要。当遵循时,它将产生以下错误:即使没有文件被编辑,git 也将它们报告为全部被更改。如果任何一个开发人员提交这种类型的变更,将会使其他开发人员感到困惑。顺便说一下,$ git diff命令在处理关于白色字符,尤其是行尾的问题时也很有用。用--check参数调用时:

$ git diff --check

输出可被视为白色字符问题的更改。记住,防止所有行尾问题的最好和最简单的解决方案之一是使用.gitattributes文件——在每个模式的基础上定义行尾。使用简单的规则* eol=lf,你可以避免所有关于行尾的问题。该解决方案在配方 13-7 中给出。

配方 13-8、13-9 和 13-10 给出了三个最典型的问题,通过定义适当的规则来忽略文件,可以解决这些问题。第一个问题涉及由各种工具自动生成的二进制文件。以下是一些例子:

  • 编译时生成的文件:*.oa.out*.exe*.lib
  • 执行应用时生成的缓存配置;它可以存储在一些特殊的目录中,比如cache/
  • 依赖项—嵌入到项目中的外部库;它们可以存储在一些特定的目录中,比如vendor/

因为所有开发人员都会不时地生成上述所有文件,所以对于这些文件,最好的解决方案是使用项目提交的.gitignore文件来忽略它们。

第二种情况与包含一些私人信息的文件有关。它可以是具有访问外部资源(如数据库)的凭证的配置。在这里,所有的开发者和用户都将面临同样的问题:如何避免无意中发布这些敏感数据。因此,排除这些类型文件的规则也应该存储在提交到存储库中的.gitignore文件中。

第三种情况涉及到每个开发者的私有设置。我使用特定的工具在项目中创建特定的文件。在这种情况下,将规则提交到存储库中是没有意义的。因此,要忽略由我使用的工具生成的文件和目录,我可以使用存储在特定存储库中的.git/info/exclude文件,也可以使用存储在用户主目录中的全局.gitignore文件。

两个最终配方展示了如何使用标签和$ git archive命令。记住 git 提供了两种不同类型的标签:带注释的标签和轻量级标签。带注释的标签存储在.git/objects数据库中。它们包含关于谁、何时以及为什么创建标签的详细信息。轻量级标签只是引用提交的别名。与分支的情况一样:没有关于谁、何时或为什么创建了轻量级标签或分支的信息。有些项目,比如 jQuery,使用轻量级标签。其他的,比如 Symfony,使用带注释的标签。选择由您决定,尽管因为带注释的标签包含作者、日期和评论,它们通常被认为是更好的选择。这两种类型的标签都是通过以下方式发布的:

$ git push --tags

要将您的标签与远程存储库同步,请使用:

$ git fetch --tags

要删除远程标记,您应该使用与删除远程分支相同的命令:

$ git push origin :remote-branch-to-delete
$ git push origin :refs/tags/remote-tag-to-delete
posted @ 2024-08-12 11:18  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报