Git入门笔记

Git 入门笔记


本文主要参考 Git官方教程git易百教程,整理介绍Git版本管理工具的一些基本概念以及基本操作。

1. Git是什么?

Git是一种版本控制工具,版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制系统具有三种类型,分别是:本地版本控制系统、集中化的版本控制系统和分布式版本控制系统。三种类型系统的形态如下图所示:

本地版本控制系统
集中式版本控制系统
分布式版本控制系统

其中,Git就是分布式版本控制系统,对于这三种系统的优缺点这里不做介绍。

个人理解,Git的入门可以划分为三个层次:第一是单分支操作,第二是多分支操作,第三是加入远程分支之后的操作。下文将分别介绍这三种情况下的一些基本概念与操作。

2. Git单分支操作

在开始使用Git工具时,我们可能并未考虑到分支操作,以及远程仓库的使用。本节将主要介绍Git工具的基本概念以及单分支本地操作,包括Git如何保存数据,Git项目管理的三个工作区域,如何获取Git仓库,如何跟新每次提交到仓库,如何查看提交历史,如何撤销Git操作。

2.1 直接记录快照,而非差异比较

首先,Git在记录每个版本时,它记录的是什么?有些版本控制系统记录的是基本文件和每个文件随时间逐步积累的差异。如下图所示:

首先,Git在记录每个版本时,他记录的是什么?有些版本控制系统记录的是基本文件和每个文件随时间逐步积累的差异,如下图所示。

存储每个文件与初始版本的差异

而Git和其他版本控制系统存在一些区别,它主要记录的是文件快照。为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流。存储项目随时间改变的快照如下图所示:

存储项目随时间改变的快照

那么,文件快照是什么?下文会做介绍。

2.2 三个区域

为之后更好理解Git,这里介绍Git项目的三个工作区域:Git仓库、暂存区域以及工作目录。三个工作区域的关系如下图所示:

工作目录、暂存区域以及 Git 仓库

工作目录工作目录是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。

暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。

Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。

2.3 获取Git仓库

在本地创建一个Git管理的项目,首先需要获取Git仓库,有两种取得 Git 项目仓库的方法: 第一种是在现有项目或目录下导入所有文件到 Git 中; 第二种是从一个服务器克隆一个现有的 Git 仓库。

第一种方式,你只需要进入项目目录并输入:

git init

该命令将创建一个名为 .git 的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。

第二种方式,你将获得一份已经存在了的 Git 仓库的拷贝。克隆仓库的命令格式是 git clone [url] 。 比如,要克隆 Git 的可链接库 libgit2,可以用下面的命令:

git clone https://github.com/libgit2/libgit2

这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹,从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。

如果你进入到这个新建的 libgit2 文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。 如果你想在克隆远程仓库的时候,自定义本地仓库的名字,你可以使用如下命令:

git clone https://github.com/libgit2/libgit2 mylibgit

这将执行与上一个命令相同的操作,不过在本地创建的仓库名字变为 mylibgit

2.4 记录每次更新到仓库

现在我们手上有了一个真实项目的 Git 仓库,接下来,对项目文件做些修改,在完成了一个阶段的目标之后,提交本次更新到仓库。本节将介绍在此过程中文件状态的转换和需要的操作。

文件状态

文件状态都不外乎两种状态:已追踪未追踪已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能处于未修改,已修改或已放入暂存区。工作目录中除已跟踪文件以外的所有其它文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。同时,已追踪状态又可分为:未修改(Unmodified)/已提交(Committed)、已修改(Modified)和已暂存(Staged)

当我们在项目中新建一个文件时,该文件处于未追踪(Untracked)状态;我们输入添加命令,文件进入暂存状态;我们输入提交命令后,文件进入已提交状态或者叫未修改状态;自上次提交后我们对文件做了修改,文件进入已修改状态;我们在此输入添加命令,文件再次进入已暂存状态。如此反复,Git项目下的文件生命周期如下图所示:

