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