Git
Chapters ▾ 2nd Edition

A2.3 Appendix A: Integrando Git en tus Aplicaciones - JGit

JGit

Si deseas utilizar Git desde dentro de un programa Java, hay una biblioteca Git completamente funcional llamada JGit. JGit es una implementación relativamente completa de Git escrita de forma nativa en Java, y que se utiliza ampliamente en la comunidad Java. El proyecto JGit está bajo el paraguas de Eclipse, y su "casa" puede encontrarse en http://www.eclipse.org/jgit.

Getting Set Up

Hay varias formas de conectar tu proyecto con JGit y empezar a escribir código usando éste. Probablemente la más fácil sea utilizar Maven -la integración se consigue añadiendo el siguiente fragmento a la etiqueta <dependencies> en tu archivo pom.xml:

<dependency>
    <groupId>org.eclipse.jgit</groupId>
    <artifactId>org.eclipse.jgit</artifactId>
    <version>3.5.0.201409260305-r</version>
</dependency>

La version es bastante probable que habrá avanzado para el momento en que leas esto; comprueba http://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit para obtener información actualizada del repositorio. Una vez que se realiza este paso, Maven automáticamente adquirirá y utilizará las bibliotecas JGit que necesites.

Si prefieres gestionar las dependencias binarias tú mismo, binarios JGit pre-construidos están disponibles en http://www.eclipse.org/jgit/download. Puedes construirlos en tu proyecto ejecutando un comando como el siguiente:

javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java
java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App

Fontanería

JGit tiene dos niveles básicos de la API: fontanería y porcelana. La terminología de éstos proviene de Git, y JGit se divide en más o menos los mismos tipos de áreas: las API de porcelana son un front-end amigable para las acciones comunes a nivel de usuario (el tipo de cosas para las que un usuario normal utilizaría la herramienta de línea de comandos de Git), mientras que las API de fontanería son para interactuar directamente a bajo nivel con los objetos del repositorio.

El punto de partida para la mayoría de las sesiones JGit es la clase Repository, y la primera cosa que querrás hacer es crear una instancia de la misma. Para un repositorio basado en sistema de archivos (sí, JGit permite otros modelos de almacenamiento), esto se logra utilizando FileRepositoryBuilder:

// Create a new repository; the path must exist
Repository newlyCreatedRepo = FileRepositoryBuilder.create(
    new File("/tmp/new_repo/.git"));

// Open an existing repository
Repository existingRepo = new FileRepositoryBuilder()
    .setGitDir(new File("my_repo/.git"))
    .build();

