Chapters ▾ 2nd Edition

A2.2 Appendix B: Incorporando o Git nos seus Aplicativos - Libgit2

Libgit2

Outra opção à sua disposição é usar a Libgit2. A Libgit2 é uma implementação do Git sem dependências, com foco em oferecer uma boa API para uso dentro de outros programas. Você pode encontrá-la em https://libgit2.org.

Primeiro, vejamos como é a API em C. Eis um tour relâmpago:

// 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);

As primeiras linhas abrem um repositório Git. O tipo git_repository representa um handle para um repositório com cache em memória. Esse é o método mais simples, usado quando você conhece o caminho exato para o diretório de trabalho ou a pasta .git de um repositório. Há também git_repository_open_ext, que inclui opções de busca; git_clone e funções relacionadas, para criar um clone local de um repositório remoto; e git_repository_init, para criar um repositório inteiramente novo.

O segundo trecho de código usa a sintaxe rev-parse (consulte Referências de Branch para saber mais) para obter o commit para o qual HEAD acaba apontando. O tipo retornado é um ponteiro git_object, que representa algo existente no banco de dados de objetos Git de um repositório. Na verdade, git_object é um tipo “parent” de vários tipos diferentes de objetos; o layout de memória de cada tipo “child” é igual ao de git_object, portanto você pode convertê-lo com segurança para o tipo correto. Neste caso, git_object_type(commit) retornaria GIT_OBJ_COMMIT, portanto é seguro convertê-lo para um ponteiro git_commit.

O próximo pedaço de código mostra como acessar as propriedades do commit. A última linha usa o tipo git_oid, que é a representação de um hash SHA-1 na Libgit2.

A partir deste exemplo, alguns padrões começam a surgir:

  • Se você declarar um ponteiro e passar uma referência a ele para uma chamada da Libgit2, essa chamada provavelmente retornará um código de erro inteiro. O valor 0 indica sucesso; qualquer valor menor indica erro.

  • Se a Libgit2 preencher um ponteiro para você, será sua responsabilidade liberá-lo.

  • Se a Libgit2 retornar um ponteiro const de uma chamada, você não precisará liberá-lo, mas ele se tornará inválido quando o objeto ao qual pertence for liberado.

  • Escrever em C é um pouco doloroso.

Esse último ponto significa que é pouco provável que você escreva em C ao usar a Libgit2. Felizmente, há vários bindings específicos de linguagem que facilitam bastante o trabalho com repositórios Git a partir da sua linguagem e do seu ambiente. Vejamos o exemplo anterior escrito com os bindings Ruby da Libgit2, chamados Rugged e disponíveis em 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

Como você pode ver, o código é muito menos carregado. Primeiro, o Rugged usa exceções; ele pode lançar erros como ConfigError ou ObjectError para sinalizar condições de erro. Segundo, não há liberação explícita de recursos, pois o Ruby possui coleta de lixo. Vejamos um exemplo um pouco mais complexo: criar um commit do zero

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. Crie um novo blob que contenha o conteúdo de um novo arquivo.

  2. Preencha o index com a árvore do commit apontado por head e adicione o novo arquivo no caminho newfile.txt.

  3. Isso cria uma nova árvore no ODB e a usa no novo commit.

  4. Usamos a mesma assinatura nos campos de autor e committer.

  5. A mensagem de commit.

  6. Ao criar um commit, você precisa especificar os pais do novo commit. Isso usa a ponta de HEAD como o único pai.

  7. O Rugged (e a Libgit2) pode, opcionalmente, atualizar uma referência ao criar um commit.

  8. O valor de retorno é o hash SHA-1 de um novo objeto de commit, o qual você pode então usar para obter um objeto Commit.

O código Ruby é simples e limpo, mas, como a Libgit2 faz o trabalho pesado, ele também será executado com bastante rapidez. Se você não programa em Ruby, abordamos outros bindings em Outros Bindings.

Funcionalidades Avançadas

O Libgit2 tem algumas capacidades que estão fora do escopo do núcleo do Git. Um exemplo é a plugabilidade: o Libgit2 permite que você providencie “backends” customizados para diversos tipos de operações, de modo que você possa armazenar as coisas de uma forma diferente daquela que o Git padrão faz. O Libgit2 permite backends customizados para a configuração, o armazenamento de referências, e a base de dados de objetos, entre outras coisas.

