: 在 @e2c89f10 这个提交及之前, 因为看的是影印版(看了3年才只看了8章...), 所以很多笔记都是英文的. 5月份第二版中文版出了, 立马买了一本, 所以后续会逐步改为中文笔记, 部分原话及术语保留英文.

3. 起步

查看帮助:

# For a complete list of git subcommands
$ git help --all

# document for git subcommand
$ git help <subcommand>
$ git <subcommand> --help

命令行中的双破折号(double dash):

About bare double dash (--), in shell, it indicate the end of the command options; The same as in git, it seperate the option and the path. ref 1 ref 2 ref 3 ref 4

查看某个特定提交的详细信息 git show:

# show the details of the most recent commit
$ git show

# To see more detail about a particular commit
$ git show <commit number>

查看所有分支的整体情况 git show-branch:

$ git show-branch --more=10

更详细的可以看看GitGuys上的图解.


4.基本的Git概念

注意: 这章太重要了, 字字都是珠玑, 真想把整篇都copy过来.

在版本库中, Git维护两个主要的数据结构: 对象库(object store)索引(index). 所有这些版本库数据存放在工作目录根目录下的.git的隐藏目录中.

索引是暂时的信息, 对版本库来说是私有的, 并且可以在需要的时候按需求进行创建和修改.

对象库是git版本库实现的核心. 包含了原始数据文件和所有日志信息、作者信息、日期, 以及其它用来重建项目任意版本或分支的信息.

对象库(Object Store)

Git对象库中的对象只有四种类型: 块(blog), 目录树(tree), 提交(commit)和标签(tag). 这四种原子对象构成Git高层数据结构的基础.

对象库会随着项目的开发一直变化和增长, 为了有效利用磁盘空间和网络带宽, Git把对象压缩并存储在打包文件(pack file)里, 这些文件也在对象库里.

索引(Index)

索引, 又称暂存区(Stage), 是一个临时的、动态的二进制文件.

$ file .git/index
.git/index: data

它捕获项目在某个时刻的整体结构的一个版本.

下面是一个简单的git object内部dag图, 经过两次的提交, 非常清晰(摘自书上图4-2):

Figure 4-2. Git objects after a second commit

可寻址内容名称

Git对象库被组织及实现成一个内容可寻址的存储系统. 对象库中每个对象都有一个唯一的名称, 这个名称是向对象的内容应用sha1得到的sha1散列值, sha1值是一个160位的数, 通常表示为一个40位的十六进制数.

Git追踪内容

Git不仅是一个VCS, 还是一个内容追踪系统(content tracking system). 主要表现为两个方式:

打包文件(pack file)

之前提到的, git 会存储每个文件的每一个版本.

但是, 这个并不是绝对的, 比如一个大文件, 每次只修改其中一行, 那么, 经过多次修改后, 这个文件的各个blob会占用非常大的空间.

实际上git不会这么笨的, git有一个有效的存储机制, 叫做 打包文件(pack file).

git 往磁盘保存对象时默认使用的格式叫松散对象 (loose object) 格式

git 时不时地将这些对象打包至一个叫 packfile 的二进制文件以节省空间并提高效率.

经过打包后, git会存储文件的最新版本, 其余的版本都以差异形式存储(delta)

例子:

$ git init
$ echo 'hello' > hello.txt; git add hello.txt; git commit -m 'add hello.txt'

$ tree .git/objects
.git/objects
├── 0b
│   └── 9d7dbd4e9e2b8be42ebe043083937acd52fccf
├── aa
│   └── a96ced2d9a1c8e72c56b253a0e2fe78393feb7
├── ce
│   └── 013625030ba8dba906f756967f9e9ca394464a
├── info
└── pack

5 directories, 3 files

# 将Python自带的BaseHTTPServer.py加到仓库里
$ git add BaseHTTPServer.py; git commit -m 'add BaseHTTPServer.py'

$ wc -l BaseHTTPServer.py
     603 BaseHTTPServer.py

# mac os 下 du 没有 -b 参数
$ stat -f%z BaseHTTPServer.py
22461

$ tree .git/objects
.git/objects
├── 04
│   └── 0fc62ff827be04d5def454bfef3ef8c49ea488
├── 0b
│   └── 9d7dbd4e9e2b8be42ebe043083937acd52fccf
├── 25
│   └── fc7b5d264c24fed7f7a843fbe9ae3224a07de8
├── aa
│   └── a96ced2d9a1c8e72c56b253a0e2fe78393feb7
├── ce
│   └── 013625030ba8dba906f756967f9e9ca394464a
├── de
│   └── af2f960b83c76b38b0c494db91202c70886833
├── info
└── pack

8 directories, 6 files

# 找到blob id是 deaf2f960b83c76b38b0c494db91202c70886833
# 显示的大小是字节, 这个是经过压缩的大小
$ stat -f%z .git/objects/de/af2f960b83c76b38b0c494db91202c70886833
8569

# 添加一行
$ echo 'a new line' >> BaseHTTPServer.py; git commit -m 'add a new line' BaseHTTPServer.py

$ find .git/objects -type f
.git/objects/04/0fc62ff827be04d5def454bfef3ef8c49ea488
.git/objects/0b/9d7dbd4e9e2b8be42ebe043083937acd52fccf
.git/objects/25/fc7b5d264c24fed7f7a843fbe9ae3224a07de8
.git/objects/28/5fb1a7ab0bcfe01588ad548ac96187366e8c74
.git/objects/3b/119f80b81e4483b12812eb72ebb2df338adbc5
.git/objects/95/16bba0fea3fb039a6e028fb975bb35d158626f
.git/objects/aa/a96ced2d9a1c8e72c56b253a0e2fe78393feb7
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
.git/objects/de/af2f960b83c76b38b0c494db91202c70886833

# 新的blob id是 9516bba0fea3fb039a6e028fb975bb35d158626f
$ stat -f%z .git/objects/95/16bba0fea3fb039a6e028fb975bb35d158626f
8575

$ git gc
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), done.
Total 9 (delta 1), reused 0 (delta 0)

$ tree .git/objects
.git/objects
├── info
│   └── packs
└── pack
    ├── pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.idx
    └── pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.pack

2 directories, 3 files

$ more .git/objects/info/packs
P pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.pack

$ stat -f%z .git/objects/pack/pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.idx
1324
$ stat -f%z .git/objects/pack/pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.pack
8334

$ git verify-pack -v .git/objects/pack/pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.idx
3b119f80b81e4483b12812eb72ebb2df338adbc5 commit 221 155 12
25fc7b5d264c24fed7f7a843fbe9ae3224a07de8 commit 228 163 167
0b9d7dbd4e9e2b8be42ebe043083937acd52fccf commit 172 125 330
9516bba0fea3fb039a6e028fb975bb35d158626f blob   22472 7596 455
ce013625030ba8dba906f756967f9e9ca394464a blob   6 15 8051
285fb1a7ab0bcfe01588ad548ac96187366e8c74 tree   82 90 8066
040fc62ff827be04d5def454bfef3ef8c49ea488 tree   82 90 8156
deaf2f960b83c76b38b0c494db91202c70886833 blob   9 20 8246 1 9516bba0fea3fb039a6e028fb975bb35d158626f
aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7 tree   37 48 8266
non delta: 8 objects
chain length = 1: 1 object
.git/objects/pack/pack-dde2d3ff207bb65df9a3a3d2f4f6e088a18622ad.pack: ok

git verify-pack是用于验证pack文件的, 查看man手册里输出格式说明:

When specifying the -v option the format used is:

   SHA-1 type size size-in-pack-file offset-in-packfile

for objects that are not deltified in the pack, and

   SHA-1 type size size-in-packfile offset-in-packfile depth base-SHA-1

对比上面的输出, 可以看到, BaseHTTPServer.py的第一个版本就是 @deaf2f96, 它的base-SHA-1是 @9516bba0, 且 @deaf2f96 的大小只有9bytes, pack中的压缩后大小是20, 而新的blob在pack中的大小是22472字节. pack中的大小是7596

第二个版本是完整保存文件内容的对象, 而第一个版本是以差异方式保存的, 这是因为大部分情况下需要快速访问文件的最新版本.

git 自动定期对仓库进行重新打包以节省空间. 也可以手工运行 git gc 命令来这么做.

参考:

另外, 在 pro git那一章还学到一个命令:

$ git cat-file -p master^{tree}

和下面效果一样:

$ git ls-tree master

底层命令例子

以下例子使用的基本都是Git的底层命令, 在实际使用中, 有更简单的命令封装了这些底层命令, 不过通过底层命令, 可以更清晰的了解Git的工作流程.

初始化的Git仓库:

# An initial git repo
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack

新建一个文件a.txt, 内容是 'hello', sha1值 ce013625030ba8dba906f756967f9e9ca394464a, 使用git cat-file查看散列的内容:

$ echo 'hello' > a.txt
$ git add a.txt
$ find .git/objects
.git/objects
.git/objects/ce
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
.git/objects/info
.git/objects/pack

$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello

使用git ls-files查看staged信息:

$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0       a.txt

捕获索引状态并保存到一个树对象:

$ git write-tree
2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1

$ git cat-file -p 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    a.txt

现在增加文件b.txt, 内容和a.txt一样, 可以看到, 两个使用同一个blob:

$ echo 'hello' > b.txt
$ git add b.txt

$ git write-tree
b5b0cccf7401633f12e0fafc6b85731251b86850

$ git cat-file -p b5b0cccf7401633f12e0fafc6b85731251b86850
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    a.txt
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    b.txt

现在改变文件a.txt内容, b.txt还是指向原来的blob:

$ echo 'world' >> a.txt
$ git add a.txt

$ git write-tree
579c3877d5f450e34ea642b3a29d2d01dcf8e392

$ git cat-file -p 579c3877d5f450e34ea642b3a29d2d01dcf8e392
100644 blob 94954abda49de8615a048f8d2e64b5de848e27a1    a.txt
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    b.txt

添加一个子目录, 里面也放一个a.txt, 内容一样:

$ mkdir subdir
$ cp a.txt subdir/
$ tree
.
├── a.txt
└── subdir
    └── a.txt

1 directory, 2 files
$ git add subdir/a.txt

$ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0       a.txt
100644 ce013625030ba8dba906f756967f9e9ca394464a 0       subdir/a.txt

$ git write-tree
ec518d6bb3cabb8e88b5458cf18d862aa0514622

$ git cat-file -p ec518d6bb3cabb8e88b5458cf18d862aa0514622
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    a.txt
040000 tree 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1    subdir

可以看到, subdir这个tree对象的sha1 id和之前父目录是一样的.

现在a.txt的blob已经有了, 树对象也有了, 接着就是提交:

$ echo -n 'commit a file' | git commit-tree ec518d6bb3cabb8e88b5458cf18d862aa0514622
7dc4ee9984a52278b3b67480feb712e36ea5a64c

$ git cat-file -p 7dc4ee9984a52278b3b67480feb712e36ea5a64c
tree ec518d6bb3cabb8e88b5458cf18d862aa0514622
author Tanky Woo <me@tankywoo.com> 1431832347 +0800
committer Tanky Woo <me@tankywoo.com> 1431832347 +0800

commit a file%

author vs committer(引用 Pro Git)

The author is the person who originally wrote the patch, whereas the committer is the person who last applied the patch. So, if you send in a patch to a project and one of the core members applies the patch, both of you get credit — you as the author and the core member as the committer.

更详细的解释

打标签:

$ git tag -m 'add tag v1.0' v1.0 7dc4ee9984a52278b3b67480feb712e36ea5a64c

$ git rev-parse v1.0
76a2a639a517e26a6c79fdcd09c0a5ffec97e099