文件的状态变化周期

查看当前文件状态

我们要查看哪些文件处于什么状态,可以用 git status 命令。 如果在克隆仓库后立即使用此命令,会看到类似这样的输出:

$ git status
On branch master
nothing to commit, working directory clean

追踪新文件

我们要追踪一个文件,可以使用命令 git add [file] 。 例如,我们要跟踪 README 文件,可以运行git add README命令。

暂存已修改文件

当我们修改了一个已被跟踪并处于暂存状态的文件,则该文件将变为已修改状态,我们需要重新运行命令 git add [file] 使其进入暂存状态。例如,如果我们修改了一个名为 CONTRIBUTING.md 的已被跟踪的文件,我们可以运行 git add CONTRIBUTING.md 将"CONTRIBUTING.md"重新放到暂存区。

提交更新到仓库

我们要提交更新到仓库,可以使用命令git commitGit只会提交处于暂存状态的文件给仓库。因此,在此之前,请一定要确认还有什么修改过的或新建的文件还没有 git add 过,否则提交的时候不会记录这些还没暂存起来的变化。 这些修改过的文件只保留在本地磁盘。 所以,每次准备提交前,先用 git status 看下,是不是都已暂存起来了, 然后再运行提交命令 git commit

尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。 Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤。

忽略文件

有时我们需要忽略一些文件,这些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。文件 .gitignore 的格式规范如下:

  • 所有空行或者以 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号(*)匹配零个或多个任意字符;[abc]匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号(*) 表示匹配任意中间目录,比如 a/**/z 可以匹配 a/z , a/b/za/b/c/z 等。我们看一个 .gitignore 文件的例子:

# no .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in the build/ directory
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory
doc/**/*.pdf

查看已暂存和未暂存的修改

有时,我们需要查看尚未暂存的文件更新了哪些部分,可以不加参数直接输入 git diff。此命令比较的是工作目录中当前文件和暂存区域快照之间的差异, 也就是修改之后还没有暂存起来的变化内容。

若要查看已暂存的将要添加到下次提交里的内容,可以用 git diff --cached 命令。(Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的,但更好记些。)

移除文件

有时,我们需要从 Git 中移除已被追踪某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。 可以用 git rm [file] 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删还没有添加到快照的数据,这样的数据不能被 Git 恢复。

另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。比如,当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。 为达到这一目的,使用 --cached 选项。例如,我们向让已被暂存的README文件从Git控制中删除,但需要其保留在磁盘中,我们可以输入命令git rm --cached README完成操作。

另外,git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。例如:git rm log/\*.log,注意到星号 * 之前的反斜杠 \, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除 log/ 目录下扩展名为 .log 的所有文件。

移动文件

有时,我们会移动或重命名文件,可以运行git mv <options> ... <args> ...完成操作。该命令存在两种形式的调用,第一种形式是重命名操作,调用方式为git mv <source> <destination>;第二种方式是移动文件操作,盗用方式为git mv <source> ... <destination directory>

例如,把 文件text.txt 移动到 mydir目录下,可以执行以下操作 -

$ git mv text.txt mydir

运行上面的 git mv 其实就相当于运行了3条命令:

$ mv test.txt mydir/
$ git rm test.txt
$ git add mydr

2.5 查看提交历史

有时,我们需要查看一下Git项目中文件的提交历史情况,本节将介绍命令git log,并列举一些该命令的常用选项。

查看提交的基本历史信息

运行 git log,应该会看到下面的输出:

$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

查看每次提交的内容差异

如果,我们想查看每次提交的内容差异,可以利用选项-p,并加上 -2 来仅显示最近两次提交:

$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
 spec = Gem::Specification.new do |s|
     s.platform  =   Gem::Platform::RUBY
     s.name      =   "simplegit"
-    s.version   =   "0.1.0"
+    s.version   =   "0.1.1"
     s.author    =   "Scott Chacon"
     s.email     =   "schacon@gee-mail.com"
     s.summary   =   "A simple gem for using Git in Ruby code."

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index a0a60ae..47c6340 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -18,8 +18,3 @@ class SimpleGit
     end

 end
