Git高手之路

978-7-115-47850-4
作者: Jakub Narębski
译者: 邓世超
编辑: 胡俊英

图书目录:

详情

本书分为12章内容,分别讲解了Git的基础知识、Git开发技巧、项目开发中的注意事项等内容,书中通过结合实际的项目,通过教读者如何管理代码,如何做好版本控制,更好地实现项目开发。本书还非常有助于读者理解和研究已有项目的开发历史,做好版本恢复和团队协作。

图书摘要

版权信息

书名:Git高手之路

ISBN:978-7-115-47850-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [波兰] 雅各布•纳热布斯基(Jakub Narębski )

译    邓世超

责任编辑 胡俊英

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright ©2016 Packt Publishing. First published in the English language under the title Mastering Git.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Git是一款免费、开源的分布式版本控制系统,可以对或大或小的项目进行高效的版本管理。时至今日,Git已经在项目开发领域发挥着重要作用,并且得到了广泛的应用。

本书旨在帮助读者深入理解Git架构,以及其内部的理念、行为和最佳实践。全书共分为12章,从基础知识讲起,陆续介绍了项目历史管理、使用Git进行程序开发、工作区管理、Git协作开发、分支应用进阶、集成变更、历史记录管理、子项目管理、Git的定制和扩展、Git日常管理、Git最佳实践等内容。

本书面向所有的Git用户,全面细致地向读者介绍有关Git的各项实用技巧,充分发掘它的潜力,更好地实现项目版本管理。


Jakub Narębski自Git诞生之初就参与了Git的开发工作。他是gitweb子系统(Git原始Web界面)的主要贡献者之一,是非官方的gitweb维护者。他创造、发布并分析了2007年至2012年的年度Git用户调查。您可以在Git Wiki上找到对这些调查的分析内容。他经常在技术问答网站StackOverflow上和他人分享自己的技术专长。

他是Eric Sink的Version Control by Example一书的审校者之一,这也是他在Git领域占有一席之地的原因。

他是波兰托伦哥白尼大学数学和计算机科学系的助理教授。他选择使用Git作为个人和专业工作的版本控制系统,将其作为课程作业的一部分讲授给数学和计算机科学系的学生。


本书是经过精心设计的,旨在帮助读者深入理解Git架构,以及其内部理念、行为和最佳实践。

本书以使用Git进行协作开发的简单项目示例入手,使得读者能够了解基本的Git操作和概念。然后,随着阅读的深入,后续的章节将会阐述不同领域的具体应用细节:从源代码历史版本管理,再到管理用户自己的工作成果,然后是和其他开发人员协作。版本控制主题应用伴随着Git架构和行为的细节讲解。

本书还有助于读者增强检查和浏览项目历史记录、创建和管理工作成果、中心式和分布式工作流中配置版本库和分支方便协作开发、集成来自其他开发人员的工作成果、自定义和扩展Git,以及恢复版本库数据等能力。通过了解Git高级应用技巧和内部工作机制,读者将会深入理解Git的行为,使得用户可以自定义和扩展现有的工具和脚本,甚至编写符合自己需要的功能。

第1章“Git应用入门”向读者提供了Git在版本控制方面的基本应用介绍。本章重点放在了技术应用方面,在一个开发示例中展示和说明了基本的版本控制操作和两名开发人员之间的协作开发。

第2章“项目历史管理”引入了修订的有向无环图(DAG)概念,并且解释了该概念和Git分支、标签和当前分支的关系。读者还会学习到如何查询、过滤和查看项目历史中的修订区间,如何使用不同条件查找修订记录。

第3章“使用Git进行程序开发”讲述了如何创建和添加这类历史记录。读者将会学习如何创建新修订以及展开新的开发工作。本章引入了提交暂存区的概念(索引),以及解释了如何查看和读取工作目录、索引和当前修订版本之间的差异。

第4章“工作区管理”的重点是解释如何为准备创建新提交而管理工作目录,同时还向读者介绍管理文件目录的细节。其中也包括需要特别处理的文件类型,引入了忽略文件和文件属性等概念。

第5章“Git协作开发”对多种协作模式进行了鸟瞰式的介绍,展示了不同中心式和分布式工作流之间的差异,它主要聚焦于协作开发过程中版本库层面的交互。这里读者还会学习到信任链的概念,以及如何使用签名标签、签名合并和签名提交。

第6章“分支应用进阶”深入介绍了分布式团队协作开发的细节,同时介绍了本地分支和远程版本库上的跟踪分支,以及如何同步分支和标签。读者将会学到多种分支技术,通过多种途径了解分支的类型和用途(包括主题分支工作流)。

第7章“集成变更”向读者介绍如何通过合并和变基操作合并来自不同开发流水线上的工作成果,同时还解释了不同类型的合并冲突,以及如何识别和解决它们。读者将会学习使用拣选提交拷贝变更,以及如何应用单个或批量补丁记录。

第8章“历史记录管理”解释了用户可能希望保持历史记录整洁的原因,以及应该这么做的时机和方法。这里读者将会找到如何重排、压缩和分割提交的详细步骤。本章还演示了如何恢复被重写过的历史记录,以及当用户无法重写历史记录时的解决方案:如何恢复提交,如何给它添加笔记,如何修改项目历史记录的视图。

