2016年春节前大概一周时间,忘了什么契机,忽然觉得自己用了Git这么久,一直处于一知半解的状态,而Git作为一个出色的VCS,自问世以来长盛不衰,一定有其道理。本着“不但要知其然,还要知其所以然”的态度,决定认真读一读《Pro Git》。
说来惭愧,组内最开始从Subversion迁移到Git时,一新就向大家推荐过这本书,当时自己简单翻了翻,由于有其他事情就耽搁下,一直没有再打开过。


Chapter 2. Git Basics

第二章主要讲了如何将一个文件添加进VCS、提交改动、历史查看、撤销操作、Tag。

文件状态

Git中有四种文件状态,相互间转换关系见下图

  • 使用Git时,要多多用git status来查看当前文件状态
  • 同一个文件是可以同时处于modifiedstaged两种状态的,试着edit-add-edit,你就会发现
  • git status --short可以用来查看更简短的文件状态描述,用处不大

.gitignore

对于不想加入到版本控制中的文件(IDE生成文件、编译中间文件等),可以使用.gitignore来告诉Git系统,不需要关注这些文件。
gitignore的例子如下,以#开头的行是注释,会被Git忽略掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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用来查看进行的改动,控制台的输出不够直观,尤其是改动很多的时候。推荐使用GUI进行查看(需要在.gitconfig中进行配置,位置是~/.gitconfig),目前笔者在用的是DiffMerge,基本满足日常使用需求。

  • git diff用来查看modified的变更。
  • git diff --cachedgit diff --staged作用一样,用来查看staged的变更

git commit

使用git commit将改动提交到仓库,以创建一个SnapshotSnapshot是一个很重要的概念,是Git内部实现的最关键机制,也是Git能够超越Subversion等其他VCS的杀手级特性,这一点将在Chapter3中进行说明。

直接输入git commit后,将会打开一个编辑器页面(笔者用的是Vim,可以在gitconfig中配置),内容如下:

1
2
3
4
5
6
7
8
9
10
11
# 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:
# new file: README
# modified: CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

可以在其中编辑本次提交信息,如果不写任何东西的话,Git是不会承认本次commit的。

如果不想打开编辑器,可以直接在commit时加上-m参数,如下

1
$ git commit -m "Story 182: Fix benchmarks for speed"

可以用-a -m或者-am把stage、commit两个操作一起完成

1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch master
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

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 file changed, 5 insertions(+), 0 deletions(-)

Removing Files

直接rm文件的话,文件是处于unstaged状态的,此时需要先add,再commit

1
2
3
4
5
6
7
8
9
10
11
$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

deleted: PROJECTS.md

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

或者直接用git rm,将文件直接置于staged状态

1
2
3
4
5
6
7
8
$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

deleted: PROJECTS.md

另外一个很有用的参数是--cached,用来将文件从staged状态转移到untracked。如果在最初编辑.gitignore时漏写了某个文件,后面发现Git已经记录了这个文件的变更,可以先用--cached参数停止track该文件,然后编辑.gitignore,将文件添加进去。

1
$ git rm --cached README

Moving Files

同样,可以用git mv来完成文件重命名

1
2
3
4
5
6
7
$ git mv README.md README
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

renamed: README.md -> README

无比重要的 git log

git log用来查看当前分支的查看历史,如果不加任何参数,默认输出每次提交的SHA1值、提交者、提交时间、提交信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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

使用-p参数可以展示每次提交的文件具体改动,使用-[num](如-2)来控制显示的commit数

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
41
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon &lt;schacon@gee-mail.com&gt;
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 &lt;schacon@gee-mail.com&gt;
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

如果觉得-p参数展示的太多了,就用--stat来看改动文件与行数

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
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon &lt;schacon@gee-mail.com&gt;
Date: Mon Mar 17 21:52:11 2008 -0700

changed the version number

Rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon &lt;schacon@gee-mail.com&gt;
Date: Sat Mar 15 16:40:33 2008 -0700

removed unnecessary test

lib/simplegit.rb | 5 -----
1 file changed, 5 deletions(-)

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon &lt;schacon@gee-mail.com&gt;
Date: Sat Mar 15 10:31:28 2008 -0700

first commit

README | 6 ++++++
Rakefile | 23 +++++++++++++++++++++++
lib/simplegit.rb | 25 +++++++++++++++++++++++++
3 files changed, 54 insertions(+)

--pretty参数可以美化信息展示,这是一个复合参数,有onelineshortfullfuller等选择

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

