Git之深入解析如何在应用中嵌入Git

网友投稿 578 2022-05-29

一、前言

到目前为止,我们已经了解了 Git 基本的运作机制和使用方式,学习了许多 Git 提供的工具简单且有效地使用它,可以高效地帮助我们工作,提升我们的效率。

如果还不清楚 Git 的基础使用流程、分支的管理、托管服务器的技术以及分布式工作流程等相关的技术和能力,请参考:

Git之深入解析Git的安装流程与初次运行Git前的环境配置;

Git之深入解析本地仓库的基本操作·仓库的获取更新和提交历史的查看撤销以及标签别名的使用;

Git之深入解析Git的杀手级特性·分支管理与变基的开发工作流以及远程分支的跟踪;

Git之深入解析如何运行自己的Git仓库托管服务器;

Git之深入解析如何使用Git的分布式工作流程与如何管理多人开发贡献的项目。

Git 的相关工具,请参考:

Git之深入解析如何选择修订的版本;

Git之深入解析如何交互式暂存;

Git之深入解析如何贮藏工作分支与清理工作目录;

Git之深入解析如何通过GPG签署和验证工作;

Git之深入解析如何重写提交历史;

Git之深入解析reset命令原理以及与checkout命令的区别;

Git之深入解析高级合并;

Git之深入解析Rerere重用记录的解决方案;

Git之深入解析如何使用Git调试项目源码中的问题;

Git之深入解析在没有合适的网络或者可共享仓库情况下的git bundle打包操作;

Git之深入解析如何替换数据库中的Git对象;

Git之深入解析凭证存储;

Git之深入解析如何在应用中嵌入Git

Git 的内部原理分析,请参考:

Git内部原理之深入解析Git对象;

Git内部原理之深入解析Git的引用和包文件;

Git内部原理之深入解析引用规范;

Git内部原理之深入解析传输协议;

Git内部原理之深入解析维护与数据恢复;

Git内部原理之深入解析环境变量。

如果我们的应用程序的目标用户是开发者,那么在其中集成源码控制功能会让他们从中受益,甚至对于文档编辑器等并非面向程序员的应用,也可以从版本控制系统中受益,Git 的工作模式在多种场景下表现得都非常出色。

如果想将 Git 整合进我们的应用程序中,那么通常有两种可行的选择:启动 shell 来调用 Git 的命令行程序,或者将 Git 库嵌入到应用中。

二、命令行 Git 方式

一种方式就是启动一个 shell 进程并在里面使用 Git 的命令行工具来完成任务,这种方式看起来很循规蹈矩,但是它的优点也因此而来,就是支持所有的 Git 的特性。它也碰巧相当简单,因为几乎所有运行时环境都有一个相对简单的方式来调用一个带有命令行参数的进程,然而这种方式也有一些固有的缺点。

首先就是所有的输出都是纯文本格式,这意味着你将被迫解析 Git 的有时会改变的输出格式,以随时了解它工作的进度和结果。更糟糕的是,这可能是毫无效率并且容易出错的。

另外一个就是令人捉急的错误修复能力,如果一个版本库被莫名其妙地损毁,或者用户使用了一个奇奇怪怪的配置,Git 只会简单地拒绝进行一些操作。

还有一个就是进程的管理,Git 会要求在一个独立的进程中维护一个 shell 环境,这可能会无谓地增加复杂性,试图协调许许多多的类似的进程(尤其是在某些情况下,当不同的进程在访问相同的版本库时)是对我们的能力的极大挑战。

三、Libgit2

另外一种可以供使用的是 Libgit2,Libgit2 是一个 Git 的非依赖性的工具,它致力于为其他程序使用 Git 提供更好的 API,可以在 libgit2 中找到它。

首先,来看一下 C API 长啥样,这是一个旋风式旅行:

// 打开一个版本库 git_repository *repo; int error = git_repository_open(&repo, "/path/to/repository"); // 逆向引用 HEAD 到一个提交 git_object *head_commit; error = git_revparse_single(&head_commit, repo, "HEAD^{commit}"); git_commit *commit = (git_commit*)head_commit; // 显示这个提交的一些详情 printf("%s", git_commit_message(commit)); const git_signature *author = git_commit_author(commit); printf("%s <%s>\n", author->name, author->email); const git_oid *tree_id = git_commit_tree_id(commit); // 清理现场 git_commit_free(commit); git_repository_free(repo);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