-
-if $0 == __FILE__
-  git = SimpleGit.new
-  puts git.show
-end
\ No newline at end of file

该选项除了显示基本信息之外,还附带了每次 commit 的变化。 当进行代码审查,或者快速浏览某个搭档提交的 commit 所带来的变化的时候,这个参数就非常有用了。

查看格式化的提交历史

另外一个常用的选项是 --pretty。 这个选项可以指定使用不同于默认格式的方式展示提交历史。 这个选项有一些内建的子选项供你使用。 比如用 oneline 将每个提交放在一行显示,查看的提交数很大时非常有用。 另外还有 shortfullfuller 可以用,展示的信息或多或少有些不同,请自己动手实践一下看看效果如何。

$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 changed the version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit

但最有意思的是 format,可以定制要显示的记录格式。 这样的输出对后期提取分析格外有用 — 因为你知道输出的格式不会随着 Git 的更新而发生改变:

$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 6 years ago : changed the version number
085bb3b - Scott Chacon, 6 years ago : removed unnecessary test
a11bef0 - Scott Chacon, 6 years ago : first commit

git log --pretty=format 常用的选项 列出了常用的格式占位符写法及其代表的意义。

当 oneline 或 format 与另一个 log 选项 --graph 结合使用时尤其有用。 这个选项添加了一些ASCII字符串来形象地展示你的分支、合并历史:

$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
*  5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
*  11d191e Merge branch 'defunkt' into local

这种输出类型会在学完分支与合并以后变得更加有趣。

2.6 撤销操作

有时,我们存在操作错误,需要撤销操作,本节将简单介绍如何取消提交操作,如何取消暂存文件,如何撤销已修改的文件,以及如何取消远程仓库的push,

提交(commit)操作的撤销

有时,我们commit之后,想撤销commit操作,分为以下几种情况:提交后发现注释写错了或有几个文件没有提交,提交后发现许多文件不应该提交。

第一种情况,我们可以先运行git add [file-name]将文件加入暂存区域,利用git commit -amend,进入注释编辑界面,输入注释,完成提交,这样的操作会覆盖之前最后一次提交操作。

第二种情况,我们可以运行git reset --soft HEAD~1撤销最后一次提交操作,但不撤销git add操作;也可以运行git reset --mixed HEAD~1撤销最后一次提交操作,并且撤销git add操作;注意命令git reset --hard HEAD~1完成这个操作后,就恢复到了上一次的commit状态。

取消暂存

有时,你已经修改了两个文件并且想要将它们作为两次独立的修改提交,但是却意外地输入了 git add * 暂存了它们两个。 如何只取消暂存两个中的一个呢?比如我么需要取消暂存 CONTRIBUTING.md 文件,我们可以运行以下命令:

$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M	CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

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:   CONTRIBUTING.md

这样 CONTRIBUTING.md 文件已经是修改未暂存的状态了。

撤销已修改的文件

如果你并不想保留对 CONTRIBUTING.md 文件的修改怎么办? 你该如何方便地撤消修改 ?我们可以执行:

$ git status
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:   CONTRIBUTING.md
$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

可以看到那些修改已经被撤消了。

取消远程仓库的push

有时,我们把错误的本地提交推送到了远程仓库中,当我们在本地撤销提交后,如何将落后于远程追踪分支的本地分支推送至远程仓库呢?一种可行的做法是运行命令git push -f [remote-name],将当前本地分支强制推送到远程分支。

3. Git多分支操作

上节介绍了Git在单分支场景下文件的状态和操作,本节将介绍Git在多分支场景下的操作,包括:Git是如何保存数据的,如何创建分支,如何切换分支,如何合并与删除分支,以及一些分支信息查看的分支管理命令。

3.1 Git是如何保存数据的?

