
Tree 클래스
전체 코드
const crypto = require("crypto");
const fs = require("fs").promises;
const path = require("path");
class Tree {
constructor() {
this.type = "tree";
this.entries = new Map();
this.hash = null;
}
addEntry(name, hash, mode = "100644") {
this.entries.set(name, { hash, mode });
this.hash = null;
}
serialize() {
let content = "";
const sortedEntries = Array.from(this.entries.entries()).sort(([a], [b]) =>
a.localeCompare(b)
);
for (const [name, { mode, hash }] of sortedEntries) {
const type = mode === "040000" ? "tree" : "blob";
content += `${mode} ${type} ${hash}\t${name}`;
}
return content;
}
calculateHash() {
if (this.hash) return this.hash; // 이미 계산된 경우 그대로 반환
const content = this.serialize();
const header = `${this.type} ${content.length}\0`;
const data = header + content;
return crypto.createHash("sha1").update(data).digest("hex");
}
async save(repoPath) {
const hash = this.calculateHash();
const objectsPath = path.join(repoPath, ".pit", "objects");
const folderName = hash.slice(0, 2);
const fileName = hash.slice(2);
const folderPath = path.join(objectsPath, folderName);
await fs.mkdir(folderPath, { recursive: true });
await fs.writeFile(path.join(folderPath, fileName), this.serialize());
this.hash = hash;
return hash;
}
}
module.exports = { Tree };
Tree 클래스는 다음 메서드로 구성되어 있다.
constructor()addEntry()serialize()calculateHash()save()
메서드 설명
1. constructor()
Tree 객체를 초기화한다.
constructor() {
this.type = "tree";
this.entries = new Map();
this.hash = null;
}객체 타입은 "tree"로 설정하며, 파일과 디렉터리 정보를 저장할 Map 객체를 생성한다. hash의 경우 null로 초기화해둔다.
2. addEntry(name, hash, mode)
Tree에 새로운 엔트리(파일 또는 디렉터리)를 추가한다.
addEntry(name, hash, mode = "100644") {
this.entries.set(name, { hash, mode });
this.hash = null;
}
entries는 다음과 같은 구조를 지닌다.
["README.md", { hash: "...", mode: "100644" }],
["src", { hash: "...", mode: "040000" }],
["test.txt", { hash: "...", mode: "100644" }]코드 설명
- 매개변수
- name: 파일/디렉터리 이름
- hash: 객체(blob/tree) 해시값
- mode: 파일 권한(기본값 "100644")
- "100644": 일반 파일
- "040000": 디렉터리
this.hash = null: entries가 변경되면 hash를 무효화
3. serialize()
Tree 객체의 각 엔트리를 Git 형식에 맞게 포맷팅한다.
serialize() {
let content = "";
const sortedEntries = Array.from(this.entries.entries()).sort(([a], [b]) =>
a.localeCompare(b)
);
for (const [name, { mode, hash }] of sortedEntries) {
const type = mode === "040000" ? "tree" : "blob";
content += `${mode} ${type} ${hash}\t${name}`;
}
return content;
}git ls-tree <hash> 명령어를 실행했을 때 다음과 같이 출력된다.

이를 적용해 <mode> <type> <hash>\t<name> 형식으로 변환한다.
처리 과정
- entries 이름순 정렬
- 각 엔트리 git 형식으로 포맷팅
코드 설명
Array.from(): 유사 배열 객체나 이터러블 객체를 배열로 변환this.entries.entries(): Map 내부 엔트리 반환.sort(([a], [b]) => a.localeCompare(b)): 이름 기준 정렬
4. calculateHash()
Tree 객체의 SHA-1 해시값을 계산한다.
calculateHash() {
if (this.hash) return this.hash;
const content = this.serialize();
const header = `${this.type} ${content.length}\0`;
const data = header + content;
this.hash = crypto.createHash("sha1").update(data).digest("hex");
return this.hash;
}처리 과정
- 내용 직렬화
- git 형식 헤더 추가(
<type> <content-length>\0<content>) - SHA-1 해시 계산 및 반환
코드 설명
if (this.hash) return this.hash;: 해시가 이미 계산된 경우 그대로 반환한다.
5. save()
Tree 객체를 파일 시스템에 저장한다.
async save(repoPath) {
const hash = this.calculateHash();
const objectsPath = path.join(repoPath, ".pit", "objects");
const folderName = hash.slice(0, 2);
const fileName = hash.slice(2);
const folderPath = path.join(objectsPath, folderName);
await fs.mkdir(folderPath, { recursive: true });
await fs.writeFile(path.join(folderPath, fileName), this.serialize());
return hash;
}코드 설명
- 매개변수
repoPath: 저장소 경로
- 리턴
- 해시값
처리 과정
- 해시값 계산
- 객체 저장 경로 생성 (
repoPath/.pit/objects/<hash의 앞 2자리>/<hash의 나머지>) - 디렉터리 생성
- 직렬화된 내용을 파일로 저장
이 프로젝트의 모든 소스 코드는 GitHub에 공개되어 있습니다. 코드 품질 개선이나 새로운 기능 제안에 대한 피드백은 언제나 환영합니다.