Git
Chapters ▾ 2nd Edition

A2.2 Appendix B: Встраивание Git в ваши приложения - Libgit2

Libgit2

© Другой доступный вам вариант — это использование библиотеки Libgit2. Libgit2 — это свободная от внешних зависимостей реализация Git, ориентирующаяся на предоставление отличного API другим программам. Вы можете найти её на http://libgit2.github.com.

Для начала, давайте посмотрим на что похож 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);

Первая пара строк открывают 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(head_commit) вернёт GIT_OBJ_COMMIT, так что мы вправе привести тип к git_commit.

Следующий блока кода показывает как получить доступ к свойствам коммита. Последняя строчка в этом фрагменте кода использует тип git_oid — это внутреннее представление SHA-1 в Libgit2.

Глядя на этот пример, можно сделать несколько выводов:

  • Если вы объявили указатель и передали его в одну из функций Libgit2, то она, скорее всего, вернёт целочисленный код ошибки. Значение 0 означает успешное выполнение операции, всё что меньше — означает ошибку.

  • Если Libgit2 возвращает вам указатель, вы ответственны за очистку ресурсов.

  • Если Libgit2 возвращает const-указатель, вам не нужно заботится о его очистке, но он может оказаться невалидным, если объект на который он ссылается будет уничтожен.

  • Писать на C — несколько сложновато.

Последний пункт намекает на маловероятность использования C при работе с Libgit2. К счастью, существует ряд обёрток над Libgit2 для различных языков, которые позволяют довольно удобно работать с Git репозиториями, используя ваш язык программирования и среду исполнения. Давайте взглянем на тот же пример, только написанный с использованием Ruby и обёртки над Libgit2 под названием 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 использует исключения: он может кинуть ошибку типа 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. Создание нового blob’а, включающего содержимое нового файла.

  2. Заполнение индекса содержимым дерева HEAD и добавление нового файла newfile.txt.

  3. Создание нового дерева в базе данных объектов и использование его для нового коммита.

  4. Мы используем одну и ту же подпись для автора и коммиттера.

  5. Сообщение коммита.

  6. При создании коммита нужно указать его предков. Для этих целей мы используем HEAD как единственного родителя.

  7. Rugged (как и Libgit2) дополнительно могут обновить HEAD при создании комита.

  8. Используя полученное значение SHA-1 хеша нового коммита, можно получить объект типа Commit.

Код на Ruby приятен и чист, а благодаря тому что Libgit2 делает основную работу ещё и выполняется довольно быстро. На случай если вы пишете не на Ruby, мы рассмотрим другие обёртки над Libgit2 в Обёртки для других языков.

Расширенная функциональность

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. Инициализация интерфейса для пустой базы данных объектов, который будет использоваться как контейнер для внутренних интерфейсов, которые будут выполнять работу.

  2. Инициализация произвольного внутреннего интерфейса базы данных объектов.

  3. Добавление внутреннего интерфейса к внешнему.

  4. Открытие репозитория и настройка на использование собственной базы для поиска объектов.

Что же скрыто внутри git_odb_backend_mine? Это ваша собственная реализация базы данных объектов, где вы можете делать что угодно, лишь бы поля структуры 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;
}

Важный момент: в my_backend_struct первое поле должно быть структурой git_odb_backend, что обеспечит расположение полей в памяти в формате, ожидаемом Libgit2. Оставшиеся поля можно располагать произвольно, а сама структура может быть любого нужного вам размера.

Функция инициализации выделяет память под структуру, устанавливает произвольный контекст и заполняет поля структуры parent, которые необходимо поддерживать. Взгляните на файл include/git2/sys/odb_backend.h в исходном коде Libgit2 чтобы узнать полный список сигнатур доступных методов; в вашем конкретном случае вы сами решаете, какие из них необходимо имплементировать.

Обёртки для других языков

У 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 код. Вот как будет выглядеть наш пример:

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

Также существует NuGet пакет для десктопных Windows-приложений, который поможет начать разработку ещё быстрее.

objective-git

Если вы пишете приложение для продукции Apple, то скорее всего оно написано на Objective-C. Обёртка над Libgit2 в этом случае называется Objective-Git: (https://github.com/libgit2/objective-git). Пример кода:

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, её можно найти на http://www.pygit2.org/. И наш пример будет выглядеть так:

pygit2.Repository("/path/to/repo") # открыть репозиторий
    .head                          # получить текущую ветку
    .peel(pygit2.Commit)           # получить коммит
    .message                       # прочитать сообщение

Дополнительные материалы

Конечно же, полное описание возможностей Libgit2 выходит далеко за пределы этой книги. Если вы хотите подробнее ознакомиться с Libgit2, можете начать с документации к API https://libgit2.github.com/libgit2 и набора руководств https://libgit2.github.com/docs. Для привязок к другим языкам, загляните в README и исходники тестов, довольно часто в них встречаются ссылки на полезные материалы по теме.