为了真正理解 Git 处理分支的方式,我们需要回顾一下 Git 是如何保存数据的。本文开头介绍了Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。那它是如何保存这些数据的呢?

在进行提交操作时,Git 会保存一个提交对象(commit object)。该对象包含包含一个指向暂存内容快照的指针作者的姓名和邮箱提交时输入的信息以及指向它的父对象的指针。其中,首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。暂存操作会为每一个文件计算校验和(使用 SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交。当进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象。这样,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。首次提交对象及其树结构如图所示:

首次提交对象及其树结构

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

提交对象及其父对象

Git 的分支,其实本质上仅仅是指向提交对象的可变指针

3.2 分支创建

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

$ git branch testing

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

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

那么,Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。在 Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 因为 git branch 命令仅仅 创建 一个新分支,并不会自动切换到新分支中去。

HEAD 指向当前所在的分支

3.3 分支切换

切换分支到testing

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

$ git checkout testing

这样 HEAD 就指向 testing 分支了,如图所示:

HEAD 指向当前所在的分支

再次提交testing分支

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

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

如图所示,testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。

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

切换回master分支

现在我们切换回 master 分支看看:

$ git checkout master
检出时 HEAD 随之移动

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

master分支修改后再次提交

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

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

现在,这个项目的提交历史已经产生了分叉(参见 项目分叉历史)。

项目分叉历史

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

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

3.4 分支合并删除

本节将一个例子讲解分支的合并。

新建iss53分支,并进行一次提交

现在,假如你需要要解决项目中的 #53 问题。 你可以在master分支的基础上新建一个iss53分支并同时切换到这个分支上,运行一个带有 -b 参数的 git checkout 命令:

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

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

$ git branch iss53
$ git checkout iss53

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

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

新建hotfix分支,并进行一次提交

现在你接到那个电话,有个紧急问题等待你来解决。你可以在master分支的基础上新建一个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(+)

master分支与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)"这个词。 由于当前 master 分支所指向的提交是你当前提交(有关 hotfix 的提交)的直接上游,所以 Git 只是简单的将指针向前移动。

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

`master` 被快进到 `hotfix`

hotfix分支删除

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

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

iss53分支完成后,合并到master分支中(无冲突合并)

现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #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(+)

假设你已经修正了 #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),做一个简单的三方合,如图所示:

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

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

一个合并提交

需要指出的是,Git 会自行决定选取哪一个提交作为最优的共同祖先,并以此作为合并的基础。Git 的这个优势使其在合并操作上比其他系统要简单很多。

iss53分支合并到master分支中(有冲突合并)

有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,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 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
#

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

3.5 分支管理

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

查看所有分支

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

$ git branch
  iss53
* master
  testing

注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。

如果需要查看每一个分支的最后一次提交,可以运行 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

查看已经合并到当前分支的分支

如果你要查看哪些分支已经合并到当前分支,可以运行 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 选项强制删除它。

4. Git远程分支操作。

本节将主要介绍本地分支与远程分支之间的交互,而远程分支是远程引用的一部分,远程引用是对远程仓库的引用,包括分支、标签等,所以我们先介绍远程仓库的一些概念与操作,再介绍远程分支的操作。

4.1 远程仓库

远程仓库即在远程服务器上的Git仓库,仓库中可能包含多个分支,我们可以通过添加远程仓库的方式来将一个远程仓库本地化。本节将分别介绍如何添加远程仓库,如何查看远程仓库的信息,如何从远程仓库中抓取(fetch)与拉取(pull)数据,如何从本地推送数据到远程仓库的远程分支上,以及如何重命名和移除远程仓库。在整个介绍过程中需要注意远程仓库的远程分支与本地分支的关系

添加远程仓库

如果你想添加一个远程仓库,运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库:

$ git remote
origin
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)
pb	https://github.com/paulboone/ticgit (fetch)
pb	https://github.com/paulboone/ticgit (push)