Vamos dar uma olhada em como isso funciona. O código abaixo foi emprestado a partir do conjunto de exemplos para backend providos pelo próprio time de atuantes no Libgit2 (os quais ali constam que podem ser achados em https://github.com/libgit2/libgit2-backends). Aqui está como um backend customizado para o banco de dados de objetos é configurado:

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)

Note que os erros são capturados, mas não tratados. Esperamos que o seu código seja melhor que o nosso.

  1. Inicializa um “frontend,” vazio do banco de dados de objetos (ODB), que atuará como contêiner dos “backends” responsáveis pelo trabalho real.

  2. Inicializa um backend de ODB customizado.

  3. Adiciona o backend ao frontend.

  4. Abre um repositório, e o configura para usar o nosso ODB para procurar por objetos.

But what is this git_odb_backend_mine thing? Bem, esse é o construtor da sua própria implementação de ODB, e você pode fazer o que quiser nele, desde que preencha corretamente a estrutura git_odb_backend. Esta é uma possível implementação:

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

A restrição mais sutil é que o primeiro membro de my_backend_struct` deve ser uma estrutura git_odb_backend; isso garante que a disposição da memória seja a esperada pelo código da Libgit2. O restante é arbitrário; essa estrutura pode ser tão grande ou pequena quanto necessário.

A função de inicialização aloca um pouco de memória para a estrutura, configura o contexto customizado, e então preenche os membros da estrutura parent que ela suporta. Dê uma olhada no arquivo include/git2/sys/odb_backend.h no código-fonte da Libgit2 para um conjunto completo de assinaturas de chamadas; seu caso de uso em particular ajudará a determinar quais dessas você irá querer suportar.

Outros Bindings

O Libgit2 possui bindings para muitas linguagens. Aqui mostramos um pequeno exemplo usando alguns dos pacotes de bindings mais completos na data em que isto é escrito; bibliotecas existem para muitas outras linguagens, incluindo C++, Go, Node.js, Erlang, e a JVM, todas em vários estágios de maturidade. A coleção oficial de bindings pode ser encontrada navegando pelos repositórios em https://github.com/libgit2. O código que nós iremos escrever vai retornar a mensagem de commit a partir do commit que eventualmente seja apontado por HEAD (mais ou menos como no git log -1).

LibGit2Sharp

Se você estiver escrevendo um aplicativo de .NET ou Mono, LibGit2Sharp (https://github.com/libgit2/libgit2sharp) é o que você está procurando. Os bindings são escritos em C#, e foi tomado um grande cuidado para envolver (wrap) as chamadas brutas (raw) do Libgit2 com APIs de CLR com uma pegada bem nativa (native-feeling). Aqui temos como nosso programa de exemplo se parece:

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

Para aplicativos de Windows no desktop, existe até mesmo um pacote no NuGet que te ajudará a iniciar nisto tudo muito rapidamente.

objective-git

Se o seu aplicativo está rodando numa plataforma da Apple, é muito provável que você esteja usando o Objective-C enquanto sendo sua linguagem para implementação. Objective-Git (https://github.com/libgit2/objective-git) é o nome para os bindings da Libgit2 para este respectivo ambiente. O programa de exemplo se parece com isto:

GTRepository *repo =
    [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL];
NSString *msg = [[[repo headReferenceWithError:NULL] resolvedTarget] message];

O Objective-git é totalmente interoperável com Swift, portanto não se preocupe se você já deixou o Objective-C para trás.

pygit2

Os bindings Python da Libgit2 são chamados Pygit2 e estão disponíveis em https://www.pygit2.org. Nosso programa de exemplo:

pygit2.Repository("/path/to/repo") # open repository
    .head                          # get the current branch
    .peel(pygit2.Commit)           # walk down to the commit
    .message                       # read the message

Leituras Adicionais

Naturalmente, uma abordagem completa dos recursos da Libgit2 está fora do escopo deste livro. Se quiser mais informações sobre a própria Libgit2, há documentação da API em https://libgit2.github.com/libgit2 e um conjunto de guias em https://libgit2.github.com/docs. Para os outros bindings, consulte o README e os testes incluídos; muitas vezes há pequenos tutoriais e indicações de leituras adicionais nesses arquivos.