Git 🌙
Chapters ▾ 2nd Edition

A2.2 Додаток 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(commit) має повернути GIT_OBJ_COMMIT, отже його безпечно приводити до вказівника на git_commit.

Наступна частина демонструє нам доступ до властивостей коміту. Останній рядок використовує тип git_oid; це представлення SHA-1 хешу в Libgit2.

З цього прикладу, видніються декілька загальних правил:

  • Якщо оголосити вказівник та передати посилання на нього до виклику Libgit2, то цей виклик імовірно поверне цілочисельний код помилки. Значення 0 означає успіх; будь-що менше — помилку.

  • Якщо Libgit2 заповнює вказівник для вас, то ви відповідальні за його звільнення.

  • Якщо виклик Libgit2 повертає константний вказівник, то ви не маєте його звільняти, проте він стане нечинним, коли звільнено обʼєкт, якому він належить.

  • Писати на C трохи боляче.

Це останнє означає, що дуже малоймовірно, що ви будете писати на C для використання Libgit2. На щастя, доступно чимало привʼязувань до окремих мов, що робить роботу зі сховищами 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 використовує винятки; він може генерувати такі речі як 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. Наповнити індекс верхівкою дерева коміту, та додати новий файл під шляхом newfle.txt.

  3. Це створює нове дерево в ODB, та використовує його для нового коміту.

  4. Ми використовуємо однаковий підпис як для автора, так і для автора коміту.

  5. Повідомлення коміту.

  6. Під час створення коміту, ви маєте задати батьків нового коміту. Це використовує верхівку HEAD для єдиного батька.

  7. Rugged (та Libgit2) може додатково оновити посилання під час створення коміту.

  8. Повертається значення SHA-1 хешу нового обʼєкту коміту, яке ви можете потім використати для отримання обʼєкту Commit.

Код Ruby гарний та чистий, проте оскільки Libgit2 робить усе можливе для оптимізації, цей код буде також працювати швидко. Якщо ви не прихильник Ruby, ми ознайомимось з іншими привʼязками в Інші привʼязки.

Заглиблений функціонал

Libgit2 має декілька можливостей, які є поза межами ядра Git. Одним прикладом є можливість використання додатків: Libgit2 дозволяє вам надавати власні обробники (backend) для декількох типів операцій, щоб ви могли зберігати речі в інший спосіб, ніж типовий 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 - object database) клієнтської частини, яка буде діяти як контейнер для обробників, які у свою чергу виконуватимуть справжню роботу.

  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 llog -1).

LibGit2Sharp

Якщо ви пишете .NET чи Mono застосунок, то LibGit2Sharp (https://github.com/libgit2/libgit2sharp) — це те, що ви шукаєте. Привʼязка написана на C#, та багато уваги було приділено тому, щоб обгорнути сирі виклики Libgit2 API, яке виглядає природнім для CLR. Ось як виглядає наш приклад:

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, та може бути знайдена за адресою 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 та тести; там часто є маленькі покрокові посібники (tutorials) та посилання на подальшу інформацію.

scroll-to-top