$ git cat-file -p v1.0
object 7dc4ee9984a52278b3b67480feb712e36ea5a64c
type commit
tag v1.0
tagger Tanky Woo <me@tankywoo.com> 1431832535 +0800

add tag v1.0

$ git cat-file -p 76a2a639a517e26a6c79fdcd09c0a5ffec97e099
object 7dc4ee9984a52278b3b67480feb712e36ea5a64c
type commit
tag v1.0
tagger Tanky Woo <me@tankywoo.com> 1431832535 +0800

add tag v1.0

5. 文件管理和索引

关于文件管理, 与其它版本管理系统类似, 不过Git在工作目录(working directory)版本库(repository)之间, 增加了一个Index(Stage)层, 称为索引(暂存)目录. 在工作目录下编辑, 在索引中积累修改, 然后把索引中积累的修改作为一次性的变更来进行提交.

Git process

(图片引用 图解Git)

The lifecycle of the status of your files

(图片引用 Pro Git - 2.2)

Linus Torvalds 在git mailing list里提到,如果不先理解Index的目的,就无法理解和领会Git的强大.

Git 的Index不存放任何文件的内容,它只简单的记录准备提交的文件,当运行git commit时,git 会检查Index而不是工作目录.

Git 把文件分为三个大类: 已追踪(Tracked), 未追踪(Untracked)被忽略(Ignored),其中Tracked又可以分为暂存(Staged)未暂存(Unstaged),在工作目录下修改Tracked的文件是Unstaged,经过git add后变为Staged.

git ls-files --stage 可以查看stage中的文件的sha1值:

$ git ls-files --stage  #  git ls-files -s
100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0       fa

git hash-object可以计算文件的sha1值并输出:

$ git hash-object fa
8d0e41234f24b6da002d962a26c2495ea16a425f

简单的说是文件fa已经在 索引(index) 中了,本质是文件在 对象库(object store) 中,Index指向它.

关于 --allgit addgit commit中有点区别,文件必须要经过add才会被tracked,git add --all 会把所有tracked 和 untracked的文件都add,但是git commit --all只会add所有tracked的文件并提交.

关于git rm,如果tracked中的文件被修改了,则可以通过git rm --cached来从git库中移除,并在本地保存为untracked的,也可以通过-f强制删除.

对于被误删的文件,如果在Index中,如下:

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       deleted:    fa
#

可以通过如下方式之一来返回:

$ git checkout HEAD -- fa

# 或者

$ git reset HEAD fa
$ git checkout -- fa

如果是移除被提交,则可以通过git reset HEAD^或rebase来取回.

关于git mv,等价于:

$ mv fa fb
$ git rm fa
$ git add fb

Git 把文件fa改为fb,会在object store中保存原始的文件内容,然后把文件名(路径名path)重新关联到这个内容:

$ git ls-files --stage
100644 15acaeb140c2805acdbb2d0dbdedeeea6bb73b06 0       fa

$ git mv fa fb
$ git ls-files --stage
100644 15acaeb140c2805acdbb2d0dbdedeeea6bb73b06 0       fb

把fa改为fb后,sha1值并没变.

经过mv操作后,使用git log fb只会看到变更后的提交(包括变更的那个提交),即关联内容并为fb文件的历史,可以通过--follow选项来查看关联这段内容的完整历史:

$ git log --follow fb

关于 .gitignore 文件, 简单语法:

Git版本库中任何目录下都可以有.gitignore文件, 且只影响当前目录及子目录. 作用规则是级联的, 可以覆盖父级以上的规则.

另外Git有多个地方可以影响ignore文件, 所以有优先级关系(从高到低):

A Detailed View of Git’s Object Model and Files 一节对 Git中对象模型和文件的详细视图, 把一次从 干净->编辑->add->commit 的原理图画出来了, 讲得非常好:

Figure 5-1. Initial files and objects:

Figure 5-1. Initial files and objects

Figure 5-2. After editing file1:

Figure 5-2. After editing file1

Figure 5-3. After git add:

Figure 5-3. After git add

Figure 5-4. After git commit:

Figure 5-4. After git commit

关于索引, 更多可以参考:


6. 提交

当提交时, Git会记录索引的快照并把快照放进对象库.

Git可以通过显示引用(explicit ref)或隐式引用(implied ref)来表示提交. 散列id(sha1)是显示引用, HEAD等是隐式引用.

引用(ref)是一个sha1散列值, 指向Git对象库中的对象.

符号引用(symref) 是一个指向引用的引用(指针), 间接的指向git对象. 有点类似C语言里指针的指针? 和Linux的软链接(soft link)也有点类似?

本地特性分支名, 远程跟踪分支名, 标签名都是引用.

每一个符号引用都有一个以 refs/ 开始的明确名称, 并且都分层存储在版本库的.git/refs/ 目录中. 基本分为三种:

(2015-07-19更新):

书上说:

Each symbolic ref has an explicit, full name that begins with refs/ and each is stored hierarchically within the repository in the .git/refs/ directory.

我查了文档, 并且看了git symbolic-ref的man手册, 发现这里的解释是错误

首先, 关于master, 即 .git/refs/heads/master:

$ more .git/refs/heads/master
f35f166562045569095169d340fec0d16eaef73b

存储的也是sha1 id, 所以它不是一个符号引用, 而只是一个引用.

另外用git symbolic-ref也可以看到说这不是一个符号引用:

$ git symbolic-ref master
fatal: ref master is not a symbolic ref

那么, 哪些是符号引用? 暂时只知道默认的有HEAD:

$ git symbolic-ref HEAD
refs/heads/master

$ more .git/HEAD
ref: refs/heads/master

接着, 符号引用有什么特征? 还是git symbolic-ref的man手册, 写的很清楚:

A symbolic ref is a regular file that stores a string that begins with `ref: refs/`. For example, your .git/HEAD is a regular file whose contents is `ref: refs/heads/master`.

很显然, HEAD符合这个特征.

符号引用也可以自己创建, 还是用上面的命令:

$ git symbolic-ref TANKYWOO refs/heads/master
$ git rev-parse TANKYWOO
f35f166562045569095169d340fec0d16eaef73b
$ git rev-parse master
f35f166562045569095169d340fec0d16eaef73b
$ more .git/TANKYWOO
ref: refs/heads/master
$ git symbolic-ref TANKYWOO
refs/heads/master

(2015-07-19更新 结束)

比如本地分支dev, 就是 refs/heads/dev 的缩写:

$ git show dev
commit e31b74d259b83af0f69683b9b12a29ebb3946748
Merge: 12e3223 5488c82
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Fri Apr 10 19:11:27 2015 +0800

    Merge branch 'project-tools' into dev

$ git show refs/heads/dev
commit e31b74d259b83af0f69683b9b12a29ebb3946748
Merge: 12e3223 5488c82
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Fri Apr 10 19:11:27 2015 +0800

    Merge branch 'project-tools' into dev

$ more .git/refs/heads/dev
e31b74d259b83af0f69683b9b12a29ebb3946748

Git 有几个特殊符号引用(除了HEAD, 其余都不是符号引用):

HEAD: .git/HEAD, 总是指向当前分支的最后一次提交, 当分支改变,HEAD也会变

$ more .git/HEAD
ref: refs/heads/master

ORIG_HEAD: .git/ORIG_HEAD, 一些操作, 如mergereset, 会记录操前的commit(HEAD). 作为一个保护措施,使操作可以回溯.

比如最近三个commits 以及 此时的ORIG_HEAD(存储的是之前某一个的commit id):

* d46546a - (HEAD, master) update d (42 seconds ago) <Tanky Woo>
* 8ed2d79 - update f (76 seconds ago) <Tanky Woo>
* 75b09c2 - (tag: v0.1) Merge branch 'dev' (3 days ago) <Tanky Woo>

$ more .git/ORIG_HEAD
015b5b99f5c9973e840f29c9f6e6b936c99b92a5

做一次reset操作:

$ git reset --soft HEAD^

查看ORIG_HEAD, 会指向之前的HEAD:

$ more .git/ORIG_HEAD
d46546a5192b7e1c834947b612e3401a6f7729c7

这样就可以回溯到reset之前的版本:

$ git reset ORIG_HEAD

然后 ORIG_HEAD 又指向 @8ed2d79 这个id

HEAD vs ORIG_HEAD HEAD and ORIG_HEAD in Git

FETCH_HEAD: .git/FETCH_HEAD, 当使用远程库时, git fetch 命令将所有抓取分支的头记录到这个文件中, 是最近fetch的分支HEAD的简写.

$ git fetch origin
$ more .git/FETCH_HEAD
f35f166562045569095169d340fec0d16eaef73b                branch 'master' of https://example.com/tankywoo/git-test-symref

MERGE_HEAD: 当一个合并操作正在进行时, 其它分支的头暂时记录在 MERGE_HEAD 中. 即是正在合并进HEAD的提交.

git symbolic-ref 操作符号引用:

(master*) $ git symbolic-ref HEAD
refs/heads/master

详细可以参考progit-9.3

sha1 id是绝对提交名, 通过~^则可以代表相对提交名.

使用前面讲到的git show-branch可以看到每个提交的相对提交名.

例子:

*   75b09c2 - (HEAD, master) Merge branch 'dev' (4 seconds ago) <Tanky Woo>
|\
| * 0aab100 - (dev) Add d (26 seconds ago) <Tanky Woo>
| * 6a9379e - Add c (31 seconds ago) <Tanky Woo>
* | 015b5b9 - Add f (14 seconds ago) <Tanky Woo>
|/
* 545851d - Add b (59 seconds ago) <Tanky Woo>
* 1509ece - Add a (80 seconds ago) <Tanky Woo>

第一个父提交:

$ git log -1 --pretty=oneline --abbrev-commit -p master^1
015b5b9 Add f

第二个父提交, 这是从dev分支合并进master的分支:

$ git log -1 --pretty=oneline --abbrev-commit -p master^2
0aab100 Add d

使用波浪号(~):

$ git log -1 --pretty=oneline --abbrev-commit -p master~1
015b5b9 Add f

master^1 等价于 master~1

组合使用:

$ git log -1 --pretty=oneline --abbrev-commit -p master^2~1
6a9379e Add c

查看提交历史

git log 默认就是 git log HEAD

使用-p/--patch 可以查看提交的修改补丁:

$ git log -1 -p HEAD

这个等价于:

$ git show HEAD

git show 还可以查看某个文件的blob内容:

$ git diff fa
diff --git a/fa b/fa
index 89b24ec..7bba8c8 100644
--- a/fa
+++ b/fa
@@ -1 +1,2 @@
 line 1
+line 2

$ git show :fa
line 1

fa在历史库中只有line 1这一行, 在unstaged中增加了line 2.

还可以查看远程追踪分支中某文件的blob内容, 如:

$ git show origin/master:setup.py

使用git log <start>..<end> 两个dot 语法来查看某一段历史, 表示 "结束" 的提交可到达 且 "开始" 的提交不可到达的一组提交. 如:

# 查看master~11, master~10, 但是不包括 master~12
$ git log master~12..master~10

如图:

Figure 6-9. Interpreting ranges as set subtraction

实际也就是:

$ git log ^X Y

TODO 这块看图6-11, 6-12, 6-13

<start>..<end> 的范围表示集合的减法运算, 而 <A>...<B> 三个dot 表示A和B的对称差(symmetric difference), 也就是 A或B可达 且又 不同时在 A和B的并集 中.

比如 dev 是从master的init这个提交衍生出来的, 随后master和dev各增加一个提交:

# master: init -> add fc
# dev:  init -> add fb

(master) $ git log master...dev --oneline
52bdb27 add fc
20d2444 add fb

下面这个命令效果是一致(TODO):

(master) $ git rev-list --abbrev-commit master...dev --not $(git merge-base --all master dev)
52bdb27
20d2444

查找提交

