주요 키워드
- Git Object
- Blob
- cat-file
- zlib
- std::string_view
- std::istreambuf_iterator
서론
두 번째로 진행한 기능은 git cat-file 이다.
cat-file 을 이해하기에 앞서 git 이 파일 정보를 어떻게 저장하는지 간략하게 숙지할 필요가 있었다.
git은 버전 관리를 위해 파일을 압축된 형식의 blob object 파일로 만들고 key-value 방식으로 관리한다. 여기서 key 는 blob object 파일의 내용을 sha1 알고리즘으로 해싱한 값이다. 이 값을 사용하면 해당 파일의 정보를 빠르게 획득할 수 있다. 이번에 작성한 git cat-file 이라는 명령어는 바로 sha1 해시를 사용하여 해당 키와 연결된 파일의 내용을 읽어들이기 위한 명령어이다.
git의 파일 구조를 살펴보며 정말 재미있다고 느껴진 부분이 있었는데. 실제 파일 정보가 압축된 형태로 만들어진 blob object 파일을 저장할 때, key 의 앞 두글자를 폴더이름으로 사용하고 나머지 38글자를 git object 파일의 이름으로 사용한다는 것이다.
어딘가 낯설지 않다고 느껴져 유니티 엔진의 라이브러리 폴더 내의 Artifacts 폴더를 살펴보니 유사한 방식으로 동작하고 있었다. (유니티의 경우 파일 이름도 key 와 동일하게 쓰는게 차이점이었다.)
Blob Object
Blob Object 는 git 에 의해 관리되는 파일의 정보가 담겨있는데, 다음과 같은 형식으로 구성되어 있으며 최종적으로는 zlib 라는 압축 라이브러리를 통해 압축된 형태로 보관된다.
blob <size>\0<content>
- blob : 블롭 오브젝트임을 나타내는 프리픽스
- <size> : 컨텐츠의 사이즈
- \0 : null 문자
- <content> : 실제 파일 내용
cat-file 에 필요한 구현
1) 입력 받은 해시값에서 폴더와 blob object 파일의 이름을 추출한다.
2) 1)에서 추출한 경로 정보에 있는 파일을 읽고 zlib 라이브러리로 압축을 해제한다.
3) 압축을 해제한 결과에서 \0(null) 문자를 기준으로 그 이후에 있는 컨텐츠 부분을 출력한다.
구현을 다 하고 나서보니 생각보다 어려울게 없었는데, 벌써부터 삽질을 조금 했던 것 같다.
우선, 개발 환경이 vcpkg 와 cmake 로 되어 있어서 내가 커밋을 했을 때 codecrafters 에 있는 자동화 시스템도 동일한 zlib 라이브러리를 가지고 빌드를 해야하기 때문에 이걸 어떻게 설정하면 될지 삽질을 했고, 그 다음은 이 zlib 를 어떻게 사용해야 할지 삽질을 했다.
이 과정에서 다른 사람들의 코드를 많이 참고 했는데, 내가 잘 쓰지 않던 (혹은 몰랐던) 기능들을 알 수 있었다.
코드
1) zlib 를 이용한 압축 해제 함수
int decompressData(std::string& des, const std::string& src) {
des.resize(src.size());
while (true){
// (압축된)데이터의 길이를 초기값으로 사용한다
uLong len = des.size();
// 버퍼 사이즈가 부족한 문제가 있다면 2배로 리사이징하여 재시도한다.
// (압축을 풀었을 때의 크기를 예측할 수 없기 때문)
if (auto res = uncompress((uint8_t*)des.data(), &len, (const uint8_t*)src.data(), src.size()); res == Z_BUF_ERROR) {
des.resize(des.size() * 2);
}
else if (res != Z_OK) {
std::cerr << "Failed to uncompress Zlib. (code: " << res << ")\n";
return EXIT_FAILURE;
}
else {
// 시도한 끝에 압축 해제에 성공했다면 실제 압축 해제된 길이만큼 리사이징한다.
des.resize(len);
break;
}
}
return EXIT_SUCCESS;
}
2) cat-file -p 구현부
// 0 1 2 3
// git cat-file -p <blob_sha>
if (argc < 4 || std::string(argv[2]) != "-p") {
std::cerr << "Invalid arguments, required -p <blob_sha>\n";
return EXIT_FAILURE;
}
// blob 의 sha 해시값을 string_view 에 저장해서 필요한 부분을 잘라서 사용한다 (C++17)
// string_view 는 문자열을 복사하지 않고 참조를 통해 동작하는 읽기 전용 클래스인데, 다른 포스팅에서 다뤄야겠다
const std::string_view blob_hash = std::string_view(argv[3], 40);
// 앞의 두 글자는 폴더명
const std::string_view blob_dir = blob_hash.substr(0, 2);
// 해시의 나머지 부분(38바이트)는 파일명
const std::string_view blob_name = blob_hash.substr(2);
// std::filesystem::path 는 / 가 연산자 오버로딩이 되어 있어서 아래와 같이 편리하게 경로를 합칠 수 있다.
const auto blob_path = std::filesystem::path(".git") / "objects" / blob_dir / blob_name;
auto in = std::ifstream(blob_path);
if (!in.is_open()) {
std::cerr << "Failed to open " << blob_path << " file.\n";
return EXIT_FAILURE;
}
// std::istreambuf_iterator 라는 이터레이터로 파일의 컨텐츠를 스트링 개체에 집어넣을 수 있다.
const std::string blob_data = std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
// 해시를 통해 파일 경로를 찾아서 읽어들인 값을 압축 해제한다
std::string decompressed = std::string();
if (auto res = decompressData(decompressed, blob_data); res == EXIT_FAILURE) {
return EXIT_FAILURE;
}
// 압축 해제한 값의 형식은 blob <size>\0<content> 이므로,
// 내용을 읽기 위해 \0 의 위치를 찾아 그 다음에 등장하는 내용을 출력하면 된다.
std::cout << std::string_view(decompressed).substr(decompressed.find('\0') + 1);
(다음 글에 계속)
'주제별 모험기 > Build Your Own X' 카테고리의 다른 글
| [Build your own git] #5 write-tree 명령어 만들기 (0) | 2025.01.07 |
|---|---|
| [Build your own git] #4 ls-tree 명령어와 tree objects (0) | 2025.01.07 |
| [Build your own git] #3 git hash-object 명령어 만들기 (0) | 2025.01.06 |
| [Build your own git] #1 git init 과 .git 폴더의 구조 (0) | 2025.01.01 |
| [Build your own git] #0 시작 (0) | 2025.01.01 |