Git
Chapters ▾ 2nd Edition

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

Libgit2

Библиотеката Libgit2 е друга опция на ваше разположение. Libgit2 е dependency-free имплементация на 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 в действителност е “parent” тип за няколко различни вида обекти, разположението в паметта за всеки от “child” типовете е същото като на git_object, така че може безопасно да се cast-ва до правилния такъв. В този случай, git_object_type(commit) ще върне GIT_OBJ_COMMIT, така че е възможно да се cast-не към git_commit указател.

Следващата част от кода показва как да се получи достъп до свойствата на къмита. Последният ред използва типа git_oid, което е Libgit2 представянето на SHA-1 хеш.

От този пример можем да направим следните изводи:

  • Ако декларирате указател и изпратите референция към него в Libgit2 повикване, това повикване вероятно ще върне целочислен код за грешка. Стойност 0 индикира успех, всичко по-малко е грешка.

  • Ако Libgit2 инициализира указател за вас, ваша е отговорността да го освободите.

  • Ако Libgit2 върне const указател от повикване, не трябва да го освобождавате, но той ще стане невалиден, когато обектът, към който принадлежи бъде освободен.

  • Писането на код на C може да бъде доста болезнено.

Последното означава, че не е много вероятно да пишете на C, когато използвате Libgit2. За щастие, налични са много language-specific bindings, които правят сравнително лесно да работите с Git хранилища от вашия специфичен език за програмиране и среда. Нека видим примера отгоре написан с помощта на Ruby bindings за 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 използва изключения (exceptions), може да подава неща като ConfigError или ObjectError за да сигнализира за грешки. Второ, няма изрично освобождаване на ресурси, понеже Ruby е garbage-collected. Нека видим по-сложен пример: създаване на къмит от нулата

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. Използваме една и съща сигнатура за author и committer полетата.

  5. Къмит съобщението.

  6. Когато създаваме къмит, трябва да укажем родителите му. В случая използваме върха на HEAD за единичен родител.

  7. Rugged (и Libgit2) може по желание да обнови референция, когато се прави къмит.

  8. Върнатата стойност е SHA-1 хеша на новия къмит обект и може да се използва за получаване на Commit обект.

Ruby кодът е чист и приятен, но понеже Libgit2 върши тежката работа, той също така ще работи и много бързо. Ако не сте привърженик на Ruby, показваме накратко и някои други bindings в секцията Други Bindings.

По-сложни функционалности

Libgit2 има доста възможности, които са извън обхвата на същността на Git. Един пример е pluggability поддръжката: Libgit2 ви позволява да подадете специализирани “backends” за няколко различни типа операции, така че можете да съхранявате неща по различен начин от Git. Libgit2 позволява custom backends за конфигурация, съхранение на референции и обектната база данни.

Нека видим как работи това. Кодът отдолу е взаимстван от множеството backend примери, които екипът на Libgit2 предоставя (на адрес https://github.com/libgit2/libgit2-backends). Ето как се настройва custom backend за базата данни с обекти:

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. Инициализираме празен object database (ODB) “frontend,” който ще служи за контейнер за “backend-те”, които всъщност вършат реалната работа

  2. Инициализираме custom ODB backend.

  3. Добавяме backend-а към frontend-а.

  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 кода очаква. Останалото е по избор, тази структура може да е толкова голяма или малка, колкото е нужно.

Инициализиращата функция запазва малко памет за структурата, настройва custom контекст и след това попълва членовете на parent структурата, която поддържа. Погледнете файла include/git2/sys/odb_backend.h от сорс кода на Libgit2 за пълния набор от call signatures, вашият специфичен случай ще ви помогне да изберете коя точно ще искате да поддържате.

Други Bindings

Libgit2 има bindings за много езици. Тук показваме малък пример за използване на някои от по-завършените (към момента на писането на книгата) bindings пакети. Библиотеки съществуват за много други платформи, включително C++, Go, Node.js, Erlang, и JVM, всяка от тях на различен етап от развитието си. Официалната колекция bindings може да се намери като разгледате хранилищата на адрес https://github.com/libgit2. Кодът, който пишем ще върне къмит съобщението на къмита, към който сочи HEAD (нещо като git log -1).

LibGit2Sharp

Ако пишете .NET или Mono приложение, LibGit2Sharp (https://github.com/libgit2/libgit2sharp) е нещото, което ви трябва. Самите bindings са написани на C# и е обърнато сериозно внимание на добрата синхронизация между чистите Libgit2 повиквания с native-feeling CLR API-та. Ето как би изглеждала примерната ни програма:

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

За desktop Windows приложения дори има и NuGet пакет, който помага да почнете по-лесно.

objective-git

Ако приложението ви работи на Apple платформа, вероятно ще използвате Objective-C като език за имплементация. Objective-Git (https://github.com/libgit2/objective-git) е името на Libgit2 binding-те за тази среда. Примерна програма:

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

Binding-ите на Libgit2 за Python се наричат Pygit2, достъпни на https://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. За другите bindings, погледнете файла README и тестовете, често там има малки указания и насоки за получаване на допълнителна информация.