git bisect 二分法查找. 一般用于查找某次坏提交造成的问题.

git blame 用于查看一个文件中的没一行最后是最提交以及commit id:

$ git blame CHANGELOG.rst
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  11) v1.3 (2015-03-04)
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  12) ===================
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  13)
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  14) 1. Add `site.time` variable, the generated time.
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  15) 2. Improve encoding
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  16) 3. Add `--update-them` when generate to improve generation speed
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  17) 4. Fix #36, add attach directory to put attachments.
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  18) 5. Fix #33, only show color logging message on Linux/MacOS
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  19)
7a6a703b (Tanky Woo         2015-03-04 11:47:40 +0800  20)
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  21) v1.2.4 (2014-12-23)
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  22) ===================
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  23)
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  24) * Fix #31 encode/decode problems
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  25) * Fix image overflow in simple themes
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  26)
211a6669 (Tanky Woo         2014-12-23 12:35:59 +0800  27)

git log -S 用于根据给定的关键字搜索出现在历史差异中的提交, 也成为pickaxe

但是需要注意: 如果某个提交 添加 和 删除 相同数量含关键词的行, 则这个提交不会被查找出来; 提交必须有添加和删除数量上的变化才能计数.

如:

line 1       line 1
row  2   ->  line 3
line 3       row  3

则无法搜出这次提交.

7. 分支

为了支持可扩展和分类组织, 可以创建一个带层次的分支名, 类似于Unix的路径名, 如

# fix bug的分支集
bug/pr-1
bug/pr-2

# 特性分支集
feature/smt-1
feature/smt-2

这样也例子筛选分支:

$ git show-branch 'bug/*'

分支名的一些限制:

使用git merge-base 可以找到两个点的共同祖先, 如master和dev分支:

$ git merge-base master dev

新建分支时, 默认是从当前分支的最近一个点衍生出新分支, 也可以指定分支或某个sha1 id:

$ git branch feature/new master
$ git branch feature/new 7a6a703b

上面命令只新建, 不切换分支, git checkout -b xxx是新建且切换.

git show-branchgit branch的参数类似, 支持-r (远程分支), -a (所有分支).

关于git show-branch输出的解释, 之前几章多次用到这个命令, 这里终于有详细的解释了:

(master*) $ git show-branch master dev category-index
* [master] Release v1.3
 ! [dev] Merge branch 'project-tools' into dev
  ! [category-index] Merge branch 'project-tools' into dev
---
 -- [dev] Merge branch 'project-tools' into dev
 ++ [dev^2] Makefile add tox and covhtml section
 ++ [dev^2^] Update Makefile clean section
 ++ [dev^2~2] Add arguments for `nosetests` command
 ++ [dev~10] rename class InitSite to Initiator and refactor
 ++ [dev~11] rename initsite.py to initiator.py
 -- [dev~12] Merge branch 'support-draft' into dev
 ++ [dev~12^2] Add tester for draft
 ++ [dev~13] Add tag after release
*++ [master] Release v1.3

默认情况下(不带参数), 会显示所有的本地分支, 我这里为了方便, 只显示3个分支, 且删除了中间很多提交.

输出被一排破折号分为两部分, 破折号长度与分支数有关.

破折号上方显示分支名, 每个分支名一行:

破折号下面是提交信息:

列表会显示到所有分支(活指定的所有分支)的公共祖先那个点, 也可以加--more来多显示一些commit.

关于checkout切换分支有冲突的情况, 比如某个文件同一块地方在两个分支都有改变, 默认无法切换, 除非-f强制切换, 一般的解决方法:

试了下第二种方法, 比较麻烦, 一般习惯还是用stash

8. Diff

Unix/Linux 中的 diff 命令:

$ diff -u -r dir1 dir2
diff -u -r dir1/fa dir2/fa
--- dir1/fa     2015-05-24 12:16:11.000000000 +0800
+++ dir2/fa     2015-05-24 12:15:54.000000000 +0800
@@ -1,2 +1 @@
-1-fa
-1-fa
+2-fa
diff -u -r dir1/sub_dir/fb dir2/sub_dir/fb
--- dir1/sub_dir/fb     2015-05-24 12:10:37.000000000 +0800
+++ dir2/sub_dir/fb     2015-05-24 12:11:02.000000000 +0800
@@ -1 +1 @@
-1-fb
+2-fb

git diff的效果类似diff -u -r dir1 dir2

四种基本比较:

还有一些常用的选项:

$ git mv log.py log2.py
$ git diff --cached -M
diff --git a/simiki/log.py b/simiki/log2.py
similarity index 100%
rename from simiki/log.py
rename to simiki/log2.py

关于git diff中的提交范围, 和git log是不一样的, 首先明确两点:

另外以下两个是等价的:

$ git diff master..dev
$ git diff master dev

如这个历史数:

      A---B---C topic
     /
D---E---F---G master

$ git diff master..topic    # A,B,C,F,G
$ git diff master...topic   # A,B,C

这块网上有几篇讲得不错的:

git diff 还可以限制路径:

# 限制在某个目录下
git diff <some_directory>

# 现在在某个文件中
git diff <some_file>

-S在git log中, git diff也有-S参数:

# 在master分支最近50个提交中搜索包含指定字符串的变更
git diff -S "octopus" master~50

9. 合并

Git支持同时合并三个、四个或多个分支. 但是大多数情况下, 一次合并只结合两个分支.

作为一般规则, 每次合并都从干净的工作目录和索引开始, 那么Git的操作会变得容易很多

关于合并冲突的详解:

配置好环境:

$ git show-branch
! [alt] one world
 * [master] all worlds
--
 * [master] all worlds
+  [alt] one world
+* [master^] init

两个分支, 一个文件hello

master分支和alt分支基于一个初始提交, 第二个提交分别是:

master:

 hello
+worlds
+Yay!

alt:

 hello
+world
+Yay!

master上合并alt, 产生冲突:

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

$ 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:   hello

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

$ cat hello
hello
<<<<<<< HEAD
worlds
=======
world
>>>>>>> alt
Yay!

现在hello这个文件的状态是unmerged. Git在处理合并冲突时, 会对冲突的文件标记为冲突的(conflicted)未合并的(unmerged).

$ git diff hello
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,7 @@@
  hello
++<<<<<<< HEAD
 +worlds
++=======
+ world
++>>>>>>> alt
  Yay!

对于有冲突的文件,git diff会比较特殊, 之前说的默认情况下是比较当前工作目录与索引的差异, 这里显示的是两个服版本作的差异, 第一个是HEAD版本, 第二个是alt版本, 第二个又称MERGE_HEAD.

另外这里的+-号不再是一列了, 而是两列, 第一列表示相对当前版本的修改, 第二列是相对另一个版本的修改. 这个输出有点类似show-branch的结果.

$ git diff HEAD
diff --git a/hello b/hello
index e63164d..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
 hello
+<<<<<<< HEAD
 worlds
+=======
+world
+>>>>>>> alt
 Yay!

$ git diff --ours
* Unmerged path hello
diff --git a/hello b/hello
index e63164d..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
 hello
+<<<<<<< HEAD
 worlds
+=======
+world
+>>>>>>> alt
 Yay!

这种就是正常的HEAD和工作目录的diff. git diff HEAD 等价于 git diff --ours

$ git diff MERGE_HEAD
diff --git a/hello b/hello
index 562080a..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
 hello
+<<<<<<< HEAD
+worlds
+=======
 world
+>>>>>>> alt
 Yay!

$ git diff --theirs
* Unmerged path hello
diff --git a/hello b/hello
index 562080a..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
 hello
+<<<<<<< HEAD
+worlds
+=======
 world
+>>>>>>> alt
 Yay!

git diff MERGE_HEAD 等价于 git diff --theirs

$ git diff $(git merge-base HEAD MERGE_HEAD)
diff --git a/hello b/hello
index ce01362..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1 +1,7 @@
 hello
+<<<<<<< HEAD
+worlds
+=======
+world
+>>>>>>> alt
+Yay!

$ git diff --base
* Unmerged path hello
diff --git a/hello b/hello
index ce01362..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1 +1,7 @@
 hello
+<<<<<<< HEAD
+worlds
+=======
+world
+>>>>>>> alt
+Yay!

merge-base会显示两个版本的公共祖先. 上面两条命令也是等价

接着做一些改动, 处理冲突, 删除三方合并标记线, 但是先不add, 还是保留unmerged状态:

$ more hello
hello
worlds xxx          # 随便添加些字符
Yay!
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,3 @@@
  hello
- worlds
 -world
++worlds xxx
  Yay!

$ more hello
hello
worlds  # 使用 --ours版本
Yay!
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello

$ more hello
hello
world  # 使用 --theirs版本
Yay!
$ git diff
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello

后两者都没有diff, 这是git diff又一个特殊的地方: 对于有冲突的文件, git diff只显示真正有冲突的部分, 如果只有一边有变化, 这部分就不显示.

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

Changes to be committed:

        modified:   hello

$ git diff --cached
diff --git a/hello b/hello
index e63164d..562080a 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,3 @@
 hello
-worlds
+world
 Yay!

如果解决冲突并add后, 就可以看到实际diff了.

在解决冲突的过程中, 可以使用git log快速找到变更的地方:

$ git log --merge --left-right -p
commit < 944b769511d84455382a53c947f262db97dcbb09
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Tue Jun 30 22:11:42 2015 +0800

    add master

diff --git a/hello b/hello
index ce01362..e63164d 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
 hello
+worlds
+Yay!

commit > 4e2d91547b64b9ee2f26de286175819502d8c262
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Tue Jun 30 09:35:05 2015 +0800

    one world

diff --git a/hello b/hello
index ce01362..562080a 100644
--- a/hello
+++ b/hello
@@ -1 +1,3 @@
 hello
+world
+Yay!

--merge会使用MERGE_HEAD来找到两者的差异.

git ls-files -u 可以查看工作树中未合并的文件:

$ git ls-files -u
100644 ce013625030ba8dba906f756967f9e9ca394464a 1       hello
100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2       hello
100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3       hello

分别是1. base, 2. ours, 3. theirs

$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello

$ git cat-file -p e63164d9518b1e6caf28f455ac86c8246f78ab70
hello
worlds
Yay!

$ git cat-file -p 562080a4c6518e1bf67a9f58a32a67bff72d4f00
hello
world
Yay!

git diff支持在这两个版本之间互相diff:

$ git diff :1:hello :3:hello
diff --git a/:1:hello b/:3:hello
index ce01362..562080a 100644
--- a/:1:hello
+++ b/:3:hello
@@ -1 +1,3 @@
 hello
+world
+Yay!

合并冲突的回退. 如果在冲突过程中想要回退, 可以:

$ git reset --hard HEAD

如果在冲突解决后想到放弃, 回退(或终止), 可以:

$ git reset --hard ORIG_HEAD

如果在解决冲突时解决方案失败, 比如弄得非常乱, 想重新回到冲突的原始状态, 重新解决, 可以:

$ git checkout -m

交叉合并(criss-cross merge), 是指修改在分支间来回合并.

TODO 给出例子

Degenerate merge(退化合并)(中文翻译真蛋疼, 还是原词好理解一些), 就是指merge后不引入一个合并提交:

关于合并的策略, 用-s参数指定. 有5种(man git-merge, 见MERGE STRATEGIES一节):

resolve 曾经是Git的默认策略, 现已改为recursive. 处理针对两个分支合并的情况, 定义两个分支的共同祖先, 然后进行三路合并(3-way merge algorithm)

recursive 是默认的策略. 和resolve类似, 也是针对两个分支合并的情况. 可以处理多个共同祖先的情况, 进行三路合并. 在Linux的开发历史上, 此策略证明会比resolve导致更少的冲突而没有故障. 此策略还有很多策略选项, 用-X来指定, 常用的有ourstheirs.

