Git
Chapters ▾ 2nd Edition

A2.2 Appendix A: Integrando Git en tus Aplicaciones - Libgit2

Libgit2

© Otra opción a tu disposición es utilizar Libgit2. Libgit2 es una implementación de Git libre de dependencias, con un enfoque en tener una buena API para su uso dentro de otros programas. Puedes encontrarla en http://libgit2.github.com.

En primer lugar, echemos un vistazo a la apariencia de la API C. He aquí una gira 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);

El primer par de líneas abre un repositorio Git. El tipo git_repository representa un identificador a un repositorio con una caché en memoria. Éste es el método más simple, para cuando se conoce la ruta exacta al directorio de trabajo de un repositorio o carpeta .git. También está el git_repository_open_ext que incluye opciones para buscar, git_clone y compañía para hacer un clon local de un repositorio remoto, y git_repository_init para la creación de un repositorio completamente nuevo.

El segundo fragmento de código utiliza la sintaxis rev-parse (ver Referencias por rama para más información) para obtener el commit al HEAD finalmente apunta. El tipo devuelto es un puntero git_object, lo que representa algo que existe en la base de datos de objetos de Git para un repositorio. git_object es en realidad un tipo 'padre' de varios tipos diferentes de objetos; el diseño de memoria para cada uno de los tipos 'hijo' es el mismo que para git_object, por lo que puedes hacer casting de forma segura hacia la derecha. En este caso, git_object_type (commit) devolvería GIT_OBJ_COMMIT, así que es seguro hacer casting a un puntero git_commit.

El siguiente fragmento muestra cómo acceder a las propiedades del commit. La última línea aquí utiliza un tipo git_oid; esta es la representación de Libgit2 para un hash SHA-1.

De esta muestra, un par de patrones han comenzado a surgir:

  • Si se declara un puntero y se pasa una referencia a él en una llamada Libgit2, la llamada devolverá probablemente un código de error entero. Un valor 0 indica éxito; cualquier otra cosa es un error.

  • Si Libgit2 rellena un puntero para ti, eres responsable de liberarlo.

  • Si Libgit2 devuelve un puntero const desde una llamada, no tienes que liberarlo, pero no será válido cuando el objeto al que pertenece sea liberado.

  • Escribir C es un poco doloroso.

Esto último significa que no es muy probable que estés escribiendo C cuando utilices Libgit2. Afortunadamente, hay una serie de vínculos específicos del lenguaje disponibles que hacen que sea bastante fácil trabajar con repositorios Git desde su entorno y lenguaje específico. Echemos un vistazo al ejemplo anterior escrito utilizando los vínculos de Ruby para Libgit2, que llevan el nombre Rugged, y se puede encontrar en 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 se puede ver, el código es mucho menos desordenado. En primer lugar, Rugged utiliza excepciones; puede elevar cosas como ConfigError o ObjectError para indicar condiciones de error. En segundo lugar, no hay liberación explícita de los recursos, ya que Ruby es recolector de basura. Echemos un vistazo a un ejemplo un poco más complicado: la elaboración de un commit desde cero

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. Se crea un nuevo blob, que contiene el contenido de un nuevo archivo.

  2. Se rellena el index con el árbol de head commit, y añadimos el nuevo archivo a la ruta newfile.txt.

  3. Esto crea un nuevo árbol en la ODB, y lo utiliza para un nuevo commit.

  4. Utilizamos la misma firma, tanto para los campos del autor como del confirmador.

  5. El mensaje del commit.

  6. Al crear un commit, tienes que especificar los nuevos padres del commit. Éste utiliza la punta de HEAD para un padre único.

  7. Rugged (y Libgit2) pueden actualizar opcionalmente una referencia al hacer un commit.

  8. El valor de retorno es el hash SHA-1 de un nuevo objeto commit, que luego se puede utilizar para obtener un objeto Commit.

El código en Ruby es bonito y limpio, pero ya que Libgit2 está haciendo el trabajo pesado, este código se ejecutará bastante rápido, también. Si no eres un rubyista, tocamos algunos otros vínculos en Otros Vínculos (Bindings).

Funcionalidad Avanzada

