Git——使用Git进行程序开发

主要介绍个人开发提交记录的主要流程,包括以下内容:

  • 索引- 提交的暂存区。
  • 查看工作的状态和内部变更。
  • 如何读取用于描述变更的已扩展统一diff格式。
  • 支持查询和交互的提交,修改提交。
  • 创建、显示和选择(切换)分支。
  • 切换分支失败的原因,以及应对策略。使用git reset对分支复位。
  • 与HEAD脱离的匿名分支(例如签出的标签等)。

1、新建提交

在使用Git开始一个新项目之前,用户需要先添加自己的姓名和电子邮件信息。上述信息主要用于标记开发人员的工作记录,无论作者还是提交者都是如此。该设置可以是适用于所有版本库的全局配置(使用git config --global命令,或者直接编辑~/.gitconfig文件),也可以是仅局限于用户本地版本库(使用git cofig命令或者编辑.git/config文件)。每个版本库的配置将会覆盖单个开发人员的配置。某些用户也许会希望在公司的版本库下工作时使用工作电子邮件,在某些公共版本库中使用非工作电子邮件。

相应的配置文件信息与下列内容类似:

[user]
  name = Joe R. Hacker
  email = joe@company.com

1.1、新建提交的DAG视图

为一个项目做贡献通常意味着为上述项目添加新的修订,然后将它们作为提交节点添加到修订视图上。

接下来假定我们现在处于master分支,如下图所示,然后我们想为项目添加一个新的修订版本。git commit命令将会创建一个新的提交对象——一个新的修订节点。该提交会创建一个特定的签出修订版本(本示例是c7cd3)。该修订会被追踪引用的起点指针HEAD引用,即当前图表中HEAD指向的节点c7cd3。
Git——使用Git进行程序开发_第1张图片
当前的分支是master,HEAD指针指向的修订版本是c7cd3,该修订也是目前签出的修订。

Git系统会移动master指针到新的节点,创建节点之后的情形如下图所示。在图中我们可以看到,新的提交节点用加粗的红框标记了出来,master分支旧的节点用半透明的形式表示。需要注意的是,HEAD指针并没有改变,它一直是指向master分支的:新的提交a3b79是用加粗的红框标记的,master分支上指针由提交c7cd3指向了提交a3b79代表分支发生了变更,如下图中的虚线所示:
Git——使用Git进行程序开发_第2张图片

1.2、索引——提交的暂存区

Git版本库对应的工作区中的每个文件对于Git系统来说包括两种,即已知的(跟踪文件)和未知的。其中未知文件对于Git系统来说又分为未跟踪和已忽略两类。

Git系统跟踪的文件一般有两种状态:已提交(未变化)和已修改。已提交状态意味着工作目录下的文件内容和最近一次提交的修订内容一致,很安全地存放在版本库中。

如果文件和最新提交的修订版本存在差异,则被认为是已修改的文件。

不过在Git系统内部,还存在另外一种状态。接下来让我看看当使用git add命令添加一个文件后会发生什么。版本控制系统都需要在某处存放上述状态信息。Git系统采用被称为索引(index)的机制实现此功能,它是存储将要提交信息的暂存区。git add 命令会暂存文件(当前版本)的当前内容,然后将之添加到索引中。

如果用户只是喜欢将某个文件标记为已添加,那么可以使用git add -N 命令,这样一来上述文件在暂存区的内容就是空白的。

索引是项目的第三个存储拷贝,之前的拷贝一个是包含用户自己项目文件拷贝的工作目录,另外一个是用户本地版本库(存放项目历史记录,专门为用户同步其他开发人员的变更)。

下图中的箭头表示了Git命令拷贝内容的主要步骤,例如git add命令会将工作区的文件内容拷贝到暂存区。
Git——使用Git进行程序开发_第3张图片
创建一个新的提交需要执行如下步骤:

  • (1)在工作目录下面使用编辑器对文件进行编辑。
  • (2)使用git add命令对上述文件进行暂存,为它们添加快照(文件当前的状态)。
  • (3)使用git commit命令创建一个新的修订,这会把存放在暂存区的文件信息作为修订版本永久地存放到本地版本库中。