octopusd 针对合并两个分支以上的情况. 当超过两个分支以上合并时, 这个是默认策略

ours 可以合并任意数量的分支. 但它实际是丢弃了其它分支的修改, 只使用当前分支的修改. 结果是和当前HEAD一样, 只是会标记其它分支也是父提交. 注意这个和recursive的-X ours策略选项不一样.

subtree 这个不理解 TODO

关于策略ours和recuresive的策略选项ours, theirs, 在我之前的博客有总结到: Git merge strategy - ours and theirs

用到现在, 策略这块基本就是用的默认策略, 其它复杂的情况还没用过, 也没有更多的体会...

10. 更改提交

更改历史的哲学 TODO

一般原则, 只要没有其它开发人员获取到你的版本库副本, 那么可以任意的修改历史. 但是如果已经被他人同步过, 则不应该重写或修改历史. 不然对其他人来说是个灾难 :(

最常用的改写历史的应该是:

$ git commit --amend

经常用这个来修改最后一次提交, 或其提交日志, 提交作者等.

其次就是reset操作, 调整HEAD引用指向给定的提交.

$ git reset

它有三个选项:

--soft软重置, 是副作用最小的, 只变化了HEAD引用, 比如:

$ git reset --soft HEAD^

会重置到上一次提交, 并把最后一次提交的修改移到索引, 原先索引和未暂存的内容不会有影响

--mixed 副作用中等. 会影响索引的内容. 比如:

$ git reset HEAD^

会重置到上一次提交, 并把最后一次提交的修改移到未暂存区, 且如果索引原先有修改, 也会被移到未暂存区.

--hard是杀伤力最大的. 影响到了工作目录的内容. 如:

$ git reset --hard HEAD^

会重置到上一次提交, 并把最后一次提交的修改删除, 且如果索引和暂存区原来有修改, 都会删除.

前阵子在这里吃了一个大亏, wiki库有些修改积压了一阵子, 没提交(甚至都没add到索引, 不然还可以通过reflog找回...), 一个误操作用了--hard, 一夜回到解放前...

另外, 这里有一个有用的引用ORIG_HEAD, 在git reset时, git会把原始的HEAD存到ORIG_HEAD, 这样如果误操作后想返回到原来的版本, 可以:

$ git reset --hard ORIG_HEAD

接着是cherry-pick, 将指定的commit应用到当前分支.

比如开发分支dev有个修复bug的提交dev~2, 现在需要临时引入到master分支应用上, 就可以用cherry-pick来操作:

# 当前在master分支上
$ git cherry-pick dev~2

cherry-pick还可以重建一系列提交, 比如一个分支的某两个提交想互换顺序, 可以引入一个新分支, 通过cherry-pick按预计顺序引入到新分支.

revert 和 cherry-pick 类似, 不过它的作用是应用指定提交的逆过程. 一般用于修复某个有问题的提交, 通过撤销来修复.

比如master分支上的某个提交master~3的修改有问题, 现在想撤销, 如果是线上公布的分支, 不建议rebase, 可以通过revert撤销:

$ git revert master~3

这是引入一个新的提交, 是master~3的逆修改, 添加的就删除, 删除的就添加.

rebase(中文"变基", 很操蛋的翻译) 是用来改变一串提交以什么为基础.

Forward-port(向前移植) local commits to the updated upstream head

常见的调用git rebase的两个命令(简化了来自man git-rebase的语法):

$ git rebase [-i | --interactive] [options] [--onto <newbase>] [<upstream> [<branch>]]
$ git rebase --continue | --skip | --abort | --edit-todo

首先最常用的功能, 是保持当前开发分支相对另一个分支是最新的.

比如一个多人协作的仓库, master分支是公共分支, 个人分支mydev, 如果mydev因一些事情耽搁几天, 这是master有了一些新提交, mydev需要用到, 可以将mydev移到master上最新的点分叉出来, 这样也可以保证分支图不会拉的太长, 简单步骤就是:

$ git checkout mydev
$ git rebase master

或者:

$ git rebase master mydev

这也是上面第一条命令的语法, 最少需要指定某个上游, 基于此上游迁移. 默认是对当前分支做迁移

如, 原始的提交DAG图是:

(master) $ git log --graph --branches --all --decorate --oneline
* 00698e1 (HEAD, master) update master_file
* d07ac54 add master_file
| * 07e33e5 (mydev) update mydev_file
| * dceb4d8 add mydev_file
|/
* a8923b3 master 1
* 942c6af init

现在我要将mydev分支移从d07ac54分叉, 作了rebase后:

(master) $ git rebase master~1 mydev
First, rewinding head to replay your work on top of it...
Applying: add mydev_file
Applying: update mydev_file

(mydev) $ git log --graph --branches --all --decorate --oneline
* 8756814 (HEAD, mydev) update mydev_file
* d88bae4 add mydev_file
| * 00698e1 (master) update master_file
|/
* d07ac54 add master_file
* a8923b3 master 1
* 942c6af init

现在8756814 和d88bae4是mydev分支的两个提交, 和以前的sha-1 id不同了, 因为基于的历史树不一样.

注意指定branch后, 执行rebase后会checkout到那个分支.

通过--onto参数, 可以把一条分支上的开发线整个移到另一个分支:

(another_dev) $ git log --graph --branches --all --decorate --oneline
* 9b81f22 (HEAD, another_dev) update another_dev_file
* e32f75f add another_dev_file
| * 00698e1 (master) update master_file
| * d07ac54 add master_file
| | * 07e33e5 (mydev) update mydev_file
| |/
|/|
* | dceb4d8 add mydev_file
|/
* a8923b3 master 1
* 942c6af init

(another_dev) $ git rebase master
First, rewinding head to replay your work on top of it...
Applying: add mydev_file
Applying: add another_dev_file
Applying: update another_dev_file

(another_dev) $ git log --graph --branches='*dev' --all --decorate --oneline
* d632eb3 (HEAD, another_dev) update another_dev_file
* 824c829 add another_dev_file
* 98f4d44 add mydev_file
* 00698e1 (master) update master_file
* d07ac54 add master_file
| * 07e33e5 (mydev) update mydev_file
| * dceb4d8 add mydev_file
|/
* a8923b3 master 1
* 942c6af init

经过常规的rebase后, 可以看到, 原先在mydev上的两个提交现在有了两次. 改为--onto方式:

(another_dev) $ git rebase --onto master mydev~1 another_dev
First, rewinding head to replay your work on top of it...
Applying: add another_dev_file
Applying: update another_dev_file

(another_dev) $ git --no-pager log --graph --branches='*dev' --all --decorate --oneline
* 6e661ce (HEAD, another_dev) update another_dev_file
* 870b906 add another_dev_file
* 00698e1 (master) update master_file
* d07ac54 add master_file
| * 07e33e5 (mydev) update mydev_file
| * dceb4d8 add mydev_file
|/
* a8923b3 master 1
* 942c6af init

如果rebase过程中发生了冲突, 则需要用到最开始说的第二条命令了.

遇到冲突时, rebase会在冲突的提交点挂起, 等待处理冲突

git rebase -i 以交互式的方式处理指定范围的rebase操作, 常用于修改以前某次的提交.

一般找到需要需要处理的某个点, 比如abcdef, 则: TODO

$ git rebase -i abcdef~1

此时会进入编辑器, 然后对每个commit 指定操作, 默认是pick, 显示如:

pick d07ac54 add master_file
pick 00698e1 update master_file

# Rebase a8923b3..00698e1 onto a8923b3 (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
#
# 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

提示内容非常详细, 如果要对某个commit进行指定操作, 则修改pick为其它即可, 可以用简写或全称, 如e 或 edit都是编辑修改

exec这个简单试了下, 不是基于某个commit id, 而是在预期的位置做一些shell操作, 如:

pick 8cc5fcb addxxx master_file
exec touch xxx
pick ac7a580 update master_file

看了下官方的man手册, 在针对每个版本都做测试时非常有用, 如:

pick 5928aea one
exec make test
pick 04d0fda two
exec make test
pick ba46169 three
exec make test
pick f4593f9 four
exec make test

另外, git rebase -i进入编辑器后的注释提示信息相当详细, 除了介绍上面的几个操作命令, 还有其它说明:

另外, rebase是把当前分支历史(带合并)线性化到了指定分支. 所以如果移动范围有合并提交, 默认会被线性化, 通过参数-p/--preserve-merges可以保留合并提交. 不然历史树就和以前有较大出入.

rebase后, 如果后悔了, 可以:

$ git reset --hard ORIG_HEAD

11. 储藏和引用日志

储藏(stash)是一个很常用的功能, 工作目录有一些修改时, 如:

直接执行git stash则储藏当前的修改, 默认是save子命令

$ git stash
Saved working directory and index state WIP on master: 7f63cf0 update master file
HEAD is now at 7f63cf0 update master file

这里WIP是work in process的缩写

stash的数据结构是一个, 即先进后出FILO(first in, last out), 相应的还原最近一个储藏则是:

$ git stash pop

查看stash栈:

$ git stash list
stash@{0}: WIP on master: 7f63cf0 update master file
stash@{1}: WIP on master: 7965691 master

这里储藏时是用的默认的信息, 指出了分支, 当前sha1 id.

stash@{0}是储藏的编号, 根据FILO的原则, 0表示最新的一个储藏

也可以手动输入信息:

$ git stash save 'do a stash'
Saved working directory and index state On master: do a stash
HEAD is now at 7965691 master
$ git stash list
stash@{0}: On master: do a stash

stash也是一个引用指针(refs/stash), 所以也可以使用这个引用来查看:

$ git show-branch stash
[stash] On master: do a stash

另外, 要注意, git stash并不是把最近一次储藏替换当前文件, 而是会做一个合并的操作, 这个是非常智能的.

git stash pop时, 如果成功, 则会删除相应储藏, 如果失败, 如产生冲突, 则会保留储藏.

删除储藏, 默认删除最近一个, 也可以手动指定某个储藏:

$ git stash drop

因为stash pop成功后会清掉, 可以使用apply只做应用还原, 但是不清理, 和drop一样, 也可以指定某个储藏:

$ git stash apply

查看储藏的内容, 不加-p则只显示stat信息:

$ git stash show -p stash@{1}

也可以

$ git show stash@{1}

一些常用的选项:

储藏是本地的操作, 所以储藏的object是不会提交到远程的

储藏还有一个给力的功能, 转化为分支:

$ git stash branch <branch name> stash@{5}

因为某个stash后可能有增加了很多大的修改, 这是可以单拉一个分支来处理.

不过stash和branch还是要区别使用, stash更多是针对一个临时的操作, 最好不要积压太久, 随时保持储藏栈清理; 所以相应也不要过多的将stash转为一个branch. 至少至今为止我还没有做过这样的操作...

引用日志(reflog)

有时, 一些危险的操作, 会导致本地丢失一些提交, 如没有push到远程仓库前误操作执行了git reset HEAD^.

使用引用分支可以恢复丢失的提交.

修改引用或更改分支头的Git操作都会记录引用日志:

默认情况下, 引用日志在非裸版本库是弃用的, 在裸版本库(bare)中是禁用的. 可以通过如下开启:

$ git config core.logAllRefUpdates true

查看引用日志:

$ git reflog [show]
7f63cf0 HEAD@{0}: reset: moving to HEAD@{1}
3dd62fb HEAD@{1}: merge mod: Fast-forward
7f63cf0 HEAD@{2}: checkout: moving from master to master
7f63cf0 HEAD@{3}: checkout: moving from master to master
7f63cf0 HEAD@{4}: checkout: moving from mod to master
3dd62fb HEAD@{5}: commit: update file in mod
...
73ed934 HEAD@{12}: commit: dev
38d4a3d HEAD@{13}: checkout: moving from master to dev
38d4a3d HEAD@{14}: commit (initial): add file

子命令show可有可无, 默认输出的引用是HEAD, 所以在上面也可以看到都是HEAD@{X}

因为分支名也是引用, 所以后接分支名可以查看某个分支的引用日志

$ git reflog show master
3dd62fb master@{0}: reset: moving to ORIG_HEAD
7f63cf0 master@{1}: reset: moving to HEAD^

或者:

$ git reflog show refs/heads/master
3dd62fb refs/heads/master@{0}: reset: moving to ORIG_HEAD
7f63cf0 refs/heads/master@{1}: reset: moving to HEAD^

针对输出结果, 第一列的sha1 id和第二列的别名是对应的, 第三列只出相应的操作类型和操作内容

HEAD@{0} 始终指向当前的HEAD, 这里可以看到HEAD@{14}是第一次提交

例子:

$ git ll
* 3dd62fb - (HEAD, mod, master) update file in mod (17 hours ago) <Tanky Woo>
* 7f63cf0 - update master file (3 days ago) <Tanky Woo>
...

$ git reflog | head -n 1
3dd62fb HEAD@{0}: checkout: moving from mod to master

(master) $ git reset --hard HEAD^
HEAD is now at 7f63cf0 update master file

$ git reflog | head -n 2
7f63cf0 HEAD@{0}: reset: moving to HEAD^
3dd62fb HEAD@{1}: checkout: moving from mod to master

现在本地执行了一次reset, 如果发现是误操作, 想要返回, 但是本地的修改没有推送到远程, 这是可以通过reflog撤回:

$ git reset --hard HEAD@{1}
HEAD is now at 3dd62fb update file in mod

表明要重置到老的HEAD版本.

当然, 这种情形下还有一个方法, 使用ORIG_HEAD:

$ git reset --hard ORIG_HEAD
HEAD is now at 3dd62fb update file in mod

HEAD@{1}, 如果使用形式@{X}, 则表示当前分支:

$ git show @{0}
commit 3dd62fb79377c7d0419ca12183db780489287731
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Sat Jun 20 21:56:27 2015 +0800

...

$ git reflog show @{0}
3dd62fb refs/heads/master@{0}: reset: moving to ORIG_HEAD
7f63cf0 refs/heads/master@{1}: reset: moving to HEAD^
...

另外, reflog的花括号内还可以指定时间限定符, 如:

TankyWoo $ /tmp/test/ (master) $ git reflog 'HEAD@{1 hours ago}'
3dd62fb HEAD@{Sat Jun 20 22:00:12 2015 +0800}: reset: moving to HEAD@{1}
3dd62fb HEAD@{Sat Jun 20 21:56:47 2015 +0800}: merge mod: Fast-forward
7f63cf0 HEAD@{Sat Jun 20 21:56:38 2015 +0800}: checkout: moving from master to master

还支持如:

这里注意以下是等价的:

$ git log 'HEAD@{2 days ago}'
$ git log HEAD@{2.days.ago}
$ git log HEAD@{2-days-ago}

注意第一个的单引号, 否则shell报错.

对于可达或不可达的引用日志, 都有一个默认的过期时限.

也可以手动设置过期时间:

$ git reflog expire --expire='1 day ago' --all
$ git gc
Counting objects: 15, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 15 (delta 1)

还可以通过git reflog delete删除指定的条目

内部细节:

引用日志都是存储在 .git/logs

$ tree .git/logs/
.git/logs/
├── HEAD
└── refs
    └── heads
        ├── master
        └── mod

引用日志也是一个本地概念, 和stash一样, 是不会被推送到远程, 也不会在克隆时被复制下来.

一篇不错的文章: Git Tip of the Week: Reflogs

12. 远程版本库

远程版本库(remote)是一个引用或句柄, 通过文件系统或网络指向另外一个版本库. 可以使用远程版本库作为简称, 代替又长又复杂的Git URL.

TODO

Git版本库分为:

在git clone时, 原始版本库的本地开发分支refs/heads/xxx, 会成为新克隆版本库的远程分支refs/remotes/xxx. 原始版本库的远程分支不会克隆过来.

clone下来默认的远程版本库名称是origin, 可以通过--origin改为其它的.

针对远程版本库的操作, 有常用的git pull, git fetch, git push, 还有git remote, git ls-remote 列出远程版本库的引用列表.(相对的 git show-ref显示本地的引用列表)

关于git remote, 这个是我比较常用的一个命令, 介绍一下一些功能:

最简单的查看远程仓库的URL:

$ git remote -v

添加一个上游版本库(upstream repo):

$ git remote add upstream <git url>

添加上游版本库后, 我想防止误操作把提交push到这个仓库, 可以修改push url:

$ git remote set-url --push origin 'do not pushing'

查看远程版本库的详细内容(这个操作会访问远程仓库, 而不是直接基于本地配置):

$ git remote -v show origin
* remote origin
  Fetch URL: git@github.com:tankywoo/simiki.git
  Push  URL: git@github.com:tankywoo/simiki.git
  HEAD branch: master
  Remote branches:
    dev              tracked
    jinja-extensions tracked
    master           tracked
    project-tools    tracked
  Local branches configured for 'git pull':
    dev              merges with remote dev
    jinja-extensions merges with remote jinja-extensions
    master           merges with remote master
    project-tools    merges with remote project-tools
  Local refs configured for 'git push':
    dev              pushes to dev              (up to date)
    jinja-extensions pushes to jinja-extensions (up to date)
    master           pushes to master           (up to date)
    project-tools    pushes to project-tools    (up to date)

虽然很多操作都可以直接修改.git/config, 但是就像修改/etc/sudoers使用visudo命令, 而不是直接vi /etc/sudoers一样, 命令的可靠性要大于手动.

git支持的传输协议有本地文件系统,git原生协议,http/https协议,rsync协议等. 以前http/https协议效率很多, 不过在1.6.6版本后,效率已经和git原生协议的效率差不多了.

查看.git/config, 可以看到fetch refspec配置, 默认是:

[remote "origin"]
        url = git@github.com:tankywoo/simiki.git
        fetch = +refs/heads/*:refs/remotes/origin/*

refspec语法:

[+]source:destination

source表示源引用, destination表示目标引用, 两者用冒号(:)分隔. 前面的加号(+)是可选的, 有加号表示不会在传输过程中进行正常的快进安全检查. 星号(*)表示通配符匹配

git fetch 和 git push 都用到refspec, refspec的源和目标是依赖于执行的操作:

操作                         目标
push        推送的本地引用     更新的远程引用
fetch       抓去的远程引用     更新的本地引用

这里有个很多人都没注意到的地方, push 的对立面不是 pull, 而是 fetch. 很多人可能因为命令名以及上手就学的push/pull, 而在这里有了错误的认识.

TODO

13. 版本库管理(略)

14. 补丁

虽然Git协议非常方便, 但是还是有些理由来使用补丁(patch)的: 1. 防火墙限制, 比如有些公司不允许做推送操作. 2. 将补丁发到公共邮箱(mailing list), 方便同行评审.

关于补丁的一系列操作从生成补丁到发送补丁最后到应用补丁:

git format-patch 命令以邮件消息的形式生成一个补丁. 常用的生成方式(参数)有几种方式:

git diff 是 git format-patch的核心, 不过还是有些区别, git diff 生成单个的差异合集, 而git format-match是为每个提交都生成单个patch; 二是git diff不带邮件头等信息.

git format-patch 和 git log -p --pretty=email的输出基本一致, 前者多了一个--stats信息, 以及最后的Git版本号(这里是2.3.5):

(master) % git format-patch -1
0001-D.patch

(master*) % more 0001-D.patch
From 50e7530441d9836b8643e3a3134b9072c7763e60 Mon Sep 17 00:00:00 2001
From: Tanky Woo <wtq1990@gmail.com>
Date: Tue, 7 Jul 2015 07:45:38 +0800
Subject: [PATCH] D

---
 file | 1 +
 1 file changed, 1 insertion(+)

diff --git a/file b/file
index b1e6722..8422d40 100644
--- a/file
+++ b/file
@@ -1,3 +1,4 @@
 A
 B
 C
+D
--
2.3.5

(master*) % git log -p -1 --pretty=email
From 50e7530441d9836b8643e3a3134b9072c7763e60 Mon Sep 17 00:00:00 2001
From: Tanky Woo <wtq1990@gmail.com>
Date: Tue, 7 Jul 2015 07:45:38 +0800
Subject: [PATCH] D

diff --git a/file b/file
index b1e6722..8422d40 100644
--- a/file
+++ b/file
@@ -1,3 +1,4 @@
 A
 B
 C
+D

例子, 首先创造环境:

(master) % git init
(master) % echo A > file ; git add file ; git ci -mA
(master) % echo B >> file ; git ci -mB file
(master) % echo C >> file ; git ci -mC file
(master) % echo D >> file ; git ci -mD file

(master) % git log --graph --oneline --decorate master
* 50e7530 (HEAD, master) D
* 1f0c2fd C
* 2d1b9ed B
* d900590 A

首先 -X 指定提交数:

(master*) % git format-patch -2
0001-C.patch
0002-D.patch

(master*) % git format-patch -3
0001-B.patch
0002-C.patch
0003-D.patch

默认情况下, git为每个补丁生成单独的文件, 用一序列数字加上提交日志消息为其命令.

指定提交范围:

(master) % git format-patch master~3..master~1
0001-B.patch
0002-C.patch

指定某次提交:

(master*) % git format-patch master~3
0001-B.patch
0002-C.patch
0003-D.patch

# 如果要包含提交A, 即首次提交:
(master*) % git format-patch --root master~3
0001-A.patch

# 全部提交, 带上 --root 参数
(master*) % git format-patch --root master
0001-A.patch
0002-B.patch
0003-C.patch
0004-D.patch

几种方式还可以互相配合:

(master*) % git format-patch --root master -2
0001-C.patch
0002-D.patch

复杂的例子:

(master) % git checkout -b alt master~2
(alt) % echo X >> file ; git ci -mX file
(alt) % echo Y >> file ; git ci -mY file
(alt) % echo Z >> file ; git ci -mZ file

# 这里使用 --all 可以画出全部分支的ASCII图
(alt) % git log --graph --oneline --decorate --all master
* fb5c9a0 (HEAD, alt) Z
* f67540b Y
* b50d656 X
| * 50e7530 (master) D
| * 1f0c2fd C
|/
* 2d1b9ed B
* d900590 A

接着合并alt分支, 处理冲突, 再增加一个新提交:

(alt) % git checkout master
(master) % git merge alt
# ... 处理冲突 ...
(master) % echo F >> file ; git ci -mF file

现在的结构:

(master) % git log --graph --oneline --decorate --all master
* bfad1bc (HEAD, master) F
*   1dd3011 Merge branch 'alt'
|\
| * fb5c9a0 (alt) Z
| * f67540b Y
| * b50d656 X
* | 50e7530 D
* | 1f0c2fd C
|/
* 2d1b9ed B
* d900590 A

(master) % git show-branch --more=10
! [alt] Z
 * [master] F
--
 * [master] F
+* [alt] Z
+* [alt^] Y
+* [alt~2] X
 * [master~2] D
 * [master~3] C
+* [master~4] B
+* [master~5] A

生成master~2..master范围的补丁:

(master) % git format-patch master~2..master
0001-X.patch
0002-Y.patch
0003-Z.patch
0004-F.patch

注意: 合并提交本身是不会生成补丁.

关于范围解析, TODO P250

邮件补丁 TODO

git有两条命令来应用补丁:

基础最开始的A-D的提交图, 增加一个新提交E, 设成patch:

(master) % echo E >> file ; git ci -mE file
(master) % git format-patch -1
0001-E.patch

(master) % git log --graph --oneline --decorate --all master
* 022cb18 (HEAD, master) E
* 50e7530 D
* 1f0c2fd C
* 2d1b9ed B
* d900590 A

# 重新构建A-D的提交历史, 或者直接reset撤回D

(master*) % more 0001-E.patch
From 022cb1861d3ae5a500c5152464cece0d3e0082b0 Mon Sep 17 00:00:00 2001
From: Tanky Woo <wtq1990@gmail.com>
Date: Tue, 7 Jul 2015 08:46:22 +0800
Subject: [PATCH] E

---
 file | 1 +
 1 file changed, 1 insertion(+)

diff --git a/file b/file
index 8422d40..8fda00d 100644
--- a/file
+++ b/file
@@ -2,3 +2,4 @@ A
 B
 C
 D
+E
--
2.3.5

(master*) % git am 0001-E.patch
Applying: E

commit b98a9b55f6952f21ea64b28a478e0936744f8039
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Tue Jul 7 08:46:22 2015 +0800

    E

diff --git a/file b/file
index 8422d40..8fda00d 100644
--- a/file
+++ b/file
@@ -2,3 +2,4 @@ A
 B
 C
 D
+E

git am后会生成新的提交

如果使用 git apply, 则会把修改保留在工作目录, 但是不会提交:

(master*) % git reset --hard HEAD^
HEAD is now at 50e7530 D

(master*) % git apply 0001-E.patch

(master*) % git diff
diff --git a/file b/file
index 8422d40..8fda00d 100644
--- a/file
+++ b/file
@@ -2,3 +2,4 @@ A
 B
 C
 D
+E

有时在仓库里作了一些修改没有提交, 也可以用git diff生成一个patch文件, 然后传给别人, 对象可以直接应用上这个patch:

$ git apply patch.file

在没有git的情况下, 可以使用patch命令:

$ patch -p1 < patch.file

参考:

关于复杂的情况, 如之前A-Z的情况, 中间有个分支alt从B点分出去, 再重复贴一次图:

(master) % git log --graph --oneline --decorate --all master
* bfad1bc (HEAD, master) F
*   1dd3011 Merge branch 'alt'
|\
| * fb5c9a0 (alt) Z
| * f67540b Y
| * b50d656 X
* | 50e7530 D
* | 1f0c2fd C
|/
* 2d1b9ed B
* d900590 A

(master) % git show-branch --more=10
! [alt] Z
 * [master] F
--
 * [master] F
+* [alt] Z
+* [alt^] Y
+* [alt~2] X
 * [master~2] D
 * [master~3] C
+* [master~4] B
+* [master~5] A

生成除A以外的patches:

(master) % git format-patch -o /tmp/patches master~5
/tmp/patches/0001-B.patch
/tmp/patches/0002-C.patch
/tmp/patches/0003-D.patch
/tmp/patches/0004-X.patch
/tmp/patches/0005-Y.patch
/tmp/patches/0006-Z.patch
/tmp/patches/0007-F.patch

现在回到提交A, 然后应用这些patches:

(master) % git reset --hard HEAD~5
HEAD is now at d900590 A

(master) % git am /tmp/patches/*
Applying: B
Applying: C
Applying: D
Applying: X
error: patch failed: file:1
error: file: patch does not apply
Patch failed at 0004 X
The copy of the patch that failed is found in:
   /path/to/myrepo/.git/rebase-apply/patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

(master) % more .git/rebase-apply/patch
---
 file | 1 +
 1 file changed, 1 insertion(+)

diff --git a/file b/file
index 35d242b..7f9826a 100644
--- a/file
+++ b/file
@@ -1,2 +1,3 @@
 A
 B
+X
--
2.3.5

(master) % git status
On branch master
You are in the middle of an am session.
  (fix conflicts and then run "git am --continue")
  (use "git am --skip" to skip this patch)
  (use "git am --abort" to restore the original branch)

nothing to commit, working directory clean

(master) % git show-branch --more=4
! [alt] Z
 * [master] D
--
 * [master] D
 * [master^] C
 * [master~2] B
+  [alt] Z
+  [alt^] Y
+  [alt~2] X
+  [alt~3] B
+* [master~3] A

执行am失败了, 并且给了一些有用的提示操作. 不过这里失败了就是失败了, 没有类似合并冲突的解决的脏数据遗留下来.

.git/rebase-apply/patch文件还保留了失败时修改的内容, 老版本git在.dotest目录下. 这个文件是要清理掉的, 不然后续执行am会报错.

The copy of the patch that failed is found in:
   /path/to/myrepo/.git/rebase-apply/patch

这时使用-3/-3way三路合并的方式来解决这个问题:

(master) % git reset --hard HEAD~3
HEAD is now at d900590 A

# 这里如果没清理 .git/rebase-apply/ 目录的话就会报错
(master) % git am -3 /tmp/patches/*
previous rebase directory /path/to/myrepo/.git/rebase-apply still exists but mbox given.

(master) % rm -rf .git/rebase-apply

继续重新执行三路合并应用patch:

(master) % git am -3 /tmp/patches/*
Applying: B
Applying: C
Applying: D
Applying: X
Using index info to reconstruct a base tree...
M       file
Falling back to patching base and 3-way merge...
Auto-merging file
CONFLICT (content): Merge conflict in file
Failed to merge in the changes.
Patch failed at 0004 X
The copy of the patch that failed is found in:
   /path/to/myrepo/.git/rebase-apply/patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

(master*) % git status
On branch master
You are in the middle of an am session.
  (fix conflicts and then run "git am --continue")
  (use "git am --skip" to skip this patch)
  (use "git am --abort" to restore the original branch)

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

        both modified:   file

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

这次和之前不一样, 虽然失败了, 但是给了一个机会来处理:

(master*) % vi file
(master*) % git add file
(master*) % git am --continue
Applying: X
Applying: Y
Using index info to reconstruct a base tree...
M       file
Falling back to patching base and 3-way merge...
Auto-merging file
Applying: Z
Using index info to reconstruct a base tree...
M       file
Falling back to patching base and 3-way merge...
Auto-merging file
Applying: F

现在的结构图:

(master) % git show-branch --more=10
! [alt] Z
 * [master] F
--
 * [master] F
 * [master^] Z
 * [master~2] Y
 * [master~3] X
 * [master~4] D
 * [master~5] C
 * [master~6] B
+  [alt] Z
+  [alt^] Y
+  [alt~2] X
+  [alt~3] B
+* [master~7] A


(master) % git --no-pager  log --graph --oneline --decorate --all
* b16dc1a (HEAD, master) F
* 79f431d Z
* 9642ba2 Y
* 816197e X
* 0d3f91b D
* b30ce22 C
* 7dd8d42 B
| * fb5c9a0 (alt) Z
| * f67540b Y
| * b50d656 X
| * 2d1b9ed B
|/
* d900590 A

应用patch后的结构是线性的

如果要想和原来的结构保持一致, 估计只能手动来处理之间的关系了:

# 当前停留在B点
(master) % git am /tmp/patches/0002-C.patch /tmp/patches/0003-D.patch
Applying: C
Applying: D

(master) % git show-branch --more=3
[master] D
[master^] C
[master~2] B
[master~3] A

(master) % git co -b alt master~2
Switched to a new branch 'alt'

(alt) % git am /tmp/patches/0004-X.patch /tmp/patches/0005-Y.patch /tmp/patches/0006-Z.patch
Applying: X
Applying: Y
Applying: Z

(alt) % git co master
Switched to branch 'master'

(master) % git show-branch --more=10
! [alt] Z
 * [master] D
--
+  [alt] Z
+  [alt^] Y
+  [alt~2] X
 * [master] D
 * [master^] C
+* [alt~3] B
+* [alt~4] A

(master) % git merge alt
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.

# ... 处理冲突 ...
(master*) % git ci
[master eab9bcf] Merge branch 'alt'

(master) % git am /tmp/patches/0007-F.patch
Applying: F

(master) % git show-branch --more=10
! [alt] Z
 * [master] F
--
 * [master] F
+* [alt] Z
+* [alt^] Y
+* [alt~2] X
 * [master~2] D
 * [master~3] C
+* [master~4] B
+* [master~5] A

15. 钩子

Git在操作如提交, 补丁等事件时, 可以通过钩子(hook)来触发一些脚本.

大多数钩子分为两类:

如果钩子以非0状态退出(如 exit 1), 则表示执行失败, Git动作中止; 不过后置的状态无法影响Git操作的结果.

谨慎的使用钩子:

每个版本库新建的时候, 会默认提供一些钩子模板, 以.sample结尾, 状态是禁止的:

(master*) $ tree .git/hooks
.git/hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit
├── pre-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── prepare-commit-msg.sample
└── update.sample

如果要使一个钩子起作用, 必须保证:

也可以自己写钩子.

因为钩子是针对特定动作的特定时刻起作用的, 所以钩子名(脚本名)需要为特定范围的名字, 可以见 git help hooks 查看可用的钩子.

提交相关钩子:

pre-commit -> prepare-commit-msg -> commit-msg -> post-commit

补丁相关钩子:

applypatch-msg -> pre-applypatch -> post-applypatch

push相关钩子(Git服务端执行):

pre-receive -> update -> post-receive -> post-update

16. 合并项目

项目管理过程中经常会遇到需要将别的项目加入到自己项目中.

比如某个网站开发, 需要用到一些前端的库. 最简单的方法就是将压缩包下载解压后放到本地指定目录, 升级的时候覆盖就行. 但是一是每次升级这些第三方库时, 还需要做一次无意义的提交, 如果频繁修改是很坑爹的; 另外就是升级也比较麻烦, 并且如果第三方依赖比较大, 也占用了很多无意义的空间.

又比如一个项目的某个子目录是一些功能函数, 现在另一个项目也想用到. 以前用svn时, 是支持部分检出(partial checkout), 但是git是不支持的.

针对上面的这类情况, 都可以考虑子模块(submodule), 说白了就是模块化, 一个模块负责好相应的功能, 其它需要用到它的都使用这个模块, 保证统一性; 并且使用了git的子模块, 对于升级维护都比较方便, 也不需要考虑太多第三方依赖库需要的空间问题.

git submodule由两个独立功能组合:

gitlink是一个从树对象(tree object)到一个提交对象(commit object)的链接. 之前介绍过git对象时, 一般情况下, 树对象指向的是一组blob对象和树对象. 所以这里是比较特殊的情况.

现在有一个仓库目录git-main, 子目录git-sub, 分别是两个库:

git-main/ (master*) $ tree
.
├── git-sub
│   └── sub.txt
└── hello.txt

1 directory, 2 files

git-main/ (master*) $ git remote -v
origin  https://git.example.com/tankywoo/git-main.git (fetch)
origin  https://git.example.com/tankywoo/git-main.git (push)

git-main/ (master*) $ cd git-sub
git-main/git-sub/ (master) $ git remote -v
origin  https://git.example.com/tankywoo/git-sub.git (fetch)
origin  https://git.example.com/tankywoo/git-sub.git (push)

git-main/ (master*) $ gst
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        git-sub/

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

现在将 git-sub 加入到 git-main库:

git-main/ (master*) $ git add git-sub

git-main/ (master*) $ gst
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   git-sub

git-main/ (master*) $ git ci -m 'import git-sub'
[master 36c4d6e] import git-sub
 1 file changed, 1 insertion(+)
 create mode 160000 git-sub

git-main/ (master) $ git ls-tree HEAD
160000 commit 1efb773d1740a7ad66e5b53bdf66f10c66440ce5  git-sub
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    hello.txt

这里git-sub子目录是commit类型, 模式码是160000. 这是一个gitlink对象.

注意: 通常情况下, git add /path/togit add /path/to/(有无斜线结束符)是一样的. 但是在这里创建gitlink时, 两者是不一样的, 如果加了斜线结束符, 则不是创建gitlink, 而是把子目录下的所有文件都添加进来.

git-main/ (master) $ cp -r git-sub git-non-sub

git-main/ (master*) $ git add git-non-sub/

git-main/ (master*) $ gst
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   git-non-sub/sub.txt

git-main/ (master*) $ git ci -m 'import git-non-sub'
[master 249c085] import git-non-sub
 1 file changed, 1 insertion(+)
 create mode 100644 git-non-sub/sub.txt

git-main/ (master) $ git ls-tree HEAD
040000 tree 04756934bd18bee46b7978441ff47dfd695e6344    git-non-sub
160000 commit 1efb773d1740a7ad66e5b53bdf66f10c66440ce5  git-sub
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    hello.txt

这里git-non-sub就是一个普通的树对象.

git 将 gitlink当做一个简单的指针值或者其它版本库的引用. 绝大部分git操作(如clone)不会对gitlink解引用, 并作用在子模块版本库上.

git-main/ (master) $ tree
.
├── git-non-sub
│   └── sub.txt
├── git-sub
│   └── sub.txt
└── hello.txt

2 directories, 3 files

git-main/ (master) $ cd ..
 $ git clone git-main git-main2
Cloning into 'git-main2'...
done.
 $ cd git-main2

git-main2/ (master) $ tree
.
├── git-non-sub
│   └── sub.txt
├── git-sub
└── hello.txt

2 directories, 2 files

git-main2/ (master) $ cd git-sub
git-main2/git-sub/ (master) $ git remote -v
origin  /Users/TankyWoo/dev_env/git-submodule/git-main (fetch)
origin  /Users/TankyWoo/dev_env/git-submodule/git-main (push)

继续在克隆出来的git-main2上测试:

git-main2/ (master) $ git ls-files --stage -- git-sub
160000 1efb773d1740a7ad66e5b53bdf66f10c66440ce5 0       git-sub

git-main2/ (master) $ rmdir git-sub
git-main2/ (master*) $ git clone https://git.example.com/tankywoo/git-sub.git git-sub
Cloning into 'git-sub'...
Username for 'https://git.example.com': tankywoo
Password for 'https://tankywoo@git.example.com':
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Checking connectivity... done.

git-main2/ (master) $ cd git-sub
git-main2/git-sub/ (master) $ git checkout 1efb773
Note: checking out '1efb773'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at 1efb773... init sub

这个操作的原理和git submodule update类似, 只不过后者的实现更复杂一些.

git-main2/ (master) $ git submodule update
No submodule mapping found in .gitmodules for path 'git-sub'

git submodule 首先需要一个基本的配置文件: 放在主库根目录下的.gitmodules文件.

git submoduel 的前期操作 init 依赖这个 TODO

可以手动或通过git submodule add创建这个文件(有点类似git remote add). 不过这里因为之前已经作了gitlink了, 所以这里只能手动创建这个文件:

git-main2/ (master*) $ cat .gitmodules
[submodule "git-sub"]
        path = git-sub
        url = https://git.example.com/tankywoo/git-sub.git

接下来执行git submodule init将.gitmodules文件中的配置复制到.git/config中:

git-main2/ (master*) $ git submodule init
Submodule 'git-sub' (https://git.example.com/tankywoo/git-sub.git) registered for path 'git-sub'
git-main2/ (master*) $ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
[remote "origin"]
        url = /Users/TankyWoo/dev_env/git-submodule/git-main
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
        remote = origin
        merge = refs/heads/master
[submodule "git-sub"]
        url = https://git.example.com/tankywoo/git-sub.git

回到git-main, 提交.gitmodules, 在git-sub目录增加一个提交:

git-main/git-sub/ (master) $ echo 'new line' >> sub.txt
git-main/git-sub/ (master*) $ git ci -m 'add new line to sub.txt' sub.txt
[master 4102106] add new line to sub.txt
 1 file changed, 1 insertion(+)

git-main/git-sub/ (master) $ cd ..
git-main/ (master*) $ gst
On branch master
Your branch is up-to-date with 'origin/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:   git-sub (new commits)

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

git-main/ (master*) $ git submodule update
Submodule path 'git-sub': checked out '1efb773d1740a7ad66e5b53bdf66f10c66440ce5'

因为子模块针对主库都是一个指针, 指向子模块的某一个版本.

所以执行git submodule update时, 会更新到指定的版本. 这里是检出之前的一个版本, 这时可以把git-sub更新到新提交.

git-main/git-sub/ (1efb773) $ git checkout master
Previous HEAD position was 1efb773... init sub
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

git-main/ (master*) $ gst
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   git-sub

git-main/ (master*) $ git ci -m 'update git-sub'
[master d052863] update git-sub
 1 file changed, 1 insertion(+), 1 deletion(-)

可以看到, git-sub在主库的对象就是一个指针:

git-main/ (master) $ git ls-tree HEAD
100644 blob 9c7efd6f991c84837049b6ce41233281b54b12a6    .gitmodules
040000 tree 04756934bd18bee46b7978441ff47dfd695e6344    git-non-sub
160000 commit 4102106db336adbf5d0ad572b64b379ab5098abc  git-sub
100644 blob ce013625030ba8dba906f756967f9e9ca394464a    hello.txt

git-main/ (master) $ cd git-sub
git-main/git-sub/ (master) $ git rev-parse master
4102106db336adbf5d0ad572b64b379ab5098abc

解引用子模块:

git-main/ (master) $ git submodule deinit git-sub
Cleared directory 'git-sub'
Submodule 'git-sub' (https://git.example.com/tankywoo/git-sub.git) unregistered for path 'git-sub'
git-main/ (master) $ more .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
[remote "origin"]
        url = https://git.example.com/tankywoo/git-main.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
        remote = origin
        merge = refs/heads/master

git submodule status和 git status类似, 可以查看所有子模块的引用sha-1 id和脏状态

之前提到, git submodle init会把.gitmodules的配置加到.git/config下, 关于这两个文件中针对子模块的配置:

# .gitmodules
[submodule "git-sub"]
        path = git-sub
        url = https://git.example.com/tankywoo/git-sub.git

# .git/config
[submodule "git-sub"]
        url = https://git.example.com/tankywoo/git-sub.git

TODO: 个人理解是这两个文件互相配合, 针对子模块的路径, 是由.gitmodules控制, 因为这个在.git/config中没有, 关于执行git submodule update的路径, 是由.git/config中的url控制. 另外, 子模块的配置在 .git/modules/<module-name>/config中, 和.git/config类似, 那么这里的作用又是?

TODO:

17. 子模块最佳实践(略)

18. 结合SVN版本库使用Git(略)

19. 高级操作

git filter-branch 是一个通用的分支操作命令, 可以通过自定义命令来利用它操作不同的git对象, 从而重写分支上的提交.

filter-branch命令会在版本库中的一个或多个分支执行一系列过滤器, 每个过滤器可以搭配一条自定义过滤器命令.

rebase, reset等类似, 改写历史的操作总是危险的, 所以最好不要在公共分支操作.

另外, filter-branch完成后, 原先包含旧提交历史的引用会存在 refs/original/目录下, refs/heads/ 存的是新的历史.

在操作filter-branch之前, 要保证refs/original/目录是空的.

如果确认新的历史OK后, 并确认旧的历史不在使用, 则可以删掉.git/refs/original(直接rm或git update-ref -d refs/original/<branch>)

如果不删除此目录, 则在版本库中拥有新旧两套历史记录, 旧的历史会阻止垃圾回收(gc).

如果不想显示删除此目录, 可以克隆一个新的版本库, 旧的历史存在旧的版本库作备份.

关于filter-branch, 最佳实践是先克隆一个心版本库, 然后再执行过滤操作. 个人感觉这个操作的破坏性比rebase等还要强, 很容易导致整个历史脱离自己的预计, 这应该也是它专门提供一个original引用的原因.

例子:

在整个历史中删除某个文件. 一般而言某个文件可能不再用了, 也可能包含了敏感的信息, 需要删除这个文件, 如果只是简单的git rm, 只会在当前版本中删除, 但是在历史版本还是可以检出这个文件.

使用--tree-filter可以实现这个功能:

# 4commit, 其中 e7fc148 引入hello这个空文件
(master) $ git log --oneline
20124f8 add hello git to git.txt
e7fc148 add hello world to world.txt <v0.1>  # 在这块打了一个tag v0.1
5406b57 add hello
f1b0b42 first commit

# 使用--tree-filter可以看到每个版本的文件有哪些
(master) $ git filter-branch --tree-filter 'ls' master
Rewrite f1b0b42d0590f35f290e1c47b6e0fc12ed11267c (1/4)t.sh
Rewrite 5406b570273078b2193fc7b890f20a56b2e697c8 (2/4)hello    t.sh
Rewrite e7fc1486aea71618c719800e8fbe4fd58ffc29e9 (3/4)hello    t.sh   world.txt
Rewrite 20124f85c45e360dff4d05b5e9eb4f73132f066b (4/4)git.txt  hello  t.sh    world.txt

WARNING: Ref 'refs/heads/master' is unchanged

# 使用--tree-filter删除hello这个文件
(master) $ git filter-branch --tree-filter 'rm -f hello' master
Rewrite 20124f85c45e360dff4d05b5e9eb4f73132f066b (4/4)
Ref 'refs/heads/master' was rewritten

# 再次查看, 从第2commit开始sha-1值都变了
(master) $ git log --oneline
891b0ec add hello git to git.txt
4fca41c add hello world to world.txt
cc1cc50 add hello
f1b0b42 first commit

# 当前工作目录下, hello这个文件没了
(master) $ ls
git.txt   t.sh   world.txt

# 再次尝试查看每个版本有哪些文件
(master) $ git filter-branch --tree-filter 'ls' master
Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

# 多了一个refs/original/
(master) $ tree .git/refs
.git/refs
├── heads
│   └── master
├── original
│   └── refs
│       └── heads
│           └── master
├── remotes
│   └── origin
│       └── HEAD
└── tags

7 directories, 3 files

# 存的老的master head
(master) $ more .git/refs/original/refs/heads/master
20124f85c45e360dff4d05b5e9eb4f73132f066b

# 存的新的master head
(master) $ more .git/refs/heads/master
891b0ece810d9d8dcbc34e8f023fb5713e6e4b32

# 如果把 .git/refs/heads/master改为老的sha-1, 这时就还是原来的历史了
(master*) $ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
        deleted:    hello


# 删除旧的引用
(master) $ git update-ref -d refs/original/refs/heads/master
(master) $ tree .git/refs
.git/refs
├── heads
│   └── master
├── original
│   └── refs
│       └── heads
├── remotes
│   └── origin
│       └── HEAD
└── tags

7 directories, 2 files

# 此时可以查看每个版本的文件了
(master) $ git filter-branch --tree-filter 'ls' master
Rewrite f1b0b42d0590f35f290e1c47b6e0fc12ed11267c (1/4)t.sh
Rewrite cc1cc501bd669ff44814ecd384f2dab7fc846cd9 (2/4)t.sh
Rewrite 4fca41c1d1237963cb62f639dac6b82e9bf2de04 (3/4)t.sh     world.txt
Rewrite 891b0ece810d9d8dcbc34e8f023fb5713e6e4b32 (4/4)git.txt  t.sh    world.txt

WARNING: Ref 'refs/heads/master' is unchanged

不过这里有个问题, tag标签没有转过来:

# 但是之前打的tag v0.1 还是指向老的commit
(master) $ git rev-parse v0.1
e7fc1486aea71618c719800e8fbe4fd58ffc29e9

在--tree-filter可以配合--tag-name-filter:

(master) $ git filter-branch --tree-filter 'rm -f hello' --tag-name-filter cat  master
Rewrite 20124f85c45e360dff4d05b5e9eb4f73132f066b (4/4)
Ref 'refs/heads/master' was rewritten
v0.1 -> v0.1 (e7fc1486aea71618c719800e8fbe4fd58ffc29e9 -> 4fca41c1d1237963cb62f639dac6b82e9bf2de04)

另外, 如果某个文件改名过, 则上面的情况会漏掉改名前的版本, 可以通过之前提到过的--follow找到:

$ git log --name-only --follow --all -- file

接着上面的例子, 使用--msg-filter把commit message的hello改为nothing, 当然这里会把最后三条都改掉, 仅仅当一个例子来使用, 正常情况下应该只改第2条, 用rebase合适些.

(master) $ git filter-branch --msg-filter 'sed -e "s/hello/nothing/"' master
Rewrite 891b0ece810d9d8dcbc34e8f023fb5713e6e4b32 (4/4)
Ref 'refs/heads/master' was rewritten

(master) $ git log --oneline
f53bafc add nothing git to git.txt
e216bec add nothing world to world.txt
47e5bce add nothing
f1b0b42 first commit

如果filter-branch需要在所有分支上操作, 则在命令最后加上--all

最后, 来一个以前用过的例子, 修改提交者的name和email, 有时会遇到这个情况, 可能个人的两个开发环境配置的name不一样, 导致提交会出现多个昵称, 这时可以统一下, github help已经给出了脚本, 用的就是filter-branch的env-filter, 文档链接 Changing author info

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

git filter-branch --env-filter '

OLD_EMAIL="your-old-email@example.com"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="your-correct-email@example.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

更多的filter可以man, 暂时也就跟着书折腾了这几个filter.

git rev-list 和 git log 类似, 不过只输出sha-1 id. 并且两者的文档很多地方也是一样. 比如对输出范围的限制. 不过git log默认参数是HEAD, 而 rev-list必须指定commit id.

git log takes options applicable to the git rev-list command to control what is shown and how, and options applicable to the git diff-* commands to control how the changes each commit introduces are shown.

例子, 找出2015-01-01之前的提交:

$ git rev-list --before="2015-01-01" master | wc -l
     266

$ git rev-list --before="2014-01-01" master | wc -l
      73

$ git rev-list -n 3 --before="2014-01-01" master
78f19370f4c67ca094565b9de6310917eaf85321
898309c61f5cea3ec2c52568ec8e0e4fed83a369
e0332b5dc692d4404b33596ff1a61ee430c36264

$ git show 78f19370f4c67ca094565b9de6310917eaf85321
commit 78f19370f4c67ca094565b9de6310917eaf85321
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Wed Dec 25 16:56:12 2013 +0800

    add server for preview

diff --git a/simiki/server.py b/simiki/server.py
...

对于基于时间检出commit时, 需要注意:

根据限制的精度, 会影响输出结果. 如果缺乏精确时间, 则相对的是当前时刻. 如上传入的2015-01-01, 默认时间点是当前的点09:00:00; 如果要限制在晚上23点, 则应该传入 2015-01-01 23:00:00. 所以包括yesterday都是这样, 会依赖当前时刻.

和log一样, rev-list也可以限制路径:

$ git rev-list master -- setup.cfg
46ff8a318b09c2d915bda22d9c5d93541e719680
70a7e9e7e55e3c125e2c81682ef21d03fe0a09fe

输出限制某个commit的某个文件, 这个功能也挺给力, 语法 commit:path

# 提交包含添加两个文件
$ git show --stat HEAD
commit c10e81d7414e7ea8055e1c36eeb6d0bb58c46c11
Author: Tanky Woo <wtq1990@gmail.com>
Date:   Sun Jun 28 09:48:34 2015 +0800

    update git.txt and world.txt

 git.txt   | 1 +
 world.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

# 只输出git.txt的内容
$ git show HEAD:git.txt
hello git
hello git.txt

这里输出HEAD这个版本是, git.txt的内容. 书上说"需要该提交中确实包含了该文件", 这里说的有歧义, 应该说提交的tree blob有这个文件, 而不是这个提交中此文件必须有diff.

关于数据块的交互式暂存, 也就是git add -pgit stash -p等, 输出提示已经很详细了.

git fsck (file system check)可以帮助找回丢失的数据. 一些操作(如reset, rebase)会使一些对象失去和其它对象的连接, 从而脱离版本库的完整数据结构.

这些对象叫做"不可及的"(unreachable) 或 "悬挂的"(dangling).

如:

$ git init
Initialized empty Git repository in /xxx/.git

$ echo 'foo' >> file
$ git add file
$ git ci -m 'add foo'
[master (root-commit) bfcce61] add foo
 1 file changed, 1 insertion(+)
 create mode 100644 file

$ echo 'bar' >> file
$ git ci -m 'add bar' file
[master 39a6f59] add bar
 1 file changed, 1 insertion(+)

$ tree .git/objects
.git/objects
├── 25
│   └── 7cc5642cb1a054f08cc83f2d943e56fd3ebe99
├── 39
│   └── a6f59fd5b646b57c42bd6928d7c36066842891
├── 3b
│   └── d1f0e29744a1f32b08d5650e62e2e62afb177c
├── 41
│   └── 31fe4d33cd85da805ac9a6697c2251c913881c
├── 4a
│   └── 1c03029e7407c0afe9fc0320b3258e188b115e
├── bf
│   └── cce61b0e90cb1cb385a9b1650c2a27bce30275
├── info
└── pack

$ git cat-file -p 39a6f59fd5b646b57c42bd6928d7c36066842891
tree 4131fe4d33cd85da805ac9a6697c2251c913881c
parent bfcce61b0e90cb1cb385a9b1650c2a27bce30275
author Tanky Woo <wtq1990@gmail.com> 1435458156 +0800
committer Tanky Woo <wtq1990@gmail.com> 1435458156 +0800

add bar

$ git reset --hard HEAD^
HEAD is now at bfcce61 add foo

$ git fsck
Checking object directories: 100% (256/256), done.

$ rm -rf .git/logs/

$ git fsck
Checking object directories: 100% (256/256), done.
dangling commit 39a6f59fd5b646b57c42bd6928d7c36066842891

因为reflog会防止意外的丢失提前, 所以在上面未删除.git/logs时, fsck没有找到dangling对象.

20. 提示、技巧和技术

垃圾回收(garbage collection), 在之前reflog expire时提到过.

git会在下面情况下自动进行垃圾回收:

手动进行垃圾回收使用:

$ git gc

当然, 如果要保留一些松散对象, 则要注意别被自动垃圾回收给干掉了.

git.auto默认值是6700, 控制版本库允许存在的松散对象数量, 可以强制关闭掉:

$ git config --global gc.auto 0

从上游rebase中恢复 TODO

定制 Git 命令: 定义脚本, 脚本名以git-开头, 并保证有可执行权限, 然后把脚本放在$PATH路径下.

如书上的例子:

$ echo $PATH | tr -s ':' '\n' | grep $HOME/bin
/Users/TankyWoo/bin

$ more ~/bin/git-top-check
#!/bin/bash

if [ -d ".git" ]; then
        echo "This is a top level Git development repository."
        exit 0
fi

echo "This is not a top level Git development repository."
exit -1

$ git top-check
This is a top level Git development repository.

这个相对于git config alias.xxx就是可以定制逻辑复杂的脚本.

比如把上面branch-filter --env-filter的脚本放在这块.

Github上的tj/git-extras 包含了很多扩展的命令工具.

快速查看变更:

git whatchanged, 又一个给力的命令! 它的参数和git log基本一致, 如果输入git whatchanged -h可以看到提示是git log和git show的usage.

例子:

# 抽取其中一部分作为例子
$ git whatchanged --oneline
12e3223 Fix Travis CI - Build #126
:100755 100755 52c844a... 486a651... M  simiki/cli.py
e477fa6 Disable logging output when unittest and refactor
:000000 100644 0000000... 0043f0c... A  tests/__init__.py
:100644 100644 6441aa8... a9ee8f0... M  tests/test_log.py
05584a3 Simplify generate argument
:100755 100755 405f776... 52c844a... M  simiki/cli.py
:100644 100644 2995ac3... e5fda56... M  simiki/utils.py
:100644 000000 7796986... 0000000... D  tests/attach/images/linux/opstools.png
:000000 100644 0000000... 7796986... A  tests/mywiki/attach/images/linux/opstools.png
:100644 100644 a0c3c56... 2e0ae86... M  tests/test_cli.py
a54174d move pages to a class variable
:100755 100755 d150c3a... 405f776... M  simiki/cli.py

在上面例子里, 每个提交有两行.

第一行是commit id 和 commit message

第二行分别是 文件位模式(提交前和提交后), blob的sha-1 id(提交前和提交后), 状态字母, 更改后blob的路径

还可以限制时间, 比如上周有哪些提交, 都修改了哪些文件:

$ git whatchanged --since='last week 00:00:00' --oneline

还可以显示文件:

$ git whatchanged --since='last week 00:00:00' /path/to/file

清理仓库工作目录:

git clean用于清理仓库的工作目录, 比如一个Python的仓库, 中间经过执行、打包等操作, 会产生一些临时文件, 可以使用此命令删除(也可以自己写Makefile).

git clean默认情况下不会删除.gitignore.git/info/exclude指定的文件, 通过-x会将列表中的文件也删掉;

如果不确定会删除哪些文件, 可以使用-n/--dry-run;

默认只删除文件, 目录会保留, -d会将目录也删除.

搜索版本库:

之前提到过git log -S <string>用于搜索提交的变更历史中包含指定字符串的功能.

git grep用于搜索历史记录上某个特定点的版本库内文件的内容. 默认情况下, 只搜索工作树上被追踪的文件.

此命令支持传统grep命令的参数.

之前用的grep就比较麻烦, 因为会搜到.git/目录下的信息.

更新和删除ref:

git update-ref可以用于更新引用、符号引用的值.

$ git rev-parse refs/heads/master
ba5ddbed95e2798d6862debe7ce434270ae392a9

$ git update-ref refs/heads/master 5406b57

$ git rev-parse refs/heads/master
5406b570273078b2193fc7b890f20a56b2e697c8

# 删除引用dev
$ git update-ref -d refs/heads/dev

跟踪移动的文件: --follow选项, 在git log时提到过

保留但不追踪文件:

也是一个很常见的需求, 开发时, 某个文件可能需要做一些参数或其它地方调整, 但是不需要提交. 这时一是有diff, 看着不舒服, 二是没法直接add所有.

git update-index --assume-unchanged <file> 可以将指定文件标记为不追踪. 如果有更改需要提交时, 先用--no-assume-unchanged改回来.

$ git diff file
diff --git a/file b/file
index 8768061..5329883 100644
--- a/file
+++ b/file
@@ -1 +1,2 @@
- bar
+foo bar

$ git update-index --assume-unchanged file

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

$ git update-index --no-assume-unchanged file

$ git status
On branch master
Your branch is up-to-date with 'origin/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:   file

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

重用已录制的解决方案:

git rerere(reuse record resolution), 用于自动解决相同的合并或变基冲突操作.

默认是关闭的, 开启选项:

$ git config --global rerere.enabled true

该功能会在 .git/rr-cache目录下记录合并冲突的左右两侧, 如果把冲突解决了, 还会记录冲突的手动解决方案.

但是rerere 属于本地的概念, 所以.rr-cache目录没法push到远程

$ tree .git/rr-cache
.git/rr-cache
└── 670909b9c4e71983c75c81b566c1ec1ba08d65b5
    ├── postimage
    └── preimage

比如我这里, 如果合并时有冲突, 会产生一个preimage, 记录冲突的diff; 如果我修复提交后, 会产生一个postimage, 记录解决冲突后的内容.

21. Git 和 Github(略)