或者自己定制输出模版

1
2
3
4
$ 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

模版参数定义如下

Option Description of Output
%H Commit hash
%h Abbreviated commit hash
%T Tree hash
%t Abbreviated tree hash
%P Parent hashes
%p Abbreviated parent hashes
%an Author name
%ae Author email
%ad Author date (format respects the –date=option)
%ar Author date, relative
%cn Committer name
%ce Committer email
%cd Committer date
%cr Committer date, relative
%s Subject

在log中使用--graph来展示ASCII格式的图形化提交历史,建议使用更傻瓜的GUI工具来查看

1
2
3
4
5
6
7
8
9
10
11
$ 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

如果想查看两周以来的提交,用--since参数,同理,也有--until可供使用,后面可以接2016-01-01这样的时间格式

1
$ git log --since=2.weeks

-S参数查看提交中包含某个特定字段变更的log,比如你想看哪些提交改到了onCreate函数,就这么写

1
$ git log --SonCreate

还有其它选择功能,如查看某人的提交--committer,查看包含某些敏感词的提交--grep等等,如下表所示

Option Description
-(n) Show only the last n commits
–since, –after Limit the commits to those made after the specified date.
–until, –before Limit the commits to those made before the specified date.
–author Only show commits in which the author entry matches the specified string.
–committer Only show commits in which the committer entry matches the specified string.
–grep Only show commits with a commit message containing the string
-S Only show commits adding or removing code matching the string

举个🌰,直接照搬书中的好了:

For example, if you want to see which commits modifying test files in the Git source code history are merged and were committed by Junio Hamano in the month of October 2008, you can run something like this:

1
2
3
4
5
6
7
8
$ git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \
--before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:$current_branch" into an unborn branch

Undoing Things

如果在一次commit后,发现忘了修改某个文件foo,那要怎么办?
当然可以修改了foo后,用git commit -am来增加一次commit。
可如果想与之前的commit并成一个呢?git为我们提供了这样的工具--amend(amend: vt&vi 改良,修改,修订)。Git会检查staged区域,将其中的变更与上一个commit合并,你还可以修改合并后的commit信息。
针对上面的场景

1
2
3
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

如果staged区域并没有任何改动,--amend可以用来单纯的修改commit message,同样十分有用。

下面讲讲,要是在stage文件后,反悔了,想要把文件回滚,怎么办?其实git status已经给了我们提示,那就是git reset,要结合HEAD使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD &lt;file&gt;..." to unstage)

renamed: README.md -&gt; README
modified: 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 &lt;file&gt;..." to unstage)

renamed: README.md -&gt; README

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

modified: CONTRIBUTING.md

上面这种用法只是回滚了staged中的文件,如果加上--hard参数,可就要慎重了——这会把commit回滚掉,而且,不留痕迹!

如果文件还没有进行stage,正处于modified,你想要将它变回到unmodified,就用checkout --。同样,这个操作不会保留之前的任何修改,不带走一丝云彩。

1
$ git checkout -- README.md

Git还提供了另外一个很有力的工具stash,用来暂存修改,限于篇幅,暂不展开。

Working With Remotes

Git是一个强有力的协作工具,必不可少地,你会将自己的代码推送到仓库供他人使用,你也会将他人完成的代码从远端拉下来进行追踪。

git remote -v用来查看本地的分支(fetch & push),对于复杂的项目,会有多个远端remote(实在找不出这个语境下合适的翻译,总不能说“分支”吧,会与branch重复)

1
2
3
4
5
6
7
8
9
10
11
$ git remote -v
bakkdoor https://github.com/bakkdoor/grit (fetch)
bakkdoor https://github.com/bakkdoor/grit (push)
cho45 https://github.com/cho45/grit (fetch)
cho45 https://github.com/cho45/grit (push)
defunkt https://github.com/defunkt/grit (fetch)
defunkt https://github.com/defunkt/grit (push)
koke git://github.com/koke/grit.git (fetch)
koke git://github.com/koke/grit.git (push)
origin git@github.com:mojombo/grit.git (fetch)
origin git@github.com:mojombo/grit.git (push)

一般我们只会看到origin一个remote,使用git remote add来添加remote

1
2
3
4
5
6
7
8
$ 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是我们起的别名,然后使用fetch来更新这个remote

1
2
3
4
5
6
7
8
$ 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

也可以不指定remote名称,直接fetch所有remote

讲完了下载,我们来看看如何上传。git push [remote-name] [branch-name],灰常简单

