Git属于分散型版本管理系统,是为版本管理而设计的软件。

Git入门

Git简介

Git属于分散型版本管理系统,是为版本管理而设计的软件。

Linux的创始人Linus Torvalds在2005年开发了Git的原型程序。当时,由于在Linux内核开发中使用的既有版本管理系统的开发方许可证发生了变更,为了更换新的版本管理系统,Torvalds开发了Git。

什么是版本管理

版本管理就是管理更新的历史记录。它为我们提供了一些在软件开发过程中必不可少的功能,例如记录一款软件添加或更改源代码的过程,回滚到特定阶段,恢复误删除的文件等。

在Git出现以前,人们普遍采用Subversion等集中型版本管理系统,而现在Git已成为主流。

集中型与分散型

不同点:

以Subversion为代表的集中型,会将仓库集中存放在服务器之中,所以只存在一个仓库。这就是为什么这种版本管理系统会被称作集中型。

集中型将所有数据集中存放在服务器当中,有便于管理的有点。但是一旦开发者所处的环境不能连接服务器,就无法获取最新的源代码,开发也就几乎无法进行。服务器宕机时也是同样的道理,而且万一服务器故障导致数据消失,恐怕开发者就再也见不到最新的源代码了。

以Git为代表的分散型,例如GitHub会将仓库Fork给每一个用户。Fork就是将GitHub的某个特定仓库复制到自己的账户下。Fork出的仓库与原仓库是两个不同的仓库,开发者可以随意编辑。分散型拥有多个仓库,相对而言稍显复杂。不过,由于本地的开发环境中就有仓库,所以开发者不必连接远程仓库就可以进行开发。

Git安装和初始设置

Mac与Linux

最近的Mac中都预装了Git。而各版本的Linux中也都以软件包(Package)的形式提供给用户了,所以各位可以直接使用。关于这两个环境特有的详细安装方法,暂时不做叙述。

Windows