Libgit2 tiene un par de capacidades que están fuera del ámbito del núcleo de Git. Un ejemplo es la conectividad: Libgit2 te permite proporcionar 'backends' a medida para varios tipos de operaciones, por lo que puedes almacenar las cosas de una manera diferente a como hace el Git original. Libgit2 permite backends personalizados para la configuración, el almacenamiento de referencias, y la base de datos de objetos, entre otras cosas.

Echemos un vistazo a cómo funciona esto. El código siguiente se ha tomado del conjunto de ejemplos de backend proporcionados por el equipo de Libgit2 (que se puede encontrar en https://github.com/libgit2/libgit2-backends). Así es como se configura un backend personalizado para una base de datos de objetos:

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)

(Ten en cuenta que los errores son capturados, pero no tratados. Esperamos que tu código sea mejor que el nuestro.)

  1. Se inicializa un 'frontend' a una base de datos de objetos (ODB), que actuará como contenedor de los 'backends', que son los que hacen el trabajo real.

  2. Se inicializa un backend ODB personalizado.

  3. Se añade el backend al frontend.

  4. Se abre un repositorio, y se configura para que use nuestra ODB para buscar objetos.

Pero, ¿qué es esta cosa git_odb_backend_mine? Bien, ese es el constructor para tu propia implementación ODB, y puedes hacer lo que quieras allí, siempre y cuando rellenes en el git_odb_backend la estructura correctamente. A esto es a lo que podría parecerse:

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

La restricción más sutil aquí es que el primer miembro de my_backend_struct debe ser una estructura git_odb_backend; esto asegura que la disposición de memoria sea la que el código Libgit2 espera. El resto es arbitrario; esta estructura puede ser tan grande o tan pequeña como necesites que sea.

La función de inicialización reserva memoria para la estructura, establece el contexto personalizado, y luego rellena los miembros de la estructura parent que soporta. Echa un vistazo al archivo include/git2/sys/odb_backend.h en el código fuente de Libgit2 para un conjunto completo de llamadas; tu caso de uso particular te ayudará a determinar cuál de éstas querrás soportar.

Otros Vínculos (Bindings)

Libgit2 tiene vínculos para muchos lenguajes. A continuación mostramos un pequeño ejemplo que usa algunos de los paquetes de vínculos más completos a fecha de este escrito; existen bibliotecas para muchos otros idiomas, incluyendo C++, Go, Node.js, Erlang, y la JVM, todos en diferentes etapas de madurez. La colección oficial de vínculos se puede encontrar navegando por los repositorios en https://github.com/libgit2. El código que escribiremos devolverá el mensaje del commit finalmente apuntado por HEAD (algo así como git log -1).

LibGit2Sharp

Si estás escribiendo una aplicación .NET o Mono, LibGit2Sharp (https://github.com/libgit2/libgit2sharp) es lo que estás buscando. Los vínculos están escritos en C#, y se ha tenido gran cuidado de envolver las llamadas a Libgit2 crudo con APIs CLR de apariencia nativa. Esta es la apariencia de nuestro programa de ejemplo:

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

Para las aplicaciones de escritorio de Windows, incluso hay un paquete NuGet que le ayudará a empezar rápidamente.

objective-git

Si la aplicación se ejecuta en una plataforma de Apple, es muy probable que use Objective-C como su lenguaje de implementación. Objective-Git (https://github.com/libgit2/objective-git) es el nombre de los vínculos Libgit2 para ese entorno. El programa de ejemplo es el siguiente:

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

Objective-git es totalmente interoperable con Swift, así que no temas, si has dejado atrás Objective-C.

pygit2

Los vínculos para Libgit2 en Python se llaman Pygit2, y se pueden encontrar en http://www.pygit2.org/. Nuestro programa de ejemplo:

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

Otras Lecturas

Por supuesto, un tratamiento completo de las capacidades de Libgit2 está fuera del alcance de este libro. Si deseas más información sobre Libgit2 en sí mismo, hay documentación de la API en https://libgit2.github.com/libgit2, y un conjunto de guías en https://libgit2.github.com/docs.

Para otros vínculos (bindings), comprobar el README incorporado y los tests; a menudo hay pequeños tutoriales y enlaces a otras lecturas allí.