Git
章節 ▾ 第二版

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 具備垃圾回收機制,因此無需顯式釋放資源。讓我們來看一個稍微複雜的範例:從頭開始建立一個 commit。

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 commit 的樹狀結構填入索引,並在路徑 newfile.txt 加入新檔案。

  3. 這會在 ODB 中建立一個新的樹狀結構,並將其用於新的 commit。

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

  5. commit 訊息。

  6. 建立 commit 時,您必須指定新 commit 的父 commit。此範例使用 HEAD 的頂端作為單一父 commit。

  7. Rugged (和 Libgit2) 可以在建立 commit 時選擇性地更新參考。

  8. 傳回值是新 commit 物件的 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 指向的 commit 返回 commit 訊息 (有點像 git log -1)。

LibGit2Sharp

如果您正在編寫 .NET 或 Mono 應用程式,LibGit2Sharp (https://github.com/libgit2/libgit2sharp) 就是您所需要的。這些綁定是以 C# 編寫的,並且已仔細處理以使用原生 CLR API 包裝原始 Libgit2 呼叫。以下是我們的範例程式的樣子

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

Python 中 Libgit2 的綁定稱為 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