Desenvolvimento

14 fev, 2014

JGit embarcável

Publicidade

Dei uma palestra relâmpago no EclipseCon Europe 2013 sobre “Embarcar o JGit” e diferentes níveis de integração com ele. Aqui estão os slides e um rascunho da transcrição da palestra.

Nível Zero

Já que o JGit é um executável, você pode simplesmente fazer um fork usando System.exec ou ProcessBuilder para executar um comando JGit. Por exemplo:

System.exec("java -jar jgit.sh --git-dir /tmp/repo init");

O jgit.sh é na verdade um shell script executável, então, se estiver rodando um sistema Unix, pode invocá-lo com ./jgit.sh.

É claro que isso é um tanto enganador, uma vez que o executável JGit não é realmente embarcável; mas isso pode ser útil para aplicativos que são sensíveis à pressão de memória ou onde a execução pode ser feita em um host na nuvem.

Essa abordagem tem diversas vantagens, especificamente o fato de que aquele que abarca já sabe como utilizá-lo, já que o JGit fornece um (sub)set dos comandos do git padrão.

Nível 1

Se precisar embarcar o JGit em um processo Java existente, então é possível usar o programa main class org.eclipse.jgit.pgm.Main e invocar o método main. Os argumentos podem ser passados ao programa como um array de strings. Isso tem a vantagem de que executar o JGit não necessita lançar um novo processo JVM e, por conta disso, pode efetuar múltiplas requisições mais rápido.

Ainda assim, é necessário parsear a saída do comando usando stream parsing para saber se algo além de “success” ou “not success” voltou (uma vez que o código de retorno do método main já vai indicar isso).

import org.eclipse.jgit.pgm.Main;

Main.main(new String[] { "--git-dir", "/tmp/repo/.git", "show", "HEAD" });

Ainda há otimizações que se perdem ao se utilizar esse nível, especialmente o fato de as bibliotecas JGit terem que parsear o conteúdo do repositório repetidamente, uma vez que nenhuma informação foi compartilhada entre as execuções.

Note que o repositório passado aqui precisa ter o diretório .git especificado.

Nível 2

A forma mais popular de interagir com o JGit envolve usar a classe Git para fazer um wrap no repositório e fornecer um conjunto de comandos porcelain. Esse é um conjunto de comandos que apenas tenta copiar os comandos de alto nível que são feitos na linha de comando; por exemplo, .add() ou .log().

import org.eclipse.jgit.api.Git

Git git = Git.open(new File("/tmp/repo/.git"));
git.clean() ...
git.lsRemote()  ...
git.log() ...

A vantagem de usar a classe Git é que você reutiliza o mesmo repositório entre as invocações, portanto comandos subsequentes podem ser mais rápidos. Você ainda possui autocompletar na IDE e correção em tempo de compilação para os argumentos, em oposição às strings não testadas nos exemplos anteriores.

Para invocar um comando, o padrão builder é usado. O resultado de .clean() é na verdade um CleanCommand. Então, para invocá-lo, você precisa invocar o método .call, depois de fornecer todos os argumentos necessários:

git.clean().setCleanDirectories(true).setIgnore(true).call();
git.lsRemote().setRemote("origin").setTags(true).setHeads(true).call();

Apesar de o builder permitir um número arbitrário de argumentos para serem compilados através de chamadas repetidas, deve-se ter cuidado para assegurar que os argumentos necessários estão configurados apropriadamente.

Nível 3

A API Git fornece uma visão de alto nível dos comandos de uma maneira portável, permitindo que o build da porcelain (comandos de alto nível que operam nas camadas mais baixas, chamadas de encanamento ou plumbing).

Para ir um nível acima, uma instância de Repository é usada.

Isso é tipicamente construído usando FileRepositoryBuilder, que utiliza, mais uma vez, o padrão builder para instanciar um repositório. Esse repositório pode ser reutilizado entre múltiplos comandos, pode ser servido via JGit usando algo como a classe org.eclipse.jgit.http.server.glue.MetaServlet.

O Repository não fornece muita informação sobre si mesmo; ele fornece meios para avaliar certas expressões “em árvores”, tais como HEAD e master~2. Entretanto, se você precisa apenas saber qual é o branch atual ou saber uma lista de tags, o Repository é tudo de que você vai precisar.

Repository repository = FileRepositoryBuilder.create(new File("/tmp/repo/.git"))
Map tags = repository.getTags();
Map refs = repository.getAllRefs();
String currentBranch = repository.getBranch();
Ref HEAD = repository.getRef("HEAD");
repository.open(HEAD.getObjectId()).copyTo(System.out)

Nível 4

Interagir com o Repository dará a você apenas informações de “somente leitura” e permitirá somente obter objetos que já são conhecidos. Para encontrar informações de um nível de caminho ou commit, alguns iteradores precisarão ser usados, conhecidos como RevWalk (iteradores de commit) e TreeWalk (iteradores de caminho/diretório).

Para implementar um comando como o log, você pode pegar um RevWalk no repositório e então iterar sobre os commits. Para expressar um ponto de início, o walker precisa saber que commits estão incluídos (e também que commits estão excluídos).

RevWalk rw = new RevWalk(repository);
Ref HEAD = repository.resolve(“HEAD”);
rw.markStart(rw.parseCommit(HEAD));
Iterator<RevCommit> it = rw.iterator();
while(it.hasNext()) {
  RevCommit commit = it.next();
  System.out.println(commit.abbreivate(6).name()
    + “ ” + commit.getShortMessage());
}
rw.dispose();

Para pegar informação sobre um caminho específico, o TreeWalk é usado para um único commit:

TreeWalk tw = new TreeWalk(repository);
ObjectId tree = repository.resolve(“HEAD^{tree}”);
tw.addTree(tree); // tree ‘0’
tw.setRecursive(true);
tw.setFilter(PathFilter.create(“some/file”));
while(tw.next()) {
  ObjectId id = tw.getObjectId(0);
  repository.open(id).copyTo(System.out);
}
tw.release();

Apesar de isso parecer uma forma complexa de processar commits e diretórios, isso mapeia para a representação Git subjacente de inúmeras maneiras. Isso também permite a habilidade de caminhar por múltiplas árvores ou trechos de commits de uma só vez; filtros adicionais como um AuthorRevFilter ou CommitTimeFilter podem ser usados para restringir um trecho de commits, ou de forma similar, os caminhos, através da subclasse de TreeFilter.

Note que o walker deve ser liberado/descartado no final do uso, para se assegurar de que ele não retém informação (e, por conta disso, memória) que pode não ter mais interesse algum. Note também que o walker não é thread-safe, portanto deve se invocado dentro de uma única thread.

Nível 5

Finalmente, para colocar e tirar objetos de um repositório Git, é necessário usar ObjectInserter e ObjectReader.

Conhecimento sobre isso está fora do escopo deste artigo, mas há exemplo de como fazer um “Hello World” com o JGit:

ObjectId hello = repository.newObjectInserter().insert(Constants.OBJ_BLOB,
  "hello world".getBytes("UTF-8"));
repository.newObjectReader().open(hello).copyTo(System.out);

Note que objetos inseridos dentro do repositório Git tornam-se elegíveis para o garbage collector, a não ser que sejam referenciados por um commit e uma árvore que é alcançável a partir de um ref em um repositório.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://alblue.bandlem.com/2013/11/embedding-jgit.html