在Windows环境中,最简单快捷的方法是使用msysGit(http://msysgit.github.io) 。请按照Downloads的向导下载安装包。安装包下载完毕后,只要双击运行,按照向导一步步安装即可。

初始设置

设置姓名和邮箱地址

首先开设置使用Git时的姓名和邮箱地址。名字请用英文输入。

1
2
git config --global user.name 'Firstname Lastname" 
git config --global user.email "your_email@example.com"

这个命令,会在“~/.gitconfig”中以如下形式输出设置文件。

1
2
3
[user] 
name = Firstname Lastname
email = your_email@example.com

想更改这些信息时,可以直接编辑这个设置文件。这里设置的姓名和邮箱地址会用在Git的提交日志中。由于在GitHub上公开仓库时,这里的姓名和邮箱地址也会随着提交日志一同被公开,所以请不要使用不便公开的隐私信息。

在GitHub上公开代码后,前来参考的程序员可能来自世界任何地方,所以请不要使用汉字,尽量用英文进行描述。当然,如果您不想使用真名,完全可以使用网络上的昵称。

提高命令输出的可读性

顺便一提,将color.ui设置为auto可以让命令的输出拥有更高的可读性。

1
git config --global color.ui auto

“~/.gitconfig”中会增加下面一行。

1
2
[color] 
ui = auto

这样一来,各种命令的输出就会变得更容易分辨。

Git学习之基本操作(一)

初始化仓库

git init

要使用Git进行版本管理,必须先初始化仓库。Git是使用git init命令进行初始化的。请实际建立一个目录并初始化仓库。

1
2
3
4
$ mkdir git-learn
$ cd git-learn/
$ git init
Initialized empty Git repository in /Users/weirubo/www/git-learn/.git/

如果初始化成功,执行了git init命令的目录下就会生成.git目录。这个.git目录里存储着管理当前目录内容所需的仓库数据。

Git中,我们将这个目录的内容称为“附属于该仓库的工作树”。文件的编辑等操作在工作树种进行,然后记录到仓库中,以此管理文件的历史快照。如果想将文件恢复到原先的状态,可以从仓库中调取之前的快照,在工作树中打开。开发者可以通过这种方式获取以往的文件。

查看仓库的状态

git status

git status命令用于显示Git仓库的状态。

工作树和仓库在被操作的过程中,状态会不断发生变化。在Git操作过程中时常用git status命令查看当前状态,可谓基本中的基本。

下面,就让我们来实际查看一下当前状态。

1
2
3
4
5
6
$ git status
On branch master

Initial commit

nothing to commit (create/copy files and use "git add" to track)

结果显示了我们当前正处于master分支下。接着还显示了没有可提交的内容。所谓提交(Commit),是指“记录工作树中所有文件的当前状态”。

尚没有可提交的内容,就是说当前我们建立的这个仓库中还没有记录任何文件的任何状态。这里,我们建议README.md文件作为管理对象,为第一次提交做前期准备。

1
2
3
4
5
6
7
8
9
10
11
12
$ touch README.md
$ git status
On branch master

Initial commit

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

README.md

nothing added to commit but untracked files present (use "git add" to track)

可以看到在Untracked files中显示了README.md文件。类似地,只要对Git的工作树或仓库进行操作,git status命令的显示结果就会发生变化。

向暂存区中添加文件

git add

如果只是用Git仓库的工作树创建了文件,那么该文件并不会被记入Git仓库的版本管理对象当中。因此我们用git status命令查看README.md文件时,它会显示在Untracked files里。

要想让文件成为Git仓库的管理对象,就需要用git add命令将其加入暂存区(Stage或者Index)中。暂存区是提交之前的一个临时区域。

1
2
3
4
5
6
7
8
9
10
$ git add README.md
$ git status
On branch master

Initial commit

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: README.md

README.md文件加入暂存区后,git status命令的显示结果发生了变化。可以看到,README.md文件显示在Changes to be commited中了。

保存仓库的历史记录

git commit

git commit命令可以将当前暂存区中的文件实际保存到仓库的历史记录中。通过这些记录,我们就可以在工作树中复原文件。

记录一行提交信息

我们来实际运行一下git commit命令。

1
2
3
4
$ git commit -m "My first commit"
[master (root-commit) 858ba4b] My first commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README.md

-m参数后的”My first commit“称作提交信息,是对这个提交的概述。

记录详细提交信息

刚才我们只简洁地记述了一行提交信息,如果想要记述得更加详细,请不加-m,直接执行git commit命令。执行后编辑器就会启动,并显示如下结果。

1
2
3
4
5
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# modified: README.md

在编辑器中记述提交信息的格式如下。

  • 第一行:用一行文字简述提交的更改内容
  • 第二行:空行
  • 第三行以后:记述更改的原因和详细内容

只要按照上面的格式输入,今后便可以通过确认日志的命令或工具看到这些记录。

在以#(井号)标为注释的Changes to be committed(要提交的更改)栏中,可以查看本次提交中包含的文件。将提交信息按格式记述完毕后,请保存并关闭编辑器,以#(井号)标为注释的行不必删除。随后,刚才记述的提交信息就会被提交。

中止提交

如果在编辑器启动后想中止提交,请将提交信息留空并直接关闭编辑器,随后提交就会被中止。

查看提交后的状态

执行完git commit命令后再来查看当前状态。

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

当前工作树处于刚刚完成提交的最新状态,所以结果显示没有更改。

查看提交日志

git log

git log命令可以查看以往仓库中提交的日志。包括可以查看什么人在什么时候进行了提交或合并,以及操作前后有怎样的差别。

我们先来看看刚才的git commit命令是否被记录了。

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit c75e47ad83d604e234eac38bb3aec57226f79974
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:01:24 2016 +0800

测试使用编辑器记述详细提交信息

commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 22:49:19 2016 +0800

My first commit

如上所示,显示了刚刚的提交操作。commit栏旁边显示的“c75e47......”是指向这个提交的哈希值。Git的其它命令中,在指向提交时会用到这个哈希值。

Author栏中显示我们给Git设置的用户名和邮箱地址。Date栏中显示提交执行的日期和时间。再往下就是该提交的提交信息。

只显示提交信息的第一行

如果只想让程序显示第一行简述信息,可以在git log命令后加上--pretty=short。这样一来开发人员就能够更轻松地把握多个提交。

1
2
3
4
5
6
7
8
$ git log --pretty=short
commit c75e47ad83d604e234eac38bb3aec57226f79974
Author: frankphper <1678723151@qq.com>

测试使用编辑器记述详细提交信息

commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>

只显示指定目录、文件的日志

只要在git log命令后加上目录名,便会只显示该目录下的日志。如果加的是文件名,就会只显示与该文件相关的日志。

1
$ git log README.md

显示文件的改动

如果想查看提交所带来的改动,可以加上-p参数,文件的前后差别就会显示在提交信息之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git log -p
commit c75e47ad83d604e234eac38bb3aec57226f79974
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:01:24 2016 +0800

测试使用编辑器记述详细提交信息

diff --git a/README.md b/README.md
index e69de29..5dbdb31 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+Git Learn

commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 22:49:19 2016 +0800

My first commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29

比如,执行下面的命令,就可以只查看README.md文件的提交日志以及提交后的差别。

1
$ git log -p README.md

如上所述,git log命令可以利用多种参数帮助开发者把握以往提交的内容。

查看更改前后的差别

git diff

git diff命令可以查看工作树、暂存区、最新提交之间的差别。

我们在刚刚提交的README.md中写点东西。

#Git学习

这里用Markdown语法写下了一行题目。

查看工作树和暂存区的差别

执行git diff命令,查看当前工作树与暂存区的差别。

1
2
3
4
5
6
7
8
$ git diff
diff --git a/README.md b/README.md
index 5dbdb31..247921a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
Git Learn
+#Git学习

由于我们尚未用git add命令向暂存区添加任何东西,所以程序只会显示工作树与最新提交状态之间的差别。
这里解释一下显示的内容。“+”号标出的是新添加的行,被删除的行则用“-”号标出。我们可以看到,这次只添加了一行。

git add命令将README.md文件加入暂存区。

$ git add README.md
查看工作树和最新提交的差别

如果现在执行git diff命令,由于工作树和暂存区的状态并无差别,结果什么都不会显示。要查看与最新提交的差别,请执行以下命令。

1
2
3
4
5
6
7
8
$ git diff HEAD
diff --git a/README.md b/README.md
index 5dbdb31..247921a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
Git Learn
+#Git学习

不妨养成这样一个好习惯:在执行git commit命令之前先执行git diff HEAD命令,查看本次提交与上次提交之间有什么差别,等确认完毕后再进行提交。这里HEAD是指向当前分支中最新一次提交的指针。

由于我们刚刚确认过这两个提交之间的差别,所以直接运行git commit命令。

1
2
3
$ git commit -m "Add #Git学习"
[master c87f9ae] Add #Git学习
1 file changed, 1 insertion(+)

保险起见,我们查看一下提交日志,确认提交是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:33:16 2016 +0800

Add #Git学习

commit c75e47ad83d604e234eac38bb3aec57226f79974
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:01:24 2016 +0800

测试使用编辑器记述详细提交信息

commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 22:49:19 2016 +0800

My first commit

成功查看了第三个提交。

Git学习之分支的操作(二)

显示分支一览表

git branch

git branch命令可以将分支名列表显示,同时可以确认当前所在的分支。

1
$ git branch * master

可以看到master分支左侧标有“*”(星号),表示这是我们当前所在的分支。也就是说,我们正在master分支下进行开发。结果中没有显示其他分支名,表示本地仓库中只存在master一个分支。

创建、切换分支

git checkout -b

如果想以当前的master分支为基础创建新的分支,我们需要用到git checkout -b命令。

切换到feature-A分支并进行提交。

执行下面的命令,创建名为feature-A的分支。

1
2
$ git checkout -b feature-A
Switched to a new branch 'feature-A'

实际上,连续执行下面两条命令也能收到同样的效果。

1
2
$ git branch feature-A
$ git checkout feature-A

创建feature-A分支,并将当前分支切换为feature-A分支。这时再来查看分支列表,会显示我们处于feature-A分支下。

1
2
3
$ git branch
* feature-A
master

feature-A分支左侧标有“*”,表示当前分支为feature-A。在这个状态下像正常开发那样修改代码、执行git add命令并进行提交的话,代码就会提交至feature-A分支。像这样不断对一个分支(例如feature-A)进行提交的操作,我们称为“培育分支”。

下面来实际操作一下,在README.md文件中添加一行。

feature-A

这里我们添加了feature-A这样一行字母,然后进行提交。

1
2
3
4
$ git add README.md
$ git commit -m "Add feature-A"
[feature-A 320739b] Add feature-A
1 file changed, 1 insertion(+)

于是,这一行就添加到feature-A分支中了。

切换到master分支

现在我们再来看一看master分支有没有受影响。首先切换至master分支。

1
2
$ git checkout master
Switched to branch 'master'

然后查看README.md文件,会发现README.md文件仍然保持原先的状态,并没有被添加文字。feature-A分支的更改不会影响到master分支,这正是在开发中创建分支的优点。只要创建多个分支,就可以在不互影响的情况下同时进行多个功能的开发。

切换回上一个分支

现在,我们再切换回feature-A分支。

1
2
$ git checkout -
Switched to branch 'feature-A'

像上面这样用“-”(连字符)代替分支名,就可以切换至上一个分支。当然,将“-”替换成feature-A同样可以切换至feature-A分支。

特性分支

特性分支顾名思义,是集中实现单一特性(主题),除此之外不进行任何作业的分支。在日常开发中,往往会创建数个特性分支,同时在此之外再保留一个随时可以发布软件的稳定分支。稳定分支的角色通常由master分支担当。

之前我们创建了feature-A分支,这一分支主要实现feature-A,除feature-A的实现之外不进行任何作业。即便在开发过程中发现了BUG,也需要再创建新的分支,在新分支中进行修正。

基于特定主题的作业在特性分支中进行,主题完成后再与master分支合并。只要保持这样一个开发流程,就能保证master分支可以随时供人查看。这样一来,其他开发者也可以放心大胆地从master分支创建新的特性分支。

主干分支

主干分支是刚才我们讲解的特性分支的原点,同时也是合并的终点。通常人们会用master分支作为主干分支。主干分支中并没有开发到一半的代码,可以随时供他人查看。

有时我们需要让这个主干分支总是配置在正式环境中,有时又需要用标签Tag等创建版本信息,同时管理多个版本发布,拥有多个版本发布时,主干分支也有多个。

合并分支

git merge

接下来,我们假设feature-A已经实现完毕,想要将它合并到主干分支master中。首先切换到master分支。

1
2
$ git checkout master
Switched to branch 'master'

然后合并feature-A分支。为了在历史记录中明确记录下本次分支合并,我们需要创建合并提交。因此,在合并时加上--no-ff参数。

1
$ git merge --no-ff feature-A

随后编辑器会启动,用于录入合并提交的信息。

1
2
3
4
5
6
7
Merge branch 'feature-A'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

默认信息中已经包含了是从feature-A分支合并过来的相关内容,所以可不必做任何更改。将编辑器中显示的内容保存,关闭编辑器,然后就会看到下面的结果。

1
2
3
Merge made by the 'recursive' strategy.
README.md | 1 +
1 file changed, 1 insertion(+)

这样一来,feature-A分支的内容就合并到master分支中了。

以图表形式查看分支

git log --graph

git log --graph命令进行查看的话,能很清楚地看到特性分支(feature-A)提交的内容已被合并。除此之外,特性分支的创建以及合并也都清楚明了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ git log --graph
* commit a5f6cbf52a36dd104e131e32e67531656640c0c3
|\ Merge: c87f9ae 320739b
| | Author: frankphper <1678723151@qq.com>
| | Date: Mon Apr 11 08:16:48 2016 +0800
| |
| | Merge branch 'feature-A'
| |
| * commit 320739b163a92ab90e24abfb814ab7fc09e9c9a0
|/ Author: frankphper <1678723151@qq.com>
| Date: Mon Apr 11 07:59:05 2016 +0800
|
| Add feature-A
|
* commit c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
| Author: frankphper <1678723151@qq.com>
| Date: Sun Apr 10 23:33:16 2016 +0800
|
| Add #Git学习
|
* commit c75e47ad83d604e234eac38bb3aec57226f79974
| Author: frankphper <1678723151@qq.com>
| Date: Sun Apr 10 23:01:24 2016 +0800
|
| 测试使用编辑器记述详细提交信息
|
* commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 22:49:19 2016 +0800

My first commit

git log --graph命令可以用图表形式输出提交日志,非常直观。

Git学习之回溯历史版本(三)

回溯历史版本

git reset

通过前面学习的操作,我们已经学会如何在实现功能后进行提交,累积提交日志作为历史记录,借此不断培育一款软件。

Git的另一特征便是可以灵活操作历史版本。借助分散仓库的优势,可以在不影响其他仓库的前提下对历史版本进行操作。

在这里,为了让各位熟悉对历史版本的操作,我们先回溯历史版本,创建一个名为fix-B的特性分支。

回溯到创建feature-A分支前

让我们先回溯到上一节feature-A分支创建之前,创建一个名为fix-B的特性分支。

要让仓库的HEAD、暂存区、当前工作树回溯到指定状态,需要用到git reset --hard命令。只要提供目标时间点的哈希值,就可以完全恢复至该时间点的状态。让我们执行下面的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ git log
commit a5f6cbf52a36dd104e131e32e67531656640c0c3
Merge: c87f9ae 320739b
Author: frankphper <1678723151@qq.com>
Date: Mon Apr 11 08:16:48 2016 +0800

Merge branch 'feature-A'

commit 320739b163a92ab90e24abfb814ab7fc09e9c9a0
Author: frankphper <1678723151@qq.com>
Date: Mon Apr 11 07:59:05 2016 +0800

Add feature-A

commit c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:33:16 2016 +0800

Add #Git学习

commit c75e47ad83d604e234eac38bb3aec57226f79974
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 23:01:24 2016 +0800

测试使用编辑器记述详细提交信息

commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc
Author: frankphper <1678723151@qq.com>
Date: Sun Apr 10 22:49:19 2016 +0800

My first commit
1
2
$ git reset --hard c87f9ae6da0ccffedda289c55e55e7bc7900dfe0 
HEAD is now at c87f9ae Add #Git学习

我们已经成功回溯到特性分支(feature-A)创建之前的状态。由于所有文件都回溯到了指定哈希值对应的时间点上,README.md文件的内容也恢复到了当时的状态。

创建fix-B分支

现在我们来创建特性分支(fix-B

1
2
$ git checkout -b fix-B
Switched to a new branch 'fix-B'

我们在README.md文件中添加一行文字。

1
2
3
Git Learn
#Git学习
fix-B

然后直接提交README.md文件。

1
2
3
4
$ git add README.md
$ git commit -m "fix-B"
[fix-B 6814da3] fix-B
1 file changed, 1 insertion(+)

接下来,我们的目标是主干分支合并feature-A分支的修改后,又合并了fix-B的修改。

推进至feature-A分之合并后的状态

首先恢复到feature-A分支合并后的状态。不妨称这一操作为“推进历史”。

git log命令只能查看以当前状态为终点的历史日志。所以这里要使用git reflog命令,查看当前仓库的操作日志。在日志中找出回溯历史之前的哈希值,通过git reset --hard命令恢复到回溯历史前的状态。

首先执行git reflog命令,查看当前仓库执行过的操作的日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git reflog
6814da3 HEAD@{0}: commit: fix-B
c87f9ae HEAD@{1}: reset: moving to c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
a5f6cbf HEAD@{2}: checkout: moving from master to fix-B
a5f6cbf HEAD@{3}: merge feature-A: Merge made by the 'recursive' strategy.
c87f9ae HEAD@{4}: checkout: moving from feature-A to master
320739b HEAD@{5}: checkout: moving from master to feature-A
c87f9ae HEAD@{6}: checkout: moving from feature-A to master
320739b HEAD@{7}: commit: Add feature-A
c87f9ae HEAD@{8}: checkout: moving from master to feature-A
c87f9ae HEAD@{9}: commit: Add #Git学习
c75e47a HEAD@{10}: commit: 测试使用编辑器记述详细提交信息
858ba4b HEAD@{11}: commit (initial): My first commit

在日志中,我们可以看到commitcheckoutresetmergeGit命令的执行纪律。只要不进行GitGCGarbage Collection,垃圾回收),就可以通过日志随意调取近期的历史状态,就像给时间机器指定一个时间点,在过去未来中自由穿梭一般。即便开发者错误执行了Git操作,基本也都可以利用git reflog命令恢复到原先的状态。

从上面数第四行表示feature-A特性分支合并后的状态,对应哈希值为a5f6cbf。我们将HEAD、暂存区、工作树恢复到这个时间点的状态。

1
2
3
4
$ git checkout master
Switched to branch 'master'
$ git reset --hard a5f6cbf
HEAD is now at a5f6cbf Merge branch 'feature-A'

之前我们使用git reset --hard命令回溯了历史,这里又再次通过它恢复到了回溯前的历史状态。

Git学习之消除冲突和修改提交信息(四)

消除冲突

现在我们只要合并fix-B分支,就会发生冲突。

1
2
3
4
$ git merge --no-ff fix-B 
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

系统告诉我们README.md文件发生了冲突(Conflict)。系统在合并README.md文件时,feature-A分支更改的部分与本次想要合并的fix-B分支更改的部分发生了冲突。

不解决冲突就无法完成合并,所以我们打开README.md文件,解决这个冲突。

查看冲突部分并将其解决

用编辑器打开README.md文件,就会发现其内容变成了下面这个样子。

1
2
3
4
5
6
7
Git Learn 
#Git学习
<<<<<<< HEAD
feature-A
=======
fix-B
>>>>>>> fix-B

=======以上的部分是当前HEAD的内容,以下的部分还要合并的fix-B分支中的内容。我们在编辑器中将其改成想要的样子。

1
2
3
4
5
Git Learn 
#Git学习

feature-A
fix-B

如上所示,本次修正让feature-Afix-B的内容并存于文件之中。但是在实际的软件开发中,往往需要删除其中之一,所以各位在处理冲突时,务必要仔细分析冲突部分的内容后再行修改。

提交解决后的结果

冲突解决后,执行git add命令与git commit命令。

1
2
3
$ git add README.md 
$ git commit -m "Fix conflict"
[master d8d0c37] Fix conflict

由于本次更改解决了冲突,所以提交信息记为“Fix conflict”。

修改提交信息

git commit --amend

要修改上一条提交信息,可以使用git commit --amend命令。

我们将上一条提交信息记为了“Fix conflict”,但它其实是fix-B分支的合并,解决合并时发生的冲突只是过程之一,这样标记实在不妥。于是,我们要修改这条提交信息。

1
$ git commit --amend

执行上面的命令后,编辑器就会启动

1
2
3
4
5
6
7
8
9
10
11
Fix conflict

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Apr 12 11:20:56 2016 +0800
#
# On branch master
# Changes to be committed:
# modified: README.md
#

编辑器中显示的内容如上所示,其中包含之前的提交信息。请将提交信息的部分修改为Merge branch 'fix-B',然后保存文件,关闭编辑器。

1
2
[master d1726c9] Merge branch 'fix-B'
Date: Tue Apr 12 11:20:56 2016 +0800

随后会显示上面这条结果。现在执行git log --graph命令,可以看到提交日志中的相应内容也已经被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 git log --graph
* commit a51fda63bfebdb1c9f83b4385d8e8cfdacff9e19
|\ Merge: a5f6cbf 6814da3
| | Author: frankphper <1678723151@qq.com>
| | Date: Tue Apr 12 11:20:56 2016 +0800
| |
| | Merge branch 'fix-B'
| |
| * commit 6814da3bcfc5935580f873190cb24f18db0017ec
| | Author: frankphper <1678723151@qq.com>
| | Date: Tue Apr 12 09:49:32 2016 +0800
| |
| | fix-B
| |
* | commit a5f6cbf52a36dd104e131e32e67531656640c0c3
|\ \ Merge: c87f9ae 320739b
| |/ Author: frankphper <1678723151@qq.com>
|/| Date: Mon Apr 11 08:16:48 2016 +0800
| |
| | Merge branch 'feature-A'
| |
| * commit 320739b163a92ab90e24abfb814ab7fc09e9c9a0
|/ Author: frankphper <1678723151@qq.com>
| Date: Mon Apr 11 07:59:05 2016 +0800
|
| Add feature-A
|
* commit c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
| Author: frankphper <1678723151@qq.com>
| Date: Sun Apr 10 23:33:16 2016 +0800
|
| Add #Git学习
|
* commit c75e47ad83d604e234eac38bb3aec57226f79974
| Author: frankphper <1678723151@qq.com>
| Date: Sun Apr 10 23:01:24 2016 +0800
|
| 测试使用编辑器记述详细提交信息
|
* commit 858ba4b5b0368c1af88856ea94c5cbbd9eab51fc

Git学习之压缩历史(五)

压缩历史

git rebase -i

在合并特性分支之前,如果发现已提交的内容中有些许拼写错误等,不妨提交一个修改,然后将这个修改包含到前一个提交之中,压缩成一个历史记录。

创建feature-C分支

首先,新建一个feature-C特性分支。

1
2
$ git checkout -b feature-C
Switched to a new branch 'feature-C'

我们在README.md文件中添加一行文字,并且故意拼写错误,以便之后修正。

1
2
3
4
5
6
Git Learn
#Git学习

feature-A
fix-B
faeture-C

我们用git commit -am命令。

1
2
3
$ git commit -am "Add feature-C"
[feature-C e57cbf9] Add feature-C
1 file changed, 1 insertion(+)

修正拼写错误

修正README.md文件的内容,修正后的差别如下所示:

1
2
3
4
5
6
7
8
9
10
11
$ git diff
diff --git a/README.md b/README.md
index a51326b..1fdef41 100644
--- a/README.md
+++ b/README.md
@@ -3,4 +3,4 @@ Git Learn

feature-A
fix-B
-faeture-C
+feature-C

然后进行提交。

1
2
3
$ git commit -am "Fix typo"
[feature-C a12e149] Fix typo
1 file changed, 1 insertion(+), 1 deletion(-)

错字漏字等失误称作typo,所以我们将提交信息记为“Fix typo”。

实际上,我们不希望在历史尽量中看到这类提交,因为健全的历史记录并不需要它们。如果能在最初提交之前就发现并修正这些错误,也就不会出现这类提交了。

更改历史

因此,我们来更改历史。将“Fix typo”修正的内容与之前一次的提交合并,在历史记录中合并为一次完美的提交。为此,我们要用到git rebase命令。

1
2
3
$ git rebase -i HEAD-2
fatal: Needed a single revision
invalid upstream HEAD~2

用上述方式执行git rebase命令,可以选定当前分支中包含HEAD(最新提交)在内的两个最新历史记录为对象,并在编辑器中打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pick e57cbf9 Add feature-C
pick a12e149 Fix typo

# Rebase a51fda6..a12e149 onto a51fda6 (2 command(s))
#
# 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
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

我们将a12e149Fix typo的历史记录压缩到e57cbf9Add feature-C里。将a12e149左侧的pick部分删除,改写为fixup

保存编辑器里的内容,关闭编辑器。

1
2
3
4
[detached HEAD ad10e53] Add feature-C
Date: Tue Apr 12 12:58:05 2016 +0800
1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/feature-C.

系统显示rebase成功。也就是以下面这两个提交作为对象,将“Fix typo”的内容合并到了上一个提交“Add feature-C”中,改写成了一个新的提交。

  • e57cbf9 Add feature-C
  • a12e149 Fix typo

现在再查看提交日志时会发现Add feature-C的哈希值已经不是e57cbf9了,这证明提交已经被更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ git log --graph
* commit ad10e53aad0618b3c3e850635a013f3e2eb44282
| Author: frankphper <1678723151@qq.com>
| Date: Tue Apr 12 12:58:05 2016 +0800
|
| Add feature-C
|
* commit a51fda63bfebdb1c9f83b4385d8e8cfdacff9e19
|\ Merge: a5f6cbf 6814da3
| | Author: frankphper <1678723151@qq.com>
| | Date: Tue Apr 12 11:20:56 2016 +0800
| |
| | Merge branch 'fix-B'
| |
| * commit 6814da3bcfc5935580f873190cb24f18db0017ec
| | Author: frankphper <1678723151@qq.com>
| | Date: Tue Apr 12 09:49:32 2016 +0800
| |
| | fix-B
| |
* | commit a5f6cbf52a36dd104e131e32e67531656640c0c3
|\ \ Merge: c87f9ae 320739b
| |/ Author: frankphper <1678723151@qq.com>
|/| Date: Mon Apr 11 08:16:48 2016 +0800
| |
| | Merge branch 'feature-A'
| |
| * commit 320739b163a92ab90e24abfb814ab7fc09e9c9a0
|/ Author: frankphper <1678723151@qq.com>
| Date: Mon Apr 11 07:59:05 2016 +0800
|
| Add feature-A
|
* commit c87f9ae6da0ccffedda289c55e55e7bc7900dfe0
| Author: frankphper <1678723151@qq.com>
| Date: Sun Apr 10 23:33:16 2016 +0800
|
| Add #Git学习
|
* commit c75e47ad83d604e234eac38bb3aec57226f79974

这样一来,Fix typo就从历史中被抹去,也就相当于Add feature-C中从来没有出现过拼写错误。这算是一种良性的历史改写。

合并至master分支

我们将feature-C分支与master分支合并。

1
2
$ git checkout master
Switched to branch 'master'
1
2
3
4
$ git merge --no-ff feature-C
Merge made by the 'recursive' strategy.
README.md | 1 +
1 file changed, 1 insertion(+)

master分支整合了feature-C分支。

Git学习之推送至远程仓库(六)

Git是分散型版本管理系统,但我们前面所学习的,都是针对单一本地仓库的操作。下面,我们将开始接触远在网络另一头的远程仓库。远程仓库顾名思义,是与我们本地仓库相对独立的另一个仓库。让我们现在GitHub上创建一个仓库,并将其设置为本地仓库的远程仓库。

为防止与其他仓库混淆,仓库名请与本地仓库保持一致,即git-learn。创建时请不要勾选Initialize this repository with a README选项。因为一旦勾选该选项,GitHub一侧的仓库就会自动生成README文件,从创建之初便与本地仓库失去了整合性。虽然到时也可以强制覆盖,但为防止这一情况发生还是建议不要勾选该选项,直接点击Create repository创建仓库。

添加远程仓库

git remote add

GitHub上创建的仓库路径为“git@github.com:用户名/git-learn.git”。现在我们用git remote add命令将它设置成本地仓库的远程仓库。

1
$ git remote add origin git@github.com:weirubo/git-learn.git

按照上述格式执行git remote add命令之后,Git会自动将git@github.com:weirubo/git-learn.git远程仓库的名称设置为origin(标识符)。

推送至远程仓库

git push

如果想将当前分支下本地仓库中的内容推送给远程仓库,需要用到git push命令。现在假定我们在master分支下进行操作。

1
2
3
4
5
6
7
8
9
$ git push -u origin master
Counting objects: 23, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (23/23), 1.83 KiB | 0 bytes/s, done.
Total 23 (delta 3), reused 0 (delta 0)
To git@github.com:weirubo/git-learn.git
* [new branch] master -> master
Branch master set up to track remote branch master from origin.

像这样执行git push命令,当前分支的内容就会被推送给远程仓库originmaster分支。-u参数可以在推送的同时,将origin仓库的master分支设置为本地仓库当前分支的upstream(上游)。添加了这个参数,将来运行git pull命令从远程仓库获取内容时,本地仓库的这个分支就可以直接从originmaster分支获取内容,省去了另外添加参数的麻烦。

执行该操作后,当前本地仓库master分支的内容将会被推送到GitHub的远程仓库中。在GitHub上也可以确认远程master分支的内容和本地master分支相同。

推送至master以外的分支

除了master分支之外,远程仓库也可以创建其他分支。举个例子,我们在本地仓库中创建feature-D分支,并将它以同名形式push至远程仓库。

1
2
$ git checkout -b feature-D
Switched to a new branch 'feature-D'

我们在本地仓库中创建了feature-D分支,现在奖它push给远程仓库并保持分支名称不变。

1
2
3
4
5
$ git push -u origin feature-D
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:weirubo/git-learn.git
* [new branch] feature-D -> feature-D
Branch feature-D set up to track remote branch feature-D from origin.

现在,在远程仓库的GitHub页面就可以查看到feature-D分支了。

Git学习之从远程仓库获取(七)

上一节中我们把在GitHub上新建的仓库设置成了远程仓库,并向这个仓库pushfeature-D分支。现在,所有能够访问这个仓库的人都可以获取feature-D分支并加以修改。

本节中我们从实际开发者角度出发,在另一个目录下新建一个本地仓库,学习从远程仓库获取内容的相关操作。这就相当于我们刚刚执行过push操作的目标仓库又有了另一名新开发者来共同开发。

获取远程仓库

git clone

获取远程仓库

首先我们换到其他目录下,将GitHub上的仓库clone到本地。注意不要与之前操作的仓库在同一目录下。

1
2
3
4
5
6
7
8
9
$ git clone git@github.com:weirubo/git-learn.git
Cloning into 'git-learn'...
remote: Counting objects: 23, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 23 (delta 3), reused 23 (delta 3), pack-reused 0
Receiving objects: 100% (23/23), done.
Resolving deltas: 100% (3/3), done.
Checking connectivity... done.
$ cd git-learn/

执行git clone命令后我们会默认处于master分支下,同时系统会自动将origin设置成该远程仓库的标识符。也就是说,当前本地仓库的master分支与GitHub端远程仓库(origin)的master分支在内容上是完全相同的。

1
2
3
4
5
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/feature-D
remotes/origin/master

我们用git branch -a命令查看当前分支的相关信息。添加-a参数可以同时显示本地仓库和远程仓库的分支信息。

结果中显示了remotes/origin/feature-D,证明我们的远程仓库中已经有了feature-D分支。

获取远程仓库的feature-D分支

我们试着将feature-D分支获取至本地仓库

1
2
3
$ git checkout -b feature-D origin/feature-D
Branch feature-D set up to track remote branch feature-D from origin.
Switched to a new branch 'feature-D'

-b参数的后面是本地仓库中新建分支的名称。为了便于理解,我们仍然将其命名为feature-D,让它与远程仓库的对应分支保持同名。新建分支名称后面是获取来源的分支名称。例子中指定了origin/feature-D,就是说以后名为origin的仓库(这里只GitHub端的仓库)的feature-D分支为来源,在本地仓库中创建feature-D分支。

向本地的feature-D分支提交更改

现在假定我们是另一名开发者,要做一个新的提交。在README.md文件中添加一行文字。

1
2
3
4
5
6
7
Git Learn
#Git学习

feature-A
fix-B
feature-C
feature-D

查看更改:

1
2
3
4
5
6
7
8
9
10
$ git diff
diff --git a/README.md b/README.md
index 1fdef41..61c9b62 100644
--- a/README.md
+++ b/README.md
@@ -4,3 +4,4 @@ Git Learn
feature-A
fix-B
feature-C
+feature-D

按照之前学过的方式提交即可。

1
2
3
$ git commit -am "Add feature-D"
[feature-D 6a0de29] Add feature-D
1 file changed, 1 insertion(+)

推送feature-D分支

现在来推送feature-D分支。

1
2
3
4
5
6
7
8
$ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 284 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:weirubo/git-learn.git
ed3fa4d..6a0de29 feature-D -> feature-D

从远程仓库获取feature-D分支,在本地仓库中提交更改,再将feature-D分支推送回远程仓库,通过这一系列操作,就可以与其他开发者相互合作,共同培育feature-D分支,实现某些功能。

获取最新的远程仓库分支

git pull

现在我们放下刚刚操作的目录,回到原先的那个目录下。这边的本地仓库中只创建了feature-D分支,并没有在feature-D分支中进行任何提交。然而远程仓库的feature-D分支中已经有了我们刚刚推送的提交。这时我们就可以使用git pull命令,将本地的feature-D分支更新到最新状态。当前分支为feature-D分支。

1
2
3
4
5
6
7
8
9
10
11
12
$ git pull origin feature-D
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:weirubo/git-learn
* branch feature-D -> FETCH_HEAD
ed3fa4d..6a0de29 feature-D -> origin/feature-D
Updating ed3fa4d..6a0de29
Fast-forward
README.md | 1 +
1 file changed, 1 insertion(+)

GitHub端远程仓库的feature-D分支是最新状态,所以本地仓库中的feature-D分支就得到了更新。今后只需要像平常一样在本地进行提交再push给远程仓库,就可以与其他开发者同时在同一个分支中进行作业,不断给feature-D增加新功能。

如果两人同时修改了同一部分的源代码,push时就很容易发生冲突。所以多名开发者在同一个分支中进行作业时,为减少冲突情况的发生,建议更频繁地进行pushpull操做。

常见问题

  1. git remote add远程仓库报错fatal: Not a git repository

    git remote add origin git@github.com:weirubo/Swift-learn.git

    fatal: Not a git repository (or any of the parent directories): .git

    原因:

    要使用Git进行版本管理,必须先初始化仓库。Git是使用git init命令进行初始化的。

    解决方案:

    初始化创库

    1
    git init
  2. git push报错error: failed to push some refs to ‘git@github.com:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ git push -u origin master
    To git@github.com:weirubo/Swift-learn.git
    ! [rejected] master -> master (fetch first)
    error: failed to push some refs to 'git@github.com:weirubo/Swift-learn.git'
    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.

    原因:

    GitHub远程仓库中的README.md文件不在本地仓库中。

    解决方案:

    1
    2
    $ git pull --rebase origin master
    $ git push -u origin master
  3. warning: push.default is unset; its implicit value has changed in Git 2.0 from ‘

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    git push
    warning: push.default is unset; its implicit value has changed in
    Git 2.0 from 'matching' to 'simple'. To squelch this message
    and maintain the traditional behavior, use:

    git config --global push.default matching

    To squelch this message and adopt the new behavior now, use:

    git config --global push.default simple

    When push.default is set to 'matching', git will push local branches
    to the remote branches that already exist with the same name.

    Since Git 2.0, Git defaults to the more conservative 'simple'
    behavior, which only pushes the current branch to the corresponding
    remote branch that 'git pull' uses to update the current branch.

    See 'git help config' and search for 'push.default' for further information.
    (the 'simple' mode was introduced in Git 1.7.11. Use the similar mode
    'current' instead of 'simple' if you sometimes use older versions of Git)

    matching和simple的区别:

    ‘matching’ 参数是 Git 1.x 的默认行为,其意是如果你执行 git push 但没有指定分支,它将 push 所有你本地的分支到远程仓库中对应匹配的分支。这意味着可能你会在不经意间push一些你原本没打算push的分支。

    ‘ simple’ 参数是Git 2.x 默认行为,意味着执行 git push 没有指定分支时,只有当前分支会被 push 到你使用 git pull 获取的代码。另外,这个过程也会同时检查各个分支的名称。