前两行打开一个 Git 版本库,这个 git_repository 类型代表了一个在内存中带有缓存的指向一个版本库的句柄。这是最简单的方法,只是必须知道一个版本库的工作目录或者一个 .git 文件夹的精确路径;另外还有 git_repository_open_ext,它包括了带选项的搜索,git_clone 及其同类可以用来做远程版本库的本地克隆,git_repository_init 则可以创建一个全新的版本库。

第二段代码使用了一种 rev-parse 语法(了解更多,请参考 Git之深入解析如何选择修订的版本的“分支引用”)来得到 HEAD 真正指向的提交,返回类型是一个 git_object 指针,它指代位于版本库里的 Git 对象数据库中的某个东西。git_object 实际上是几种不同的对象的“父”类型,每个“子”类型的内存布局和 git_object 是一样的,所以能安全地把它们转换为正确的类型。在上面的例子中,git_object_type(commit) 会返回 GIT_OBJ_COMMIT,所以转换成 git_commit 指针是安全的。

如下展示了如何访问一个提交的详情,最后一行使用了 git_oid 类型,这是 Libgit2 用来表示一个 SHA-1 哈希的方法。从这个例子中,可以看到一些模式:

如果声明了一个指针,并在一个 Libgit2 调用中传递一个引用,那么这个调用可能返回一个 int 类型的错误码,值 0 表示成功,比它小的则是一个错误;

如果 Libgit2 为我们填入一个指针,那么我们有责任释放它;

如果 Libgit2 在一个调用中返回一个 const 指针,我们不需要释放它,但是当它所指向的对象被释放时它将不可用;

用 C 来写有点蛋疼。

最后一点意味着应该不会在使用 Libgit2 时编写 C 语言程序。但幸运的是,有许多可用的各种语言的绑定,能在特定的语言和环境中更加容易的操作 Git 版本库。我们来看一下下面这个用 Libgit2 的 Ruby 绑定写成的例子,它叫 Rugged,可以在 Rugged 找到它:

repo = Rugged::Repository.new('path/to/repository') commit = repo.head.target puts commit.message puts "#{commit.author[:name]} <#{commit.author[:email]}>" tree = commit.tree

1

2

3

4

5

可以发现,代码看起来更加清晰了。首先,Rugged 使用异常机制,它可以抛出类似于 ConfigError 或者 ObjectError 之类的东西来告知错误的情况;其次,不需要明确资源释放,因为 Ruby 是支持垃圾回收的。来看一个稍微复杂一点的例子:从头开始制作一个提交。

blob_id = repo.write("Blob contents", :blob) # (1) index = repo.index index.read_tree(repo.head.target.tree) index.add(:path => 'newfile.txt', :oid => blob_id) # (2) sig = { :email => "bob@example.com", :name => "Bob User", :time => Time.now, } commit_id = Rugged::Commit.create(repo, :tree => index.write_tree(repo), # (3) :author => sig, :committer => sig, # (4) :message => "Add newfile.txt", # (5) :parents => repo.empty? ? [] : [ repo.head.target ].compact, # (6) :update_ref => 'HEAD', # (7) ) commit = repo.lookup(commit_id) # (8)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

分析说明:

(1) 创建一个新的 blob,它包含了一个新文件的内容;

(2) 将 HEAD 提交树填入索引,并在路径 newfile.txt 增加新文件;

(3) 这就在 ODB 中创建了一个新的树,并在一个新的提交中使用它;

(4) 在 author 栏和 committer 栏使用相同的签名;

(5) 提交的信息;

(6) 当创建一个提交时,必须指定这个新提交的父提交,这里使用了 HEAD 的末尾作为单一的父提交;

(7) 在做一个提交的过程中,Rugged (和 Libgit2 )能在需要时更新引用;

(8) 返回值是一个新提交对象的 SHA-1 哈希,可以用它来获得一个 Commit 对象。

Ruby 的代码很好很简洁,另一方面因为 Libgit2 做了大量工作,所以代码运行起来其实速度也不赖。

Libgit2 有几个超过核心 Git 的能力,例如它的可定制性:Libgit2 允许为一些不同类型的操作自定义的“后端”,让我们得以使用与原生 Git 不同的方式存储东西,Libgit2 允许为自定义后端指定配置、引用的存储以及对象数据库。

我们来看一下它究竟是怎么工作的,如下所示,借用自 Libgit2 团队提供的后端样本集 (可以在 ibgit2-backends 上找到)。一个对象数据库的自定义后端是这样建立的:

git_odb *odb; int error = git_odb_new(&odb); // (1) git_odb_backend *my_backend; error = git_odb_backend_mine(&my_backend, /*…*/); // (2) error = git_odb_add_backend(odb, my_backend, 1); // (3) git_repository *repo; error = git_repository_open(&repo, "some-path"); error = git_repository_set_odb(repo, odb); // (4)

1

2

3

4

5

6

7

8

9

10

11

分析说明:

(1) 初始化一个空的对象数据库( ODB )“前端”,它将被作为一个用来做真正的工作的“后端”的容器;

(2) 初始化一个自定义 ODB 后端;

(3) 为这个前端增加一个后端;

(4) 打开一个版本库,并让它使用我们的 ODB 来寻找对象。

但是 git_odb_backend_mine 是个什么东西呢? 这是一个我们自己的 ODB 实现的构造器,并且能在那里做任何想做的事,前提是能正确地填写 git_odb_backend 结构。它看起来应该是这样的:

typedef struct { git_odb_backend parent; // 其它的一些东西 void *custom_context; } my_backend_struct; int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/) { my_backend_struct *backend; backend = calloc(1, sizeof (my_backend_struct)); backend->custom_context = …; backend->parent.read = &my_backend__read; backend->parent.read_prefix = &my_backend__read_prefix; backend->parent.read_header = &my_backend__read_header; // …… *backend_out = (git_odb_backend *) backend; return GIT_SUCCESS; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

my_backend_struct 的第一个成员必须是一个 git_odb_backend 结构,这是一个微妙的限制:这样就能确保内存布局是 Libgit2 的代码所期望的样子,其余都是随意的,这个结构的大小可以随心所欲。

这个初始化函数为该结构分配内存,设置自定义的上下文,然后填写它支持的 parent 结构的成员。阅读 Libgit2 的 include/git2/sys/odb_backend.h 源码以了解全部调用签名,特定的使用环境会决定使用哪一种调用签名。

Libgit2 有很多种语言的绑定,在上文中,我们展现了一个使用了几个更加完整的绑定包的小例子,这些库存在于许多种语言中,包括 C++、Go、Node.js、Erlang 以及 JVM,它们的成熟度各不相同。官方的绑定集合可以通过浏览这个版本库得到:libgit2。 我们写的代码将返回当前 HEAD 指向的提交的提交信息(就像 git log -1 那样)。

LibGit2Sharp:

如果在编写一个 .NET 或者 Mono 应用,那么 LibGit2Sharp 就是所需要的,这个绑定是用 C# 写成的,并且已经采取许多措施来用令人感到自然的 CLR API 包装原始的 Libgit2 的调用。我们的例子看起来就像这样:

new Repository(@"C:\path\to\repo").Head.Tip.Message;

1

对于 Windows 桌面应用,一个叫做 NuGet 的包会让我们快速上手。

objective-git:

如果应用运行在一个 Apple 平台上,很有可能使用 Objective-C 作为实现语言。Objective-Git 是这个环境下的 Libgit2 绑定。如下所示:

GTRepository *repo = [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL]; NSString *msg = [[[epo headReferenceWithError:NULL] resolvedTarget] message];

1

2

3

Objective-git 与 Swift 完美兼容,所以把 Objective-C 落在一边的时候不用恐惧。

pygit2:

Python 的 Libgit2 绑定叫做 Pygit2,可以在 pygit2 - libgit2 bindings in Python 找到它,示例程序:

pygit2.Repository("/path/to/repo") # 打开代码仓库 .head # 获取当前分支 .peel(pygit2.Commit) # 找到对应的提交 .message # 读取提交信息

1

2

3

4

四、JGit

如果想在一个 Java 程序中使用 Git,有一个功能齐全的 Git 库,那就是 JGit。JGit 是一个用 Java 写成的功能相对健全的 Git 的实现,它在 Java 社区中被广泛使用。JGit 项目由 Eclipse 维护,它的主页在 JGit。

有很多种方式可以让 JGit 连接项目,并依靠它去写代码。最简单的方式也许就是使用 Maven,可以通过在 pom.xml 文件里的 标签中增加像下面这样的片段来完成这个整合:

org.eclipse.jgit org.eclipse.jgit 3.5.0.201409260305-r

1

2

3

4

5

在读到这段文字时 version 很可能已经更新了,所以请浏览 JGit Core 以获取最新的仓库信息,当这一步完成之后,Maven 就会自动获取并使用所需要的 JGit 库。

如果想自己管理二进制的依赖包,那么可以从 eclipse 获得预构建的 JGit 二进制文件,可以像下面这样执行一个命令来将它们构建进项目:

javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App

1

2

JGit 的 API 有两种基本的层次:底层命令和高层命令,这个两个术语都来自 Git,并且 JGit 也被按照相同的方式粗略地划分:高层 API 是一个面向普通用户级别功能的友好的前端(一系列普通用户使用 Git 命令行工具时可能用到的东西),底层 API 则直接作用于低级的仓库对象。

大多数 JGit 会话会以 Repository 类作为起点,首先要做的事就是创建一个它的实例。对于一个基于文件系统的仓库来说(JGit 允许其它的存储模型),用 FileRepositoryBuilder 完成它:

// 创建一个新仓库 Repository newlyCreatedRepo = FileRepositoryBuilder.create( new File("/tmp/new_repo/.git")); newlyCreatedRepo.create(); // 打开一个存在的仓库 Repository existingRepo = new FileRepositoryBuilder() .setGitDir(new File("my_repo/.git")) .build();

1

2

3

4

5

6

7

8

9

无论程序是否知道仓库的确切位置,builder 中的那个流畅的 API 都可以提供给它寻找仓库所需所有信息,它可以使用环境变量 (.readEnvironment()),从工作目录的某处开始并搜索 (.setWorkTree(…).findGitDir()),或者仅仅只是像上面那样打开一个已知的 .git 目录。

当拥有一个 Repository 实例后,就能对它做各种各样的事,如下是一个速览:

// 获取引用 Ref master = repo.getRef("master"); // 获取该引用所指向的对象 ObjectId masterTip = master.getObjectId(); // Rev-parse ObjectId obj = repo.resolve("HEAD^{tree}"); // 装载对象原始内容 ObjectLoader loader = repo.open(masterTip); loader.copyTo(System.out); // 创建分支 RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1"); createBranch1.setNewObjectId(masterTip); createBranch1.update(); // 删除分支 RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1"); deleteBranch1.setForceUpdate(true); deleteBranch1.delete(); // 配置 Config cfg = repo.getConfig(); String name = cfg.getString("user", null, "name");

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

分析说明:

第一行获取一个指向 master 引用的指针,JGit 自动抓取位于 refs/heads/master 的 真正的 master 引用,并返回一个允许获取该引用的信息的对象,可以获取它的名字(.getName()),或者一个直接引用的目标对象(.getObjectId()),或者一个指向该引用的符号指针 (.getTarget())。 引用对象也经常被用来表示标签的引用和对象,所以可以询问某个标签是否被“削除”了,或者说它指向一个标签对象的(也许很长的)字符串的最终目标。

第二行获得以 master 引用的目标,它返回一个 ObjectId 实例,不管是否存在于一个 Git 对象的数据库,ObjectId 都会代表一个对象的 SHA-1 哈希。

第三行与此相似,但是它展示了 JGit 如何处理 rev-parse 语法(要了解更多,请参考 Git之深入解析如何选择修订的版本 的“分支引用”),可以传入任何 Git 了解的对象说明符,然后 JGit 会返回该对象的一个有效的 ObjectId,或者 null。

接下来两行展示了如何装载一个对象的原始内容,在这个例子中,我们调用 ObjectLoader.copyTo() 直接向标准输出流输出对象的内容,除此之外 ObjectLoader 还带有读取对象的类型和长度并将它以字节数组返回的方法。对于一个( .isLarge() 返回 true 的)大的对象,可以调用 .openStream() 来获得一个类似 InputStream 的对象,它可以在没有一次性将所有数据拉到内存的前提下读取对象的原始数据。

接下来几行展现了如何创建一个新的分支,我们创建一个 RefUpdate 实例,配置一些参数,然后调用 .update() 来确认这个更改,删除相同分支的代码就在这行下面。记住必须先 .setForceUpdate(true) 才能让它工作,否则调用 .delete() 只会返回 REJECTED,然后什么都没有发生。

最后一个例子展示了如何从 Git 配置文件中获取 user.name 的值,这个 Config 实例使用我们先前打开的仓库做本地配置,但是它也会自动地检测并读取全局和系统的配置文件。

这只是底层 API 的冰山一角,另外还有许多可以使用的方法和类。还有一个没有放在这里说明的,就是 JGit 是用异常机制来处理错误的。JGit API 有时使用标准的 Java 异常(例如 IOException ),但是它也提供了大量 JGit 自己定义的异常类型(例如 NoRemoteRepositoryException、 CorruptObjectException 和 NoMergeBaseException)。

底层 API 更加完善,但是有时将它们串起来以实现普通的目的非常困难,例如将一个文件添加到索引,或者创建一个新的提交。为了解决这个问题,JGit 提供了一系列高层 API,使用这些 API 的入口点就是 Git 类:

Repository repo; // 构建仓库…… Git git = new Git(repo);

1

2

3

Git 类有一系列非常好的构建器风格的高层方法,它可以用来构造一些复杂的行为。我们来看一个例子,做一件类似 git ls-remote 的事:

CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd"); Collection remoteRefs = git.lsRemote() .setCredentialsProvider(cp) .setRemote("origin") .setTags(true) .setHeads(false) .call(); for (Ref ref : remoteRefs) { System.out.println(ref.getName() + " -> " + ref.getObjectId().name()); }

1

2

3

4

5

6

7

8

9

10

这是一个 Git 类的公共样式,这个方法返回一个可以串连若干方法调用来设置参数的命令对象,当调用 .call() 时它们就会被执行。在这情况下,我们只是请求了 origin 远程的标签,而不是头部。还要注意用于验证的 CredentialsProvider 对象的使用。

在 Git 类中还可以使用许多其它的命令,包括但不限于 add、blame、commit、clean、push、rebase、revert 和 reset。

五、go-git

如果想要将Git集成到用 Golang 编写的服务中,还有一个纯 Go 库实现,此实现没有任何本机依赖项,因此不容易出现手动内存管理错误。对于标准的 Golang 性能分析工具(如 CPU、内存分析器、竞赛检测器等)来说,它也是透明的。

go-git 专注于扩展性、兼容性,并支持大多数管道 API,具体请参考:go-git。

如下是一个使用 Go API 的基本例子:

import "gopkg.in/src-d/go-git.v4" r, err := git.PlainClone("/tmp/foo", false, &git.CloneOptions{ URL: "https://github.com/src-d/go-git", Progress: os.Stdout, })

1

2

3

4

5

6

一旦有了 Repository 实例,就可以访问信息并对其进行修改:

// retrieves the branch pointed by HEAD ref, err := r.Head() // get the commit object, pointed by ref commit, err := r.CommitObject(ref.Hash()) // retrieves the commit history history, err := commit.History() // iterates over the commits and print each for _, c := range history { fmt.Println(c) }

1

2

3

4

5

6

7

8

9

10

11

12

13

go-git 有一些值得注意的高级特性,其中之一是一个可插拔的存储系统,它类似于 Libgit2 后端,默认的实现是内存存储,它非常快。

r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ URL: "https://github.com/src-d/go-git", })

