git-bundle 文件是一种将 git objects 数据(git packfile)与仓库引用 refs 结合在一起的一种数据包格式,通过 objects + refs 就能恢复出一个完整的 git 仓库,因此可以用 git-bundle 文件来备份 git 仓库。

本文详细介绍了 git bundle 的文件格式、命令用法及实际应用场景,包括如何创建全量与增量备份,以及在不同机器间传输仓库数据的操作示例。

格式

git bundle v2 格式:

1
2
3
4
5
6
7
8
bundle    = signature *prerequisite *reference LF pack
signature = "# v2 git bundle " LF

prerequisite = "-" obj-id SP comment LF
comment = *CHAR
reference = obj-id SP refname LF

pack = ... ; packfile

git bundle v3 格式:

1
2
3
4
5
6
7
8
9
10
11
bundle    = signature *capability *prerequisite *reference LF pack
signature = "# v3 git bundle " LF

capability = "@" key ["=" value] LF
prerequisite = "-" obj-id SP comment LF
comment = *CHAR
reference = obj-id SP refname LF
key = 1*(ALPHA / DIGIT / "-")
value = *(%01-09 / %0b-FF)

pack = ... ; packfile

注:以上均采用 ABNF 标记法

可以看出:

git bundle v2 包含四个部分:

  1. 签名。标识 git bundle 版本

  2. 必要依赖。不包含在当前 bundle包 中,但是被 bundle包 中的数据引用到的数据。

  3. 引用。当前 bundle包 中包含的引用。

  4. pack文件git packfile

git bundle v3 仅比 v2 多了一个 capability 部分。

前面提到过 prerequisite 的概念,其格式为 prerequisite = "-" obj-id SP comment LF,所以来看下这个实际是什么意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
先打包 master 分支上最近三次提交
$ git bundle create recent.bundle master~3..master
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Compressing objects: 100% (8/8), done.
Total 9 (delta 3), reused 0 (delta 0), pack-reused 0

# 再用 git bundle verify 验证一下这个 bundle 包有没有外部依赖
$ git bundle verify recent.bundle
The bundle contains this ref:
0a831168aa94dffaa92f5d73f3e873ef5fd89603 refs/heads/master
The bundle requires this ref:
d5d9b1c95f0012cb7da18deaff6a806f89e48867
recen.bundle is okay

上面的结果很明显:

1
2
The bundle requires this ref:
d5d9b1c95f0012cb7da18deaff6a806f89e48867

再打开这个 bundle 包文件看下:

1
2
3
# v2 git bundle
-d5d9b1c95f0012cb7da18deaff6a806f89e48867 demo.tar.gz
0a831168aa94dffaa92f5d73f3e873ef5fd89603 refs/heads/master

第二行的内容 -d5d9b1c95f0012cb7da18deaff6a806f89e48867 demo.tar.gz 即是 prerequisite

并且符合格式 prerequisite = "-" obj-id SP comment LF

我们看一个实际的 git pack 文件:

1
2
3
4
5
6
7
8
PACK^@^@^@^B^@^@^@^C<91>^Kx^A^A±^@Nÿtree 212ccb4755ab7c489bee69200388139d7f081e7c
author Li Linchao <lilinchao@oschina.cn> 1646124003 +0800
committer Li Linchao <lilinchao@oschina.cn> 1646124003 +0800

add aa.txt
©&5A5x^A^A^E^@úÿaaaa
^E]^A<8f>¢^Bx^A^A"^@Ýÿ100644 aa.txt^@]0<8e>^]^F^K^L8}E,ôt^?<89>ì¹<93>XQ£÷^KlK<9a><93>pÃ^@ò&bL37©<85>(ÐâWa^X
~

而此时的 git bundle v2 文件如下:

git bundle create master.bundle master

1
2
3
4
5
6
7
8
9
10
v2 git bundle
e0caba68a7281d4ff86693745a1617ffc72c3e7d refs/heads/master

PACK^@^@^@^B^@^@^@^C<91>^Kx^A^A±^@Nÿtree 212ccb4755ab7c489bee69200388139d7f081e7c
author Li Linchao <lilinchao@oschina.cn> 1646124003 +0800
committer Li Linchao <lilinchao@oschina.cn> 1646124003 +0800

