Git
Chapters ▾ 2nd Edition

A2.3 Appendix B: 애플리케이션에 Git 넣기 - JGit

JGit

Java에는 JGit이라는 훌륭한 Git 라이브러리가 있다. JGit에는 Git 기능이 한가득 구현돼 있다. 순수하게 Java로 작성됐고 Java 커뮤니티에서 널리 사용한다. The JGit 프로젝트는 Eclipse 재단에 둥지를 틀었고 홈페이지는 http://www.eclipse.org/jgit에 있다.

설치하기

JGit을 프로젝트에 추가해서 코딩을 시작하는 방법은 여러 가지다. 그중 Maven을 사용하는 방법이 가장 쉽다. pom.xml 파일에 <dependencies> 태그를 아래와 같이 추가한다.

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

version 은 시간에 따라 올라갈 것이기 때문에 http://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit에서 최신 버전을 확인해야 한다. 추가하면 Maven이 우리가 명시한 버전의 JGit을 자동으로 추가해준다.

반면 수동으로 바이너리를 관리하고 싶을 수도 있다. http://www.eclipse.org/jgit/download 에서 빌드된 바이너리를 내려받는다. 이 바이너리를 이용해서 아래와 같이 컴파일할 수 있다:

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

Plumbing

JGit의 API는 크게 Plumbing과 Porcelain으로 나눌 수 있다. 이 둘은 Git 용어이고 JGit도 이에 따라 나눈다. 일반 사용자가 사용하는 Git 명령어를 Porcelain 명령어라고 부르는데 이와 관련된 API도 Procelain API라고 부른다. 반대로 Plumbing API는 저장소 개체를 저수준에서 직접 사용하는 API다.

JGit을 사용하는 것은 Repository 클래스의 인스턴스를 만드는 것으로 시작한다. 파일 시스템에 있는 저장소에 접근할 때는 FileRepostiorybuilder 를 사용한다.

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

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

Git 저장소를 나타내는 정보를 하나씩 이 빌더 넘긴다. 넘기는 정보에 따라 조금 다른 API를 사용한다. 환경 변수를 읽고(.readEnvironment()) 워킹 디렉토리를 주고 Git 디렉토리를 찾을 수도 있고(.setWorkTree(…).findGitDir()) 예제로 보여준 것처럼 아예 .git 디렉토리를 바로 넘겨 줄 수도 있다.

Repository 인스턴스를 기점으로 온갖 일을 다 할 수 있다. 예제를 하나 보자.

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

이 예제가 어떤 뜻인지 하나씩 살펴보자.

첫 라인에서 master Ref를 얻었다. Jgit은 refs/heads/master 에 있는 진짜 master Ref를 가져와서 인스턴스를 리턴한다. 이 객체로 Ref에 대한 정보를 얻을 수 있다. 이름(.getName()), Ref가 가리키는 개체(.getObjectId()), Symbolic Ref가 가리키는 Ref(.getTarget())를 이 객체로 얻을 수 있다. Ref 인스턴스는 태그 Ref와 개체를 나타내고 태그가 “Peeled” 인지도 확인할 수 있다. “Peeled” 은 껍질을 다 벗긴 상태 그러니까 커밋 개체를 가리키는 상태를 말한다.

두 번째 라인은 master 가 가리키는 ObjectId 인스턴스를 리턴한다. ObjectId는 객체의 SHA-1 해시 정보다. 실제로 객체가 Git 객체 데이터베이스에 존재하는지는 상관없다. 셋째 라인도 ObjectId 인스턴스를 리턴하는데 JGit에서 rev-parse 문법을 어떻게 다뤄야 하는지 보여준다. 이 문법은 브랜치로 가리키기에서 설명했다. Git이 이해하는 표현은 전부 사용 가능하다. 표현식이 맞으면 해당 객체를 리턴하고 아니면 null을 리턴한다.

그다음 두 라인은 객체의 내용을 읽어서 보여준다. ObjectLoader.copyTo() 함수로 객체의 내용을 표준출력으로 출력(Stream)했다. ObjectLoader에는 객체의 타입과 크기를 알려주거나 객체의 내용을 바이트 배열에 담아서 리턴하는 메소드도 있다. 파일이 큰지도 확인할 수 있다. .isLarge() 라는 메소드가 true 를 리턴하면 큰 파일이다. 큰 파일이면 .openStream() 호출해서 ObjectStream 인스턴스를 얻는다. 이 인스턴스는 일종의 InputStream으로 한 번에 전부 메모리로 올리지 않고 데이터를 처리할 수 있게 해준다.

그다음 몇 라인은 새 브랜치를 만드는 것을 보여준다. RefUpdate 인스턴스를 만들고, 파라미터를 설정하고 나서 .update() 를 호출하면 브랜치가 생성된다. 그다음 몇 라인은 만든 브랜치를 삭제하는 코드다. .setForceUpdate(true) 는 꼭 필요하다. 이것을 빼먹으면 .delete()REJECTED 를 리턴하고 아무 일도 일어나지 않는다.

마지막 예제는 user.name 이라는 설정 값을 가져오는 것이다. 이 코드는 마치 해당 저장소의 local 설정만 읽어서 Config 객체를 리턴하는 것 같지만, global 설정과 system 설정까지 잘 찾아서 적용해준다.

여기서는 Plumbing API의 맛보기 정도만 보여줬다. 이용 가능한 메소드와 클래스가 많이 있다. 그리고 JGit의 에러를 처리하는 방법도 생략했다. JGIT API에서는 JGit에서 정의한 NoRemoteRepositoryException, CorruptObjectException, NoMergeBaseException 같은 예외뿐만 아니라 IOExceptioin 같은 Java 표준 예외도 던진다.

Porcelain

Plumbing API로도 모든 일을 다 할 수 있지만, 일반적인 상황에 사용하기에는 좀 귀찮다. Index에 파일을 추가하거나 새로 커밋하는 것 같은 일은 Porcelain API가 낫다. Porcelain API는 고수준에서 사용하기 편하게 했고 Git 클래스의 인스턴스를 만드는 것으로 시작한다.

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

Git 클래스는 빌더 스타일의 메소드의 집합이라서 복잡해 보이는 일을 쉽게 할 수 있다. 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());
}

Git 클래스는 이런 식으로 사용한다. 메소드가 해당 Command 인스턴스를 리턴하면 체이닝으로 메소드를 호출해서 파라미터를 설정하고 .call() 을 호출하는 시점에 실제로 실행된다. 이 예제는 origin 리모트의 'tag’를 요청하는 예제다. 'head’는 빼고 요청한다. 사용자 인증은 CredentialsProvider 객체를 사용한다는 점을 기억하자.

Git 클래스로 실행하는 명령은 매우 많다. 우리에게 익숙한 add, blame, commit, clean, push, rebase, revert, reset 명령 말고도 많다.

읽을거리

여기서는 JGit을 아주 조금만 보여줬다. 자세히 알고 싶다면 아래 링크에서 도움받을 수 있다.