第9章“子项目管理——构建活动框架”讲述和展示了通过不同方法在单个版本库的框架项目中连接不同项目,从通过将项目代码嵌入其他项目(子树)的强制性添加,到通过版本库(子模块)嵌套的方式连接两个项目。本章还介绍了处理大型版本库和大型文件的若干种解决方案。

第10章“Git的定制和扩展”介绍了通过配置和扩展Git来满足用户的实际需要,还简要介绍了图形化接口。读者将会了解到如何配置命令行,使它更易用。本章解释了如何在Git中使用钩子完成自动化任务(重点是客户端钩子),例如如何让Git检查创建提交时,相关操作是否遵循了特定的代码规范的指导意见。

第11章“Git日常管理”旨在帮助Git管理员提高日常管理工作的效率。它简要介绍了Git版本库服务的知识。这里读者将会学习如何使用服务端钩子处理日志记录、访问控制、强制性开发策略和其他任务。

第12章“Git最佳实践”列出了一组通用的版本控制方法和特定于Git的建议和最佳实践。这些内容涵盖了工作目录管理,创建单个提交和一系列提交(pull请求),提交附加变更和同行的代码审核等内容。

为了运行本书涉及的示例和提供的命令,读者将需要安装2.5.0版本及以上的Git软件。Git是开源的,并且兼容所有平台(例如Linux、Windows和Mac OS X)。所有示例使用的都是Git文本化接口和bash shell。

为了编译和运行第1章用到的示例程序(该程序主要用来演示基本的版本控制方法),读者需要安装C编译器和make程序。

如果读者已经是一名Git用户,并且掌握了分支、合并、暂存和工作流等基本概念,那么本书是为你而写的。如果读者是Git的资深用户,本书可以帮助你了解Git的工作机制,充分挖掘它的潜力,并可以了解若干高级工具、技术和工作流。安装Git的基本知识和软件配置管理的概念是必需的。

在本书中,读者将会接触到表达不同含义的若干种文本样式。这里将对它们逐一举例,并说明其代表的含义。

文本中的代码、命令以及选项、文件夹名、文件名、文件扩展名、路径名、分支和标签名、简易URL地址、用户输入、环境变量、配置选项和它们的参数值将会以如下格式表示:

"例如,git log – foo命令中显式声明了历史路径 foo."

此外,本书采用下列规范:<file>表示用户输入(这里是一个文件名),$HOME 表示环境变量的值,路径名中的波浪字符表示用户的主目录(例如~/.gitignore)。

代码块或配置文件片段以下列格式表示:

void init_rand(void)
{
    srand(time(NULL));
}

当代码块中需要引起读者的注意时(极个别情况),相关行会使用粗体表示:

void init_rand(void)
{
    srand(time(NULL));
}

任何命令行的输入和输出采用如下格式表示:

carol@server ~$ mkdir -p /srv/git
carol@server ~$ cd /srv/git
carol@server /srv/git$ git init --bare random.git

新的术语和关键字会加粗表示。读者在屏幕上看到的词语(例如在菜单或者对话框中显示的文本),将会以如下格式表示:

“The default description that Git gives to a stash (WIP on branch).”

 

 

警告或需要特别注意的内容。

 

 

 

 

提示或者诀窍。

 

我们非常欢迎读者的反馈。告诉我们您觉得本书怎么样,以及您喜欢哪部分或不喜欢哪部分。有了读者的反馈,我们才能继续写出真正能让大家充分受益的作品。

如果您想反馈信息,很简单,请在异步社区网站与本书对应的页面发表评论,或者发私信给责任编辑。

如果您也是某个领域的专家,并且有兴趣编写或者合作出版一本书,请发送邮件至contact@epubit.com.cn。

很荣幸您是本书的读者,我们将为您提供物超所值的增值服务。

您可以访问异步社区的网站,在本书对应的页面内下载配套文件。

另外,我们也提供本书中快照和图表的彩色PDF格式文件,彩色图片有助于您理解输出结果的变化。您可以在异步社区网站上与本书对应的页面内下载到彩图文件。

虽然我们会全力确保书中内容的准确性,但错误仍在所难免。如果您在某本书中发现了错误(文字错误或代码错误),而且愿意向我们提交这些错误,我们将感激不尽。这样不仅可以消除其他读者的疑虑,也有助于改进后续版本。若您想提交所发现的错误,请访问异步社区网站,在本书对应的页面内提交勘误。一经核实,您所提交的勘误将在本书对应的勘误区域呈现给读者。

对所有媒体来说,互联网盗版都是一个棘手的问题,我们一直都很重视版权保护。如果您在互联网上发现我们公司出版物的任何非法复制品,请及时告知我们网址或网站名称,以便我们采取补救措施。

请通过315@ptpress.com.cn联系我们,并提供疑似盗版材料的链接信息。

感谢您帮助我们保护作者的权益,使我们能够为您提供更有价值的内容。