El constructor tiene una API fluida para proporcionar todo lo que necesitas para encontrar un repositorio Git, tanto si tu programa sabe exactamente donde se encuentra como si no. Puede utilizar variables de entorno ((.readEnvironment()), empezar a partir de un lugar en el directorio de trabajo y buscar (.setWorkTree(…).findGitDir()), o simplemente abrir un directorio .git conocido como más arriba.

Una vez que tengas una instancia Repository, se pueden hacer todo tipo de cosas con ella. He aquí una muestra rápida:

// Get a reference
Ref master = repo.getRef("master");

// Get the object the reference points to
ObjectId masterTip = master.getObjectId();

// Rev-parse
ObjectId obj = repo.resolve("HEAD^{tree}");

// Load raw object contents
ObjectLoader loader = repo.open(masterTip);
loader.copyTo(System.out);

// Create a branch
RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
createBranch1.setNewObjectId(masterTip);
createBranch1.update();

// Delete a branch
RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
deleteBranch1.setForceUpdate(true);
deleteBranch1.delete();

// Config
Config cfg = repo.getConfig();
String name = cfg.getString("user", null, "name");

Hay bastantes cosas que suceden aquí, así que vamos a examinarlo sección a sección.

La primera línea consigue un puntero a la referencia master. JGit obtiene automáticamente la referencia master real, que reside en refs/heads/master, y devuelve un objeto que te permite obtener información acerca de la referencia. Puedes obtener el nombre (.getName()), y también el objeto destino de una referencia directa (.getObjectId()) o la referencia a la que apunta mediante una referencia simbólica (.getTarget()). Los objetos Ref también se utilizan para representar referencias a etiquetas y objetos, por lo que puedes preguntar si la etiqueta está 'pelada', lo que significa que apunta al objetivo final de una (potencialmente larga) cadena de texto de objetos etiqueta.

La segunda línea obtiene el destino de la referencia master, que se devuelve como una instancia ObjectId. ObjectId representa el hash SHA-1 de un objeto, que podría o no existir en la base de datos de objetos de Git. La tercera línea es similar, pero muestra cómo maneja JGit la sintaxis rev-parse (para más información sobre esto, consulta Referencias por rama); puedes pasar cualquier especificador de objeto que Git entienda, y JGit devolverá una ObjectId válida para ese objeto, o null.

Las dos líneas siguientes muestran cómo cargar el contenido en bruto de un objeto. En este ejemplo, llamamos a ObjectLoader.copyTo() para transmitir el contenido del objeto directamente a la salida estándar, pero ObjectLoader también tiene métodos para leer el tipo y el tamaño de un objeto, así como devolverlo como un array de bytes. Para objetos grandes (donde .isLarge() devuelve true), puedes llamar a .openStream() para obtener un objeto similar a InputStream del cual puedes leer los datos del objeto en bruto si almacenarlo en memoria en seguida.

Las siguientes líneas muestran lo que se necesita para crear una nueva rama. Creamos una instancia RefUpdate, configuramos algunos parámetros, y llamamos a .update() para activar el cambio. Inmediatamente después de esto está el código para eliminar esa misma rama. Ten en cuenta que se requiere .setForceUpdate(true) para que esto funcione; de lo contrario la llamada .delete() devolverá REJECTED, y no pasará nada.

El último ejemplo muestra cómo buscar el valor user.name a partir de los archivos de configuración de Git. Este ejemplo de configuración utiliza el repositorio que abrimos anteriormente para la configuración local, pero detectará automáticamente los archivos de configuración global y del sistema y leerá los valores de ellos también.

Ésta es sólo una pequeña muestra de la API de fontanería completa; hay muchos más métodos y clases disponibles. Tampoco se muestra aquí la forma en la que JGit maneja los errores, que es a través del uso de excepciones. La API de JGit a veces lanza excepciones Java estándar (como IOException), pero también hay una gran cantidad de tipos de excepciones específicas de JGit que se proporcionan (tales como NoRemoteRepositoryException, CorruptObjectException, y NoMergeBaseException).

Porcelana

Las APIs de fontanería son bastante completas, pero puede ser engorroso encadenarlas juntas para alcanzar objetivos comunes, como la adición de un archivo en el index, o hacer un nuevo commit. JGit proporciona un conjunto de APIs de más alto nivel para facilitar esto, y el punto de entrada a estas APIs es la clase Git:

Repository repo;
// construct repo...
Git git = new Git(repo);

La clase Git tiene un buen conjunto de métodos estilo builder de alto nivel que se pueden utilizar para construir un comportamiento bastante complejo. Echemos un vistazo a un ejemplo - haciendo algo como git ls-remote:

CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd");
Collection<Ref> remoteRefs = git.lsRemote()
    .setCredentialsProvider(cp)
    .setRemote("origin")
    .setTags(true)
    .setHeads(false)
    .call();
for (Ref ref : remoteRefs) {
    System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
}

Este es un patrón común con la clase Git; los métodos devuelven un objeto de comando que te permite encadenar llamadas a métodos para establecer los parámetros, que se ejecutan cuando se llama .call(). En este caso, estamos solicitando las etiquetas del repositorio remoto origin, pero no las cabezas (heads). Observa también el uso de un objeto CredentialsProvider para la autenticación.

Muchos otros comandos están disponibles a través de la clase Git, incluyendo, aunque no limitado, a add, blame, commit, clean, push, rebase, revert, y reset.

Otras Lecturas

Esta es sólo una pequeña muestra de todas las posibilidades de JGit. Si estás interesado y deseas aprender más, aquí tienes dónde buscar información e inspiración: