Git 🌙
Chapters ▾ 2nd Edition

A2.2 부록 B: 애플리케이션에 Git 넣기 - Libgit2

Libgit2

다른 방법으로는 Libgit2 라이브러리가 있다. Libgit2는 Git에 의존하지 않는다. 일반 프로그램에서 사용하기 좋게 API를 설계했다. http://libgit2.github.com에서 내려받을 수 있다.

먼저 API가 어떻게 생겼는지 구경해보자.

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

첫 두 라인은 Git 저장소를 여는 코드다. git_repository 타입은 메모리에 있는 저장소 정보에 대한 핸들을 나타낸다. git_repository_open 메소드는 워킹 디렉토리나 .git 폴더 경로를 알 때 사용한다. 저장소 경로를 정확히 모를 때는 git_repository_open_ext 메소드로 찾는다. git_clone 메소드와 관련된 메소드는 원격에 있는 저장소를 로컬에 Clone 할 때 사용한다. 그리고 git_repository_init 은 저장소를 새로 만들 때 사용한다.

rev-parse 문법을 사용하는 두 번째 코드는 HEAD가 가리키는 커밋을 가져온다. (자세한 내용은 브랜치로 가리키기 참고) git_object 포인터는 Git 개체 데이터베이스에 있는 개체를 가리킨다. git_object 는 몇 가지 “자식” 타입의 “부모” 타입이다. 이 “자식” 타입들은 git_object 에 해당하는 부분에 대해서는 메모리 구조가 같다. 그래서 맞는 자식이라면 이렇게 캐스팅해도 안전하다. git_object_type(commit) 처럼 호출하면 GIT_OBJ_COMMIT 을 리턴한다. 그래서 git_commit 포인터로 캐스팅해도 된다.

그다음 블록은 커밋 정보를 읽는 코드다. 마지막 라인의 git_oid 는 Libgit2에서 SHA-1 값을 나타내는 타입이다

이 예제를 보면 몇 가지 코딩 패턴을 알 수 있다.

  • 포인터를 정의하고 그 포인터와 Ref 스트링을 주고 Libgit2 메소드를 호출한다. 메소드는 정수 타입의 에러 코드를 리턴한다. 0 값이 성공이고 다른 값은 에러다.

  • Libgit2가 포인터에 값을 할당해주지만, 사용자가 꼭 해제해야 한다.

  • Libgit2가 리턴하는 const 포인터는 해제하지 말아야 한다. 해당 메모리가 속한 객체가 해제될 때 문제가 된다.

  • C로 코딩하는 것은 원래 좀 고통스럽다.

마지막 라인을 이유로 Libgit2를 C에서 사용할 가능성은 매우 낮다. 다양한 언어나 환경에서 사용할 수 있는 Libgit2 바인딩이 있어서 Git 저장소를 쉽게 다룰 수 있다. Rugged라는 Ruby 바인딩을 사용해서 위의 예제를 재작성해 보자. Rugged에 대한 자세한 정보는 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

비교해보면 코드가 더 간결해졌다. Rugged는 예외를 사용해서 더 간결하다. 하지만 ConfigErrorObjectError 같은 에러가 발생할 수 있다. 그리고 Ruby는 가비지 콜렉션을 사용하는 언어라서 리소스를 해제하지 않아도 된다. 좀 더 복잡한 예제를 살펴보자. 새로 커밋하는 예제다.

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. 파일 내용이 담긴 Blob을 만든다.

  2. Index에 Head 커밋의 Tree를 채우고 만든 Blob을 newfile.txt 파일로 추가한다.

  3. ODB(Object Database)에 새 트리 개체를 만든다. 커밋할 때는 새 트리 개체가 필요하다.

  4. Author와 Committer정보는 한 사람(Signature)으로 한다.

  5. 커밋 메시지를 입력한다.

  6. 커밋할 때 부모가 필요하다. 여기서는 HEAD를 부모로 사용한다.

  7. Rugged (and Libgit2)는 커밋할 때 Ref 갱신 여부를 선택할 수 있다.

  8. 리턴한 커밋 개체의 SHA-1 해시로 Commit 객체 가져와 사용한다.

Ruby 코드는 간결하고 깔끔하다. Libgit2을 사용하는 것이기 때문에 여전히 빠르다. 루비스트가 아니라면 다른 바인딩에 있는 다른 바인딩을 사용할 수 있다.

고급 기능