在项目启动之初,工作区的被跟踪文件、暂存区的文件和最近一次提交的修订版本在内容上都是一致的。

不过用户通常会采用提交记录命令的快捷方式,即git commit –a(是git commit --all的快捷方式),该命令会把被跟踪并且发生变更的文件添加到暂存区中(在当前的Git系统中和git add -u命令的效果一样),然后创建一个新的提交(如上图所示)。需要注意的是,新增的文件仍然需要使用git add命令告知Git系统对其进行跟踪,然后才能将之添加到新的提交对象中。

1.3、查看已提交的变更

在提交变更和创建新的修订(新的提交)之前,用户也许希望看看自己的工作成果。

除非用户在命令行中声明了提交注释信息,例如git commit -m"简短的描述信息",否则Git会在提交信息模版中显示将要提交的记录,并且该模版可以根据用户需要进行编辑配置。

用户还可以通过不修改文件或者使用一个空的提交信息终止提交操作(注释信息中以#号开头的内容会被忽略)。

1、工作目录状态

用户使用工具检查文件状态的目的在于查看哪个文件发生了变更、哪些地方新增了文件等,这就是git status命令的主要用途。

上述命令默认输出结果的信息非常详尽。如果项目没有发生变更,例如直接克隆版本库之后,用户会看到类似以下内容的信息:

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

如果上述分支(本示例中当前用户处于master分支)是一个本地分支,打算添加一些变更之后发布到公共版本库中,并且事先配置好了对应的上游分支origin/master,那么用户还会看到对应的远程跟踪分支的相关信息:

Your branch is up-to-date with 'origin/master'.

接下来的示例中,我们将会忽略上述信息。

例如用户打算给项目新增两个文件,一个是包含版权信息的文件,名字叫COPYING;另外一个是空白文件,名字叫NEWS。为了跟踪刚才引入的COPYING文件,需要执行git add COPYING命令。用户一不留神,使用git rm README命令将README文件从工作区删除了;然后编辑了Makefile文件,用git mv命令把文件rand.c的名字改成random.c(并没有修改其中的内容)。
一般来说,完整信息输出格式的好处是易读并且描述信息详尽:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD ..." to unstage)

        new file:   COPYING
        renamed:    src/rand.c -> src/random.c

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working
directory)

         modified:   Makefile
         deleted:    README

Untracked files:
  (use "git add ..." to include in what will be committed)

         NEWS