现在你可以在命令行中使用字符串 pb 来代替整个 URL。 例如,如果你想拉取 Paul 的仓库中有但你没有的信息,可以运行 git fetch pb

$ git fetch pb
remote: Counting objects: 43, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 43 (delta 10), reused 31 (delta 5)
Unpacking objects: 100% (43/43), done.
From https://github.com/paulboone/ticgit
 * [new branch]      master     -> pb/master
 * [new branch]      ticgit     -> pb/ticgit

现在 Paul 的 master 分支可以在本地通过 pb/master 访问到。

查看远程仓库

如果想查看你已经配置的远程仓库服务器,可以运行 git remote 命令。 它会列出你指定的每一个远程服务器的简写。 如果你已经克隆了自己的仓库,那么至少应该能看到 origin(这是 Git 给你克隆的仓库服务器的默认名字):

$ git clone https://github.com/schacon/ticgit
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 268.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.
$ cd ticgit
$ git remote
origin

你也可以指定选项 -v,会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。

$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)

如果你的远程仓库不止一个,该命令会将它们全部列出。

如果想要查看某一个远程仓库的更多信息,可以使用 git remote show [remote-name] 命令。 如果想以一个特定的缩写名运行这个命令,例如 origin,会得到像下面类似的信息:

$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

它同样会列出远程仓库的 URL 与跟踪分支的信息。 这些信息非常有用,它告诉你正处于 master 分支,并且如果运行 git pull,就会抓取所有的远程引用,然后将远程 master 分支合并到本地 master 分支。 它也会列出拉取到的所有远程引用。

从远程仓库中抓取与拉取

如果你想从远程仓库中抓取数据,可以执行git fetch [remote-name],这个命令会访问远程仓库,将所有远程仓库中的远程分支更新到本地。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。

如果你使用 clone 命令克隆了一个仓库,命令会自动将其添加为远程仓库并默认以 “origin” 为简写。必须注意 git fetch 命令会将数据拉取到你的本地仓库,它并不会自动合并或修改你当前的工作,当准备好时你必须手动将其合并入你的工作。

如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

$ git pull origin

上面命令表示,本地的当前分支自动与对应的origin主机”追踪分支”(remote-tracking branch)进行合并。

推送到远程仓库

如果你想将本地项目推送到远程仓库,你可以运行命令git push [remote-name] [branch-name]。 例如,当你想要将 master 分支推送到 origin 服务器时(再次说明,克隆时通常会自动帮你设置好那两个名字),那么运行这个命令就可以将你所做的备份到远程仓库:

$ git push origin master

**只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。 **当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先将他们的工作拉取下来并将其合并进你的工作后才能推送。

远程仓库的重命名与移除

如果想要重命名引用的名字可以运行 git remote rename 去修改一个远程仓库的简写名。 例如,想要将 pb 重命名为 paul,可以用 git remote rename 这样做:

$ git remote rename pb paul
$ git remote
origin
paul

值得注意的是这同样也会修改你的远程分支名字。 那些过去引用 pb/master 的现在会引用 paul/master

如果因为一些原因想要移除一个远程仓库 ,你已经从服务器上搬走了或不再想使用某一个特定的镜像了,又或者某一个贡献者不再贡献了 - 可以使用 git remote rm

$ git remote rm paul
$ git remote
origin

4.2 远程分支

远程分支指在远程仓库中的分支,而我们在将远程仓库中的远程分支通过抓取数据命令本地化后,Git都会为远程分支创建远程追踪分支。远程追踪分支是不能移动的本地引用,以 (remote)/(branch) 形式命名,当我们做任何网络通信操作时,它们会自动移动。本节将介绍如何添加远程分支,如何追踪远程分支,如何查看追踪分支信息,如何把本地分支推送到远程分支,以及如何删除远程分支。

添加远程分支

如果我们要从远程仓库的远程分支上抓取数据一般会通过两种方式:git fetchgit pull

