다른 방법으로는 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가 가리키는 커밋을 가져온다. (자세한 내용은 ch07-git-tools.asc 참고)
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는 예외를 사용해서 더 간결하다. 하지만 ConfigError
나 ObjectError
같은 에러가 발생할 수 있다.
그리고 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 => "[email protected]",
: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)
-
파일 내용이 담긴 Blob을 만든다.
-
Index에 Head 커밋의 Tree를 채우고 만든 Blob을
newfile.txt
파일로 추가한다. -
ODB(Object Database)에 새 트리 개체를 만든다. 커밋할 때는 새 트리 개체가 필요하다.
-
Author와 Committer정보는 한 사람(Signature)으로 한다.
-
커밋 메시지를 입력한다.
-
커밋할 때 부모가 필요하다. 여기서는 HEAD를 부모로 사용한다.
-
Rugged (and Libgit2)는 커밋할 때 Ref 갱신 여부를 선택할 수 있다.
-
리턴한 커밋 개체의 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)
(에러는 처리하지 않았다. 실제로 사용할 때는 완벽하리라 믿는다.)
-
“Frontend” 로 사용할 ODB(Object DataBase)를 하나 초기화한다. 실제로 저장하는 “Backend” 의 컨테이터로 사용한다.
-
ODB Backend를 초기화한다.
-
Frontend에 Backend를 추가한다.
-
저장소를 열고 우리가 만든 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가 가리키는 커밋의 메시지를 가져오는 코드를 보여준다.
이 바인딩은 C#으로 작성했고 Libgit2를 감쌌음에도 네이티브 느낌이 나도록 꼼꼼하게 설계했다. 커밋 메시지를 가져오는 예제를 보자.
new Repository(@"C:\path\to\repo").Head.Tip.Message;
Windows 데스크톱 애플리케이션에서 쉽게 사용할 수 있도록 NuGet 패키지도 존재한다.
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가 아니라고 걱정하지 않아도 된다.
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 파일과 테스트를 참고해야 한다. 읽어보면 어디서부터 시작해야 하는지 알려준다.