add aa.txt
©&5A¢^Bx^A^A"^@Ýÿ100644 aa.txt^@]0<8e>^]^F^K^L8}E,ôt^?<89>ì¹<93>XQ£÷^Kl5x^A^A^E^@úÿaaaa
^E]^A<8f>,^B<9d>ï<89>DIr^\<87>*<9b>Æû^X¹É}PÖ

git bundle v3 文件如下:

git bundle create --version = 3 master-v3.bundle master

1
2
3
4
5
6
7
8
9
10
11
v3 git bundle
@object-format=sha1
e0caba68a7281d4ff86693745a1617ffc72c3e7d refs/heads/master

PACK^@^@^@^B^@^@^@^C<91>^Kx^A^A±^@Nÿtree 212ccb4755ab7c489bee69200388139d7f081e7c
author Li Linchao <lilinchao@oschina.cn> 1646124003 +0800
committer Li Linchao <lilinchao@oschina.cn> 1646124003 +0800

add aa.txt
©&5A¢^Bx^A^A"^@Ýÿ100644 aa.txt^@]0<8e>^]^F^K^L8}E,ôt^?<89>ì¹<93>XQ£÷^Kl5x^A^A^E^@úÿaaaa
^E]^A<8f>,^B<9d>ï<89>DIr^\<87>*<9b>Æû^X¹É}PÖ

操作实践

在机器 A 中的仓库 R1 中:

1
2
3
$ git bundle create file.bundle master
# 做个标记
$ git tag -f lastR2bundle master

file.bundle 转移到机器 B 上,克隆仓库 R2

1
2
3
4
$ git clone -b master /path/to/file.bundle R2
Cloning into 'R2'...
Receiving objects: 100% (38/38), 7.97 KiB | 7.97 MiB/s, done.
Resolving deltas: 100% (6/6), done.

回到仓库 R1,仓库 R1 有了新的提交。然后继续打包:

1
2
3
$ git bundle create file.bundle lastR2bundle..master
# 做个标记,下次打包时使用
$ git tag -f lastR2bundle master

file.bundle 转移到机器 B 上,再到 R2 仓库中进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git fetch
Receiving objects: 100% (41/41), 8.28 KiB | 8.28 MiB/s, done.
Resolving deltas: 100% (7/7), done.
From /home/git/test-git/example/repo.bundle
d4f54d9..6a33e12 master -> origin/master

# 还是在 cloned-repo 中
$ git ls-remote
From /path/to/file.bundle
6a33e12f3d7116863a19bf48471c74c597421f8a HEAD
45eab37d3cb8b5be02a59d3690cb63d3f692f6b9 refs/remotes/origin/master
45eab37d3cb8b5be02a59d3690cb63d3f692f6b9 refs/remotes/origin/HEAD
6a33e12f3d7116863a19bf48471c74c597421f8a refs/heads/master

以上操作,看起来都跟读取 Git 远程仓库的一样。
但是不支持写仓库操作,即 git push 操作。

以上操作就完成了仓库的全量备份和增量备份。

还可以用其它形式进行打包:

1
2
3
$ git bundle create mybundle v1.0.0..master
$ git bundle create mybundle --since=10.days master
$ git bundle create mybundle -10 master

只要是 git rev-list 能接受的参数,就可以放在 git bundle create mybundle 后面。比如可以使用 --max-age 选项,实现根据时间戳来进行备份,大致过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
``` Bash
# 当前 HEAD 的时间戳,作为下次备份的起点时间
$ git cat-file commit HEAD | sed -n " s/^committer .*> \([0-9]*\) .*/\1/p "
1660809497

# --branches 获取 refs/heads/下所有相关 commits
# --tags 获取 refs/tags/下所有相关 commits
# --remotes 获取 refs/remotes/下所有相关 commits
# --all 获取 refs/下所有相关 commits
# --objects 表示 commit 所关联的所有对象
$ git bundle create latest.bundle --max-age = 1660807920 --all --objects

# 获取 bundle 包中的引用信息,可以通过其它选项筛选出部分引用。
$ git bundle list-heads latest.bundle > latest.list

# 通过 git-ls-remote 获取 bundle 包中的所有引用信息。
$ git ls-remote latest.bundle > latest.lsremote

应用

目前 Gitlab 的仓库导出、导入功能以及阿里云效 CodeUp 的仓库备份功能主要就是利用 Git-bundle 特性对 Git 仓库进行打包。

参考链接

  1. https://git-scm.com/docs/git-bundle

  2. https://git-scm.com/docs/bundle-format