Libgit2으로 Git을 확장하는 일도 가능하다. Libgit2에서는 커스텀 “Backend” 를 만들어 사용할 수 있다. 그래서 Git이 저장하는 방법 말고 다른 방법으로도 저장할 수 있다. 이것을 'Pluggability’라고 부른다. 설정, Ref 저장소, 개체 데이터 베이스를 커스텀 “Backend” 에 저장할 수 있다.

이게 무슨 소리인지 예제를 살펴보자. 아래 코드는 Libgit2 팀이 제공하는 Backend 예제에서 가져왔다. Libgit2 팀이 제공하는 전체 예제는 https://github.com/libgit2/libgit2-backends에 있다. 개체 데이터베이스의 Backend를 어떻게 사용하는지 보자.

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)

(에러는 처리하지 않았다. 실제로 사용할 때는 완벽하리라 믿는다.)

  1. “Frontend” 로 사용할 ODB(Object DataBase)를 하나 초기화한다. 실제로 저장하는 “Backend” 의 컨테이터로 사용한다.

  2. ODB Backend를 초기화한다.

  3. Frontend에 Backend를 추가한다.

  4. 저장소를 열고 우리가 만든 ODB를 사용하도록 설정한다. 그러면 개체를 우리가 만든 ODB에서 찾는다.

그런데 git_odb_backend_mine 는 뭘까? 이 함수는 우리의 ODB 생성자다. 여기서 원하는 대로 Backend를 만들어 주고 git_odb_backend 구조체만 잘 채우면 된다. 아래처럼 만든다.

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

my_backend_struct 의 첫 번째 맴버는 반드시 git_odb_backend 가 돼야 한다. Libgit2가 동작하는 메모리 구조에 맞아야 한다. 나머지 멤버는 상관없다. 구조체 크기는 커도 되고 작아도 된다.

이 초기화 함수에서 구조체를 메모리를 할당하고 커스텀 멤버에 필요한 정보를 설정한다. 그리고 Libgit2에서 필요한 parent 구조체를 채운다. include/git2/sys/odb_backend.h 소스를 보면 git_odb_backend 구조체의 멤버가 어떤 것이 있는지 알 수 있다. 목적에 따라 어떻게 사용해야 하는지 확인해야 한다.

다른 바인딩

Libgit2 바인딩은 많은 언어로 구현돼 있다. 이 글을 쓰는 시점에서 거의 완벽하게 구현됐다고 생각되는 것은 여기서 소개한다. 그 외에도 C++, Go, Node.js, Erlang, JVM 등 많은 언어로 구현돼 있다. https://github.com/libgit2에 가서 살펴보면 어떤 바인딩이 있는지 찾아볼 수 있다. 여기서는 HEAD가 가리키는 커밋의 메시지를 가져오는 코드를 보여준다.

LibGit2Sharp

NET이나 Mono 애플리케이션을 만드는 중이라면 LibGit2Sharp (https://github.com/libgit2/libgit2sharp)를 사용하면 된다.

이 바인딩은 C#으로 작성했고 Libgit2를 감쌌음에도 네이티브 느낌이 나도록 꼼꼼하게 설계했다. 커밋 메시지를 가져오는 예제를 보자.

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

Windows 데스크톱 애플리케이션에서 쉽게 사용할 수 있도록 NuGet 패키지도 존재한다.

objective-git

Apple 플랫폼용 애플리케이션을 만들고 있다면 언어가 Objective-C일 것이다. 이 환경에서는 Objective-Git(https://github.com/libgit2/objective-git)을 사용할 수 있다. Objective-C 예제를 보자.

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

Objective-git는 Swift에서도 사용할 수 있기 때문에 Objective-C가 아니라고 걱정하지 않아도 된다.

pygit2

Python용 바인딩은 Pygit2라고 부른다. http://www.pygit2.org/에서 찾을 수 있다. 예제를 보자.

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

읽을거리

Libgit2를 자세히 설명하는 것은 이 책의 목적에서 벗어난다. Libgit2 자체에 대해서 공부하고 싶다면 Libgit2 가이드(https://libgit2.github.com/docs)와 API 문서(https://libgit2.github.com/libgit2)를 참고한다. Libgit2 바인딩에 대해서 알고 싶다면 해당 프로젝트의 README 파일과 테스트를 참고해야 한다. 읽어보면 어디서부터 시작해야 하는지 알려준다.

scroll-to-top