git简介—快速入门

前言

这篇简介借鉴了狂神的文章,此处给出链接。另外copy了一下git官网对于分支的介绍。重要的是实战,git上手很快的


一、版本控制

1.什么是版本控制

版本控制(Revision control)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。

  • 实现跨区域多人协同开发
  • 追踪和记载一个或者多个文件的历史记录
  • 组织和保护你的源代码和文档
  • 统计工作量(conmmit
  • 并行开发、提高开发效率
  • 跟踪记录整个软件的开发过程
  • 减轻开发人员的负担,节省时间,同时降低人为错误

简单说就是用于管理多人协同开发项目的技术

没有进行版本控制或者版本控制本身缺乏正确的流程管理,在软件开发过程中将会引入很多问题,如软件代码的一致性、软件内容的冗余、软件过程的事物性、软件开发过程中的并发性、软件源代码的安全性,以及软件的整合等问题

2.常见的版本控制工具

主流的版本控制器有如下这些:

  • Git
  • SVN(Subversion)
  • CVS(Concurrent Versions System)
  • VSS(Micorosoft Visual SourceSafe)
  • TFS(Team Foundation Server)
  • Visual Studio Online

版本控制产品非常的多(Perforce、Rational ClearCase、RCS(GNU Revision Control System)、Serena Dimention、SVK、BitKeeper、Monotone、Bazaar、Mercurial、SourceGear Vault),现在影响力最大且使用最广泛的是Git与SVN

3.版本控制分类

1、本地版本控制

记录文件每次的更新,可以对每个版本做一个快照,或是记录补丁文件,适合个人用,如RCS

2、集中版本控制 SVN

所有的版本数据都保存在服务器上,协同开发者从服务器上同步更新或上传自己的修改
所有的版本数据都存在服务器上,用户的本地只有自己以前所同步的版本,如果不连网的话,用户就看不到历史版本,也无法切换版本验证问题,或在不同分支工作。而且,所有数据都保存在单一的服务器上,有很大的风险这个服务器会损坏,这样就会丢失所有的数据,当然可以定期备份。代表产品:SVN、CVS、VSS

3、分布式版本控制 Git

每个人都拥有全部的代码

所有版本信息仓库全部同步到本地的每个用户,这样就可以在本地查看所有版本历史,可以离线在本地提交,只需在连网时push到相应的服务器或其他用户那里。由于每个用户那里保存的都是所有的版本数据,只要有一个用户的设备没有问题就可以恢复所有的数据,但这增加了本地存储空间的占用

不会因为服务器损坏或者网络问题,造成不能工作的情况

4.Git与SVN的主要区别

SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而工作的时候,用的都是自己的电脑,所以首先要从中央服务器得到最新的版本,然后工作,完成工作后,需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作,对网络带宽要求较高。

Git是分布式版本控制系统,没有中央服务器,每个人的电脑就是一个完整的版本库,工作的时候不需要联网了,因为版本都在自己电脑上。协同的方法是这样的:比如说自己在电脑上改了文件A,其他人也在电脑上改了文件A,这时,你们两之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。Git可以直接看到更新了哪些代码和文件

二、git

1.Git的历史

同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代

Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码

Linux社区中存在很多的大佬破解研究 BitKeeper

到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统。(2周左右!) 也就是后来的 Git!

Git是免费、开源的,最初Git是为辅助 Linux 内核开发的,来替代 BitKeeper

2.Git环境配置

打开git官网下载git对应操作系统的版本

可以使用淘宝镜像下载:http://npm.taobao.org/mirrors/git-for-windows/

安装:无脑下一步即可(记得选择安装位置

3.启动Git

安装成功后在开始菜单中会有Git项,菜单下有3个程序:任意文件夹下右键也可以看到对应的程序!

Git Bash:Unix与Linux风格的命令行,使用最多,推荐最多

Git CMD:Windows风格的命令行

Git GUI:图形界面的Git,不建议初学者使用,尽量先熟悉常用命令

4.Git配置

所有的配置文件,其实都保存在本地

查看不同级别的配置文件:

#查看配置 
git config -l
#查看系统config
git config --system --list  
#查看当前用户(global)配置
git config --global  --list

Git相关的配置文件:

1)、Git\etc\gitconfig :Git 安装目录下的 gitconfig --system 系统级

2)、C:\Users\Administrator\ .gitconfig 只适用于当前登录用户的配置 --global 全局