如果您对本书有疑问,请在异步社区与本书对应的页面内提问,我们会竭力为您解答。


Markus Maiwald是一名互联网服务提供商,商业网站推广员和域名提供商。 例如,他为客户提供一站式白色标签解决方案(从注册域名到部署Web服务器)。

因此,他的口号是:我们的系统,您的生意。

在专业领域,他是一名顾问和系统管理员,拥有超过15年的Linux经验。 他喜欢构建高性能的服务器系统,还开发出了不少可重用、高安全性的标准系统。

作为一名真正的Webworker 2.0,他的客户遍及全球,其范围从欧洲的一家保险公司到泰国的Web开发工作室。

这也是他热衷于本书相关工作的主要原因。作为一名伟大的团队协作成员,他在国际团队合作方面拥有丰富的经验,他在工作中引进了Git这样能够大幅度提高生产力的工具。

必须感谢来自Packt出版社的项目协调人Bijal Patel,我得到了他的大力支持,并且度过了愉快的时光。

还要感谢Sarah在我完成此项目过程中给予的耐心和鼓励。


本书是专门为Git初学者和高级用户撰写的,希望能够在他们精通Git要义的道路上有所帮助。因此,接下来的章节会假定读者已经了解了Git的基本使用,并且度过了学习Git的新手阶段。

本章的内容可以作为Git版本控制基础知识的简单回顾。本章的重点会放在实际应用方面,通过开发一个简易示例项目,演示和解说基本的版本控制操作,以及两个开发者之间的协作流程。

本章将会介绍以下知识。

版本控制系统(有时也称修订控制)是一种用户可以根据时间追溯项目文件(存放于版本库中)修改历史和属性的工具,它还可以帮助团队成员协作开发。当前流行的版本控制系统可以为每个开发人员提供专属的沙箱,防止他们的工作发生冲突,同时采用冲突合并和同步机制,实现以非阻塞的方式进行高效协作。

像Git这类分布式版本控制系统为每个用户提供专属于其自己的项目历史副本、版本库的副本。Git系统如此高效的原因有以下几个:首先,几乎所有操作都是在用户本机上执行,而且非常灵活;其次,你可以使用多种方式建立版本仓库。版本仓库对于开发来说意味着每个开发人员都有整个项目文件的独立工作区(也称工作目录)。Git采用的分支模型支持本地分支,而且分支的发布也非常灵活,用户可以使用分支进行内容切换,还可以在开发过程中将不同工作放置于相互隔离的沙箱中(有可能构建出独立、灵活的主题分支工作流)。

事实上,版本库的整个变更历史都是可以访问的,用户可以撤销或者回退更改过的内容到最后一个工作版本等。当每个修改被提交之后,用户的修改提交记录也被记录下来,因此提交代码修改的用户也就很容易被定位。你还可以比较文件的不同版本,将代码回退到某个用户提交bug报告之前的版本,甚至可以知道哪个版本的变更导致了上述bug。其实,Git主要是通过reflog命令来跟踪分支的变更记录信息并实现回退和覆盖目的的。

Git有一个独特的功能是它支持显式访问暂存区以便创建注释(对项目进行新的修订)。这为用户管理工作区和确定将来的注释信息带来了更多灵活性。

所有这些灵活、强大的特性都是要付出代价的。虽然掌握Git的基本使用非常简单,但是精通Git的使用并不是那么容易。本书将会帮助你在成为Git专家的道路上披荆斩棘,不过在此让我们先来回顾一下Git的基本使用。

让我们通过两个开发人员在一个简单项目上使用Git进行协作开发来一步一步地构建一个简单示例。读者可以从http://www.packtpub.com下载相关的项目示例代码。你会发现本章的示例代码文件中包含3个版本库(一个是服务端的,另外两个是开发者的),而且你可以浏览版本库的代码、查询修改历史、执行reflog命令等。

某公司准备研发一款新产品。该产品主要的用途是从特定区间内随机获取若干个整数。