如果我们要从远程仓库抓取某个分支的数据,可以运行git fetch [remote-name] [branch-name]命令。 例如,运行git fetch origin master命令,你可以将远程仓库origin上的master分支抓取下来,Git会在本地新建一个origin/master远程追踪分支,但不会自动与本地分支合并。

如果我们想在origin/master基础上进行工作,就需要一个本地分支来追踪origin/master分支。如何追踪?下文将介绍。

有时,先利用git fetch命令再利用本地分支进行追踪会嫌麻烦,我们可以利用git pull命令进行简化。例如,运行git pull origin master命令,Git会在本地新建一个origin/master远程追踪分支,并且会自动创建本地分支master来追踪origin/master分支。

git fetch和git pull的区别

git fetch:相当于是从远程获取最新版本到本地,不会自动合并。

$ git fetch origin master
$ git log -p master..origin/master
$ git merge origin/master

以上命令的含义:

  • 首先从远程的originmaster主分支下载最新的版本到origin/master分支上
  • 然后比较本地的master分支和origin/master分支的差别
  • 最后进行合并

上述过程其实可以用以下更清晰的方式来进行:

$ git fetch origin master:tmp
$ git diff tmp 
$ git merge tmp

git pull:相当于是从远程获取最新版本并merge到本地

$ git pull origin master

上述命令其实相当于git fetchgit merge两个命令。

在实际使用中,git fetch更安全一些,因为在merge前,我们可以查看更新情况,然后再决定是否合并。

追踪分支

我们可以自己创建一个本地分支,然后将本地分支与远程追踪分支进行合共来追踪远程分支。我们可以运行 git checkout -b [branch] [remotename]/[branch]来完成以上操作, 这是一个十分常用的操作,所以 Git 提供了 --track 快捷方式:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

如果想要将本地分支与远程分支设置为不同名字,你可以轻松地使用上一个命令增加一个不同名字的本地分支:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

现在,本地分支 sf 会自动从 origin/serverfix 拉取。

查看追踪分支信息

如果想要查看设置的所有跟踪分支,可以使用 git branch-vv 选项。 这会将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
  master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  testing   5ea463a trying something new

这里可以看到 iss53 分支正在跟踪 origin/iss53 并且 “ahead” 是 2,意味着本地有两个提交还没有推送到服务器上。 也能看到 master 分支正在跟踪 origin/master 分支并且是最新的。 接下来可以看到 serverfix 分支正在跟踪 teamone 服务器上的 server-fix-good 分支并且领先 3 落后 1,意味着服务器上有一次提交还没有合并入同时本地有三次提交还没有推送。 最后看到 testing 分支并没有跟踪任何远程分支。

推送本地分支到远程分支

如果当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 ,你必须显式地推送想要分享的分支。 这样,你就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。

例如,如果你希望和别人一起在名为 serverfix 的分支上工作,可以推送第一个分支,运行 git push [remot-name] [branch-name]:

$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
 * [new branch]      serverfix -> serverfix

这里有些工作被简化了。 Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix,那意味着,“推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。” 如果并不想让远程仓库上的分支叫做 serverfix,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。

当然,如果远程仓库中已存在severfix分支,则推送就会出现冲突,需要先将severfix抓取下来,与本地分支合并之后,再进行推送。

删除远程分支

如果你已经通过远程分支做完所有的工作了 ,例如,你和你的协作者已经完成了一个特性并且将其合并到了远程仓库的 master 分支。可以运行带有 --delete 选项的 git push 命令来删除一个远程分支。 例如,想要从服务器上删除 serverfix 分支,运行下面的命令:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]         serverfix

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

总结

本文主要是摘抄及整理了两个教程的文字,通过三个层次(单分支、多分支及远程分支)来介绍Git的概念及使用。首先,在不讨论分支概念的情况下,介绍单分支场景下的文件操作;然后,在本地环境下,介绍多分支的分支操作;最后,加入远程仓库,介绍远程分支与本地分支的交互。

参考资料

git官方教程

git易百教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值