5.设置用户名与邮箱

安装Git后首先要做的事情是设置用户名称和e-mail地址

因为每次Git提交都会使用该信息

#名称
git config --global user.name "username" 
#邮箱
git config --global user.email 123456789@qq.com   

三、Git基本理论

1.三个区域

Git本地有三个工作区域:工作目录(Working Directory)、暂存区(Stage/Index)、资源库(Repository或Git Directory)。如果在加上远程的git仓库(Remote Directory)就可以分为四个工作区域。文件在这四个区域之间的转换关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bcW2Drg-1643530524744)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20220130112814879.png)]

  • Workspace:工作区,就是你平时存放项目代码的地方

  • Index / Stage:暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息

  • Repository:仓库区(或本地仓库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本

  • Remote:远程仓库,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换

  • Directory:使用Git管理的一个目录,也就是一个仓库,包含我们的工作空间和Git的管理空间。

  • WorkSpace:需要通过Git进行版本控制的目录和文件,这些目录和文件组成了工作空间。

  • .git:存放Git管理信息的目录,初始化仓库的时候自动创建。(隐藏文件夹)

  • Index/Stage:暂存区,或者叫待提交更新区,在提交进入repo之前,我们可以把所有的更新放在暂存区。

  • Local Repo:本地仓库,一个存放在本地的版本库;HEAD会只是当前的开发分支(branch)。

  • Stash:隐藏,是一个工作状态保存栈,用于保存/恢复WorkSpace中的临时状态。

2.工作流程

git的工作流程一般是这样的:

  1. 在工作目录中添加、修改文件(test.java
  2. 将需要进行版本管理的文件放入暂存区域(git add …
  3. 将暂存区域的文件提交到git仓库(git commit …

四、 Git项目搭建

1.创建工作目录与常用指令

工作目录(WorkSpace)一般就是Git帮助你管理的文件夹,可以是你项目的目录,也可以是一个空目录

在这里插入图片描述

2.本地仓库搭建

创建本地仓库的方法有两种:一种是创建全新的仓库,另一种是克隆远程仓库

  1. 创建全新的仓库,需要用GIT管理的项目的根目录执行

    # 在当前目录新建一个Git代码库
    git init
    
  2. 执行后可以看到,仅仅在项目目录多出了一个.git目录,关于版本等的所有信息都在这个目录里面

3.克隆远程仓库

将远程服务器上的仓库完全镜像一份至本地

# 克隆一个项目和它的整个代码历史(版本信息)
git clone url
# https://gitee.com/12345/class.git

五、Git文件操作

1.文件的四种状态

版本控制就是对文件的版本控制,要对文件进行修改、提交等操作,首先要知道文件当前在什么状态,不然可能会提交了现在还不想提交的文件,或者要提交的文件没提交上

  • Untracked: 未跟踪, 此文件在文件夹中, 但并没有加入到git库, 不参与版本控制. 通过git add 状态变为Staged.
  • Unmodify: 文件已经入库, 未修改, 即版本库中的文件快照内容与文件夹中完全一致. 这种类型的文件有两种去处, 如果它被修改, 而变为Modified. 如果使用git rm移出版本库, 则成为Untracked文件
  • Modified: 文件已修改, 仅仅是修改, 并没有进行其他的操作. 这个文件也有两个去处, 通过git add可进入暂存staged状态, 使用git checkout 则丢弃修改过, 返回到unmodify状态, 这个git checkout即从库中取出文件, 覆盖当前修改
  • Staged: 暂存状态. 执行git commit则将修改同步到库中, 这时库中的文件和本地文件又变为一致, 文件为Unmodify状态. 执行git reset HEAD filename取消暂存, 文件状态为Modified

2.查看文件状态

通过如下命令可以查看到文件的状态:

#查看指定文件状态
git status [filename]
#查看所有文件状态
git status
# git add .  添加所有文件到暂存区
# git commit -m "消息内容"    提交暂存区中的内容到本地仓库 -m 提交信息

3.忽略文件

有些时候我们不想把某些文件纳入版本控制中,比如数据库文件,临时文件,设计文件等

在主目录下建立".gitignore"文件,此文件有如下规则

  1. 忽略文件中的空行或以井号(#)开始的行将会被忽略
  2. 可以使用Linux通配符。例如:星号(*)代表任意多个字符,问号(?)代表一个字符,方括号([abc])代表可选字符范围,大括号({string1,string2,…})代表可选的字符串等
  3. 如果名称的最前面有一个感叹号(!),表示例外规则,将不被忽略
  4. 如果名称的最前面是一个路径分隔符(/),表示要忽略的文件在此目录下,而子目录中的文件不忽略
  5. 如果名称的最后面是一个路径分隔符(/),表示要忽略的是此目录下该名称的子目录,而非文件(默认文件或目录都忽略)
# 为注释
*.txt        #忽略所有 .txt结尾的文件,这样的话上传就不会被选中!
!lib.txt     #但lib.txt除外
/temp        #仅忽略项目根目录下的TODO文件,不包括其它目录
tempbuild/   #忽略build/目录下的所有文件
doc/*.txt    #会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

六、GIT分支

以下copy自git官网,认为讲的很通俗易懂

分支在GIT中相对较难,分支就是科幻电影里面的平行宇宙,如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,我们就需要处理一些问题了

git分支中常用指令:

# 列出所有本地分支
git branch
# 列出所有远程分支
git branch -r
# 新建一个分支,但依然停留在当前分支
git branch [branch-name]
# 新建一个分支,并切换到该分支
git checkout -b [branch]
# 合并指定分支到当前分支
git merge [branch]
# 删除分支
git branch -d [branch-name]
# 删除远程分支
git push origin --delete [branch-name]$ git branch -dr [remote/branch]

几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。 在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。

有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。 理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。

分支简介

为了真正理解 Git 处理分支的方式,我们需要回顾一下 Git 是如何保存数据的

Git 保存的不是文件的变化或者差异,而是一系列不同时刻的 快照

在进行提交操作时,Git 会保存一个提交对象(commit object)。 知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。 首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象,

为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。 暂存操作会为每一个文件计算校验和,然后会把当前版本的文件快照保存到 Git 仓库中 (Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'

当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。

现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个 对象 (记录着目录结构和 blob 对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)。

首次提交对象及其树结构。

Figure 9. 首次提交对象及其树结构

做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。

提交对象及其父对象。

Figure 10. 提交对象及其父对象

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 master 分支会在每次提交时自动向前移动。

NoteGit 的 master 分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它

分支及其提交历史。

Figure 11. 分支及其提交历史

Git 是怎么创建新分支的呢? 很简单,它只是为你创建了一个可以移动的新的指针。 比如,创建一个 testing 分支, 你需要使用 git branch 命令:

$ git branch testing

这会在当前所在的提交对象上创建一个指针。

两个指向相同提交历史的分支。

Figure 12. 两个指向相同提交历史的分支

那么,Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。 请注意它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念完全不同。 在 Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 因为 git branch 命令仅仅 创建 一个新分支,并不会自动切换到新分支中去。

HEAD 指向当前所在的分支。

Figure 13. HEAD 指向当前所在的分支

你可以简单地使用 git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 --decorate

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

正如你所见,当前 mastertesting 分支均指向校验和以 f30ab 开头的提交对象

要切换到一个已存在的分支,你需要使用 git checkout 命令。 我们现在切换到新创建的 testing 分支去:

$ git checkout testing

这样 HEAD 就指向 testing 分支了。

HEAD 指向当前所在的分支。

Figure 14. HEAD 指向当前所在的分支

那么,这样的实现方式会给我们带来什么好处呢? 现在不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'

HEAD 分支随着提交操作自动向前移动。

Figure 15. HEAD 分支随着提交操作自动向前移动

如图所示,你的 testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。 这就有意思了,现在我们切换回 master 分支看看:

$ git checkout master

检出时 HEAD 随之移动。

Figure 16. 检出时 HEAD 随之移动

这条命令做了两件事。 一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。 也就是说,你现在做修改的话,项目将始于一个较旧的版本。 本质上来讲,这就是忽略 testing 分支所做的修改,以便于向另一个方向进行开发。

Note分支切换会改变你工作目录中的文件在切换分支时,一定要注意你工作目录里的文件会被改变。 如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。 如果 Git 不能干净利落地完成这个任务,它将禁止切换分支。

我们不妨再稍微做些修改并提交:

$ vim test.rb
$ git commit -a -m 'made other changes'

现在,这个项目的提交历史已经产生了分叉(参见 项目分叉历史)。 因为刚才你创建了一个新分支,并切换过去进行了一些工作,随后又切换回 master 分支进行了另外一些工作。 上述两次改动针对的是不同分支:你可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。 而所有这些工作,你需要的命令只有 branchcheckoutcommit

项目分叉历史。

Figure 17. 项目分叉历史

你可以简单地使用 git log 命令查看分叉历史。 运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此的简单能不快吗?

这与过去大多数版本控制系统形成了鲜明的对比,它们在创建分支时,将所有的项目文件都复制一遍,并保存到一个特定的目录。 完成这样繁琐的过程通常需要好几秒钟,有时甚至需要好几分钟。所需时间的长短,完全取决于项目的规模。 而在 Git 中,任何规模的项目都能在瞬间创建新分支。 同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(译注:即共同祖先)也是同样的简单和高效。 这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支

分支的新建与合并

让我们来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到类似的工作流。 你将经历如下步骤:

  1. 开发某个网站。
  2. 为实现某个新的用户需求,创建一个分支。
  3. 在这个分支上开展工作。

正在此时,你突然接到一个电话说有个很严重的问题需要紧急修补。 你将按照如下方式来处理:

  1. 切换到你的线上分支(production branch)。
  2. 为这个紧急任务新建一个分支,并在其中修复它。
  3. 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支。
  4. 切换回你最初工作的分支上,继续工作。

新建分支

首先,我们假设你正在你的项目上工作,并且在 master 分支上已经有了一些提交。

一个简单的提交历史。

Figure 18. 一个简单提交历史

现在,你已经决定要解决你的公司使用的问题追踪系统中的 #53 问题。 想要新建一个分支并同时切换到那个分支上,你可以运行一个带有 -b 参数的 git checkout 命令:

$ git checkout -b iss53
Switched to a new branch "iss53"

它是下面两条命令的简写:

$ git branch iss53
$ git checkout iss53

创建一个新分支指针。

Figure 19. 创建一个新分支指针

你继续在 #53 问题上工作,并且做了一些提交。 在此过程中,iss53 分支在不断的向前推进,因为你已经检出到该分支 (也就是说,你的 HEAD 指针指向了 iss53 分支)

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

 分支随着工作的进展向前推进。

Figure 20. iss53 分支随着工作的进展向前推进

现在你接到那个电话,有个紧急问题等待你来解决。 有了 Git 的帮助,你不必把这个紧急问题和 iss53 的修改混在一起, 你也不需要花大力气来还原关于 53# 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。 你所要做的仅仅是切换回 master 分支。

但是,在你这么做之前,要留意你的工作目录和暂存区里那些还没有被提交的修改, 它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。 最好的方法是,在你切换分支之前,保持好一个干净的状态。 有一些方法可以绕过这个问题(即,暂存(stashing) 和 修补提交(commit amending)), 我们会在中看到关于这两个命令的介绍。 现在,我们假设你已经把你的修改全部提交了,这时你可以切换回 master 分支了:

$ git checkout master
Switched to branch 'master'

这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。 请牢记:当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。

接下来,你要修复这个紧急问题。 我们来建立一个 hotfix 分支,在该分支上工作直到问题解决:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
 1 file changed, 2 insertions(+)

基于  分支的紧急问题分支(hotfix branch)。

Figure 21. 基于 master 分支的紧急问题分支 hotfix branch

你可以运行你的测试,确保你的修改是正确的,然后将 hotfix 分支合并回你的 master 分支来部署到线上。 你可以使用 git merge 命令来达到上述目的:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

在合并的时候,你应该注意到了“快进(fast-forward)”这个词。 由于你想要合并的分支 hotfix 所指向的提交 C4 是你所在的提交 C2 的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

现在,最新的修改已经在 master 分支所指向的提交快照中,你可以着手发布该修复了。

 被快进到 。

Figure 22. master 被快进到 hotfix

关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。 然而,你应该先删除 hotfix 分支,因为你已经不再需要它了 —— master 分支已经指向了同一个位置。 你可以使用带 -d 选项的 git branch 命令来删除分支:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #53 问题的那个分支(iss53 分支)。

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

继续在  分支上的工作。

Figure 23. 继续在 iss53 分支上的工作

你在 hotfix 分支上所做的工作并没有包含到 iss53 分支中。 如果你需要拉取 hotfix 所做的修改,你可以使用 git merge master 命令将 master 分支合并入 iss53 分支,或者你也可以等到 iss53 分支完成其使命,再将其合并回 master 分支。

分支的合并

假设你已经修正了 #53 问题,并且打算将你的工作合并入 master 分支。 为此,你需要合并 iss53 分支到 master 分支,这和之前你合并 hotfix 分支所做的工作差不多。 你只需要检出到你想合并入的分支,然后运行 git merge 命令:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

这和你之前合并 hotfix 分支的时候看起来有一点不一样。 在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。

一次典型合并中所用到的三个快照。

Figure 24. 一次典型合并中所用到的三个快照

和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。

一个合并提交。

Figure 25. 一个合并提交

既然你的修改已经合并进来了,就不再需要 iss53 分支了。 现在你可以在任务追踪系统中关闭此项任务,并删除这个分支。

$ git branch -d iss53

遇到冲突时的分支合并

有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们。 如果你对 #53 问题的修改和有关 hotfix 分支的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:

<div id="footer">
please contact us at email.support@github.com
</div>

上述的冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。 在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。

如果你想使用图形化工具来解决冲突,你可以运行 git mergetool,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果你想使用除默认工具(在这里 Git 使用 opendiff 做为默认的合并工具,因为作者在 Mac 上运行该程序) 外的其他合并工具,你可以在 “下列工具中(one of the following tools)” 这句后面看到所有支持的合并工具。 然后输入你喜欢的工具名字就可以了。

Note如果你需要更加高级的工具来解决复杂的合并冲突,我们会在 高级合并 介绍更多关于分支合并的内容。

等你退出合并工具之后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决: 你可以再次运行 git status 来确认所有的合并冲突都已被解决:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

如果你对结果感到满意,并且确定之前有冲突的的文件都已经暂存了,这时你可以输入 git commit 来完成合并提交。 默认情况下提交信息看起来像下面这个样子:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

如果你觉得上述的信息不够充分,不能完全体现分支合并的过程,你可以修改上述信息, 添加一些细节给未来检视这个合并的读者一些帮助,告诉他们你是如何解决合并冲突的,以及理由是什么

分支管理

现在已经创建、合并、删除了一些分支,让我们看看一些常用的分支管理工具。

git branch 命令不只是可以创建与删除分支。 如果不加任何参数运行它,会得到当前所有分支的一个列表:

$ git branch
  iss53
* master
  testing

注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。 这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。 如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令:

$ git branch -v
  iss53   93b412c fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 add scott to the author list in the readmes

--merged--no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。 如果要查看哪些分支已经合并到当前分支,可以运行 git branch --merged

$ git branch --merged
  iss53
* master

因为之前已经合并了 iss53 分支,所以现在看到它在列表中。 在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d 删除掉;你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。

查看所有包含未合并工作的分支,可以运行 git branch --no-merged

$ git branch --no-merged
  testing

这里显示了其他分支。 因为它包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败:

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

如果真的想要删除分支并丢掉那些工作,如同帮助信息里所指出的,可以使用 -D 选项强制删除它。

Tip上面描述的选项 --merged--no-merged 会在没有给定提交或分支名作为参数时, 分别列出已合并或未合并到 当前 分支的分支。你总是可以提供一个附加的参数来查看其它分支的合并状态而不必检出它们。 例如,尚未合并到 master 分支的有哪些?$ git checkout testing $ git branch --no-merged master topicA featureB

变基

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。 在本节中我们将学习什么是“变基”,怎样使用“变基”,并将展示该操作的惊艳之处,以及指出在何种情况下你应避免使用它。

变基的基本操作

请回顾之前在分支的合并中的一个例子,你会看到开发任务分叉到两个不同分支,又各自提交了更新。

分叉的提交历史。

Figure 35. 分叉的提交历史

之前介绍过,整合分支最容易的方法是 merge 命令。 它会把两个分支的最新快照(C3C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。

通过合并操作来整合分叉了的历史。

Figure 36. 通过合并操作来整合分叉的历史

其实,还有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做 变基(rebase)。 你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

在这个例子中,你可以检出 experiment 分支,然后将它变基到 master 分支上:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。 (译注:写明了 commit id,以便理解,下同)

将  中的修改变基到  上。

Figure 37. 将 C4 中的修改变基到 C3

现在回到 master 分支,进行一次快进合并。

$ git checkout master
$ git merge experiment

 分支的快进合并。

Figure 38. master 分支的快进合并

此时,C4' 指向的快照就和 the merge exampleC5 指向的快照一模一样了。 这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

变基的风险

呃,奇妙的变基也并非完美无缺,要用它得遵守一条准则:

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

让我们来看一个在公开的仓库上执行变基操作所带来的问题。 假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。 你的提交历史如图所示:

克隆一个仓库,然后在它的基础上进行了一些开发。

Figure 44. 克隆一个仓库,然后在它的基础上进行了一些开发

然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:

抓取别人的提交,合并到自己的开发分支。

Figure 45. 抓取别人的提交,合并到自己的开发分支

接下来,这个人又决定把合并操作回滚,改用变基;继而又用 git push --force 命令覆盖了服务器上的提交历史。 之后你从服务器抓取更新,会发现多出来一些新的提交。

有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交。

Figure 46. 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交

结果就是你们两人的处境都十分尴尬。 如果你执行 git pull 命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:

你将相同的内容又合并了一次,生成了一个新的提交。

Figure 47. 你将相同的内容又合并了一次,生成了一个新的提交

此时如果你执行 git log 命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。 此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 很明显对方并不想在提交历史中看到 C4C6,因为之前就是他把这两个提交通过变基丢弃的。

用变基解决变基

如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。

实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。

如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。

举个例子,如果遇到前面提到的有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交那种情境,如果我们不是执行合并,而是执行 git rebase teamone/master, Git 将会:

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4’)
  • 把查到的这些提交应用在 teamone/master 上面

从而我们将得到与你将相同的内容又合并了一次,生成了一个新的提交中不同的结果,如图在一个被变基然后强制推送的分支上再次执行变基所示。

在一个被变基然后强制推送的分支上再次执行变基。

Figure 48. 在一个被变基然后强制推送的分支上再次执行变基

要想上述方案有效,还需要对方在变基时确保 C4'C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。

在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master

如果你习惯使用 git pull ,同时又希望默认使用选项 --rebase,你可以执行这条语句 git config --global pull.rebase true 来更改 pull.rebase 的默认配置。

如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交, 那你就有大麻烦了,你的同事也会因此鄙视你。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs. 合并

至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebasefilter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值