该公司指派了两名开发人员负责这个新项目,他们的名字分别是Alice和Bob。两名远程办公的开发人员经过和公司领导协商之后,决定使用C语言开发一套命令行应用来完成该产品的研发,并且使用Git 2.5.0(http://git-scm.com/)进行程序代码版本控制管理。该产品的用途主要是用来进行过程演示的,而且也非常简单。程序代码的细节并不是重点,我们关注的是如何管理代码变更。

团队规模很小,因此他们决定以图1-1所示的组织结构启动项目。

图1-1 启动项目的组织结构

 

 

这个启动配置非常自由,虽然有中心版本库,但是却没有专人负责维护它(项目启动之初,所有开发者的角色都是平等的)。启动项目的组织形式多样,如果你希望深入了解与此有关的知识,可以参考第5章的内容。

 

Alice在项目启动时请求管理员Carol为她创建一个新的版本库以方便团队协作。

 

 

命令行示例遵循的是UNIX系统风格,命令行前面的提示信息由“用户名@主机名/文件目录”组成,这样一眼就可以看出由谁执行该命令,属于哪台计算机以及文件目录是什么。上述风格在UNIX环境中很常见(在Linux系统下也是如此)。可以参考第10章的内容,让Git系统显示特定信息,例如版本库名称、版本库下的子目录名、当前分支,甚至工作区的状态信息。

 

carol@server ~$ mkdir -p /srv/git
carol@server ~$ cd /srv/git
carol@server /srv/git$ git init --bare random.git

 

 

我认为显示服务端配置的细节对于本章来说有点多余了。因此,为了行文简洁,不相关的信息就省略了。如果希望了解详情,参考第11章即可。

你还可以使用工具来管理Git的版本库(例如Gitolite),在服务端创建一个版本库也许看上去会稍有不同。通常情况下是使用git init(不带“--bare”参数)命令创建版本库,然后根据特定的URL地址将其推送到服务端,执行上述操作之后,服务端会自动创建一个公共版本库。或者也可以使用带Web接口工具的网站创建该版本库,例如GitHub、Bitbucket和GitLab(可以托管或内部部署)。

 

Bob知道项目版本库就绪的消息之后,就开始了编写代码的工作。

因为这是他在本项目中首次使用Git,因此,他在自己的版本库根目录下建立了对应的~/.gitconfig文件,该文件主要是用来帮助他标记日志文件中特定的注释信息:

[user]
  name = Bob Hacker
  email = bob@company.com

现在他需要获取自己的版本库实例:

bob@hostB ~$ git clone https://git.company.com/random
Cloning into random...
Warning: You appear to have cloned an empty repository.
done.
bob@hostB ~$ cd random
bob@hostB random$

 

 

本章所有示例使用的都是命令行接口。这些命令也可以使用Git的GUI应用程序或者IDE集成环境完成。Packt出版社发行的The Git: Version Control for Everyone Book详细介绍了GUI界面替代命令行执行任务的具体细节。

 

Bob注意到Git系统提示说这是一个空的版本库,还没有代码文件,所以他开始编写代码了。他打开了文本编辑器,为本项目创建了第一版代码程序:

#include <stdio.h>
#include <stdlib.h>

int random_int(int max)
{
  return rand() % max;
}

int main(int argc, char *argv[])
{
  if (argc != 2) {
    fprintf(stderr, "Usage: %s <number>\n", argv[0]);
    return EXIT_FAILURE;
  }

  int max = atoi(argv[1]);

  int result = random_int(max);
  printf("%d\n", result);

  return EXIT_SUCCESS;
}

一般来说,和大部分原始程序实现类似,该程序还非常简陋。不过这是一个很好的开端。在提交代码之前,Bob希望这个程序可以通过编译并能够运行:

bob@hostB random$ gcc –std=c99 random.c
bob@hostB random$ ls –l
total 43
-rwxr-xr-x  1 bob  staff  86139  May 29 17:36 a.out
-rw-r--r--  1 bob  staff    331  May 19 17:11 random.c
bob@hostB random$ ./a.out
Usage: ./a.out <number>
bob@hostB random$ ./a.out 10
1

好的!现在把该文件添加到版本库中:

bob@hostB random$ git add random.c

Bob使用“status”命令来确保先前的所有修改都没什么问题:

 

 

为了节省示例篇幅,我们截取了git status命令的部分内容。你可以在本章后续内容中看到执行该命令后的完整内容输出。

 

bob@hostB random$ git status –s
A  random.c
?? a.out

Git系统显示了警告信息,因为它不知道该如何处理a.out文件:它既没有在跟踪列表中,也没有在忽略列表中。它是编译过程中生成的可执行体,不应该存放于版本库中。Bob可以暂时不用理会这个提示信息。

现在,向服务端提交(commit)该代码文件:

bob@hostB random$ git commit –a –m "Initial implementation"
[master (root-commit) 2b953b4] Initial implementation
1 file changed, 22 insertions(+)
Create mode 100644 random.c

 

 

一般来说,在添加注释信息时,不仅可以使用-m <信息> 命令行选项,还可以让Git打开一个文本编辑器完成相关操作。本示例使用这种格式是为了让示例代码看起来更紧凑。

-a/--all选项的意思是接受被追踪文件的所有变更,你可以在暂存区创建一个注释来实现操作隔离,不过这是另外一个问题了。详情可以参考第4章内容。

 

在完成项目的初始版本之后,Bob准备发布它们(提交到服务器,供团队其他成员访问)。他将自己的工作成果推送到了服务端:

bob@hostB random$ git push
warning: push.default is unset; its implicit value has changed in
Git 2.0 from 'matching' to 'simple'. To squelch this message [...]
To https://git.company.com/random
 * [new branch]      master -> master
bob@hostB random$ git config --global push.default simple

 

 

注意:

Git会根据用户的网络速度,显示诸如clonepushfetch操作的具体进度。为了简单起见,本书示例省略了这些内容,当需要查看代码文件历史和变更记录时才会显示相关信息。

 

因为这是Alice第一次在她的个人电脑上使用Git,因此,她必须告诉Git系统如何识别她提交的注释:

alice@hostA ~$ git config --global user.name "Alice Developer"
alice@hostA ~$ git config --global user.email alice@company.com

现在Alice需要建立专属于她自己的版本库实例:


alice@hostA ~$ git clone https://git.company.com/random
Cloning into random...
done.

Alice打算查看一下工作目录:

alice@hostA ~$ cd random
alice@hostA random$ ls –al
total 1
drwxr-xr-x    1 alice staff      0 May 30 16:44 .
drwxr-xr-x    4 alice staff      0 May 30 16:39 ..
drwxr-xr-x    1 alice staff      0 May 30 16:39 .git
-rw-r--r--    1 alice staff    353 May 30 16:39 random.c

 

 

.git目录下包含Alice的版本库的拷贝(克隆),并且这些文件是以Git内部格式存在的,同时还包含一些针对版本库的管理信息。你可以在Git帮助手册中的gitrepository-layout(5)章节找到文件格式的详细说明,只需要在命令行中键入git help repository-layout命令即可。

 

她希望查看日志的细节信息(查看项目历史记录):

alice@hostA random$ git log
commit 2b953b4e80abfb77bdcd94e74dedeeebf6aba870
Author: Bob Hacker <bob@company.com>
Date:   Thu May 29 19:53:54 2015 +0200

    Initial implementation

 

 

修订追踪:

在最底层实现中,Git历史版本识别是通过一个SHA-1哈希码实现的,例如2b953b4e80。Git支持多种形式的版本查询,其中就包括SHA-1码精确匹配(最少提供4个字符)。请参考第2章了解详情。

 

当Alice决定浏览一遍代码时,她突然发现了一个严重的问题:随机数生成部分一直都没有初始化!她经过一个快速测试发现程序生成的结果都是同一个数。幸运的是,她并不需要修改main()内部的代码,只需要在顶部加入相应的#include引用即可:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int random_int(int max)
{
  return rand() % max;
}

int main(int argc, char *argv[])
{
  if (argc != 2) {
    fprintf(stderr, "Usage: %s <number>\n", argv[0]);
    return EXIT_FAILURE;
  }

  int max = atoi(argv[1]);

  srand(time(NULL));
  int result = random_int(max);
  printf("%d\n", result);

  return EXIT_SUCCESS;
}

改完代码之后,她运行了几次程序,来确认程序真的可以生成随机数。一切似乎都进行得很顺利。她使用git status命令查看了之前的文件变更:

alice@hostA random$ git status –s
 M random.c

不必感到大惊小怪。Git知道random.c文件被修改了。然后Alice使用git diff命令再次确认对代码的修改:

 

 

从现在开始,我们将不会显示未被跟踪的文件信息,除非它和讨论的主题相关。现在假定Alice已经生成了一个忽略配置文件。该配置文件的详情,可以参考第4章。

 

alice@hostA random$ git diff
diff --git a/random.c b/random.c
index cc09a47..5e095ce 100644
--- a/random.c
+++ b/random.c
@@ -1,5 +1,6 @@
 #include <stdio.h>
 #include <stdlib.h>
+#include <time.h>

int random_int(int max)
{
@@ -15,6 +16,7 @@ int main(int argc, char *argv[])

   int max = atoi(argv[1]);

+  srand(time(NULL));
   int result = random_int(max);
   printf("%d\n", result);

现在,可以提交变更,然后将它们发布到公共版本库了:

alice@hostA random$ git commit -a -m "Initialize random number generator"
[master db23d0e] Initialize random number generator
 1 file changed, 2 insertions(+)
alice@hostA random$ git push
To https://git.company.com/random
   3b16f17..db23d0e master -> masterRenaming and moving files

Bob接下来的工作是重构工作区目录。他不希望版本库的顶层目录文件太多,所以他决定将所有源代码文件移动到“src/”子目录下:

bob@hostA random$ mkdir src
bob@hostA random$ git mv random.c src/
bob@hostA random$ git status –s
R  random.c -> src/random.c
bob@hostA random$ git commit –a –m "Directory structure"
[master 69e0d3d] Directory structure
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename random.c => src/random.c (100%)

现在,他为了确保目录重构之后使用“diff”命令输出结果的差异不至于太大,将Git系统配置为始终执行重命名和拷贝的检测:

bob@hostB random$ git config --global diff.renames copies

Bob觉得是时候为项目添加一个合适的Makefile配置文件,以及一个README帮助文件了:

bob@hostA random$ git add README Makefile
bob@hostA random$ git status –s
A   Makefile
A   README
bob@hostA random$ git commit -a -m "Added Makefile and README"
[master abfeea4] Added Makefile and README
2  files changed, 15 insertions(+)
create mode 100644 Makefile
create mode 100644 README

Bob将“random.c”文件的名字改为“rand.c”:

bob@hostA random$ git mv src/random.c src/rand.c

上述操作当然也需要修改Makefile文件:

bob@hostA random$ git status –s
 M Makefile
R  src/random.c -> src/rand.c

然后,他提交了这些修改。

项目文件重组完成之后,Bob打算将这些变更推送到服务端:

bob@hostA random$ git push
$ git push
To https://git.company.com/random
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://git.company.com/random'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository
pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for
details.

但是Alice同时也在这个项目中工作,而且她已经向服务端推送了自己的代码。Git系统现在不允许Bob推送他对项目代码的变更,因为Alice已经推送了一些内容到master分支上,系统会保护她提交的变更。

 

 

为了行文简洁,Git命令行中输出的提示和帮助信息在后续篇章中会略去。

 

Bob使用“pull”命令将服务端版本库的内容与自己的版本库同步(像命令行提示信息建议的那样):

bob@hostB random $ git pull
From https://git.company.com/random
 + 3b16f17...db23d0e master    -> origin/master
Auto-merging src/rand.c
Merge made by the 'recursive' strategy.
 src/rand.c | 2 ++
 1 file changed, 2 insertions(+)

执行“pull”命令之后,Git系统会将服务端版本库中的变更下载到Bob本机,然后自动将它们和Bob本机版本库的变更合并,最后把合并后的变更提交到本机版本库中。

现在万事俱备了:

bob@hostB random$ git show
commit ba5807e44d75285244e1d2eacb1c10cbc5cf3935
Merge: 3b16f17 db23d0e
Author: Bob Hacker <bob@company.com>
Date:   Sat May 31 20:43:42 2015 +0200

    Merge branch 'master' of https://git.company.com/random

合并后的提交也完成了。Git系统可以直接将Alice提交的变更与Bob移动或重命名后的文件合并,是不是很神奇呢?

Bob检查编译(因为自动合并之后并不能绝对保证代码没问题)了一下变更合并后的代码,准备将合并变更后的代码推送到服务端:

bob@hostB random$ git push
To https://git.company.com/random
   db23d0e..ba5807e    master -> master

Alice和Bob认为项目可以进行更大范围的发布了。Bob创建了一个标签(tag),以便日后他们方便地访问/引用发布过的预览版本。他为此使用了一个带注释的标签,当然大家一般采用的替代性方案是使用带数字签名的标签,该标签通常会包含一个PGP数字签名(以后的验证需要用到它):

bob@hostB random$ git tag -a -m "random v0.1" v0.1
bob@hostB random$ git tag --list
v0.1
bob@hostB random$ git log -1 --decorate --abbrev-commit
commit ba5807e (HEAD -> master, tag: v0.1, origin/master)
Merge: 3b16f17 db23d0e
Author: Bob Hacker <bob@company.com>
Date:   Sat May 31 20:43:42 2015 +0200

    Merge branch 'master' of https://git.company.com/random

当然,v0.1版的标签如果只放在Bob本地的版本库中是没有什么意义的。接下来他将刚创建的标签推送到服务端:

bob@hostB random$ git push origin tag v0.1
Counting objects: 1, done.
Writing objects: 100% (1/1), 162 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (1/1), done.
To https://git.company.com/random
* [new tag]         v0.1 -> v0.1

Alice为了获得这个标签,更新了她的版本库,然后开始了她的日常工作:

alice@hostA random$ git pull
From https://git.company.com/random
   f4d9753..be08dee  master     -> origin/master
 * [new tag]         v0.1       -> v0.1
Updating  f4d9753..be08dee
Fast-forward
 Makefile               | 11 +++++++++++
 README                 |  4 ++++
 random.c => src/rand.c |  0
 3 files changed, 15 insertions(+)
 create mode 100644 Makefile
 create mode 100644 README
 rename random.c => src/rand.c (100%)

Alice认为将生成伪随机数功能修改成一个单独的子程序是个好主意。这样一来,初始化和生成随机数的功能都独立封装起来了,将来需求变更的时候更容易实现一些。她决定给程序添加一个init_rand()函数:

void init_rand(void)
{
  srand(time(NULL));
}

接下来,编译运行一下代码,看看有没有什么问题:

alice@hostA random$ make
gcc -std=c99 -Wall -Wextra    -o rand src/rand.c
alice@hostA random$ ls -F
Makefile  rand*  README  src/

通过编译,程序没什么问题之后就可以提交变更了:

alice@hostA random$ git status –s
 M src/rand.c
alice@hostA random$ git commit -a -m "Abstract RNG initialization"
[master 26f8e35] Abstract RNG initialization
 1 files changed, 6 insertions(+), 1 deletion(-)

从提示信息可以看到,提交成功了。

同时,Bob在与rand()函数相关的开发文档中发现,它是用来生成简单的伪随机数的标准函数,可能并不能满足实际需要:

bob@hostB random$ git pull
Already up-to-date.

他决定在提交代码变更的注释中添加对此问题的备注说明:

bob@hostB random$ git status –s
 M src/rand.c
bob@hostB random$ git diff
diff --git a/src/rand.c b/src/rand.c
index 5e095ce..8fddf5d 100644
--- a/src/rand.c
+++ b/src/rand.c
@@ -2,6 +2,7 @@
 #include <stdlib.h>
 #include <time.h>

+// TODO: use a better random generator
 int random_int(int max)
 {
         return rand() % max;

他提交代码的变更之后,将它们推送到了服务端:

bob@hostB random$ git commit -m 'Add TODO comment for random_int()'
[master 8c4ceca] Use Add TODO comment for random_int()
 1 files changed, 1 insertion(+)
bob@hostB random$ git push
To https://git.company.com/random
   ba5807e..8c4ceca    master -> master

因此,当Alice准备推送她的变更到服务端时,Git系统拒绝了该操作:

alice@hostA random$ git push
To https://git.company.com/random
 ! [rejected]       master -> master (non-fast-forward)
error: failed to push some refs to 'https://git.company.com/random'
[...]

Bob一定是推送了不少变更到服务端。Alice需要再次从服务端的版本库上下载最新版本的项目文件,然后亲自把自己的变更和Bob的变更合并:

alice@hostA random$ git pull
From https://git.company.com/random
   ba5807e..8c4ceca  master     -> origin/master
Auto-merging src/rand.c
CONFLICT (content): Merge conflict in src/rand.c
Automatic merge failed; fix conflicts and then commit the result.

该合并操作并没有像上次那样被顺利执行。Git系统无法自动合并Alice和Bob两人提交的变更。显然,文件变更之间有冲突。Alice决定用文本编辑器打开文件“src/rand.c”一探究竟(她也可以使用图形化的合并工具查看代码差异):

<<<<<<< HEAD
void init_rand(void)
{
        srand(time(NULL));
}

=======
// TODO: use a better random generator
>>>>>>> 8c4ceca59d7402fb24a672c624b7ad816cf04e08
int random_int(int max)

Git系统中现在既有Alice提交的代码变更(在<<<<<<<< HEAD和======== 冲突标记之间),也有Bob提交的代码变更(在========和 >>>>>>>>之间)。我们希望的结果是将两人的代码融为一体。Git系统无法自动合并它们,因为这些代码块并不是独立的。Alice的init_rand()函数可以简单地插入Bob添加的代码注释之前。执行上述操作之后,结果如下:

alice@hostA random$ git diff
diff --cc src/rand.c
index 17ad8ea,8fddf5d..0000000
--- a/src/rand.c
+++ b/src/rand.c
@@@ -2,11 -2,7 +2,12 @@@
  #include <stdlib.h>
  #include <time.h>

 +void init_rand(void)
 +{
 +     srand(time(NULL));
 +}
 +

+ // TODO: use a better random generator
  int random_int(int max)
  {
        return rand() % max;

这样冲突应该就可以解决了。Alice重新编译运行了一下程序,然后提交了这一变更:

alice@hostA random$ git status –s
UU src/rand.c
alice@hostA random$ git commit -a -m 'Merge: init_rand() + TODO'
[master 493e222] Merge: init_rand() + TODO

然后她尝试将该变更推送到服务端版本库:

alice@hostA random$ git push
To https://git.company.com/random
   8c4ceca..493e222  master -> master

大功告成!

Bob打算给项目添加一个和版权声明有关的COPYRIGHT文件,当然还打算添加一个记录软件新特性的文件(不过还没有创建),所以他使用批处理命令添加了工作区中的所有文件到版本库中:

bob@hostB random$ git add –v
add 'COPYRIGHT'
add 'COPYRIGHT~'

因为Bob没有配置的忽略模式,因此作为备份文件的“COPYRIGHT~”也被提交到了版本库。接下来我们移除该文件:

bob@hostB random$ git status -s 
A  COPYRIGHT
A  COPYRIGHT~
bob@hostB random$ git rm COPYRIGHT~
error: 'COPYRIGHT~' has changes staged in the index
(use --cached to keep the file, or -f to force removal)
bob@hostB random$ git rm -f COPYRIGHT~
rm 'COPYRIGHT~'

检查一下文件状态,然后提交变更:

bob@hostB random$ git status –s
A  COPYRIGHT
bob@hostB random$ git commit -a -m 'Added COPYRIGHT'
[master ca3cdd6] Added COPYRIGHT
 1 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 COPYRIGHT

百无聊赖之际,Bob决定调整一下文件rand.c的代码缩进格式,使之符合统一的命名规范。

bob@hostB random$ indent src/rand.c

他统计了一下该文件源代码的变更记录:

bob@hostB random$ git diff --stat
 src/rand.c |    40 ++++++++++++++++++++++------------------
 1 files changed, 22 insertions(+), 18 deletions(-)

样式调整的变更太多了(对于如此小的文件来说),在合并时可能会出问题。Bob冷静了一下,然后撤销了对文件rand.c的样式调整:

bob@hostB random$ git status –s
 M src/rand.c
bob@hostB random$ git checkout -- src/rand.c
bob@hostB random$ git status -s

 

 

如果你不记得如何回退一个特定类型的变更或者更新某个已提交的变更(使用不带“-a”参数的命令“git commit”),执行“git status”命令(不带“-s”参数)后的输出结果会包含如下所示的帮助信息:

 

bob@hostB random$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working
directory)
#
#   modified:  src/rand.c

Alice注意到代码中取模运算返回给定区间内随机数的分布并不是均匀的,因为大部分情况下返回的都是较小的数。她打算修复这个问题。为了将相关的开发工作和代码中的其他变更隔离,她决定在自己的版本库中创建一个新分支(分支的具体使用可以参考第6章),然后切换到该分支:

alice@hostA random$ git checkout -b better-random
Switched to a new branch 'better-random'
alice@hostA random$ git branch
* better-random
  master

 

 

除了使用“git checkout –b better-random”命令创建一个新分支,然后使用命令切换到该分支之外,她还可以首先使用“git branch better-random”命令创建一个新分支,然后使用“git checkout better-random”命令切换到该分支。

 

她决定使用RAND_MAX常量控制rand()函数生成随机数的范围。相关的代码修改如下:

alice@hostA random$ git diff
diff --git a/src/rand.c b/src/rand.c
index 2125b0d..5ded9bb 100644
--- a/src/rand.c
+++ b/src/rand.c
@@ -10,7 +10,7 @@ void init_rand(void)
 // TODO: use a better random generator
 int random_int(int max)
 {
-    return rand() % max;
+    return rand()*max / RAND_MAX;
 }

 int main(int argc, char *argv[])

她提交了上述代码变更,然后将它们推送到了服务端,她知道上述推送操作可以顺利执行,因为这些操作都是在她的私有分支上进行的:

alice@hostA random$ git commit -a -m 'random_int: use rescaling'
[better-random bb71a80]  random_int: use rescaling
 1 files changed, 1 insertion(+), 1 deletion(-)
alice@hostA random$ git push
fatal: The current branch better-random has no upstream branch.
To push the current branch and set the remote as upstream, use

   git push --set-upstream origin better-random

通过上述信息可以知道,Git系统希望Alice为她新创建的分支(它采用的推送策略是simple模式)在远程版本库中添加对应的上游分支,这样可以让分支推送到远程分支的目标更明确。

alice@hostA random$ git push --set-upstream origin better-random
To https://git.company.com/random
 * [new branch]      better-random -> better-random

 

 

如果她希望更直观地管理她自己的分支结构并且只对自己可见,那么她需要配置好与服务端的相关映射,或者使用诸如Gitolite之类Git版本库管理软件来管理自己的分支。

 

与此同时,在默认的主分支下,Bob打算推送自己给项目添加COPYRIGHT文件的变更:

bob@hostB random$ git push
To https://git.company.com/random
 ! [rejected]       master -> master (non-fast-forward) 
[…]

出现上述错误提示是因为Alice当时正在忙着将初始化生成随机数的部分代码封装为一个子程序(解决合并冲突),她首先向服务端版本库推送了自己的变更:

bob@hostB random$ git pull
From https://git.company.com/random
   8c4ceca..493e222  master    -> origin/master
 * [new branch]      better-random   -> origin/better-random
Merge made by 'recursive' strategy.
 src/rand.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

Git系统可以轻松地将Alice推送的变更合并,但是现在出现了一个新分支。接下来我们来看看具体细节。为了节省篇幅,下文只显示和该新分支“better- random”相关的内容(双点符号的详细用法可以参考第2章):

bob@hostB random$ git log HEAD..origin/better-random
commit bb71a804f9686c4bada861b3fcd3cfb5600d2a47
Author: Alice Developer <alice@company.com>

Date:   Sun Jun 1 03:02:09 2015 +0200

    random_int: use rescaling

有趣的是,Bob希望从服务端获取Alice创建的新分支然后合并到自己的版本库的默认分支下(该新分支在远程版本库上已经存在):

bob@hostB random$ git merge origin/better-random
Merge made by the 'recursive' strategy.
 src/rand.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Bob意识到何时将该特性添加到主分支中应该由Alice来做决定。他打算撤销上述合并操作。因为这些变更还没有发布,因此只需要简单地将自己的本地主分支回退到上一次提交的变更状态即可:

bob@hostB random$ $ git reset --hard @{1}
HEAD is now at 3915cef Merge branch 'master' of https://git.company.com/ 
random

 

 

本示例演示了使用日志引用(reflog)机制实现变更回退操作。另外一种解决方案是使用“HEAD^”代替“@{1}”,这样也可以实现同样的效果。

 

本章的主要内容是演示了一个小型开发团队在一个简单项目中协作开发的大致流程。

回顾了如何将Git和实际的研发工作集成以提高工作效率,以及创建新的版本库或者克隆一个已有的版本库;还学习了提交项目变更之前对文件的添加、编辑、移动和重命名等操作,以及如何查看项目状态和已提交的变更历史,创建预览版项目标签。

回顾了如何使用Git在同一项目中的团队协作,如何发布自己的变更,如何获取团队其他成员的成果。因为使用Git可以帮助团队成员同步开发,有时在多个团队成员之间,Git需要用户手动解决合并冲突。

回顾了如何为软件预览版程序创建标签,以及为某一个特定功能创建独立分支。Git需要用户显式推送标签和分支,但是获取它们是自动的。我们还学习了如何合并分支。


相关图书

微服务之道
微服务之道
微服务实战
微服务实战
Istio实战指南
Istio实战指南
微服务实践
微服务实践
Spring微服务实战
Spring微服务实战
深入理解Spring Cloud与微服务构建
深入理解Spring Cloud与微服务构建

相关文章

相关课程