章節 ▾ 第二版

A2.2 附錄B:在應用程式中嵌入Git - Libgit2

Libgit2

另一個可供選擇的方案是使用 Libgit2。Libgit2 是 Git 的一個無依賴實現,其重點在於提供一個友好的 API 供其他程式使用。你可以在 https://libgit2.org 找到它。

首先,我們來看看 C API 是怎樣的。以下是快速概覽

// Open a repository
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Dereference HEAD to a commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// Print some of the commit's properties
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);

// Cleanup
git_commit_free(commit);
git_repository_free(repo);

前幾行程式碼開啟一個 Git 倉庫。git_repository 型別代表一個帶有記憶體快取的倉庫控制代碼。這是最簡單的方法,適用於你知道倉庫工作目錄或 .git 資料夾的確切路徑的情況。此外還有 git_repository_open_ext,它包含搜尋選項;git_clone 及其相關函式用於建立遠端倉庫的本地克隆;以及 git_repository_init 用於建立全新的倉庫。

第二段程式碼使用 rev-parse 語法(更多資訊請參閱分支引用),以獲取 HEAD 最終指向的提交。返回的型別是一個 git_object 指標,它代表了 Git 倉庫物件資料庫中存在的事物。git_object 實際上是幾種不同型別物件的“父”型別;每個“子”型別的記憶體佈局與 git_object 相同,因此你可以安全地轉換為正確的型別。在這種情況下,git_object_type(commit) 將返回 GIT_OBJ_COMMIT,因此安全地轉換為 git_commit 指標是可行的。

下一段程式碼展示瞭如何訪問提交的屬性。這裡最後一行使用了 git_oid 型別;這是 Libgit2 對 SHA-1 雜湊值的表示。

從這個示例中,我們可以看到一些模式開始浮現

  • 如果你宣告一個指標並將其引用傳遞給 Libgit2 呼叫,該呼叫可能會返回一個整數錯誤程式碼。值 0 表示成功;任何小於 0 的值都是錯誤。

  • 如果 Libgit2 為你填充了一個指標,你有責任釋放它。

  • 如果 Libgit2 從呼叫中返回一個 const 指標,你不需要釋放它,但當其所屬的物件被釋放時,它將變得無效。

  • 編寫 C 程式碼有點痛苦。

最後一點意味著你在使用 Libgit2 時不太可能編寫 C 程式碼。幸運的是,有許多針對特定語言的繫結可用,這使得你可以相當容易地在你的特定語言和環境中處理 Git 倉庫。讓我們看看上面示例使用 Libgit2 的 Ruby 繫結(名為 Rugged)的寫法,它可以在 https://github.com/libgit2/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

如你所見,程式碼簡潔了許多。首先,Rugged 使用異常;它可以丟擲像 ConfigErrorObjectError 這樣的異常來指示錯誤情況。其次,由於 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. 建立一個新的 blob 物件,其中包含新檔案的內容。

  2. 使用 HEAD 提交的樹填充索引,並將新檔案新增到路徑 newfile.txt

  3. 這會在 ODB 中建立一個新的樹物件,並將其用於新的提交。

  4. 我們為作者和提交者欄位使用相同的簽名。

  5. 提交資訊。

  6. 建立提交時,你必須指定新提交的父級。這裡使用 HEAD 的尖端作為唯一的父級。

  7. Rugged(和 Libgit2)在進行提交時可以選擇性地更新引用。

  8. 返回值是新提交物件的 SHA-1 雜湊值,你可以使用它來獲取一個 Commit 物件。

Ruby 程式碼既漂亮又簡潔,而且由於 Libgit2 承擔了繁重的工作,這段程式碼執行速度也很快。如果你不是 Ruby 開發者,我們在其他繫結中會介紹一些其他繫結。

高階功能

Libgit2 具有一些超出核心 Git 範圍的能力。一個例子是可插拔性:Libgit2 允許你為幾種型別的操作提供自定義“後端”,這樣你就可以用與標準 Git 不同的方式儲存資料。Libgit2 允許為配置、引用儲存和物件資料庫等提供自定義後端。

我們來看看這是如何工作的。以下程式碼摘自 Libgit2 團隊提供的一組後端示例(可在 https://github.com/libgit2/libgit2-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. 初始化一個空的(物件資料庫)ODB“前端”,它將作為執行實際工作的“後端”的容器。

  2. 初始化一個自定義 ODB 後端。

  3. 將後端新增到前端。

  4. 開啟一個倉庫,並將其設定為使用我們的 ODB 來查詢物件。

但這個 git_odb_backend_mine 是什麼呢?嗯,那是你自己的 ODB 實現的建構函式,你可以在其中做任何你想做的事情,只要你正確地填充了 git_odb_backend 結構體。它可能看起來像這樣

typedef struct {
    git_odb_backend parent;

    // Some other stuff
    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;
}

這裡最微妙的約束是 my_backend_struct 的第一個成員必須是 git_odb_backend 結構體;這確保了記憶體佈局符合 Libgit2 程式碼的預期。其餘部分是任意的;這個結構體可以根據你的需要變得或大或小。

初始化函式為該結構體分配記憶體,設定自定義上下文,然後填充其支援的 parent 結構體成員。請檢視 Libgit2 原始碼中的 include/git2/sys/odb_backend.h 檔案以獲取完整的呼叫簽名集;你的特定用例將有助於確定你需要支援哪些。

其他繫結

Libgit2 具有多種語言的繫結。在此,我們展示一個使用本文撰寫時一些更完整的繫結包的小例子;許多其他語言(包括 C++、Go、Node.js、Erlang 和 JVM)也有庫,都處於不同的成熟階段。官方的繫結集合可以在 https://github.com/libgit2 瀏覽倉庫找到。我們將編寫的程式碼將返回 HEAD 最終指向的提交的提交資訊(類似於 git log -1)。

LibGit2Sharp

如果你正在編寫 .NET 或 Mono 應用程式,那麼 LibGit2Sharp(https://github.com/libgit2/libgit2sharp)是你正在尋找的。這些繫結是用 C# 編寫的,並且非常注意將原始的 Libgit2 呼叫封裝為具有原生感的 CLR API。以下是我們的示例程式的樣子

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

對於桌面 Windows 應用程式,甚至還有一個 NuGet 包可以幫助你快速入門。

objective-git

如果你的應用程式執行在 Apple 平臺上,你很可能正在使用 Objective-C 作為你的實現語言。Objective-Git(https://github.com/libgit2/objective-git)是 Libgit2 在該環境下的繫結的名稱。示例程式如下所示

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

Objective-git 與 Swift 完全相容,所以如果你已經放棄了 Objective-C,也無需擔心。

pygit2

Libgit2 在 Python 中的繫結稱為 Pygit2,可以在 https://www.pygit2.org 找到。我們的示例程式

pygit2.Repository("/path/to/repo") # open repository
    .head                          # get the current branch
    .peel(pygit2.Commit)           # walk down to the commit
    .message                       # read the message

延伸閱讀

當然,全面介紹 Libgit2 的功能超出了本書的範圍。如果你想了解更多關於 Libgit2 本身的資訊,可以在 https://libgit2.github.com/libgit2 找到 API 文件,並在 https://libgit2.github.com/docs 找到一系列指南。對於其他繫結,請查閱捆綁的 README 和測試;那裡通常會有一些小的教程和進一步閱讀的指引。

scroll-to-top