如你所见,Git不仅记录和描述了哪些文件发生了变更,而且还在提交中解释了文件的变更状态,以及哪些将要提交的变更被删除了。上述结果大致可以分为3个部分:

  • 拟提交的变更:这是已经放入暂存区,准备使用git commit命令提交的变更(没有-a选项)。它是暂存区的文件快照,和最近一次提交(HEAD)是有显著差异的。
  • 未暂存的变更:这些列表是工作区中和暂存区的快照存在差异的文件列表。这些变更将不会使用git commit命令提交,不过可以通过git commit -a命令将上述内容作为被跟踪文件的变更提交。
  • 未跟踪的文件:这类文件对于Git系统来说是未知的,而且也是可以被忽略的(这类文件可以使用添加操作命令git add.在顶层目录中进行批量添加。用户还可以使用–untracked-files=no(简写为-uno)选项忽略该操作。

如果用户不希望充分利用暂存区提供的灵活性,那么可以简单地使用git add命令,只添加新文件,然后使用git add -a命令获取所有被跟踪文件的变更,继而创建提交。这种情况下,用户创建的提交包含将要提交的变更以及不在暂存区的变更。

还有一种简洁的输出格式使用–short选项声明。–porcelain选项适合脚本执行,因为在保证一致稳定性的同时,使用–short选项还支持用户对输出结果的编辑。对于某些相同的变更,输出结果和下列内容类似:

$ git status –short
A  COPYING
 M Makefile
 D README
R  src/rand.c -> src/random.c
?? NEWS

采用格式之后,每个路径的状态会用双字母状态码表示。第一个字母表示索引的状态(暂存区和最近一次提交的差异),第二个字母表示工作区的状态(工作区和暂存区的差异)。

并不是所有的状态码组合都是存在的,状态码A、R和C只可能出现在第一个列,即表示索引的状态。一个特别的情况是??符号,它主要用来表示未知(未跟踪)文件的,!!符号表示已忽略文件(当使用git status --short --ignored命令时)。
Git——使用Git进行程序开发_第4张图片

2、最新修订的差异比较

如果用户不仅希望知道哪些文件发生了变更(使用git status命令),而且还想知道具体发生了哪些变更,那么可以使用git diff命令。

Git系统中包含3个区域:工作目录、暂存区和版本库(通常是最近一次修订版本)。因此,我们不止有一组差异,而是有3组,如下图所示。
Git——使用Git进行程序开发_第5张图片

用户可以要求Git回答如下问题:
用户编辑了哪些内容但是还未暂存的?换句话说就是,暂存区和工作区之间的差异是什么?哪些内容是已暂存准备提交的?也就是说,最近一次提交(HEAD)和暂存区的差异是什么?

用户希望了解自己编辑了哪些文件但是还未将其暂存,那么可以使用不带任何参数的git diff命令。该命令会比较用户的工作目录和暂存区直接的差异。上述变更是可以被添加的,但是如果使用git commit命令提交变更却不会被显示:这些变更并未放入暂存区,因此执行git status命令之后,查询结果不会包含上述变更。

用户如果希望查看已经暂存的变更并且打算提交这些变更,那么可以使用git diff --staged(或者git diff –cached)命令,该命令会比较最近的一次提交和暂存区之间的差异。上述变更可以使用git commit命令(不带-a选项)进行添加,同时也是执行git status命令后,输出结果中将要提交的变更记录。还可以使用git diff --staged<提交记录> 命令比较暂存区和任意提交历史记录之间的差异;HEAD(最近的一次提交)是默认的比较对象。

可以使用git diff HEAD命令比较用户当前的工作目录和最近一次提交的修订之间的差异(可以使用git diff<提交记录>与任意历史修订记录比较差异)。上述变更可以使用git commit -a命令快速地添加到版本库中。如果用户没有使用git commit –a命令,而且也不需要充分利用暂存区,那么通常使用git diff命令查看将要提交的变更记录就可以满足需要了。

如果只使用命令git add,唯一需要处理的问题是新增文件,除非用户使用git add --intent-to-add命令新增文件到版本库(也可以使用git add -N),否则使用git diff命令之后,新增的文件将不会显示在查询结果中。

4、Git的统一diff格式

一般来说,Git系统在大部分情况下都会采用统一的diff输出格式显示变更记录。

理解这种输出格式对于用户来说是非常重要的,不仅在查看将要提交的变更记录时会用到,而且在审核和检查变更记录时也会用到(例如在代码审核审查中,或者执行git bisect命令之后,查找可疑提交记录)。

用户可以使用–stat或者–dirstat选项只统计变更记录数量,或者使用–name-only查看名字发生变更的文件数目,或者使用–name-status选项查看文件名类型发生变更的记录,或者使用–raw选项查看项目的目录结构发生的变化,或者使用–summary选项查看扩展首部信息的摘要。用户还可以使用–word-diff选项进行字符之间的差异比较,这比行间差异比较精确度更高,这样一来,即使文件内部的标题和段落标题相似,但是只改变段落的格式也可以检测出其中的差异。Diff生成工具还可以通过设置特定的gitattributes信息,实现特定文件或者某类文件的差异比较。用户甚至可以指定diff助手,即描述变更的命令,或者还可以为二进制文件指定文本转换过滤器。

如果用户喜欢使用图形化工具(通常支持逐行比较)查看上述变更记录,那么可以使用git的difftool代替使用git diff命令。这可能需要预先做适当的配置。

接下来介绍一个使用diff命令查看git项目历史记录的高级示例。首先使用diff命令查看git.git版本库中的提交1088261f。当然用户也可以使用浏览器查看该提交,例如通过GitHub。下列内容是该提交的第三条补丁记录:

diff --git a/builtin-http-fetch.c b/http-fetch.c
similarity index 95%
rename from builtin-http-fetch.c
rename to http-fetch.c
index f3e63d7..e8f44ba 100644
--- a/builtin-http-fetch.c
+++ b/http-fetch.c
@@ -1,8 +1,9 @@
 #include "cache.h"
 #include "walker.h"

-int cmd_http_fetch(int argc, const char **argv, const char *prefix)
+int main(int argc, const char **argv)
 {
+       const char *prefix;
        struct walker *walker;
        int commits_on_stdin = 0;
        int commits;
@@ -18,6 +19,8 @@ int cmd_http_fetch(int argc, const char **argv,
        int get_verbosely = 0;
        int get_recover = 0;

+       prefix = setup_git_directory();
+
        git_config(git_default_config, NULL);

        while (arg < argc && argv[arg][0] == '-') {

1.4、可查询的提交

有时,在看过一些未提交的变更记录之后,你也许会发现工作目录下面有两个(或者更多)无甚关联的变更分别属于不同的业务逻辑,它是引起工作拷贝混淆的诱因。用户需要将这些不相关的变更分别放到对应的提交中,即隔离变更集。这种做法也和软件开发的最佳实践不谋而合。一种做法是先创建提交,然后再修复它(将之一分为二)。

不过有时候,其中某些变更急需马上上线(例如一个在线网站的bug修复),同时其余变更还有待进一步完善。因此这时用户需要把上述变更分割成两个独立的提交。

1、文件提交查询

最简单的情形就是这些不相关的变更分布于若干文件中。例如说,如果bug只存在于文件view/entry.tmpl中,并且该文件不存在其他变更,那么可以专门为该文件创建一个修复bug的提交,相关命令如下:

$ git commit view/entry.tmpl

该命令会忽略已经暂存到索引中的变更(即暂存区的内容),取而代之的是提交当前给定文件或目录(工作目录中的变更)中的内容。

2、变更的交互式查询

不过有时变更无法以这种方式分类,文件中的变更都集中到了一起。用户可以尝试git commit命令和–interactive选项一起使用,对上述变更进行梳理:

$ git commit --interactive
           staged     unstaged path
   1:   unchanged        +3/-2 Makefile
   2:   unchanged       +64/-1 src/rand.c

*** Commands***
  1: status       2: update      3: revert      4: add untracked
  5: patch        6: diff        7: quit        8: help
What now>

这里Git系统为用户显示了工作区的状态和变更摘要信息,以及暂存区/索引(暂存的)-状态子命令的输出结果。描述变更的方式是添加和删除文件的数量(和git diff --numstat输出结果类似):

What now> h
status        - show paths with changes
update        - add working tree state to the staged set of changes
revert        - revert staged set of changes back to the HEAD version
patch         - pick hunks and update selectively
diff          - view diff between HEAD and index
add untracked - add contents of untracked files to the staged set of
changes
*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help

为了对这些变更进行梳理分类,用户需要用到patch子命令(例如使用5或者s)。之后Git系统会弹出一个Update>>对话框让用户选择如何处理这些文件,然后用户需要根据上面的状态信息,输入希望更新的文件对应的数字标记,然后按下回车键即可。用户还可以输入*选择所有文件。确认输入完毕目标文件的信息之后,可以按下回车键输入一个空行,告诉系统选择结束了(用户还可以使用–patch选项直接忽略批量文件的选择)。Git系统将会为用户逐个显示特定文件的变更区域,然后让用户选择分类,下面的选项是专门操作单个变更区域的:

y - stage this hunk(暂存该区域)
n - do not stage this hunk(不暂存该区域)
q - quit; do not stage this hunk or any of the remaining ones(退出,不暂存任何区域)

s - split the current hunk into smaller hunks(分隔该区域为更小的区域)
e - manually edit the current hunk(手工编辑当前区域)
? - print help (打印帮助)

区域的输出结果和对话提示和下列内容类似:

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

         int max = atoi(argv[1]);
+        srand(time(NULL));
         int result = random_int(max);
         printf("%d\n", result);

Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y

一般来说,上述选项可以应付大部分在提交内部进行区域选择的操作。不过在一些极端情况下,用户可以将上述区域分割成更小的区域,甚至手工编辑其中的差异。

3、提交创建入门

使用git commit --interactive进行交互式提交记录查询有一个缺点,那就是它无法对将要提交的变更进行测试。当然用户也可以在创建一个提交之后随时对其进行检查(编译或者运行测试),发现bug之后,及时修复它。不过这只是一种替代性的解决方案。用户可以使用git add --interactive命令将准备提交的变更放在暂存区中,或者采用类似的解决方案(使用Git的图形化提交工具,例如 git gui)。交互式的提交只是交互式添加变更然后提交的一种快捷方式。之后用户还可以使用git diff --cached命令查看这些提交,以及使用git add 、git checkout 和git reset 命令对这些提交做相应的修改。

理论上来说,不管这些变更正确与否,用户都可以对它们进行测试,至少可以确认它们是否可以通过程序编译。为此,首先要使用git stash save --keep-index命令保存当前的状态,然后将工作目录的状态存放到暂存区中(索引)。执行该命令之后,用户就可以执行测试程序了(至少确认一下程序是否能够通过编译)。如果测试通过,那么用户就可以执行git commit命令创建一个新的修订版本,如果测试不通过,用户就可以使用git stash pop --index命令,从暂存区读取工作目录的状态,将之恢复到之前的状态;也有可能会需要用到git reset --hard命令对工作区进行重置。后者可能是非常有必要的,因为Git在保存用户的工作记录时过于保守,而且并不知道用户只是将某些记录隐藏暂存了。首先,索引中未提交的变更不能阻止Git用其他变更将其覆盖。其次,工作区的变更和暂存区的变更大体相同,因此当然会发生冲突。

1.5、 修改提交

Git系统非常明显的一个优点是,用户可以随心所欲地恢复其中的任何东西。无论用户如何认真地编辑准备提交的变更记录,或多或少都会出现一些问题,例如忘记添加某个变更或者提交的信息有错别字等。这就是git commit命令的–amend选项大显身手的时候了,它能够帮助用户方便地修改最近的提交记录(如下图所示)。注意,用户还可以使用该选项修改合并提交(例如修复一个合并错误)。
Git——使用Git进行程序开发_第6张图片
修订的DAG图,C1到C2的变更代表对最近一次提交修改之前的状态,C5代表当前签出的提交。这里我们使用数字代替SHA-1码表示相关的提交记录。

如果用户希望对提交的历史记录做进一步的修改(假定该提交未发布,至少没有人在该提交的基础上提交新的修订),那么可能会用到交互式变基操作或者是某些特殊的工具,例如StGit(一个基于Git的提交历史批量管理工具)。

如果用户只是想修正提交备注信息中的错误,那么只需要再添加一条备注信息即可,不需要将它暂存(注意,我们使用git commit命令时并没有使用-a / --all选项):

$ git commit --amend

如果用户希望为最近一次的提交添加一些变更记录(见下图),那么可以先使用git add命令将这些变更暂存,然后像前面的示例一样再次提交该修订记录,或者使用git commit -a --amend命令提交这些变更:
Git——使用Git进行程序开发_第7张图片
经过编辑的最近一次修订记录(上上图)的DAG图,这里新的C5修订是基于添加了更多变更记录之后的C5,它替换了旧版本的提交对象。

这里有一个非常重要的忠告:你永远不要修改一个已经发布了的修订!这是因为修改操作会创建一个新的提交对象替换原有的对象,如上图所示。如果开发工作只有一个人的话,那么这么做不会出什么问题。不过如果是多人协作开发,当你把原有的修订记录发布到远程版本库中后,团队其他人员可能已经基于该修订版本做了一些开发工作了。使用经过修订的版本替换原有的版本之后,可能会导致下游出现问题。

如果你尝试将一个已经发布过的提交修订,然后将之推送(发布)到某个分支上,Git将会阻止用户重写已发布的历史提交记录,然后询问用户是否真的希望替换旧的版本进行强制推送(除非用户配置了默认强制推送选项)。修订的历史版本在被编辑之前在分支的引用日志和HEAD引用日志中仍然是有效的,例如刚经过修订后不久,它仍然可以通过@{1}的形式访问。一般来说,除非手动清除,Git将会保存旧版本修订一个月。

2、使用分支

分支是一系列开发工作的符号名称。在Git中,每个分支都可以看作修订DAG中指向某些提交的具名指针,因此它也被称为分支首部。

分支在Git中的表现形式:目前Git在硬盘中采用了两种截然不同的方式来表示分支:松散格式和压缩格式。例如master分支(该分支是Git采用的默认分支名,用户在创建新的版本库时默认的分支名就是它)。采用松散格式时,它是.git/refs/heads/master中的一行文本,其中指代分支的内容是十六进制的SHA-1码。在压缩格式中,它是.git/packed-refs中的一行文本,使用最顶部修订的SHA-1码和分支全名一起表示该分支。

一系列的开发工作就是指从分支首部为起点所有可达的修订集合。而且它并不一定是线性的修订,还可以是分支分流或者联合。

2.1、新建分支

用户可以使用git branch命令创建一个新分支,例如在当前分支上新建一个名为testing的分支(参见下图右上部分),可以执行如下命令:

$ git branch testing

Git——使用Git进行程序开发_第8张图片
新建一个testing分支,然后切换到该分支,或者新建一个分支并且马上切换到该分支上(使用命令)。

执行上述命令之后会发生什么呢? 该命令会为用户创建一个可供访问的新指针(一个新引用)。如果用户希望在创建新分支的同时将它指向某些修订提交,那么可以使用相应的参数。

不过需要注意的是,git branch命令并不会改变HEAD指针的位置(指向当前分支的符号引用),并且不会改变工作目录中的内容。

如果用户希望创建一个新分支并切换到该分支上(可以马上在新建分支上工作),那么可以使用如下快捷方式:

$ git checkout -b testing

如果我们在当前的版本库中创建分支,checkout -b命令的差异仅在于它还会将HEAD指针移动到新建的分支上,如上图所示。

2.2、孤儿分支

有时用户也许希望在版本库中新建一个和主分支没什么关联的孤儿分支。例如为每个预览版程序单独存放对应的用户手册,使得用户不需要安装转换工具或者解析器(例如AsciiDoc解析器)就可以方便地阅读用户手册(例如HTML格式的帮助页面)。又或者用户希望在版本库中的每个项目存放一些Web页面,这和GitHub网站项目中Web页面的用途类似。有些用户希望开源自己的项目代码,但是首先要把代码清理一番(例如修改版权和授权许可文件)。

一种办法是为包含上述内容的孤儿分支建立一个独立的版本库,然后从远程跟踪分支上获取它们,然后用户可以基于该孤儿分支创建一个本地分支。

用户还可以执行如下命令达到上述目的:

$ git checkout --orphan gh-pages
Switched to a new branch 'gh-pages'

上述命令会重新生成某些状态,在执行完git init:操作之后将HEAD指针指向gh-pages分支,如果该分支不存在的话,会先创建该分支然后执行上述操作。而且该分支的创建是基于第一个提交对象的。

如果用户希望使用类似GitHub页面这种简洁的目录结构启动项目,那么还需要将起点分支的内容移除(HEAD指向默认分支,即当前分支和当前工作目录的状态)。例如使用如下命令:

$ git rm -rf .

对于开源目代码中需要排除的私人信息,用户需要在工作目录中认真编辑,进行相应的替换。

2.3、分支的查询和切换

为了切换到一个已有的本地分支上,用户需要执行git checkout命令。例如在创建testing分支之后,可以使用如下命令切换到该分支上:

$ git checkout testing

1、分支切换释疑

在切换分支时,Git也会将签出的内容放入工作目录中。那么如果有未提交的变更,又会发生什么呢(先不考虑Git将会切换到哪个分支上)?

切换分支时,让当前分支保持整洁的状态是一个好习惯。如果有必要的话,在切换前清空暂存区或者创建一个提交。在极个别情况下,将未提交的变更一并执行分支切换操作是很有用的。

如果当前分支对应的文件变更和将要切换的目标分支没有关联,那么当前分支未提交的变更也会一并移动到新分支中。这对于用户开始着手研究新的东西时非常有用。随着时间的推移,用户正好可以将这一系列的工作放到独立的特性分支中。如果未提交的变更和给定分支有冲突,那么Git将会拒绝切换到目标分支,从而防止用户的工作成果遭到破坏:

$ git checkout other-branch
error: Your local changes to the following files would be overwritten by
checkout:
        file-with-local-changes
Please, commit your changes or stash them before you can switch branches.

在这种情况下,有以下几个解决方案可供选择:

  • 用户可以隐藏自己的变更,当切换到原来的分支时再恢复它们(通常这是比较推荐的做法)。或者可以简单地为这些正在进行中的工作创建一个临时提交,然后在切换到原来分支时,可以对该提交进行修改或回退。
  • 用户还可以尝试通过合并操作将自己现有的变更记录移动到新分支中,可以使用git branch --merge命令(该操作会执行三路合并,其中包括当前分支、工作区中未保存的变更和目标分支);
  • 还可以在签出之前隐藏用户的变更记录,然后在切换分支之后将隐藏的变更记录恢复到暂存区中。
  • 用户还可以使用git checkout --force命令丢弃原有的变更。

2、匿名分支

如果用户尝试签出一个非本地分支会如何呢?例如任意的修订(像HEAD^),或者一个标签(像v0.9),或者一个远程跟踪分支(像origin/master)。在这种情况下,Git会假定用户需要在当前工作目录状态的基础上创建一个提交。

旧版本的Git程序会拒绝执行没有目标分支的切换操作。不过现在的Git程序会通过与HEAD指针分离创建一个匿名分支,即直接指向一个提交对象,而不是使用一个引用符号指向一个分支,如下图所示。为了在当前位置显式创建一个匿名分支,用户可以在执行checkout命令时使用–detach选项。被分离的HEAD引用会在分支列表中被当作Git的一个历史修订版本存在,又或者是作为较新的版本(修订从HEAD脱离或者HEAD在某处脱离)。
Git——使用Git进行程序开发_第9张图片
无分支的签出结果,Git中签出HEAD操作之后的状态,HEAD被分离了,或者称之为匿名分支。

如果用户因为失误而将HEAD和分支脱离了,那么还可以将之恢复到脱离之前的状态(这里的"-"代表上一分支):

$ git checkout -

因为当用户在创建一个匿名分支时,Git会给用户一些警示信息,因此用户还可以使用git checkout -b 命令为该分支命名。

3、Git的智能签出

还有一种非常特殊的情况属于签出时的内容不是分支的。如果你使用分支的简称(本示例是next)签出了一个远程跟踪分支(例如origin/next),但是如果当前状态下有一个同名的本地分支的话,Git会认为用户希望在远程跟踪分支的基础上添加一些新的内容,可以完成用户的预期目标。“做我所想”(DWIM)机制将会创建一个新的本地分支,以便跟踪远程跟踪分支。

相关命令如下:

$ git checkout next

它等效于如下命令:

$ git checkout -b next --track origin/next

只有在不会产生歧义的情况下Git才会执行上述操作:本地分支必须不存在(否则该命令会简单地切换到目标分支),而且必须只有一个远程跟踪分支与之对应。这种情况可是使用git show-ref next(使用分支简称)命令检查远程跟踪分支是否只有唯一符合条件的记录(最近的一条记录可以使用refs/remotes/前缀和引用名进行识别)。

2.4、分支列表

如果用户在执行git branch命令时不带任何选项参数的话,那么它会显示所有的分支,并使用*标记当前分支。

该命令是专门为最终用户设计的,它的输出结果在未来的Git程序中可能会发生变化。在shell脚本中,可以通过编写代码实现如下功能:

  • 可以使用git symbolic- ref HEAD命令获取当前分支的名称。
  • 可以使用git rev-parse HEAD命令找到当前提交的SHA-1码。
  • 可以使用git show-ref或者git for- each-ref命令显示所有分支。

上述命令都是管道化的,因此可以将它们应用到脚本中。

用户可以使用-v (–verbose)或者-vv选项查询更多信息;还可以对分支进行条件查询,例如可以在给定的shell程序中使用git branch --list<模式>命令进行模式匹配查询(如果有必要的话,可以使用引号将模式包裹起来,防止它被shell解析)。

用户可以使用git remote show命令查询远程版本库的信息,当然其中也包括远程分支的查询。

2.5、分支的回退和复位

如果用户想丢弃最近一次提交的修订记录,或者回退(重置)当前的分支到上一分支,他该怎么做?为此,用户需要使用reset命令。它可以改变当前分支指针的指向。不过需要注意的是,和checkout命令不同,reset命令在默认情况下不会改变工作目录的内容,用户需要使用相应的命令参数才行,例如git reset --keep命令(尝试保留未提交的变更)或者git reset --hard(强行删除未提交的变更)。

下图展示了在指定分支和无分支参数的情况下checkout命令和reset命令之间的差异。总之,reset命令会改变当前分支指针的指向(移动引用),而chekout命令既可以进行分支切换,又可以在没有无分支的情况下在给定的修订中和HEAD分离。
Git——使用Git进行程序开发_第10张图片

在指定分支(例如maint)和无分支(例如HEAD^)参数的情况下,checkout命令和reset 命令之间的差异对照。

2.6、分支的删除

在Git中,一个分支其实就是一个指针,它在修订的DAG图上就是一个指向节点的外部引用,因此删除一个分支实际上就是删除一个指针。

实际上当删除一个分支时,同时也永久性地(至少对于当前版本的Git是如此)移除了和该分支对应的引用日志,即它的本地历史日志。

用户可以使用git branch -d命令删除分支。当然,在删除该分支前,有一个必须要考虑的问题,那就是其中是否还有其他引用指向该分支相关的项目历史记录。否则该修订将会变得无法访问,Git系统会在HEAD引用日志过期之后将之删除(一般来说,默认配置的过期时间是30天后)。

Git允许用户删除已经完全合并过的分支,因为它的所有提交都还是可以通过HEAD访问的,如下图所示(或者可以通过它的上游分支访问,如果它存在的话)。
Git——使用Git进行程序开发_第11张图片
当我们目前所在的主分支(这里是master)包含一个名为base-doc的分支时,可以先将它的变更与主分支合并,然后使用命令git branch -d base-doc 对该分支进行定向删除。

如果要删除一个没有经过合并的分支,那么风险就在于,该分支在DAG图上是不可达的。用户需要一个更强大的命令,即git branch -D(Git在拒绝用户删除一个分支时会建议用户使用该命令),参见下图:
用户甚至可以检查某个分支是否已经和其他分支合并过,相关的检查命令是git branch --contains 。用户不能删除当前的分支。
Git——使用Git进行程序开发_第12张图片
使用git branch -D osx-port命令删除一个未经过合并的分支osx-port。

2.7、分支的重命名

有时在选择分支名称时常需要对分支进行重命名。这是经常发生的,例如在开发过程中特征分支的涵盖范围发生了改变等。

用户可以使用git branch -m命令给分支重命名(如果分支名存在而且用户希望重写该分支,还可以使用-M选项);该操作会对分支重命名并且移动相应的引用日志(将重命名操作添加到引用日志中),然后修改与之相关的所有配置(例如描述信息、上游信息等)。

你可能感兴趣的:(DevOps,#,git)