1
$ git push origin master

git remote show [remote-name]命令可以用来观察某个remote的状态,注意!这里观察的并不是远端实时状态,而是本地上一次下载下来的版本,如果你想观察最新状态,需要先fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git remote show origin
* remote origin
URL: https://github.com/my-org/complex-project
Fetch URL: https://github.com/my-org/complex-project
Push URL: https://github.com/my-org/complex-project
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
markdown-strip tracked
issue-43 new (next fetch will store in remotes/origin)
issue-45 new (next fetch will store in remotes/origin)
refs/remotes/origin/issue-11 stale (use 'git remote prune' to remove)
Local branches configured for 'git pull':
dev-branch merges with remote dev-branch
master merges with remote master
Local refs configured for 'git push':
dev-branch pushes to dev-branch (up to date)
markdown-strip pushes to markdown-strip (up to date)
master pushes to master (up to date)

如果你觉得某个remote的名字起的太土气了,使用git remote rename [old_name] [new_name]来修改

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

同样,使用git remote rm [remote_name]来删除(可怜的paul)

1
2
3
$ git remote rm paul
$ git remote
origin

Tagging

你完成了一个版本的全部需求,已经信心满满地进行了上线,并且线上全面回归过,一切正常——你以为全部的工作都完成了吗?并没有!你需要记录下本次提交作为一个里程碑,Git提供了Tag这个强大的工具。

查看目前的tag,注意是以alphabetical顺序排列的,并非时间

1
2
3
$ git tag
v0.1
v1.3

使用-l参数查看包含某个敏感词的tag

1
2
3
4
5
6
7
8
9
$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3
v1.8.5.1
v1.8.5.2
v1.8.5.3

有两种Tag:lightweighted和annotated。前者非常简单,只是一个tag name,不包含任何其他信息。后者则记录了tagger name、email、date、message等信息,并且可以用GNU Privacy Guard (GPG)进行签名和校验。笔者强烈建议使用annotated进行打Tag。

annotated tag

1
2
3
4
5
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4

git show [tag_name]查看一下

1
2
3
4
5
6
7
8
9
10
11
12
$ git show v1.4
tag v1.4
Tagger: Ben Straub &lt;ben@straub.cc&gt;
Date: Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon &lt;schacon@gee-mail.com&gt;
Date: Mon Mar 17 21:52:11 2008 -0700

changed the version number

lightweighted tag

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700

changed the version number

看到了没,对于这种tag,并没有提供commit之外的任何信息。

如果你想要对几天之前的某一个commit打tag,怎么办?难道git只能在最新提交上打Tag吗?图样图森破!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ git tag -a v1.2 9fceb02
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
v1.5

$ git show v1.2
tag v1.2
Tagger: Scott Chacon &lt;schacon@gee-mail.com&gt;
Date: Mon Feb 9 15:32:16 2009 -0800

version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon &lt;mchacon@gee-mail.com&gt;
Date: Sun Apr 27 20:43:35 2008 -0700

updated rakefile
...

看到没,只需要在后面拼上checksum(可以只是一部分),就可以了

按照上面的指示,你在本地打好了Tag,让我们把它推送到仓库

1
2
3
4
5
6
7
8
$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.5 > v1.5

如果tag很多,用复数!

1
2
3
4
5
6
7
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.4 > v1.4
* [new tag] v1.4-lw > v1.4-lw

tag本身是无法进行修改的,这在一定程度上保证了代码的安全,当然,如果tag可以随便修改,那它跟普通的branch有什么区别?我们可以将tag拉成本地一个branch,用git checkout -b [branchname] [tagname]

1
2
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'

Git Aliaes(别名)

(也许是使用Git不够重度的原因,笔者认为这部分内容并不十分重要,几个常用的命令都很容易记,不常用的起了别名也记不住…)

git config --global alias.[alias_name] [original_command]来创建alias,在~/.gitconfig中查看这些alias。

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

比如,你可以创建这样的alias,来回滚一个stage

1
$ git config --global alias.unstage 'reset HEAD --'

这样,你就可以用

1
$ git unstage fileA

来代替

1
$ git reset HEAD -- fileA

如果你想要运行Git外部命令(rather than a Git subcommand),在命令前加上!(原文给的解释是This is useful if you write your own tools that work with a Git repository.笔者绞尽脑汁也没有想到这个功能用处在哪里…)

1
$ git config --global alias.visual '!gitk'

===Ending===