Git-秘籍-全-
Git 秘籍(全)
原文:Git Recipes
一、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 程序,如ssh
、curl
、ls
、mkdir
、rm
和find
。使用默认设置,安装程序将 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
的配置选项设置为以下值之一:true
、input
或false
。该设置的含义总结在表 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 版本。
提示如果您想将当前目录更改为 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.com
或Bitbucket.org
上的存储库的本地副本?
一旦我们知道如何克隆一个存储库,我们就可以分析它的结构。然后我们将探索工作目录 git 目录及其内容。在这一点上,我们将能够将一个存储库分类为裸露的或者非裸露的。
接下来,我们将讨论打印存储库信息的各种命令,例如
- 修订列表
- 贡献者列表
- 修订的数量
- 贡献者的数量
- git 目录和工作目录的磁盘使用情况
为了使输入长 git 命令变得更容易,我将定义它们的别名。
注我已经在 Linux 和 Windows 两个平台上测试了本章和书中介绍的所有命令。我的目的是提供一套不管你的平台如何都能工作的指令。为了实现这个目标,文件系统操作是用 Linux 命令执行的,比如
ls
和rm
。此外,清单以$
开头,路径使用/
作为分隔符——表明它们是为 Linux 准备的。但是,如果您使用的是 Windows,请不要担心。只需使用 git 发布的 bash 命令解释器,所有的命令都会运行良好。
如果您使用的是不同于 Linux 的类 Unix 系统,一些命令(即du
或echo
)可以使用与本书中介绍的不同的开关。因此,您需要自定义这些命令。
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
注意在克隆命令期间,git 将一个完整的存储库从其原始位置复制到您的本地存储系统。根据您的带宽和项目大小,此操作可能需要相当长的时间。但是不用担心。克隆只在您设置本地存储库时进行一次。所有后续的数据传输都非常高效,因为 git 只传输丢失的数据部分。克隆的内部原理在第十章(配方 10-1)中有解释。
在您运行清单 2-1 中的命令后,您的git-recipes/02-01/
目录将包含如图 2-1 中所示的文件和目录。注意,jQuery 存储在子目录jquery/
下,而不仅仅是在git-recipes/02-01/
中。
图 2-1 。git 克隆命令后 git-recipes/02-01/目录的内容
注意事项 图 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 所示的页面。
图 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 网址。
提示在第十三章我们将创建并配置一个 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.
注意git 克隆命令的语法是:
$ git clone URL [directory]
。如果使用的话,可选的[directory]
参数为克隆的存储库设置目标目录。
它是如何工作的
启动您的网络浏览器并转到https://bitbucket.org/atlassian/aui
。亚特兰大 AUI 知识库的主页如图 2-3 所示。
图 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 帐户时使用。
提示git 最流行的两个托管解决方案是
Github.com
和Bitbucket.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 中所示的文件。
图 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 中创建的存储库之间的区别。
提示一旦您知道可以使用标准的文件系统操作(如
cp
)来复制一个存储库,您可以使用rsync
或scp
来达到同样的效果。在第十一章中,我们将使用 scp 命令启动一个新项目。
2-5.探索 git 存储库的内容
问题
git clone
命令创建的目录包含什么?要回答这个问题,您需要用cd
、ls
和cat
命令探索git-recipes/02-03/
目录的内容。你也可以使用你最喜欢的文件管理器。
解决办法
git-recipes/02-03/
目录的内容如图图 2-5 所示,包含 jQuery 文件和目录以及一个名为.git
的特殊目录。
图 2-5 。在配方 2-3 中创建的存储库
名为.git
的目录称为 git 目录 。它包含关于存储库的所有信息。.git
目录的内容如图 2-6 所示。
图 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"]
指向本地目录。
提示我假设你的
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 所示。
图 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 目录的内容。它不包含工作目录。这种类型的存储库仅用于同步目的。我们会在第十章用到。
记住可以用
$ 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 假设之一。正如你将在第十一章中看到的。整个同步过程都依赖于它。
提示用户不能给版本命名。所有的名字都是 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
提示参数
--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.
提示
git log
手册中提供了占位符的完整列表。您可以使用git help log
命令来访问它。
以下是过滤输出中包含的修订的一些参数:
-n
—修订数,例如–7 将 git 日志限制为最近七次修订--since="yyyy-mm-dd"
—开始日期--until="yyyy-mm-dd"
—完成日期--author="John Doe"
—由给定作者提交
提示传递给
--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
提示 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
然后我们得到期望的天数。
提示这是我用来宣布我写一本书的工作天数的大致标准。当命令
$ 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"
提示您不必键入清单 2-14 中显示的别名。它们都可以在
https://github.com/gajdaw-git-recipes/aliases
存储库中找到。
注意清单 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
注意别名 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
文件中的所有内容。
提示
.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
提示在 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
注意如果追加了&符号
$ gitk &
,gitk 应用将在后台运行,可以使用命令行执行其他命令。
它是如何工作的
命令$ gitk
将显示图 2-8 中所示的窗口。它包含五个面板:
- 修订
- 作者
- 日期
- 修改列表
- 已修改文件的列表
图 2-8 。gitk 应用的主窗口
使用gitk
你不仅可以轻松检查修订列表,还可以检查每个修订中引入的修改。
尝试向下滚动修订。你会发现这些修订并不一定形成一个线性的历史。非线性历史如图 2-9 所示。我们将在第五章、第六章和第七章中讨论非线性历史。
图 2-9 。HTML 5 样板库的历史
注意
$ 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 。
图 3-1 。配方 3-1 中的储存库
解决办法
在这个菜谱中,您将使用 git commit 命令创建您的第一个修订。Git 不允许提交,除非您将您的身份存储在配置文件中。如果到目前为止您还没有这样做,请运行以下两个命令,用您的个人信息替换John Doe
和john.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 所示。
图 3-2 。第一次修订后配方 3-1 的储存库
让我们创建第二个修订版。请遵循以下步骤:
-
创建文件
john-grisham.txt
$ vi john-grisham.txt
-
键入文件的内容:
Novels 1989 | A Time to Kill 1991 | The Firm 1992 | The Pelican Brief
-
保存文件并关闭编辑器。
-
Check the status of the repository:
$ git status -s
输出:
?? john-grisham.txt
通知您一个新的、未被跟踪的文件:
john-grisham.txt
-
将工作目录的当前状态保存为新版本:
$ git add -A
$ git commit -m "Second revision [John Grisham]"
-
Check the status of the repository with:
$ git status -s
空输出证明存储库是干净的。
-
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]
-
存储库应该看起来像图 3-3 。
图 3-3 。第二次修订后配方 3-1 的储存库
完成配方,创建第三个文件stephen-king.txt
和第三个版本。程序如下:
-
创建文件
stephen-king.txt
$ vi stephen-king.txt
-
输入内容:
Novels 1974 | Carrie 1975 | Salem’s Lot 1977 | The Shining
-
保存文件并关闭编辑器。
-
Check the status of the repository:
$ git status -s
存储库不干净:
?? stephen-king.txt
-
为工作目录的当前状态创建修订:
$ git add -A $ git commit -m "Third revision [Stephen King]"
-
Check the status of the repository:
$ git status -s
现在,输出是空的;因此,我们知道存储库是干净的。
-
Check the log with:
$ git l
输出将包含三个修订:
ffa6 Third revision [Stephen King] 0468 Second revision: [John Grisham] de36 First commit [Agatha Christie]
-
储存库如图 3-1 所示。
注意配方 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
提示仓库可以被描述为干净或者肮脏。当我们说库是干净的;这意味着工作目录中的所有文件都存储在最新版本中。在这种状态下,命令:
$ 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
发送三个参数:a
、b
和c
。您可以使用带引号的$*
变量"$*"
来访问所有三个参数。
条件语句 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
创建包含“唱一首六便士的歌”的歌词的第一个修订版。
-
创建文件
sing-a-song-of-sixpence.txt
$ vi sing-a-song-of-sixpence.txt
-
键入文件的内容:
Sing a song of sixpence, A pocket full of rye. Four and twenty blackbirds, Baked in a pie. ...
-
保存文件并关闭编辑器。
-
Check the status of the repository with
$ git status -s
存储库不干净。
-
将工作目录的当前状态保存为新版本:
$ git snapshot Sing a song of sixpence
-
Check the status of the repository with
$ git status -s
仓库是干净的。
-
Check the log of the repository with
$ git l
输出将包含一个版本:
7cfb Sing a song of sixpence
存储库现在看起来像图 3-4 。
图 3-4 。“儿童歌曲”项目第一次改版
创建包含“咩,咩,黑羊”歌曲歌词的第二个修订版。
-
创建
baa-baa-black-sheep.txt
文件:$ vi baa-baa-black-sheep.txt
-
键入文件的内容:
Baa, baa, black sheep, Have you any wool? Yes, sir, yes, sir, Three bags full; ...
-
保存文件并关闭编辑器。
-
Check the status of the repository with
$ git status -s
存储库不干净。
-
将工作目录的当前状态保存为新版本:
$ git snapshot Baa, baa black sheep
-
Check the status of the repository with
$ git status -s
仓库是干净的。
-
Check the log of the repository with
$ git l
输出将包含两个修订:
564f Baa, baa black sheep 7cfb Sing a song of sixpence
“儿童歌曲”项目的现状如图 3-5 所示。
图 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 所示。
图 3-6 。第三次改版后的“儿童歌曲”项目
创建新文件夹PL
:
$ mkdir PL
现在,检查状态:
$ git status -s
多奇怪,储存库是干净的!这是因为 git 不跟踪空目录。
提示绕过 git 禁止提交空目录的限制的公认方法是创建一个名为
.gitkeep
的空文件。
现在准备包含波兰歌曲“Bajka iskierki”的修订版:
-
创建
bajka-iskierki.txt
文件:$ vi PL/bajka-iskierki.txt
-
键入文件的内容:
Na Wojtusia z popielnika Iskiereczka mruga....
-
保存文件并关闭编辑器。
-
Check the status of the repository:
$ git status -s
存储库不干净。
-
将工作目录的当前状态保存为新版本:
$ git snapshot [PL] Bajka iskierki
-
Check the status of the repository with:
$ git status –s
仓库是干净的。
-
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 所示。
图 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 中。
图 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
。因此,你的修改被分配给三个不同的作者:johny
、John Doe
和Paul "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
。遵循程序:
-
创建目录
FR/
和文件FR/alouette-gentille-alouette.txt
:$ mkdir FR $ vi FR/alouette-gentille-alouette.txt
-
键入文件的内容:
Alouette, gentille alouette, Alouette, je te plumerai. ...
-
保存文件并关闭编辑器。
-
创建修订:
$ git snapshot [FR] Alouette, gentille alouette
现在,$ git shortlog -s
产生的输出将包括两个作者:
5 Włodzimierz Gajda
1 johny
遵循相同的程序,通过John Doe
创建新版本:
-
把你在
.gitconfig
的名字改成无名氏:[user] name = John Doe email = john.doe@example.net
-
创建一个新文件
little-skylark.txt
:$ vi EN/little-skylark.txt
-
键入文件的内容:
Little skylark, lovely little skylark, Little lark, I'll pluck your feathers off. ...
-
保存文件并关闭编辑器。
-
将工作目录的当前状态保存为新版本:
$ git snapshot [EN] Little skylark, lovely little skylark
-
Check the list of authors with:
$ git shortlog -s -n
由于有了
-n
选项,输出将按照修订数量降序排列:5 Włodzimierz Gajda 1 John Doe 1 johny
接下来,在名称
Paul "Moo" Cowboy
下创建修订:-
Change your name in
.gitconfig
:[user] name = "Paul \"Moo\" Cowboy" email = moo@wild-west.example.net
请注意,您必须用反斜杠对引号进行转义。
-
创建一个新文件
frere-jacques.txt
:$ vi FR/frere-jacques.txt
-
键入文件的内容:
Frère Jacques, frère Jacques, Dormez-vous? Dormez-vous? Sonnez les matines! Sonnez les matines! Ding, daing, dong. Ding, daing, dong.
-
保存文件并关闭编辑器。
-
将工作目录的当前状态保存为新版本:
$ git snapshot [FR] Frere Jacques
-
Check the list of authors with:
$ git shortlog -s -n
输出将包含四个条目:
5 Włodzimierz Gajda 1 John Doe 1 Paul "Moo" Cowboy 1 johny
-
提示我们现在有四个不同的作者,我们将继续邮件映射。然而,当存储库包含由以下作者创作的修订时,我鼓励您检查由
$ 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.net
到John 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
那么您的存储库的历史将包含两个作者:johny
和John 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.net
到Proper Name
和proper@example.net
。
可以将.mailmap
文件的位置从工作目录的根目录更改为由mailmap.file
配置选项定义的任意位置。.gitconfig
和.mailmap
文件都可以是 utf-8 编码的,因此你可以在里面使用非 ascii 字符,比如,。如果你想分析现实生活中的.mailmap
例子,请访问 jQuery 资源库:https://github.com/jquery/jquery
。
文档
.mailmap
文件的完整规范可在 shortlog 命令: $ git shortlog --help
的手册中找到。
请记住,当您定义一个奇怪的名称时,例如:
Paul "Moo" Cowboy
你必须使用引号和反斜杠:
[user]
name = "Paul \"Moo\" Cowboy"
email = john.doe@example.net
提示命令:
$ 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 中创建的所有文件。
小心配方 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
返回到最新版本并恢复正常状态。
提示配方 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 名称。
图 3-9 。配方 3-8 中讨论的储存库
提示图 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
目前,存储库包含两个修订版本,注释分别为lorem
和ipsum
。命令:
$ 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.txt
、ipsum.txt
和dolor.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 Stand
和The 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.txt
和graham-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.txt
和no.txt
。第一个文件包含文本yes
,第二个文件包含文本no
。for
循环:
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 所示的储存库。
提示秘籍 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
用注释a
、b
、c
创建三个修订:
$ 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
—但其他两个修订版— 5c1ec
和4580b
—仍然在数据库中可用。您可以使用它们的名称或 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
这意味着修订版5c1ee9ac
和45800ddb
在符号名称HEAD@{1}
和HEAD@{2}
下可用。我们称这种类型的修订为悬空修订。让我们用以下命令清除 reflog:
$ git reflog expire --all --expire=now
执行此命令后,reflog 变为空。$ git
reflog 命令返回空结果。这意味着现在修订版5c1ee9ac
和45800ddb
只能通过它们的名字获得。没有符号名称导致修改b
和c
。如果是这种情况,git 可以从数据库中删除修订。这种类型的修订被称为不可达修订。
让我们检查一下,哪些存储在.git/objects
数据库中的对象只能通过 SHA-1 名称访问:
$ git prune --dry-run
输出将包含——除其他内容外——两个修订:
45800ddc19fa325296437fdbd7cc7e5654619597 commit
5c1ee9a3f19f854c783fa87003cb1ecc5508971d commit
如果您比较一下$ git l
的输出,您将会看到输出中包含了修订名称5c1ec
和4580b
。换句话说,如果你现在执行命令$ git prune
,那么两个修改b
和c
将最终丢失。就这么办吧。执行命令:
$ git prune
如果您现在尝试使用其名称将存储库重置为修订版c
:
$ git reset --hard 5c1e
您将得到以下错误:
fatal: ambiguous argument '5c1e': unknown revision or path not in the working tree.
该修订版不再可用。你刚刚失去了它!永远!
提示你可以用
$ 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
参考日志已清除。因此,我们删除了所有指向修订版b
和c
的符号引用。这就是为什么修订版b
和c
变得遥不可及。接下来调用:
$ git prune
已从数据库中删除这些修订。
结论
方法 3-12 的目的是向你展示,即使是提交的变更也可以从存储库中删除。您可以通过保留符号引用来避免这种情况。为此,最简单的解决方案是使用分支。配方 5-4 也讨论了丢失已提交变更的问题。
摘要
这一章向前迈出了非常重要的一步。现在,您可以在日常工作中使用 git,而没有任何丢失数据的风险。git 最简单的工作流程是:
-
修改您的文件
-
Save the snapshot of the working directory with two commands:
$ git add -A $ git commit -m "...".
该操作由这两个命令实现,如图 3-10 中的所示
图 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
然后,按照以下步骤操作:
-
用
$ echo new > new.txt
创建一个新文件 -
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
文件被列为未跟踪的。 -
Check the shortened output of git status. The command
$ git status -s will print: ?? new.txt
未跟踪的文件用
??
表示。 -
使用
$ git add new.txt
命令暂存文件。 -
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 #
这一次,文件显示在待提交的变更部分下。这意味着该文件是暂存的。
-
Check the status with the
$ git status -s
command. The command will print:A_ new.txt
这个显示的新状态由两个字符
A
和空格组成。为了使输出更具可读性,我使用了下划线_
来代替空格。 -
用
$ git commit -m "Staging and committing a new file"
创建一个新版本
-
用
$ 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_
表示。
提示你可以使用通配符
*
和?
作为$ git add
命令。
当文件位于临时区域时,您可以使用以下命令创建新版本:
$ git commit -m "一些解释…
临时区域中包含的所有更改都将进入修订版。该文件存储在存储库中,现在可以描述为未修改的。您可能注意到了这些命令:
$ git status
$ git status -s
不打印任何关于未修改文件的信息。
配方 4-1 将储存在储存库中的文件分为三组:
- 无人探索过的
- 阶段
- 未更改的
图 4-1 显示了新文件从未跟踪到暂存和从暂存到未修改的过程。
图 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
别名创建并提交。
现在,按照程序进行:
-
用
$ echo Some other text > modified.txt
修改modified.txt
文件 -
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 一样,该文件列在未提交的变更下。 -
Check the simplified form of status. The output of
$ git status -s
will be:_M modified.txt
状态由两个字符表示:空格和字母
M
。同样,我使用了下划线而不是空格。 -
用
$ git add modified.txt
将modified.txt
添加到暂存区 -
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 #
该文件现在被列为要提交的变更。
-
Check the simplified form of status with the
$ git status -s
command. The output will be:M_ modified.txt
状态由两个字符表示:字母
M
和一个空格。 -
用
$ git commit -m "Staging and committing a modified file"
提交更改 -
用
$ 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 中描述。
图 4-2 。使用登台区提交修改文件的过程
两个字母状态_M
和M_
提供了关于暂存区和工作目录中文件状态的信息。
状态_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
。
现在,请遵循以下步骤:
-
用
$ echo Yet another text > modified.txt
修改
modified.txt
文件 -
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")
-
检查状态的简化形式。
$ git status -s will be: _M modified.txt
的输出
-
使用
$ git commit -a -m "Committing modified file: modified.txt"
提交更改
-
用
$ 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]
提示命令
$ 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 中给出。如您所见,这两个命令都跳过了临时区域。
图 4-3 。提交修改文件的过程(跳过暂存区)
注意命令
$ 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
。
遵循程序:
-
用
$ git rm deleted.txt
删除deleted.txt
文件 -
用$ ls 检查工作目录的内容。文件
deleted.txt
已被移除。 -
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
。这意味着删除文件的操作是上演的。 -
Check the simplified form of status. The output of
$ git status -s
will be:D_ deleted.txt
这一次,分段文件删除由两个字符表示:一个字母
D
和一个空格。 -
用
$ git commit -m "Staging and committing removed file"
命令提交更改。 -
用
$ git status -s
命令检查状态。输出是空的,因此存储库是干净的。工作目录不包含deleted.txt
文件。
它是如何工作的
我们从一个包含文件deleted.txt
的干净的存储库开始菜谱。命令$ git rm deleted.txt
从工作目录中删除文件,并执行该操作。暂存和删除的文件通过$ git status -s
命令用D_
表示。在另一个$ git commit
命令之后,操作被提交:存储在修订版中的快照不包含文件deleted.txt
。
配方 4-4 的流程如图图 4-4 所示。
图 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
。
遵循以下程序:
-
用
$ rm removed.txt
删除removed.txt
文件 -
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
。 -
Check the simplified form of status. The output of
$ git status -s
will be:_D removed.txt
状态由两个字符表示:一个空格和一个字母
D
。 -
用
$ git commit -a -m "Staging and committing removed file"
提交更改
-
用
$ 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 中。
图 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
。
遵循以下程序:
-
用
$ git rm --cached untracked.txt
删除未跟踪的. txt 文件
-
用
$ ls
命令检查存储库的内容。正如您所看到的,文件没有被删除,它仍然存在于工作目录中。 -
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
该文件同时被列为已暂存 ( 待提交变更)和未跟踪(未跟踪文件)。
-
Check the simplified form of the status. The output of
$ git status -s will be: D_ untracked.txt ?? untracked.txt
该文件被列出两次:作为
D_
和??
。 -
用
$ git commit -m "Committing removed file"
提交更改 -
Check the status with the
$ git status -s
command.输出是:??untracked.txt
该文件不再被跟踪,并且不包括在最新的快照中。
它是如何工作的
这一次是单一命令:
$ git rm --cached [filename]
将一个未修改的文件转换成两种不同的状态,表示为D_ ??
。第一种状态表示文件已暂存。更准确地说,我们可以说文件删除操作是分阶段的:下一次提交将存储工作目录的快照,而不存储该文件。
第二种状态表示为??
,指定工作目录包含一个不被跟踪的文件。在发出下一个$ git add
命令之前,该文件不会受到下一个提交操作的影响。
配方 4-6 如图 4-6 所示。
图 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
。
遵循以下程序:
-
用
$ git mv old-name.txt new-name.txt
重命名文件 -
用
$ ls
命令检查工作目录的内容。工作目录现在包含一个名为new-name.txt
的文件。文件old-name.txt
已经消失了。 -
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 #
说明移动文件的操作是分阶段进行的。
-
Check the simplified form of status. The output of
$ git status -s
will be:R_ old-name.txt -> new-name.txt
状态由两个字符表示:字母
R
后跟一个空格。 -
用
$ git commit -m "Staging and committing moved file"
命令提交更改。 -
用
$ git status -s
命令检查状态。输出是空的,因此存储库是干净的。
它是如何工作的
$ git mv
的语法如下所示:
$ git mv [old-filename] [new-filename]
$ git mv [filename] [directory]
第一个命令重命名文件,而第二个命令将文件移动到目录中。用$ git mv
命令重命名或移动的文件用R_
表示。
操作如图 4-7 中的所示。
图 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
。
遵循以下程序:
-
用
$ mv old-name.txt new-name.txt
重命名文件 -
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 认为它是两个独立的操作:删除和创建。 -
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
视为一个新的未被跟踪的文件。因此,将其表示为??
。 -
用
$ git add new-name.txt
命令暂存一个新文件。正如你在配方 4-1 中所记得的,除了使用$ git add
命令,没有其他方法来暂存一个新的未跟踪文件。现在命令$ git status -s
打印:A_ new-name.txt _D old-name.txt
-
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 足够聪明,能够猜到文件被移动了。
-
用
$ git commit -m "Committing a file moved with mv"
创建版本。 -
用
$ 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.txt
、deleted.txt
、old-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.txt
、deleted.txt
和old-name.txt
。
遵循以下程序:
-
创建一个新文件
$ echo new > new.txt
-
修改
modified.txt
文件$ echo Some new contents > modified.txt
的内容 -
用
$ rm deleted.txt
删除deleted.txt
文件 -
重命名
old-name.txt
文件$ mv old-name.txt new-name.txt
-
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")
将所有变更列为未暂存。工作目录还包含一个未跟踪的文件。
-
查看状态的简化形式。
$ git status –s
的输出将是:_D deleted.txt _M modified.txt _D old-name.txt ?? new-name.txt ?? new.txt
-
用一个命令
$ git add -A
准备所有的改变。 -
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 #
所有的变化都是阶段性的。
-
使用
$ git status -s
命令检查缩短状态。您将获得:D_ deleted.txt M_ modified.txt R_ old-name.txt -> new-name.txt A_ new.txt
-
用
$ git commit -m "Committing all the changes"
命令提交更改。 -
用
$ 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
存储库是干净的,工作目录不包含任何文件。
遵循以下程序:
-
创建一个新文件
$ echo Some info > file.txt
-
用
$ git add file.txt
命令暂存文件。此时$ git status -s
打印:A_ file.txt
-
用
$ echo Some other info > file.txt
修改文件 -
用
$ git status -s
命令检查状态。输出将是下面的代码:AM file.txt
-
该代码由两个字母组成,通知您某个文件在某个时间点被暂存,但此后被修改过。
-
用
$ git commit -m "First revision"
命令创建修订。该命令创建一个新的版本,存储内容为Some info
的文件。仓库仍然很脏。缩短状态命令打印:_M file.txt
-
包含
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 显示了所讨论文件的状态图。
图 4-8 。文件的不同状态
请注意:
- 跟踪每个未修改的文件。
- 跟踪和修改每个暂存和未暂存的文件。
因此,我们可以毫不含糊地使用这四个形容词来描述文件:
- 无人探索过的
- 未更改的
- 阶段
- 未分级
包含git status -s
命令返回的短代码的状态简化图如图图 4-9 所示。
图 4-9 。git status–s 返回的带有短代码的不同文件状态的简图
知识库的结构
为了引入登台区,我扩展了存储库的结构。事实上它由三个不同的区域组成,如图图 4-10 所示:
- 工作目录
- 存储对象的数据库
- 集结地
图 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 所示。
图 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
。这种提交方法有两个重要的特点,可以帮助你练习分支和提交:
- 没有冲突。
- 对于每个版本,你可以很容易地猜出工作目录的内容。
图 5-1 。在配方 5-1 中创建的存储库
当图 5-1 中的所示的存储库完成时,您想要验证每个分支只包含分支及其祖先所指向的版本中存储的文件。
解决办法
启动命令行并创建一个新的存储库:
$ cd git-recipes
$ git init 05-01
$ cd 05-01
然后创建三个版本m1
、m2
和m3
、??。您可以使用以下步骤:
- 用
$ echo m1 > m1.txt
创建一个新文件m1.txt
- 使用
$ git snapshot m1
提交快照 - 用
$ echo m2 > m2.txt
创建一个新文件m2.txt
- 使用
$ git snapshot m2
提交快照 - 用
$ echo m3 > m3.txt
创建一个新文件m3.txt
- 使用
$ git snapshot m3
提交快照
存储库现在看起来像图 5-2 。
图 5-2 。配方 5-1 中的存储库以及在主分支中创建的前三个版本
默认情况下,任何新初始化的存储库都包含一个名为master
的分支。您可以使用以下命令验证这一点:
$ git branch
该命令打印分支列表。现在它的输出将是:
* master
让我们创建一个名为doc
的新分支。以下是您需要的命令:
$ git branch doc
现在,如果您用$ git branch
命令列出分支,您会注意到存储库包含两个分支,如下所示:
doc
* master
星号表示当前分支。说明你还在master
分支。存储库现在看起来像图 5-3 。
图 5-3 。配方 5-1 中的存储库,有两个分支 master 和 doc
现在您需要在名为doc
的分支上创建三个版本。首先,您必须使用命令切换到这个分支:
$ git checkout doc
现在命令:
$ git branch
印刷品:
* doc
master
上面的输出通知您当前位于名为doc
的分支上。您的提交现在将转到这个新分支。您可以通过以下步骤创建三个提交d1
、d2
和d3
、:
- 用
$ echo d1 > d1.txt
创建一个新文件d1.txt
- 使用
$ git snapshot d1
提交快照 - 用
$ echo d2 > d2.txt
创建一个新文件d2.txt
- 使用
$ git snapshot d2
提交快照 - 用
$ echo d3 > d3.txt
创建一个新文件d3.txt
- 使用$ git snapshot d3 命令提交快照。
存储库现在看起来像图 5-4 。
图 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 。
图 5-5 。配方 5-1 中的存储库,带有一个名为 info 的新分支
您可以创建三个版本i1
、i2
和i3
来完成配方:
- 用
$ echo i1 > i1.txt
创建一个新文件i1.txt
- 使用
$ git snapshot i1
提交快照 - 用
$ echo i2 > i2.txt
创建一个新文件i2.txt
- 使用
$ git snapshot i2
提交快照 - 用
$ echo i3 > i3.txt
创建一个新文件i3.txt
- 使用
$ git snapshot i3
提交快照
存储库已完成。看起来像图 5-1 。您可以使用以下命令来验证它:
$ gitk --all &
gitk
应用绘制的图形如图图 5-6 所示。注意,在这个图中,当前分支的名称是用粗体书写的,而当前的修订是用白点表示的。在你的屏幕上,圆点将是黄色的——这是默认情况下gitk
使用的约定。
图 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
打开符号引用,如HEAD
、master
、doc
和info
。用于$ git log
和$ gitk
命令的参数--all
具有相同的含义:它使得存储在存储库中所有可用分支中的修订被包括在内。默认情况下,只显示当前分支中包含的提交。
如果您现在使用命令切换到名为master
的分支:
$ git checkout master
工作目录将只包含三个文件m1.txt
、m2.txt,
和m3.txt
。可以用$ ls
命令 来验证。同理,如果你用$ git checkout doc
切换到doc
分支,那么工作目录将包含六个文件:d1.txt
、d2.txt
、d3.txt
、m1.txt
、m2.txt
和m3.txt
。如果您切换到分支info
,工作目录将包含文件:i1.txt
、i2.txt
、i3.txt
、m1.txt
、m2.txt
和m3.txt
。这就是为什么我坚持创建诸如m1
、m2
、d1
、d2
等提交。您可以很容易地验证您发出的命令如何影响工作目录。一个简单的$ ls
命令将显示工作目录的完整内容——没有必要检查文件。
提示您可以使用
$ 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/xyz
、ref: refs/heads/foo
和ref: 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
文件中。
提示分支中的最后一次提交被称为分支的尖端。图 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
注意特殊名称
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。
提示
$ 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 中创建的存储库的精确副本。您希望副本包含原始存储库中存储的所有分支:master
、doc
和info
。您还想创建两个名为foo
和bar
的新分支——它们应该指向与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'
在上面的命令之后,存储库将包含两个分支:master
和info
。可以用$ git branch
命令来验证。
以同样的方式,您可以创建名为doc
的分支:
$ git checkout doc
存储库05-02
现在包含三个分支master
、info
和doc
。完成制作分支foo
和bar
的配方;
$ 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
的普通本地分支 。这是您当前的分支,它指向由注释m3
的7c9bc41
标识的修订。
第二行:
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
包含三个分支:master
、info
和doc
。当我们在05-02
库中工作时,我们将把存储在05-01
库中的分支称为远程分支 。您永远不会通过 commit 或 add 命令与它们进行交互。远程分支不可用—无法从当前存储库中登录远程存储库并与之交互。与远程分支交互的唯一方法是使用 push 和 fetch 命令——这些主题的详细讨论包含在第十章中。
在05-02
本地创建和存储的分支将被称为本地分支。有三种类型的本地分支:
- 普通地方分行
- 远程跟踪分支机构
- 本地跟踪分支机构
我再强调一次这一点:都是局部。远程跟踪分支也是。
当您发出命令时:
$ git checkout -b foo master
$ git checkout -b bar master
他们创建了两个普通本地分支 ,命名为foo
和bar
。可以肯定的是,没有人会用“普通”这个形容词。我只是“一时兴起”发明了它,以便让你清楚所有类型的分支。他们通常被称为地方分行。但是让我们暂时使用这个术语——它将有助于避免混淆。
远程跟踪分支 是远程分支的本地副本。它们会保留远程分支在初始克隆或上次获取操作期间的状态。创建远程跟踪分支的要点非常简单:每当您想要检查远程分支的状态时,您应该咨询远程跟踪分支。远程跟踪分支被命名为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
的三个分支info
、origin/info
和foo
的属性汇总在表 5-1 中。
图 5-7 。四种类型的分支:远程分支、普通本地分支、本地跟踪分支和远程跟踪分支
表 5-1 。存储库 05-02 的三个分支 info,origin/info,foo 的属性如图 5-7 所示
提示记住:远程跟踪分支是本地分支,充当远程分支的只读副本。
表 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
包含三个本地分支master
、doc
和info
。你可以使用$ git branch
命令来检查它。这三个分支,即master
、doc
、info
,就是局部跟踪分支。
提示命令
$ 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
只打印五个本地分支doc
、master
、info
、foo
和bar
。目前,没有远程存储库;因此所有分支都是普通的本地分支。
提示本地跟踪分支和远程跟踪分支只存在于配置文件中至少包含一个远程部分的存储库中。在一个孤立的存储库中谈论远程跟踪分支和本地跟踪分支是没有意义的。
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
因此,我们知道原始存储库包含三个名为doc
、info
和master
的分支。第一个别名$ 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 新手,那么本地跟踪分支和远程跟踪分支的概念可能不清楚。我们将更详细地讨论本地跟踪分支和远程跟踪分支,在有关同步的章节中再次强调它们的作用。然后,我希望,他们的目的会变得更清楚。
提示别名
$ 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 中克隆存储库,并创建修订版x1
、x2
和x3
,如图 5-8 中的所示。请注意,该图不包含任何指向x1
、x2
或x3
的分支。不能通过符号引用(比如分支和标签)访问的修订被称为悬空修订 。
图 5-8 。悬挂修订 x1、x2 和 x3
一旦您创建了这三个提交,您想要切换到master
分支,并验证x1
、x2
和x3
修订是否可用。他们迷路了吗?你如何找回x1
、x2
和x3
,以及你如何最终移除它们?
解决办法
克隆配方 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 中所示。
图 5-9 。处于分离状态的存储库
现在,使用我们在配方 3-11 中创建的别名创建x1
修订版:
$ git simple-commit x1
x1
的 SHA-1 将存储在.git/HEAD
中。x1
的父代将是i2
。仓库的这种状态如图图 5-10 所示。
图 5-10 。在分离头状态下创建的新提交 x1
创建另外两个版本x2
、x3
,使用:
$ git simple-commit x2 x3
存储库现在看起来像图 5-11 。
图 5-11 。在分离的头状态下创建的新提交 x1、x2 和 x3
在图 5-11 所示的状态下,.git/HEAD
文件包含了x3
版本的 SHA-1 名称。如果你现在用$ git checkout master
命令切换到分支master
,那么库的状态将会改变,如图图 5-12 所示。这正是图 5-8 中所示的状态,其中.git/HEAD
被明确显示。图中通常会跳过对.git/HEAD
的引用,因为相同的信息——在必要时——可以在主图前加上星号。
图 5-12 。存储库中的当前分支可以通过加在主分支前面的星号或者包含一个. git/HEAD 引用来显示
当仓库处于图 5-12 所示的状态时,命令$ ls
只返回三个文件m1.txt
、m2.txt
、m3.txt
—x1.txt
、x2.txt
和x3.txt
不可用。
如何检索悬空修订?首先,分析$ git checkout master
命令的输出。它包含了您的三个修订版本x1
、x2
和x3
需要用类似(当然,您的 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
输出应该包含三个提交(x1
、x2
、x3
)、三个文件、 4 、 ( x1.txt
、x2.txt
、x3.txt
)和三棵树(每个提交一棵树)。
提示一棵树是给定目录的快照。每次提交都包含一个工作目录主文件夹的树。
现在,如果您运行$ 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∼2
、info∼5
、doc∼5
均指向m1
。分支info
、doc
和master
的所有祖先参考在图 5-13 中显示。
图 5-13 。分支文档、信息和主文档的祖先引用
提示引用
[REVISION]∼1
和[REVISION]∼
的意思完全相同:给定版本的第一个父版本。
您在配方 5-1 中了解到,您当前的版本可以始终通过符号引用HEAD
来引用。正如您所猜测的那样,HEAD
可以与HEAD∼
、HEAD∼2
、HEAD∼3
等祖先引用结合使用。如果您使用$ git checkout info
切换到info
分支,那么使用HEAD
和波形符将得到图 5-14 中的结果。
图 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
存在于每个分支中。
问题你能想象出一个别名,把切换分支只限制在干净状态吗?
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
。第一个将您的更改与您切换到的分支合并;第二种方法会丢弃您的修改。
提示当然,如果没有冲突的话,可以应用隐藏。因此,在方法 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
分支的过程:
- 用
$ git checkout master
命令将当前分支更改为master
。 - 用
$ 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
分支删除错误版本:
- 用
$ git checkout doc
命令将当前分支更改为doc
。 - 用
$ 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
假设您决定master
和info
分支不再重要。删除master
分支执行:
$ git branch -d master
接下来尝试用相同的命令删除info
分支:
$ git branch -d info
上面的命令无法删除info
分支,因为它包含一些可能会丢失的修订。要强制删除,请使用命令:
$ git branch -D info
它是如何工作的
存储库中每两个分支可以被表征为合并或不合并。如果a
中包含的所有修订也包含在b
中,我们说a
分支合并为b
分支。图 5-15 呈现了两个分支a
和b
。分支a
合并到分支b
中。分支a
包含两个修订a1
和a2
——这两个修订都可以从b
指向的修订开始并在父修订之后找到。
图 5-15 。分支 a 并入分支 b;分支 b 没有合并到分支 a 中
这种关系通常不是反映性的。在图 5-15 的库中分支b
没有合并master
中的。如您所见,修订版b1
、b2
和b3
包含在分支b
中,但不包含在分支a
中。这种关系反映的唯一情况是当两个分支指向完全相同的版本时,如图图 5-16 所示。
图 5-16 。分支 a 并入分支 b,分支 b 并入分支 a
第三种情况是既没有分支a
并入分支b
也没有分支b
并入分支a
。然后我们说分支a
和b
?? 已经分叉。这种情况如图图 5-17 所示。
图 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 检索它。
注本秘籍描述了删除本地分支的方法。您可以使用它来删除普通的本地分支和本地跟踪分支。所有远程跟踪分支都可以用
$ 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 中的所示的存储库中工作,并且您想要重命名分支:
info
至information
master
至doc
解决办法
克隆在配方 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.txt
、m2.txt
和m3.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
命令再次检查。它将打印出i3
、i2
和i1
提交。
它是如何工作的
在非空存储库中切换分支的命令是:
$ 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-commit
和clone-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 所示。
图 6-1 。制作方法 6-1 的起点
图 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
分支中。
图 6-3 。图 6-2 的替代图形表示
6-2.快进分支
问题
你已经完成了配方 6-1 的工作,并决定将你在feature
分支的工作合并到master
分支中。你想要创建如图图 6-4 所示的存储库。
图 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.txt
、f2.txt
、f3.txt
、m1.txt
、m2.txt
和m3.txt
。
它是如何工作的
这个秘籍展示了合并分支的最简单的例子。有两个分支master
和feature
,当前分支为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 。
图 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
它是如何工作的
这个配方强调了这样一个事实,即不同分支中的工作可以并行地继续。在这种情况下,我们说两个分支master
和feature
分叉。提交m4
和f1
被创建的确切时间并不重要,插图通常会在同一级别显示它们,如图 6-5 中的所示。实际上,它们总是在不同的时刻创建的。因此,可视化修订图的命令,如$ git --oneline --graph --decorate --all
或$ gitk --all&
,总是在不同的级别显示它们,如图图 6-6 。修订版m4
和m5
是后来创建的,因此它们将出现在修订版f1
、f2
和f3
之上。
图 6-6 。提交 m4 和 m5 是在提交 f1、f2、f3 之后创建的,因此它们被绘制在上面
$ gitk
--all &
命令从图 6-6 中的图 6-7 中呈现仓库。
图 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 强调了m4
、m5
和f1
、f2
、f3
修订的创建顺序。在这个配方中,这个顺序并不重要,因此图 6-8 也可以像图 6-9 那样绘制。
图 6-8 。将分叉的分支合并后得到的知识库
图 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
其中X
和Y
是任意正整数。
Git 支持引用,这些引用允许它使用脱字符号(^)选取合并提交的任何父元素。引用[REVISION]^[n]
指向用[REVISION]
标识的提交的第 n 个父提交。对于图 6-9 中的库,master¹
指向版本m5
;master²
指向修订f3
,如图图 6-10 所示。我将它们称为第 n 个父引用。
图 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.txt
、f2.txt
、f3.txt
、m1.txt
、m2.txt
、m3.txt
、m4.txt
、m5.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
分支的所有修订被可视化地分组在一个灯泡中。
图 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
改变了强制创建合并提交的默认行为,即使合并可以作为快进来执行。通过这种方式,您保留了提交f1
、f2
和f3
相互关联的信息。它们都处理开发的同一个方面,共同构成一个完整的作品。如果出于某种原因,您需要将整个分支恢复或复制到历史中的其他地方,像这样组织提交将使操作更容易。
6-7.发散 多个分支
问题
您想要创建如图 6-12 所示的存储库。它包含五个分叉的分支。请注意,创建修订的实际顺序并不重要。该图显示了在完全相同的时刻创建的来自不同分支的修订,我们知道这是不正确的。您是单独工作的,并且您的所有修订都是按顺序创建的,一次一个,而不是并行的。但是这个图像的作用是强调,不管你提交的顺序是什么,配方 6-7 中描述的过程都工作得很好。
图 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 中的所示。
图 6-13 。由$ git log - oneline - graph - all 命令绘制的来自图 6-12 的存储库
6-8.合并 多个分支
问题
在图 6-12 中所示的存储库中工作,您想要将四个分支a
、b
、c
和d
合并成master
分支。您的目标是将图 6-12 所示的存储库转换成图 6-14 所示的存储库。
图 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 d
在master
中创建新的提交,标记为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 中呈现。
图 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.改变分叉分支的基础
问题
您在一个拥有两个名为master
和feature
的分支的存储库中工作。分支分叉了,你的存储库现在看起来像图 7-1(a) 。你想以这样的方式改造 ?? 分公司:
- 历史是线性的(这意味着分支不再分叉)。
master
分支合并为feature
分支。- 在
feature
分支中进行的所有提交都在master
分支的最顶端。
你想要实现的储存库在图 7-1(b) 中给出。
图 7-1 。在重设基础之前(a)和重设基础之后 (b)来自配方 7-1 的储存库
解决办法
用分支克隆配方 6-4 中的存储库:
$ cd git-recipes
$ git clone-with-branches 06-04 07-01
$ cd 07-01
然后按照以下步骤操作:
- 用
$ git checkout feature
命令检查feature
分支。 - 用
$ git rebase master
命令将feature
分支复位到master
分支上。 - 用
$ git checkout master
命令检查master
分支。
它是如何工作的
由$ git rebase master
执行的转换可以被描述为在另一个分支的顶部应用由来自当前分支的提交引入的改变。这在图 7-2 中进行了描述。
图 7-2 。将提交从当前分支“移动”到另一个分支之上
正如你从配方 5-8 中已经知道的,没有办法将版本从一个地方移动到另一个地方。你所能做的就是创建一个新的修订版,它将有相同的注释,并且将相同的变化引入到你的文件中。这就是为什么图 7-1(b) 和图 7-2 中的修改包含了素数。它强调了这样一个事实,即这些是用不同的 SHA-1 名称进行的新修订。
最初的修订版f1
、f2
和f3
会发生什么?没什么?它们完好无损。更准确地说,可以如图 7-3 中的所示描述重置基础。最初的修订f1
、f2
和f3
不再被任何分支引用——它们变成了悬空的修订。但是它们在数据库中保持不变。至少只要不使 reflog 过期并清理数据库。
图 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 中存储一个对提交的新引用。多亏了管道化的sort
和uniq
命令,清单 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
导致工作目录的内容完全相同。工作目录包含两个分支的文件:feature
和master
。
一般来说,图 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
然后按照以下步骤操作:
-
用
$ git checkout feature
结账feature
分支 -
用
$ git format-patch --ignore-if-in-upstream master
为
f1
、f2
和f3
版本生成补丁 -
进入分离头状态,其中
HEAD
指向与master
分支相同的版本。您将通过执行$ git checkout
git rev-parse master`` 来实现 -
使用
$ git am *.patch
应用补丁 -
使用
$ git checkout -B feature
将feature
分支移动到当前版本 -
用
$ rm *.patch
删除补丁
它是如何工作的
feature
分支包含三个提交f1
、f2
和f3
,它们不包含在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.patch
、0002-f2.patch
和0003-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
上面的命令将一个符号引用,比如HEAD
、HEAD∼
、feature
、info²
,转换成阿沙-1 名称。使用反勾运算符将$ git rev-parse
的结果作为参数传递给 checkout,您将进入所需的分离头部状态。完整的命令如清单 7-2 中的所示。您的存储库现在看起来如图 7-4 所示。
清单 7-2。
$ git checkout `git rev-parse master`
图 7-4 。该库来自图 7-1(a)后的命令来自清单 7-2
当存储库看起来像图 7-4 时,我们应用存储在带有.patch
后缀的文件中的补丁:
$ git am *.patch
上面的命令使用HEAD
指针作为父指针来再现提交f1
、f2
和f3
。存储库现在看起来像图 7-5 。
图 7-5 。从图 7-4 中的存储库在应用了$ git am *补丁后。补丁命令
最后一步是现在改变feature
分支。我们希望它指向我们当前的修订。为此我们可以使用$ git checkout
命令。然而,命令$ git checkout -b feature
将不起作用。原因相当明显:feature
分支已经存在。不过,我们可以使用-B
开关强制结帐:
$ git checkout -B feature
更新后的知识库如图 7-6 所示。提交f1
、f2
和f3
在数据库中仍然作为悬空修订可用,但是它们没有显示在图中。
图 7-6 。来自的储存库图 7-5 后的$ git checkout -B 特性
我们不再需要补丁了。用$ rm *.patch
命令删除它们。
使用樱桃采摘 rebase
您可以使用$ git cherry-pick
命令获得类似的结果
- 进入分离头状态:
$ git checkout
git rev-parse master`` - 用
$ git cherry-pick feature∼2
重新应用HEAD
中的修订f1
- 用
$ git cherry-pick feature∼1
重新应用HEAD
中的修订f2
- 用
$ git cherry-pick feature
重新应用HEAD
中的修订f3
- 使用
$ git checkout -B feature
将feature
分支移动到当前版本
上述解决方案的主要缺点是,这里您必须知道您想要重新应用哪些修订。命令$ git format-patch
将卸下你肩上的重担!另外$ git cherry-pick
不会创建补丁。要通过电子邮件发送补丁,你必须用$ git format-patch
命令生成补丁。
提示配方 7-2 背后的主要原因是为了让你更深入地了解
$ git rebase
是如何工作的。不要使用$ git format-patch
、$ git am
或$ git cherry-pick
来重置你的分支,除非你想通过电子邮件把补丁发给其他人。请使用$ git rebase
命令。配方 7-2 中的分析将有助于你理解重置基础的内部原理。根据您的工作流程,每次您想要发布您的作品时,都可能需要重新设定基准。
7-3.将分歧的分支连接成线性历史
问题
你在一个拥有两个分支master
和feature
的存储库中工作,如图图 7-7(a) 所示。您希望将feature
分支合并到master
分支中,这样得到的历史是线性的,也就是说,它不包含合并提交。该配方的起点如图 7-7(a) 中的所示。您想要获取的存储库在图 7-7(b) 中显示。
图 7-7 。起点和结果将分歧分支加入线性历史
解决办法
用分支克隆配方 6-4 中的存储库:
$ cd git-recipes
$ git clone-with-branches 06-04 07-03
$ cd 07-03
然后按照以下步骤操作:
- 用
$ git rebase master feature
将feature
分支重置到master
分支上 - 用
$ 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.分叉的三个分支
问题
你的存储库包含两个分叉的分支master
和feature
,如图图 7-8(a) 所示。首先,你想在你的feature
分部的最新修订版的基础上,研究一些新的想法。您需要创建一个名为brave-idea
的新分支,并将您的更改提交为修订版b1
和b2
。接下来您想要切换到feature
分支并创建三个新的修订f4
、f5
和f6
。你想要实现的库如图图 7-8(b) 所示。
图 7-8 。通过对存储库(a)应用配方 7-4,您将得到存储库(b)
解决办法
用分支克隆配方 6-4 中的存储库:
$ cd git-recipes
$ git clone-with-branches 06-04 07-04
$ cd 07-04
然后按照以下步骤操作:
- 用
$ git checkout -b brave-idea feature
创建并检查brave-idea
分支 - 用
$ git simple-commit b1 b2
在brave-idea
分支中创建两个版本 - 用
$ git checkout feature
检查feature
分支 - 用
$ git simple-commit f4 f5 f6
在feature
分支中创建三个版本 - 用
$ git checkout master
将当前分支更改为master
它是如何工作的
这个秘籍解释了如何创建许多不同的分支。我们只使用众所周知的命令来实现这一点:clone
、checkout
和simple-commit
。这就是如何轻松生成具有给定结构的存储库。如果您想分析 git 命令及其对图形结构的影响,这个功能非常有用。
请记住,当您使用带有--graph
选项的$ git log
命令时,您得到的图形可能会稍有不同。从图 7-8(b) 中得到的一个库的$ git log --oneline --graph --decorate --all
结果如图图 7-9 所示。
图 7-9 。该库来自图 7-8(b) 由$ git 绘制的 log-one line-graph-decoration-all
一旦你创建了如图 7-8 所示的存储库,许多关于修订图的问题可能会出现。例如以下内容:
- 如何找到分支
a
和b
的共同祖先? - 如何寻找任意支数的共同祖先?
- 如何找到两个分支的差异
a - b
,即包含在分支a
中但不包含在分支b
中的修订? - 如何找到两个分支的对称差异
a ∆ b
,即包含在a
或b
中但不同时包含在两者中的修订? - 如何查找包含在
a
、b
、c
分支中,不包含在d
、e
、f
分支中的修订?
两个分支的共同祖先是包含在两个分支中的最新修订。对于分支feature
和brave-idea
,它是f3
。对于master
和feature
是m3
。您可以使用以下方法找到共同祖先:
$ 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
打印提交的b2
、b1
、f3
、f2
和f1
,同时:
$ git log feature..master
输出版本m4
和m5
。
由两个分支引入的新提交集由...
操作符解析。这是分支的对称差异。的输出:
$ git log feature...brave-idea
由f6
、f5
、f4
、b2
和b1
组成。
指定包含和排除修订的更详细的方法是使用--not
操作符。命令:
$ git log a b c --not d --not e --not f
打印包含在a
、b
或c
中,不包含在d
、e
和f
中的修订。这也可以写成:
$ git log a b c ^d ^e ^f
使用上面的语法,你可以用清单 7-3 中的命令列出master
、feature
和brave-idea
分支中引入的新修订。该命令输出修订:
f6
、f5
、f4
——在feature
分支引入的提交b2
、b1
——在brave-idea
分行引入的提交m5
、m4
——在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
然后排除通过master
和feature
分支的共同祖先可用的提交(它是修订版 m3):
^`git merge-base master feature`
并且排除通过feature
和brave-idea
分支的共同祖先可用的提交(它是修订版f3
):
^`git merge-base feature brave-idea`
使用用反勾运算符定义的 shell 子命令,我们不必复制/粘贴嵌入的m3
和f3
的 SHA-1 名称。
在研究修订图时,您可能还会发现生成带有给定注释的提交的 SHA-1 名称的命令很有用:
$ git log --format="%h" --grep=XXX --all
上述命令考虑了所有分支(--all
选项),并搜索包含XXX
字符串的修订。多亏了--format
参数,输出只包含缩短的 SHA-1 名字。
7-5.部分重置和
问题
您决定在图 7-10(a) 所示的库中的brave-idea
分支中引入的代码现在可以与他人共享了。因此,您想要将修订版b1
和b2
移动到master
分支。你想要实现的库如图图 7-10(b) 所示。如果在你制作brave-idea
的过程中,你最初的想法不断发展,变得足够大,可以作为独立的特征来对待,这种情况就会发生。
图 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 中的命令可以在任何分支中执行。结果总是一样的:提交b1
和b2
将作为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) 所示。
图 7-11 。将配方 7-6 应用于图 6-5 中的存储库后,您将获得该存储库
解决办法
用分支克隆配方 6-4 中的存储库:
$ cd git-recipes
$ git clone-with-branches 06-04 07-06
$ cd 07-06
然后按照以下步骤操作:
- 用
$ git rebase master feature
将feature
分支重置到master
分支上 - 用
$ git checkout master
切换到master
分支 - 用
$ git merge --no-ff feature
将feature
分支合并成master
分支
它是如何工作的
配方 7-6 包括两个步骤:
- 首先,我们使用配方 7-1 来转换存储库,如图 7-1 所示。
- 接下来,我们使用配方 6-6 进行合并,形成一个灯泡。
7-7.在子分支 中创建灯泡
问题
你的知识库看起来像图 7-12(a) 。你想将brave-idea
分支合并回feature
分支作为灯泡。您希望实现的知识库如图图 7-12(b) 所示。
图 7-12 。配方 7-7 中考虑的储存库
解决办法
用分支克隆配方 7-4 中的存储库:
$ cd git-recipes
$ git clone-with-branches 07-04 07-07
$ cd 07-07
然后按照以下步骤操作:
- 用
$ git rebase feature brave-idea
将brave-idea
分支重置到feature
分支上 - 用
$ git checkout feature
切换到feature
分支 - 用
$ git merge --no-ff brave-idea
将brave-idea
分支合并成feature
分支 - 用
$ git branch -d brave-idea
删除brave-idea
分支 - 用
$ git checkout master
检查master
分支
它是如何工作的
配方 7-7 显示了如何将配方 7-6 应用于feature
和brave-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 feature
将feature
分支复位到master
分支上。您将获得如图图 7-13 所示的存储库。注意feature
分支不再包含merge branch 'brave-idea'
修订。
图 7-13 。该库来自图 7-12 后的$ git rebase master 特性
它是如何工作的
正如你在图 7-13 中所看到的,重置基础只在非合并提交时起作用。所有合并提交都将丢失。重定基础总是产生一条直线的提交,没有灯泡或合并。在有球茎的树枝的情况下,它不一定是你所期望的。如果你想保留合并和灯泡,你不能使用简单的 rebase 命令。您必须移动整个分支,然后重新生成合并提交。
好好看看图 7-13 。提交b1
和b2
用两个素数b1''
和b2''
表示。双撇号强调了这些是新提交的事实。它们引入了与图 7-12 中的b1'
和b2'
以及图 7-8 中的b1
和b2
相同的变化,但它们的 SHA-1 名称不同。
7-9.重设基础期间保留合并
问题
你的知识库看起来像图 7-14(a) 。你要将feature
树枝重新放在master
树枝上,保留灯泡。你想要实现的库如图图 7-14(b) 所示。
图 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
也是提交将被重新应用的分支。要移动的提交集由part
和src
分支定义。该操作移动包含在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
包括a
、b
和c
中可用的提交,排除d
、e
和f
中可用的提交。
八、修改历史记录
本章介绍修改修订图结构的各种命令。有时你需要将三个不同的版本合并成一个版本。在其他时候,您可能需要相反的操作:将一个单独的提交分成许多单独的提交。无论是哪种情况,请记住 git 版本是永久的。他们永远不会改变。一旦创建了修订,就无法对其进行修改。你所能做的就是用秘籍 3-12 和 5-4 中介绍的方法把它扔掉。因此,每当我说“让我们修改一个修订”这样的话,我脑海中的操作是创建一个类似于原始版本的新修订。原始版本在 git 数据库中保持不变。它可以通过符号引用而不是 reflog 来访问,但它仍然存在。直到下一次数据库清除。
如果任何操作产生的结果令您不满意,您可以随时返回到以前的状态。您所需要的只是包含正确快照的修订名称。可以使用 reflog 来查找该名称,但也可以创建一个临时分支来保存对所需修订的引用。因为修订不会改变,所以您不需要担心修改修订的后果。无论您做什么,都不会改变已经存储在数据库中的修订。提交、重置基础和合并只会产生新的修订,这些操作不会修改现有的修订。修改修订是不可能的。
这对于我研究 git 来说是一个真正的突破。一旦我学会了如何重构修订图以及如何撤销各种操作,我就获得了自由使用该工具的信心。
8-1.修订最新版本
问题
您刚刚提交了一组变更到存储库中,一分钟后您意识到有一些额外的修改应该被合并到先前的版本中。您不想再创建另一个提交;您可能希望通过添加一些额外的更改来修改现有的修订。
解决办法
创建新的存储库:
$ cd git-recipes
$ git init 08-01
$ cd 08-01
然后按照以下步骤操作:
- 用$ echo lorem > lorem.txt 创建文件 lorem.txt
- 使用$ git add lorem.txt 暂存文件
- 用$ 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.name
和user.email
配置设置。您可以通过以下方式实现这一目标:
$ git config --local user.name "John Doe"
$ git config --local user.email john@example.net
最后,按照以下步骤修改版本:
- 用$ echo Lorem Ipsum Dolor > lorem.txt 修改 lorem.txt 的内容
- 使用$ git add lorem.txt 暂存文件
- 使用$ 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 数据库中一直悬而未决,直到最终被垃圾收集操作删除。
提示记住——git 版本是永久的!不可能更改提交中存储的任何信息并保留相同的 SHA-1。
$ git commit --amend
命令创建一个全新的版本,然后更新主分支以指向新的版本。
为什么我们在这个配方中改变了user.name
和user.email
的配置?它将帮助您理解 git 处理日期和分配作者身份的方式。每个提交包含四个属性:Author
、AuthorDate
、Commit
和CommitDate
。他们储存:
Author
—作者的名字AuthorDate
—最初提交的日期Commit
—委托人的姓名CommitDate
—历史中引入提交的日期
当您第一次创建提交时,Author
和Commit
都将被设置为您的名字。存储在AuthorDate
和CommitDate
中的日期将是相同的。这种情况如清单 8-1 所示。
如果用--amend
选项修改提交会发生什么?Git 保留原来的Author
和AuthorDate
字段,并为Commit
和CommitDate
字段设置新值。这显示在清单 8-2 中。同样的规则也适用于你的提交。
提示 Git 不关心你的文件的最后修改日期。Git 跟踪内容——文件的最后修改日期不会以任何方式影响您的修订。存储在数据库中的每个提交都包含
AuthorDate
和CommitDate
。这些日期是在提交、rebase 或 cherrypick 时设置的。文件的最后修改日期是在您签出文件时设置的。例如,这是在切换分支时完成的。
细心的读者会注意到,$ git commit --amend
引入的修改的作者被错误地归于原作者。在配方 8-1 中,两个词Ipsum
和Dolor
是由用户 John Doe 创作的,但被认为是 odzimierz Gajda。然而,实际上这种情况从来不会发生,因为不允许修改其他开发人员提交的内容。你可以对它们进行精选或重新排序,但这样作者才能被正确地归属。
8-2.删除最近的版本
问题
您希望从当前分支中删除两个最新的修订。图 8-1 中描述了您想要实现的转变。
图 8-1 。删除两个最新修订
解决办法
创建一个包含修订版本a
、b
、c
和d
的新存储库:
$ 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 从历史中删除。
提示你可以用这个方法删除任意数量的最近提交。命令
$ 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 中执行的操作。修订版c
和d
在数据库中保持可用,直到它们在 reflog 中的所有符号引用都被清除并且数据库被清理。要撤消操作,您可以随时使用 reflog。
图 8-2 。修订版 c 和 d 一直悬而未决,直到它们被 git gc 清除
提示配方 8-2 中讨论的命令在配方 3-5 中使用,以检验所需的版本。
8-3.将许多修订压缩成一个修订
问题
您的存储库包含相当多的修订。你想把最后三个版本压缩成一个版本。图 8-3 中的描述了您想要执行的操作。
图 8-3 。压缩最后三次修订
解决办法
创建一个包含修订版本a
、b
、c
和d
的新存储库:
$ 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
该命令将只打印两个提交a
和b
。根据$ ls
命令,工作目录仍然包含四个文件。命令:
$ git show --name-only HEAD
证明最后一次提交现在包含三个文件b.txt
、c.txt
和d.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 命令不允许您修改结果提交的注释。您将获得包含来自提交b
和c
的变更集的提交,并且用提交b
的原始注释来表示。
最后一个命令:
fixup 46c002f d
执行一次挤压。这一次,git 将 commit d
压缩为将c
压缩为b
的结果。因此,您将以注释b
表示的单个提交结束,并合并来自提交b
、c
和d
的变更集。
交互式重置基础执行的所有操作都存储在 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 相反。
图 8-4。将最近的修订分成许多不同的修订
解决办法
创建一个新的存储库,它将包含两个修订版本:a
和b
。第一次修订应包括一个文件a.txt
,第二次修订应包括三个文件:b.txt
、c.txt
和d.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.txt
、c.txt
和d.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
命令检查历史包含四个版本a
、b
、c
和d
。每个修订版都包含一个文件。要验证它,请使用以下命令:
$ 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
表 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.txt
、c.txt
和d.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.重新排序修订
问题
您的存储库包含许多修订。最后三个版本被标记为b
、c
和d
。它们在历史中按以下顺序出现:d
是最近的版本,c
在d
之前创建,b
在c
之前。您需要对修订版b
、c
和d
重新排序,以符合图 8-5 。
图 8-5 。重新排序修订
解决办法
创建一个包含修订版本a
、b
、c
和d
的新存储库:
$ 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
修订顺序为a
、b
、c
、d
。最老的是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
这些提交现在按c
、b
、d
、a
排序(从最新的到最早的)。
它是如何工作的
交互式重置基础的子命令pick
可以缩写为p
。通过更改编辑器中子命令的顺序,可以修改应用修补程序的顺序。重置基础根据补丁在编辑器窗口中的顺序应用补丁。清单 8-6 中的第一个子命令,即p-cc595c7-d
,定义了要应用的第一个补丁。因此,在生成的历史中,修订版d
将出现在a
之后。
原始修订保留在数据库中,可以通过引用日志引用进行访问。
8-6.删除几个版本
问题
您的存储库包含几个修订版。最后五次修订被标记为b
到f
。您想要删除修订版b
、d
和f
。图 8-6 中描述了您想要实现的转变。
图 8-6 。删除修订
解决办法
创建一个包含修订版a
到f
的新存储库:
$ 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.txt
和y.txt
。你还想在提交x
之后引入一个新的版本z
。其他修订应保持不变。图 8-7 中显示了您想要实现的转换。
图 8-7 。编辑旧版本
解决办法
创建一个包含修订版本a
、b
、x
、c
、d
的新存储库:
$ 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
引入的更改。
图 8-8 。恢复修订
解决办法
创建一个包含修订版本a
、b
、c
的新存储库:
$ 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.正在恢复合并提交修订
问题
您在项目中使用两个分支:master
和feature
。分支出现分歧,你决定将feature
分支合并为master
分支。当你完成合并后,feature
分支被删除。
过了一段时间,你意识到feature
branch 引入了许多 bug 和严重的问题。因此,您希望将分支feature
的合并恢复为master
分支。
您想要实现的转换如图 8-9 所示。
图 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.txt
到f3.txt
和m1.txt
到m6.txt
。
当存储库准备就绪时,您可以使用以下命令恢复合并提交:
$ git revert --no-edit -m 1 HEAD∼
该命令将历史向前移动。存储库现在将包含一个带有注释Revert "Merge branch 'feature'"
的新版本。这个修订删除了您在feature
分支中创建的提交中引入的所有变更。工作目录现在只包含六个文件m1.txt
到m6.txt
。文件f1.txt
、f2.txt
、f3.txt
都没了。您可以使用$ ls
命令来验证它。
它是如何工作的
合并提交有两个或多个父提交。如果您恢复合并提交,您必须指出应该恢复历史的哪一部分。在图 8-9 中标为Merge branch
’feature
的提交有两个父级:
- 第一个是提交
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∼
上述命令将产生由m1
、m2
、m3
、m4
、m5
、m6
修订组成的快照。这种情况如图 8-10 中的所示。
图 8-10 。用$ git revert - no-edit -m 1 HEAD 获得的快照
如果你想把历史保存在合并提交的第二个父项下,那么使用-m 2
参数,比如:
$ git revert --no-edit -m 2 HEAD∼
该命令产生由修订版 m1
、m2
、m3
、f1
、f2
、f3
、m6
组成的快照。这种情况如图 8-11 所示。
图 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
分公司。
图 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
分支包含三个版本f1
、f2
和f3
。你想把它们压缩成一个单独的版本,出现在master
分支中。
图 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 所示。
图 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.txt
和y.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.txt
和y.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
列出了两个版本x
和y
。为了创建这些修订版的补丁,我们使用:
$ 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 处理日期的方式。每个提交存储四个不同的属性:Author
、Commit
、AuthorDate
、CommitDate. Author
、Commit
保存在提交中引入的代码作者的身份(Author
属性),以及在项目历史中引入提交的人的身份(Commit
属性)。如你所知,当你执行$ git commit
命令时Author
被设置(没有--amend
参数)。当您使用$ git commit --amend
或$ git cherry-pick
、$ git rebase
修改提交时,git 只改变提交者的用户名——作者身份保持不变。请注意,当您压缩一些提交时,没有办法保留原始作者。被挤压的提交将归因于第一个提交的作者。
其他属性AuthorDate
和CommitDate
是时间戳。第一个存储创作提交时的信息,第二个存储历史中引入提交时的信息。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 编辑旧版本,你将更容易解决冲突。
注意请注意,本章中的配方在用于修改已与他人共享的修订时会导致严重的问题。您只能使用配方 8-8 和 8-9 中的
$ git revert
命令来修改已发布的修订。所有其他配方只能用于没有发送到共享存储库的修订。记住:你的存储库的公共历史只能向前发展。否则,在团队内部同步工作将会非常困难和麻烦。
九、解决冲突
到目前为止,我们主要关注修订图的结构。我们通常使用别名$ git simple-commit
提交;因此,我们生成的文件非常简单。事实上,到目前为止我们创建的几乎每个文件都只包含一个单词。一旦创建,这些文件很少被修改。此外,秘籍是以这样一种方式编写的,我们通常在不同的分支中使用不同的文件名。这种简化的提交过程是学习修订图上各种操作的非常有效的方式,例如合并和重新建立基础。然而,它们并没有为您在团队中工作做好充分的准备,在团队中,您的同事正在对同一个文件进行更改。在这一章中,我们将填补这个空白。您将学习如何控制文件的内容,直到解决问题。
在实际项目中工作,你迟早会遇到冲突。当您合并包含对文件的完全相同行的不同修改的分支时,就会发生这种情况。如果在某个文件中,例如readme.txt
,一个开发人员键入第一行如下:
Lorem ipsum
和其他开发人员类型:
Dolor sit amet
git 将不能自动合并两个版本。您必须在Lorem ipsum
和Dolor sit amet
之间手动选择。因为冲突通常会引起很多关注和恐慌——它们被认为是令人害怕的事情——四个秘籍将准确地解释如何在文本和二进制文件的合并和重定基础期间处理冲突。一旦你熟悉了冲突,好好看看冲突是如何解决的。在 git 中,当你暂存一个文件时,冲突就解决了。这意味着在所有四个关于冲突的方案中,最重要的工作是由一个著名的$ git add
指挥完成的。
9-1.在文本文件 中创建冲突的更改
问题
您想要创建一个包含两个分支的存储库,这两个分支在合并或改变基础时会在文本文件中产生冲突。您想要创建的存储库如图 9-1 所示。
图 9-1 。带有分支 en 和 fr 的存储库将在文本文件中产生冲突
解决办法
创建新的存储库:
$ cd git-recipes
$ git init 09-01
$ cd 09-01
然后按照以下步骤操作:
-
Create the file
numbers.txt
with the contents shown in Listing 9-1清单 9-1。 在主控分支中创建 numbers.txt 文件
1
2
3
-
用
$ git snapshot Numbers: 1, 2, 3
提交numbers.txt
文件 -
用
$ git branch en
创建名为en
的分支 -
用
$ git branch fr
创建名为fr
的分支 -
在
en
分支中创建新的提交-
a.用
$ git checkout en
切换到en
分支 -
b. Change the contents of
numbers.txt
. Replace2
withtwo
as shown in Listing 9-2清单 9-2。 恩分公司提交的 numbers.txt 文件
1
two
3
-
c.用
$ git snapshot Numbers: two
提交更改
-
-
在
fr
分支中创建新的提交-
a.用
$ git checkout fr
切换到fr
分支 -
b. Change the contents of
numbers.txt
. Replace2
withdeux
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 中创建的存储库中的分支en
和fr
。您希望使用在配方 9-1 中创建的存储库作为起点。你想要达到的结果如图 9-2 所示。numbers.txt
文件的第二行应该包含清单 9-4 中显示的内容。你想保留两个互相冲突的单词two
和deux
。
图 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 fr
将fr
分支合并成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
上面的输出不包含分支en
和fr
分叉之前的原始行。如果这对您很重要,请使用以下命令:
$ 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
提示在配方 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
中止正在进行的合并。撤销合并的方法在第六章中讨论。
提示解决合并冲突包括三个步骤:1)编辑文件;2)用
$ git add
命令暂存文件;3)用$ git commit --no-edit
命令完成合并。
9-3.重置基础后解决文本冲突
问题
对于在配方 9-1 中创建的存储库,您想要将分支en
重置到fr
上。你的目标是产生如图 9-3 所示的库。numbers.txt
文件的冲突行应该包含清单 9-8 中显示的内容。
图 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
中止操作。这个操作的撤销在第七章中讨论过。
提示解决 rebase 冲突包括三个步骤:1)编辑文件;2)用
$ git add
命令暂存文件;3)用$ git rebase --continue
命令完成复位。
9-4 在二进制文件中创建冲突的更改
问题
您想要创建一个包含两个分支的存储库,这两个分支在合并或重置时会在二进制文件中产生冲突。您想要创建的存储库如图 9-4 所示。
图 9-4 。带有分支 a 和 b 的存储库将产生二元冲突
解决办法
创建新的存储库:
$ cd git-recipes
$ git init 09-04
$ cd 09-04
然后按照以下步骤操作:
- 用
$ git commit --allow-empty --allow-empty-message -m " "
命令创建第一个版本。这就是你如何产生一个带有空注释的空提交。 - 用
$ git branch a
创建名为a
的分支 - 用
$ git branch b
创建名为b
的分支 - 在
a
分支中创建新的提交- a.用
$ git checkout a
切换到a
分支 - b.创建显示一只猫的图像,并将其保存在名为
picture.jpg
的文件中 - c.用
$ git snapshot Cat
提交更改
- a.用
- 在
b
分支中创建新的提交- a.用
$ git checkout b
切换到b
分支 - b.创建显示一只狗的图像,并将其保存在名为
picture.jpg
的文件中 - c.用
$ git snapshot Dog
提交更改
- a.用
用$ git checkout a
命令完成配方。
它是如何工作的
这一次,存储库包含以相同名称保存在文件中的两个不同的图像。存储在名为a
的分支中的文件显示一只猫,存储在b
分支中的文件显示一只狗。
注意,命令$ git commit --allow-empty --allow-empty-message -m " "
产生了带有空消息的空提交。
9-5.合并期间解决二元冲突
问题
你想用$ git merge
命令合并配方 9-4 中创建的分支a
和b
。您想要获得图 9-5 中所示的库。合并后的存储库应该包含来自b
分支的picture.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]
图 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
应该包含一张显示一只猫的图片。
图 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 中创建的存储库。你想要合并两个分支en
和fr
,以二进制模式合并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
然后按照以下步骤操作:
- 使用
$ git init
初始化新的存储库 - 添加远端的网址:
$ git remote add origin
https://github.com/creationix/js-git.git
- 从远端获取 git 数据库和远程跟踪分支:
$ git fetch --no-tags origin master:refs/remotes/origin/master
- 创建一个本地的
master
分支,它将指向与origin/master
远程跟踪分支相同的版本:$ git rev-parse origin/master > .git/refs/heads/master
- 用
$ git branch --set-upstream-to=origin/master
设置master
分支为远程跟踪分支origin/master
的本地跟踪分支 - 将默认分支的信息存储在本地的远程存储库中:
$ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master
- 签出工作目录中的文件:
$ 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 的信息。
提示 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
提示
$ 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
相连。
提示你也可以用
$ git branch master origin/master
命令创建指向origin/master
的master
分支。我避免了上面的命令,因为它不仅创建了一个本地的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
分支继续他或她的工作 - 一个开发人员将他或她的修改发送到共享的回购协议中
- 另一个开发人员获取新的版本
提示这个菜谱展示了如何围绕一个中心库来组织团队的工作。这个工作流程类似于集中式系统使用的客户端/服务器方法,如 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.name
、user.email
和remote.origin
。
图 10-1 。初始化后的两个存储库
然后,John 在他的私有存储库 中创建了一些修订:
# john's command
$ git simple-commit a1 a2 a3
现在,John 的存储库中包含一个名为master
的普通分支。$ git branch -a -vv
的输出类似于:
* master dc30648 a3
存储库看起来像图 10-2 。
图 10-2 。约翰的 a1、a2、a3 提交后的储存库状态
约翰将他的a1
、a2
和a3
修订发送到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 。
图 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 所示。
图 10-4 。莎拉发行的$ git 克隆共享回购 sarahs-repo 的效果
现在轮到莎拉为这个项目做贡献了。她创建了两个版本b1
和b2
:
# sarah's command
$ git simple-commit b1 b2
存储库现在看起来像图 10-5 。
图 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 所示。
图 10-6 。萨拉的$ git push -u origin master 命令之后的仓库
现在轮到约翰下载莎拉的修改。他跑着说:
# john's command
$ git pull origin master
这导致了图 10-7 中所示的状态。
图 10-7 。后一库约翰的$ git 拉原点主控命令
上述模式可以重复任意多次。在约翰和莎拉的主分支分开之前,没有什么特别的事情发生。我们来分析一个这样的案例。
约翰和莎拉的作品
这一次,John 和 Sarah 都在他们的存储库中独立工作。约翰创建修订版a4
和a5
,而莎拉创建修订版b3
:
# john's command
$ git simple-commit a4 a5
# sarah's command
$ git simple-commit b3
图 10-8 中描述了您将获得的存储库。
图 10-8 。约翰和莎拉的分支分叉的存储库
现在约翰和莎拉都想把他们的修改推进shared-repo
。我们假设 Sarah 是第一个执行$ git push
命令的人。之后:
# sarah's command
$ git push origin master
存储库看起来像图 10-9 。
图 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 。
图 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 。
图 10-11 。John 成功的$ git push origin master 命令之后的存储库
最后,Sarah 使用以下方法完成了 John 的工作:
# sarah's command
$ git pull origin master
产生的储存库如图 10-12 所示。
图 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/objects
、sarahs-repo/.git/objects
和shared-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 将三个版本a1
、a2
、a3
从johns-repo
的数据库复制到shared-repo
的数据库,然后更新分支。
默认情况下,git 将$ git push
操作限制为快进情况。这意味着只有当远程分支可以快速推进您的工作时,$ git push
才会成功。这是约翰将图 10-2 中的状态变为图 10-3 中的状态的情况。图 10-2 中库shared-repo
为空;因此它可以安全地接收三个提交a1
、a2
和a3
。当 Sara 将存储库从图 10-5 中的更改为图 10-6 中的所示的存储库时,也会发生同样的情况。图 10-5 中所示的库shared-repo
包含了a1
、a2
和a3
三个版本。不包含sarahs-repo
中a3
之前的b1
和b2
修订。shared-repo
中的master
分支可以用b1
和b2
版本快进,因此操作成功。
图 10-9 所示的情况要复杂得多。共享存储库中的master
分支 和 John 的存储库中的master
分支已经分离。约翰的包含a4
和a5
,共享存储库包含b3
。因此约翰在图 10-9 所示的库中执行的$ git push origin master
失败。Git 打印了 John 首先从远程存储库获取修订所需的信息。
当与-u
参数一起使用时,$ git push
命令存储关于跟踪的信息。命令:
$ git push -u origin master
当约翰将他的a1
、a2
和a3
修订版本推送到约翰的存储库中的远程跟踪分支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-repo
和sarahs-repo
看起来像图 10-13 。
图 10-13 。克隆后立即共享回购和 sarahs 回购
我们从莎拉的角度来看这个情况。因此sarahs-repo
是本地存储库,shared-repo
是远程存储库,如图 10-13 中的所示。该图显示了三种类型的分支:远程分支、本地跟踪分支和远程跟踪分支。Sarah 的存储库不包含任何普通的本地分支。当 Sarah 克隆存储库时,$ git clone
命令会自动为她创建两个本地分支:master
和origin/master
。第一个是本地跟踪分支;第二个是远程跟踪分支。您可以使用$ git branch -a -vv
命令验证这一点。输出将包含两行重要的内容:
* master 36c7205 [origin/master] a3
remotes/origin/master 36c7205 a3
第一行表示master
是origin/master
分支的本地跟踪分支。第二行表示origin/master
是一个远程分支。
当 Sarah 用$ git simple-commit b1 b2
提交时,她将她的master
分支(即本地跟踪分支)向前移动。$ git simple-commit b1 b2
后的分支状态如图图 10-14 所示。
图 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
包含指向适当修订的标签master
和origin/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
的结果。
图 10-15 。在 Sarah-repo 中执行$ git push origin master 命令后,图 10-14 中的存储库的状态
您应该注意到,为了决定应该发送哪些修订,git 只需找到 Sarah 的存储库中两个分支master
和origin/master
之间的差异。由于配方 7-2 中讨论的两点,这可以用$ git log --oneline origin/master..master
命令来完成。该命令打印包含在master
中但不包含在origin/master
中的修订列表。
提示请记住,远程跟踪分支,比如
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
中的两个修订b1
和b2
。拉操作将这些修订带入johns-repo
,并将分支更新到图 10-17 所示的状态。
图 10-16 。在$ git pull origin master 命令之前的 John 的存储库
图 10-17 。约翰的仓库在$ git pull origin master 命令后在约翰-repo 中执行如图图 10-16 所示
图 10-18 和 10-19 说明了第二种情况。这是一个快进操作。如果在$ git pull origin master
期间发生合并,本地跟踪分支master
和远程跟踪分支origin/master
以完全相同的方式改变。它们将指向自动生成的合并提交,如图图 10-18 和图 10-19 所示。
图 10-18 。共享回购和约翰回购的主要分支出现分化
图 10-19 。在图 10-18 中显示的约翰回购中发出的$ git 拉式来源主数据的结果
总而言之,拉操作更新本地跟踪分支和远程跟踪分支,而保持远程分支不变。
提示记住:远程分支是远程存储库中的一个分支。远程跟踪分支是本地分支,用作您的工作和远程存储库内容之间的链接。图 5-7 清楚地显示了所有类型的分支。
10-3.为一个提交生成(n-1)个合并提交
问题
如果项目是由 n 个开发人员使用配方 10-2 实现的,您想要检查历史看起来像什么。为此,您需要模拟三个开发人员的工作:John、Sarah 和 Peter。你必须在三个不同的存储库中并行提交:johns-repo
、sarahs-repo
、peters-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
将产生一个非常复杂的修订图,其中包含大量多余的合并提交。
提示如果你认为清白的历史很重要,你应该把秘籍 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-repo
、shared-repo
和marks-repo
包含相同的提交i1
和i2
。$ git status -sb
命令只打印当前分支的名称master
。所有的存储库都是干净的,并且分支是同步的。所有储存库的状态如图 10-20 所示。
图 10-20 。配方 10-4 中所有三个储存库的初始状态
约翰和马克平行工作
约翰和马克同时工作。约翰创建了三个提交j1
、j2
、j3
,马克创建了两个提交m1
、m2
:
# 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 所示。
图 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 。马克的知识库中没有约翰的修订版j1
、j2
和j3
。
图 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]
现在马克知道他的master
和origin/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 的存储库和共享存储库的状态。
图 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 中所示。
图 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 所示。
图 10-25 。Mark 成功执行$ git push -u origin master 后的 Mark-repo 和 shared-repo
马克的工作完成了。$ git status -sb
的输出中没有超前或滞后信息。这意味着马克回购中的master
和origin/master
分支是同步的。
约翰下载马克的作品
现在约翰的回购和共享回购看起来像图 10-26 。
图 10-26 。在 John 的$ git fetch 命令之前的 Shared-repo 和 johns-repo
约翰可以用下面的命令获取马克的工作:
# command issued in johns-repo
$ git fetch
该命令将把图 10-26 中所示的存储库转换成图 10-27 中所示的形式。
图 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 中所示的状态。所有存储库的历史都是线性的。
图 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 对项目进行了两次修订s1
和s2
:
# 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
为了检查莎拉在s1
和s2
版本中编写的代码,约翰带着:
# john's command
$ git checkout sarah
该分支还不包含s1
和s2
修订,证明如下:
# john's command
$ git status -sb
输出通知您,他的当前分支(sarah
)比它的远程跟踪分支(origin/master
)晚两次提交。John 更新了他的sarah
分支:
# john's command
$ git rebase
现在约翰的存储库中的sarah
分支包含了s1
和s2
修订。约翰可以分析莎拉的贡献。当他认为这些修改没问题时,他可以将它们合并到自己的工作中:
# john's commands
$ git checkout master
$ git rebase sarah
现在是约翰进行更多修改的时候了。他跑$ git simple-commit j4 j5 j6
。然后他给莎拉发电子邮件询问他的工作。
轮到莎拉下载约翰在修订版j4
、j5
和j6
中贡献的代码。她用$ 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
她检查约翰的修改j4
、j5
、j6
。然后,她将约翰的工作并入她的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/master
和origin/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/doc
和origin/special-unit-tests
。
一段时间过去了,doc
和special-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
命令验证这一点。
提示本地跟踪分公司和普通本地分公司有什么区别?当然,你可以在这两种情况下都做出承诺。但是本地跟踪分支连接到远程跟踪分支;因此,您可以使用
$ git status -sb
来检查本地跟踪分支是在相应的远程跟踪分支之前还是之后。
因此,shared-repo
不再包含doc
分支。在shared-repo
中执行的$ git branch
命令打印两个分支:master
和special-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
中发出):
- 为您的贡献创建分支:
$ git checkout -b new-web-interface
- 在您的
new-web-interface
分支中提交:$ git simple-commit a b c
- 将分支发送到共享存储库:
$ 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
中发出):
- 您获取投稿:
$ git fetch
- 您签出远程分支
$ git checkout new-web-interface
- 你用任意的命令和工具检查文件,例如,
$ ls
、$ cat a.txt
、vi b.txt
- 您可以使用任意命令检查修订,例如,
$ git log --oneline
、$ git log --oneline --name-only HEAD∼3..HEAD
- 你决定代码是正确的,应该合并到
master
分支。 - 你检查
master
分支:$ git checkout master
- 你把工作合并到
master
和$ git merge new-web-interface
中 - 您将作品发布给所有其他团队成员:
$ 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
中运行):
- 转到
new-web-interface
分支:$ git checkout new-web-interface
- 创建新的提交:
$ git simple-commit n1 n2 n3 n4 n5
- 发布您的作品:
$ git push origin new-web-interface
领导如何从远程分支机构下载最新版本?
领导者更新他的存储库(命令应该在10-09/leaders-repo
中运行):
- 他取你的修改:
$ git fetch
- 他去了
new-web-interface
分支:$ git checkout new-web-interface
- 他更新了
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-repo
的new-web-interface
工作的开发者:
您的本地new-web-interface
分支包含三个版本a
、b
和c
。您希望压缩它们并更新远程分支。
以下是您必须遵循的程序(所有命令都将在10-10/johns-repo
中执行):
-
转到
new-web-interface
分支:$ git checkout new-web-interface
-
您的
new-web-interface
分支包含三个版本a
、b
和c
。你可以和$ git log --oneline
核实一下。 -
修订
a
、b
、c
还没有合并到master
分支。你可以用$ git log --oneline master..new-web-interface
查一下 -
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 中描述。 -
您的
new-web-interface
分支包含一个新的修订abc
。你可以和$ git log --oneline
核实一下。 -
版本
abc
还没有合并到master
分支中。你可以用$ git log --oneline master..new-web-interface
查一下 -
使用
$ 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
中,你将禁止所有改变历史的推送。
提示其他关于推送的选项可以在
$ 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
中运行:
- 您用
$ git fetch
更新您的项目。 - 转到
master
分支:$ git checkout master
- 用
$ git rebase origin/master
更新您的master
分支 - 检查可以安全移除的分支:
$ git branch --merged
。输出应包括—除其他外—new-web-interface
分支。这意味着可以安全地删除new-web-interface
分支。命令$ git branch --merged
是一个安全检查:如果分支new-web-interface
没有被打印,那么删除分支是不安全的。 - 移除
shared-repo
中的远程分支new-web-interface
以及带有$ git push origin :new-web-interface
的本地跟踪分支origin/new-web-interface
- 最后用
$ 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.txt
、two.txt
和three.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 所示。
提示这个配方可以当作一个部署工具。
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 日提交的修订,为了他的书第十章的“总结”。没有其他同名的修订版。
一旦您理解了如何添加和删除遥控器以及如何复制修订,掌握组工作的下一步就是分支。直到现在,我们都集中在普通的地方分行。这些是你工作中使用的个人本地分支。没人知道他们。你不必向任何人咨询你在这些部门的工作。您可以创建、修改和销毁它们。
同样的规则适用于所有的存储库——不仅仅是您的。因此,我们需要既允许独立又允许合作的规则。这些规则非常简单:您的本地分支存储在以远程名称命名的单独目录中的其他存储库中。
当处于松散格式时,您的普通本地分支a
和b
存储在:
.git/refs/heads/a
.git/refs/heads/b
如果有人将您的存储库命名为foo
,并带有:
$ git remote add foo [URL]
并使用$ git fetch foo
从您的存储库中获取数据,那么您的本地分支a
和b
将被存储在他或她的存储库中:
.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
,就像你使用任何其他方法来引用提交、HEAD
、master∼5
、doc²
等等。
当您考虑同步时,三种重要的分支类型是:
- 远程分支机构
- 本地跟踪分支
- 远程跟踪分支
它们之间的关系以及$ git commit
、$ git fetch
、$ git push
对它们的影响方式如图 10-29 所示。
图 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。
解决办法
-
启动您的网络浏览器,访问
https://www.virtualbox.org/wiki/Downloads
并下载适用于您平台的 VirtualBox 4.2.18。 -
在您的计算机上安装 VirtualBox 4.2.18 软件包。您可以保留所有选项的默认值。
-
访问
http://downloads.vagrantup.com
并为您的平台下载流浪者 1.3.1。 -
使用默认设置在您的计算机上安装 vagger 1 . 3 . 1。
-
启动命令行并执行
$ 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
主菜单选项,您可以关闭所有可用的虚拟机。
图 11-1 。VirtualBox 的主窗口列出了 所有可用的虚拟机
注意在你开始下一个配方之前,我建议你关闭当前配方的虚拟机。您可以使用
$ 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
注意由
$ 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 虚拟机。
提示在
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
的绿色箭头表示。
提示不要使用
$ 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
命令。您也可以通过以下步骤解锁帐户:
-
使用
# Guest OS (Ubuntu 12.04) $ sudo passwd root
为 root 用户设置新密码
-
使用
# Guest OS (Ubuntu 12.04) $ sudo passwd -u root
解锁 root 用户的帐户
-
使用
# 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
您可以通过以下步骤进行验证:
-
使用
# Host OS (e.g., Windows, Linux, OS X) $ vagrant up
启动虚拟机
-
用
# Host OS (e.g., Windows, Linux, OS X) $ echo lorem > lorem.txt
在主机系统中创建文件
-
使用
# Host OS (e.g., Windows, Linux, OS X) $ vagrant ssh
打开 SSH 会话
-
List the contents of
/vagrant
directory with:# Guest OS (Ubuntu 12.04) $ ls /vagrant
如您所见,文件
/vagrant/lorem.txt
在 ssh 会话中是可用的。 -
接下来,用
# Guest OS (Ubuntu 12.04) $ cat /vagrant/lorem.txt
检查
/vagrant/lorem.txt
文件的内容 -
使用:
# Guest OS (Ubuntu 12.04) $ echo ipsum > /vagrant/ipsum.txt
在 ssh 会话中创建一个文件
-
使用
# Guest OS (Ubuntu 12.04) $ exit
关闭 ssh 会话
-
List the files in the
git-recipes/11-02
directory with:# Host OS (e.g., Windows, Linux, OS X) $ ls
文件
ipsum.txt
在您的主机操作系统中可用。 -
用
# Host OS (e.g., Windows, Linux, OS X) $ cat ipsum.txt
检查
git-recipes/11-02/ipsum.txt
文件的内容 -
使用
```
# Host OS (e.g., Windows, Linux, OS X)
$ vagrant halt
```
停止虚拟系统
正如你所看到的,这两个文件lorem.txt
和ipsum.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 项目做贡献
解决办法
-
启动虚拟机:
# 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
-
从 Ubuntu 可用的二进制包安装 git:
# Guest OS (Ubuntu 12.04) $ git --version $ sudo apt-get install -y git $ git --version
-
编译并安装来自源代码的 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
-
完成秘籍:
# 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
提示如果你愿意,你也可以为刚刚安装的版本生成文档。您可以使用以下两个命令来实现这一点:
$ 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 中所做的那样把你的提交作为补丁发送。您不能使用浅层存储库进行克隆、提取或推送。使用浅层存储库的主要优点是克隆速度更快。
提示请注意,
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 中创建的存储库,以便每个开发人员都可以使用该存储库进行克隆、获取和推送
- 通过提交和推送存储在虚拟系统中的远程存储库来检查一切是否按预期工作
提示有了这个方法,即使你不能访问 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
提示如果您想通过网络访问虚拟系统,您需要配置端口转发,将下面一行添加到流浪者文件:
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 库可以用标准命令复制,比如cp
、scp
、rsync
。要建立一个中央共享存储库,您只需将一个空存储库复制到虚拟系统中。我们使用流浪者提供的默认帐户访问虚拟系统:
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
提示不用这三个命令:
$ 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
。
注意我经常创建虚拟系统只是为了测试、检查或验证。这个系统只使用很短的时间,然后就被破坏了。对主机的远程访问被阻止,因为我没有打开端口转发。在这种情况下,我通常对虚拟用户(如 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'
提示代替一号命令:
$ 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 的第一个修订版。
它是如何工作的
在这个秘籍中,你将使用三个不同的账户:vagrant
、git
和peter
。
第一个帐户vagrant
,是您主机的管理员。它初始化所有的存储库并设置授权规则。
第二个用户git
是一个虚拟用户。它不允许您打开 ssh 连接。它唯一的目的是允许其他开发人员使用 git clone、fetch 和 push 命令连接 git 存储库。当一个开发人员,比如 Peter,发出一个 git 命令——比如$ git fetch
—那么 Peter 的 git 客户端将使用这个虚拟 git 帐户连接到服务器,然后使用 git 帐户权限执行一些命令。要做到这一点,管理员(我们示例中的流浪者用户)必须允许 Peter 使用服务器上的 git 帐户。这将通过 git 账户上的authorized_keys
文件来完成。
最后一个账号peter
,是你用来模仿别人作品的账号。我们将在虚拟系统上使用该帐户,但您也可以在您组织中的任何其他计算机上使用任何其他帐户。
默认情况下,第一个帐户vagrant
可用。另外两个账户,git
和peter
,是用$ 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
命令登录该帐户。
提示你可以通过创建一个目录
∼/git-shell-commands
来削弱git-shell
所施加的限制。如果该目录包含 shell 脚本,那么您将被允许打开到该帐户的 ssh 连接并执行该脚本。目录和 shell 脚本都应该拥有r
和x
权限。
彼得的账户是普通用户账户。它被其中一个开发人员用于他或她的日常工作。您可以使用两种方法登录该帐户:
# 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_rsa
和id_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
该命令将在当前目录下生成两个文件:peter
和peter.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 客户端的配置。
提示您可以将
∼/.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
,您必须将它应用到您的默认配置中。
提示有些选项,比如端口号,可以通过
$ 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
提示要创建如清单 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
提示在这个秘籍中,我们不需要发出
$ 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 目录中的所有存储库在客户机系统上可用。这是通过Vagrantfile
的config.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 的所有请求都由运行在同一端口上的客户机操作系统上的守护程序处理。
提示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
命令。
提示如果您想推送至
/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
文件。
图 11-2 。Gitweb 应用的主页列出了所有可用的存储库
图 11-3 。Gitweb 展示的 03-10 知识库主页
提示好好看看图 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,允许您克隆存储库。
图 11-4 。cgit 主页
图 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 拥有对分支a
的RW+
访问权。
接下来,切换到分支b
,尝试删除最后一次提交,并对分支b
执行强制推送:
# Guest OS (Ubuntu 12.04)
# as peter user
$ git checkout b
$ git reset --hard HEAD∼
$ git push -f origin b
这一次强制推送将被拒绝,因为 Peter 只有分支b
的RW
特权。
将您的本地分支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 就是规避这些缺点的工具。它安装和使用都非常简单。
提示完整的 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
键认证的人执行。在该配方中,用户是vagrant
。admin.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
库定义的。sarah
、paul
和ann
的密钥应存储在:
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_rsa
和id_rsa.pub
文件,那么您必须生成它们。您需要的命令是:
$ ssh-keygen -t rsa -C your.email@example.net
该命令将询问您两个问题,这两个问题已在 RSA 密钥部分的方法 11-5 中解释过。第一个问题用键设置两个文件的名称;第二个设置使用密码来保护您的密钥。如果您按下输入,文件将被存储在默认位置,密码将不会被使用。
当id_rsa
和id_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 所示。
图 12-1 。在 Github 帐户上添加新的 SSH 密钥
图 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 中找到相同的按钮。
图 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
图 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.txt
、b.txt
、c.txt
,如图 12-5 中的 B 箭头所示。
图 12-5 。$ git push -u origin 主命令将主分支从您的驱动器发送到您的由 Github 托管的存储库
它是如何工作的
要在 Github 上创建一个新的存储库,你应该使用 web 界面。创建新存储库后,您可以克隆它。使用本地克隆创建新的提交,当您想要在 Github 上发送更新的存储库时,使用$ git push
命令。使用图 12-5 中 C 箭头所指的按钮,您可以显示储存在库中的修订列表。列表如图图 12-6 所示。
图 12-6 。Github 提供的修订列表
正如你在配方 8-1 中所记得的,每个版本都存储了两个特殊的属性来分配作者。分别是Author
和Commit
。Author
是创造代码的人的名字。Commit
是版本创建者的名字。对于图 12-6 中出现的修改,Author
和Commit
都设置为同一个人,Włodzimierz Gajda
和 email gajdaw@gajdaw.pl
。Github 用这封邮件来猜测账户的名字。因此,我的提交被标注为由gajdaw
创作。因为Author
和Commit
数据相同,所以作者简化为简单的gajdaw
。这意味着gajdaw
创建了代码和提交。
如果提交包含不同名称的Author
和不同名称的Commit
会发生什么?Github 显示这些信息,如图图 12-7 所示。在图 12-7 中展示的修订版是由 John Doe 编写的,并由 gajdaw 重新修改。Github 显示这些消息:
John Doe authored...
Gajdaw committed...
图 12-7 。对于不同作者和提交数据的修订,Github 会同时显示两者
提示如何创建如图图 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 存储库应该包含来自您的项目的所有修订:a
、b
、c
和d
。
它是如何工作的
如果您想使用 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 中的按钮操作。
图 12-8 。创建新组织
新建组织按钮将打开如图图 12-9 所示的对话框。输入您的组织名称和电子邮件地址。我在 Github 上的用户名是 gajdaw,我使用 gajdaw-learning-git 作为我的组织的名称,但是你可以选择任何你喜欢的名称。然后点击页面底部的创建组织。
图 12-9 。新组织的属性
创建组织后,您可以使用以下 URL 访问其页面:
https://github.com/gajdaw-learning-git
不要使用 gajdaw-learning-git,而是使用您在图 12-9 所示的对话框中输入的名称。我的组织 gajdaw-learning-git 的主页如图图 12-10 所示。
图 12-10 。gajdaw-learning-git 组织的主页位于https://github.com/gajdaw-learning-git
它是如何工作的
Github 界面允许你创建组织。因为每个 Github 都可以拥有自己的库,所以您可以使用您的组织来模拟 pull 请求的工作。
注意 Github 数据库许可证严格禁止一个人创建多个帐户。因此,您不能创建两个不同的用户帐户来处理拉请求。
12-6.创建由组织托管的新项目
问题
您想要启动一个由您的组织托管的新项目。
解决办法
访问您的 Github 帐户并:
- 按下创建新的回购按钮
- 从所有者下拉列表中选择您的组织名称
- 将新存储库的名称填写为 12-06
- 点击页面下部的创建存储库按钮
所有步骤如图 12-11 所示。
图 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。
图 12-12 。您希望贡献的 your-organization/12-06 知识库的主页
使用标识为 B 的 Fork 按钮来创建您个人的库副本。当您按下按钮时,将会询问您“我们应该在哪里分叉这个存储库?”选择您的个人帐户(不是您的组织)来回答问题。这将重定向到如图图 12-13 所示的网页。您应该注意到,这一次存储库在您的用户名下可用,如下所示:
your-github-username/12-06
图 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 中所示的按钮。
图 12-14 。你推的分支在你的叉子里有。您可以使用箭头所指的按钮来查看它
现在,您已经准备好将您的提交发送到原始存储库。按比较&拉动要求如图图 12-15 所示。
图 12-15 。按钮比较&拉动请求
图 12-15 中的按钮将打开图 12-16 中所示的对话框。字母 A 显示了关于以下内容的准确信息:
-
将接收拉取请求的分支
your-organization:master
-
将被推送的分支
your-github-username:numbers
图 12-16 。发送拉请求的对话框
图 12-16 中的字母 B 包含您需要键入的标题。我用 foo bar 作为标题。C 指向您的拉取请求背后的目的的完整描述。d 列出将被推送的提交。要继续请求拉动,点击发送拉动请求。
点击发送拉取请求后,你将被重定向到图 12-17 所示的网页。这是您派生的原始存储库。
图 12-17 。提取请求已成功发送,并且在贵组织拥有的原始存储库中可用
你的工作结束了。提取请求已发送到原始存储库。这取决于该存储库的所有者是否接受您的“拉”请求。
它是如何工作的
拉请求的工作包括三个存储库,如下所示:
- Github 上托管的原始存储库
- 你的 fork 托管在 github
- 存储在机器上的本地存储库
您负责其中的两个:您的 fork 和您的本地存储库。第一个是在图 12-8 所示的 Githhub 界面中点击 Fork 创建的。本地存储库是作为原始存储库的克隆创建的。三个仓库之间的关系如图图 12-18 所示。
图 12-18 。处理拉请求所需的三个存储库之间的关系
使用拉取请求时,工作流组织如下:
- 您只能在您的本地存储库中提交、合并和重设基础。
- 您将更改从本地存储库推送到您的分支。
- 您将 pull 请求从 fork 发送到原始存储库。
- 您从原始存储库获取最新的更新到您的本地存储库。
命令流程如图 12-19 中的所示。
图 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 所示的拉取请求按钮。
图 12-20 。“拉请求”按钮提供了所有拉请求的列表
现在从列表中检查您的拉取请求。你会在窗口的下部看到你的两个修改,如图图 12-21 所示。
图 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 中的所示的链接进行操作。
图 12-22 。由您的组织托管的存储库中所有提取请求的列表
拉动请求的细节如图 12-23 中的所示。您可以分析所有贡献的代码。当你确定代码是正确的,你可以点击图 12-23 所示的按钮进行合并。
图 12-23 。用于合并拉取请求的按钮
如图 12-24 所示,当拉动请求被合并时,它将包含在项目历史中。
图 12-24 。合并的拉取请求在项目历史记录中可用
它是如何工作的
发送到存储库的所有拉请求都列在图 12-22 所示的页面上。你可以彻底检查他们每一个人。特别是,Github 允许您:
- 列出包含在拉请求中的提交
- 列出拉请求修改的文件
- 要列出修改的行
- 与其他开发人员讨论拉取请求
当您确定拉动请求正确时,您可以使用图 12-23 中的所示的合并拉动请求按钮来接受它。
摘要
在这一章中,我向你提供了使用 Github 作为项目托管平台的基本信息,以及为他人所有的项目做贡献的程序。
如果您想使用 Github 托管一个开源项目,请遵循秘籍 12-3、12-4 和 12-6。每当你收到一个拉动请求,你可以按照秘籍 12-9 接受它。
也许你想为一个开源项目做贡献?你可以用秘籍 12-7 和 12-8 中解释的方法来做这件事。
如果你主持一个由你信任的开发团队开发的项目,你可以避免你和你的朋友使用拉请求。设置适当的权限,你可以允许其他 Github 用户直接推送到原始存储库。在图 12-25 中所示的团队管理链接下有一个对话框。
图 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
文件。
接下来,更改文件的内容。将四个单词four
、five
、six
和seven
替换为包含单词foo
和bar
的两行。您应该获得的文件如清单 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 中的完全相同。
提示命令
$ 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,b
和c,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
生成的,因此描述被单线包围(它们包含单词three
和eight
):
@@ -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
提示命令
$ git diff
有一个非常有用的参数--check
,可以用来验证提交没有引入只影响白色字符的更改。命令$ git diff --check
报告处理白色字符的问题。
13-2.提交文件而不进行行尾转换
问题
您想要启动一个新的存储库,其中包含具有不同行尾类型的文本文件。有些使用类似 Linux 的由单个LF
字符组成的行尾,有些使用类似 Windows 的由两个字符CRLF
组成的行尾。您的存储库甚至包含使用两种类型的文件:混合在一个文件中的LF
和CRLF
。您希望提交所有文件,而不对行尾字符进行任何转换。
提示首先,您可能会认为同时使用了
LF
和CRLF
的文件已损坏。但无论如何你都可能需要它们。当我在一个库上工作来处理由外部工具产生的文本文件时,我发现它们作为测试的静态附件非常有用。原来,我使用的应用生成了损坏的文件,不仅包含LF
和CRLF
,还包含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
的第二个文件包含了LF
和CRLF
行尾。最后一个名为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
同时使用LF
和CRLF
windows/abcd.txt
用途CRLF
工作目录和临时区域中的文件完全相同。
它是如何工作的
Git 配置包含一个选项core.autocrlf
。该选项控制 git 处理行尾转换的方式。它可以取三个不同的值:true
、input
和false
。因为行尾的转换可以在签出或提交文件时执行,所以我们必须分析这两种情况下每个值的含义。
第一个值true
,影响签出和签入。在结帐过程中,git 将LF
字符转换成CRLF
。提交时,执行反向转换:CRLF
行尾转换为LF
。
第二个值input
,在检入操作期间开启从CRLF
到LF
的转换。使用此设置执行签出时,没有转换。
最后一个值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 中三个快照的行尾
所有三个快照都使用完全相同的行尾。
13-3.在不进行行尾转换的情况下签出文件
问题
您的 git 配置包含设置为true
的core.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 中三个快照的行尾
现在,工作目录包含存储在 HEAD revision 中的不同行尾。因此,您可能认为您的存储库很脏;然而,事实并非如此。如果您使用$ git status
命令,您将看到存储库是干净的。这是因为 git 会为您处理行尾的转换。这可能导致另一个问题:如何提交带有CRLF
行尾的文件?我们将在秘籍 13-4 中讨论这个问题。
现在,您认为您的工作目录已经损坏。您的意图是让工作目录中的文件具有与 HEAD 中相同的行尾。要再次执行签出操作,这次不对行尾进行任何转换,请按照以下步骤操作:
- 删除所有被跟踪的文件:
$ git ls-files | xargs rm
- 移除暂存区:
$ rm .git/index
- 关闭新行转换:
$ git config --local core.autocrlf false
- 重新创建工作目录和暂存区:
$ git reset --hard
现在暂存区和工作目录包含的文件与它们在HEAD
版本中存储的完全一样。存储库中使用的行尾与表 13-2 中的相同。
它是如何工作的
如果您将选项core.autocrlf
设置为true
,然后克隆存储库,使用LF
行尾的文件将被转换为使用CRLF
行尾。因此,紧接着:
$ git clone 13-02 13-03
文件linux/abcd.txt
现在包含了CRLF
行尾。包含LF
和CRLF
的文件,如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
:
- 用
$ git config --local core.autocrlf true
将core.autocrlf
选项设置为true
- 用
$ git ls-files | xargs rm
删除所有被跟踪的文件 - 用
$ 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 false
将core.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 的最终状态中的三个快照中的行结束
它是如何工作的
如果您:
- 用
$ 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
并遵循以下程序:
- 用一个规则
* text=auto
创建文件.gitattributes
文件。你可以用$ echo "* text=auto" >>.gitattributes
来做 - 用
$ rm .git/index
移除暂存区 - 用
$ git reset
重新创建.git/index
文件
用$ git status -sb
检查所有文件的状态。如你所见,这次mixed/abcd.txt
和windows/abcd.txt
都被列为修改过的。使用以下命令完成提交所有已更改文件的配方:
$ git snapshot Standardization: line endings changed to LF.
所有三个快照HEAD
、工作目录和暂存区现在都包含了LF
行尾。配方 13-5 的结果总结在表 13-5 中。
表 13-5 。存储库 13-05 的最终状态中的三个快照中的行结束
注意,这个配方转换了文件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
它将打印关于行尾变化的警告。
提示如果你用的是 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
提示这个解决方案用在一个 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
您的存储库现在包含七个提交a
、b
、c
、d
、f
、g
和两个标记v1.2.3
和temp-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
提示你可以把标签当成不能动的树枝。
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 给出了三个最典型的问题,通过定义适当的规则来忽略文件,可以解决这些问题。第一个问题涉及由各种工具自动生成的二进制文件。以下是一些例子:
- 编译时生成的文件:
*.o
、a.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