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_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. Создание нового дерева в ODB и использование его для нового коммита.

  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(odb); (4)

(Заметьте, ошибки перехватываются, но не обрабатываются. Мы надеемся, ваш код лучше нашего.)

  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;
}

Важный момент: первое поле структуры 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 -l).

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.resolve()                # получить прямую ссылку
    .get_object().message          # получить коммит, прочитать сообщение
pygit2.Repository("/path/to/repo") # открыть репозиторий
    .head                          # получить текущую ветку
    .peel(pygit2.Commit)           # получить последний коммит ветки
    .message                       # прочитать сообщение

Дальнейшее чтение

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