1

2

3

可插拔存储提供了许多有趣的选择,例如,go-git 允许在 Aerospike 数据库中存储引用、对象和配置。

另一个特性是灵活的文件系统抽象,使用 go-billy 可以很容易地以不同的方式存储所有文件,例如将所有文件打包到磁盘上的单个归档文件,或者将所有文件保存在内存中。

另一个高级用例包括一个可调优的 HTTP 客户机,如在 go-git 上找到的客户机。

customClient := &http.Client{ Transport: &http.Transport{ // accept any certificate (might be useful for testing) TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Timeout: 15 * time.Second, // 15 second timeout CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse // don't follow redirect }, } // Override http(s) default protocol to use our custom client client.InstallProtocol("https", githttp.NewClient(customClient)) // Clone repository using the new client if the protocol is https:// r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: url})

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

六、Dulwich

还有一个纯 python 的 Git 实现——Dulwich,该项目托管在 Dulwich下。它旨在提供一个接口,Git 存储库(本地和远程),不直接调用 Git,而是使用纯 Python,它有一个可选的 C 扩展,可以显著提高性能。

Dulwich 遵循 Git 设计,并将 API 分为两个基本层次:管道和瓷器。

如下,是一个使用低级 API 访问上次提交的提交消息的例子:

from dulwich.repo import Repo r = Repo('.') r.head() # '57fbe010446356833a6ad1600059d80b1e731e15' c = r[r.head()] c # c.message # 'Add note about encoding.\n'

1

2

3

4

5

6

7

8

9

10

11

要使用高级的 API 打印提交日志,可以使用:

from dulwich import porcelain porcelain.log('.', max_entries=1) #commit: 57fbe010446356833a6ad1600059d80b1e731e15 #Author: Jelmer Vernooij #Date: Sat Apr 29 2017 23:57:34 +0000

1

2

3

4

5

6

Git 任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:零基础学前端.jQuery入门与实践教程
下一篇:Java之三 标识符、关键字、数据类型
相关文章