Skip to content

Commit

Permalink
treeオブジェクトの作成
Browse files Browse the repository at this point in the history
  • Loading branch information
hyphen-o committed Oct 3, 2024
1 parent 041dff8 commit e725032
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 2 deletions.
48 changes: 48 additions & 0 deletions src/commands/commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { COMMIT_OPTIONS, GIT_INDEX } from "../constants.js";
import { coloredLog } from "../functions/colored-log.js";
import { GitIndex } from "../models/git-index.js";
import { TreeObject } from "../models/tree-object.js";

export const commit = async (options: Array<string>): Promise<void> => {
//ファイル名指定でコミットはできない仕様とする
const option = options[0];
const message = options[1];

//optionもしくはmessageが存在しない場合
if (!(option && message)) {
coloredLog({
text: "invalid command",
color: "red",
});
return;
}

//optionがあらかじめ用意したものと一致しない場合
if (!COMMIT_OPTIONS.some((OPTION) => OPTION.name === option)) {
coloredLog({
text: `error: unknown switch '${option}'\n`,
color: "red",
});
console.log("Commit options:");
COMMIT_OPTIONS.forEach((option) => {
console.log(` ${option.name} ${option.description}\n`);
});
}

const gitIndex = new GitIndex(GIT_INDEX);
await gitIndex.initialize();
const fileData = gitIndex.getFileData();

const treeObject = new TreeObject(fileData);
const rootTreeHash = await treeObject.dumpAllTrees();

//ファイルがステージングされていない場合
if (!rootTreeHash) {
console.log(
'nothing added to commit but untracked files present (use "git add" to track)',
);
return;
}

console.log("rootTreeHash: ", rootTreeHash);
};
2 changes: 2 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { add } from "./add.js";
import { commit } from "./commit.js";
import { log } from "./log.js";

export const validCommand = {
add: add,
commit: commit,
log: log,
help: () => {
console.log("Available commands:");
Expand Down
7 changes: 7 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export const CWD = process.cwd();
export const GIT_DIR = join(process.cwd(), ".git");
export const GIT_OBJECTS = join(GIT_DIR, "objects");
export const GIT_INDEX = join(GIT_DIR, "index");

export const COMMIT_OPTIONS = [
{
name: "-m",
description: "commit message",
},
];
12 changes: 10 additions & 2 deletions src/models/git-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ export class GitIndex {
this.entries = [];
};

public getFilePaths = (): Array<string> => {
return this.entries.map((entry) => entry.filePath);
public getFileData = (): Array<{
filePath: string;
hash: string;
}> => {
return this.entries.map((entry) => {
return {
filePath: entry.filePath,
hash: entry.hash,
};
});
};

public checkDuplicate = (filePath: string, hash: string): boolean => {
Expand Down
170 changes: 170 additions & 0 deletions src/models/tree-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import crypto from "crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { deflateSync } from "zlib";

import { exists } from "../functions/exists.js";
import { generateObjectPath } from "../functions/path.js";

interface TreeEntry {
mode: string;
hash: string;
name: string;
type: "blob" | "tree";
}

interface FileSystem {
[key: string]: FileSystem | { hash: string };
}

interface FileData {
filePath: string;
hash: string;
}

export class TreeObject {
private fileSystem: FileSystem = {};
private treeObjects = new Map<string, Array<TreeEntry>>();

constructor(fileData: Array<FileData>) {
this.buildFileSystem(fileData);
this.createTreeObjects();
}

//ファイルパスとハッシュからファイル構造を構築
private buildFileSystem(fileData: Array<FileData>): void {
fileData.forEach(({ filePath, hash }) => {
const parts = filePath.split("/");
let current = this.fileSystem;
parts.forEach((part, index) => {
if (index === parts.length - 1) {
current[part] = { hash };
} else {
if (!(part in current)) {
current[part] = {};
}
current = current[part] as FileSystem;
}
});
});
}

private createTreeObjects(): void {
this.createTreeObjectsRecursive("", this.fileSystem);
}

private createTreeObjectsRecursive(path: string, node: FileSystem): string {
const entries: Array<TreeEntry> = [];

for (const [name, value] of Object.entries(node)) {
if ("hash" in value) {
entries.push({
mode: "100644",
hash: String(value.hash),
name,
type: "blob",
});
} else {
const subPath = path ? `${path}/${name}` : name;
const hash = this.createTreeObjectsRecursive(subPath, value);
entries.push({
mode: "040000",
hash,
name,
type: "tree",
});
}
}

//一意なtreeオブジェクトを生成するためにentryを名前順にsortしておく
const sortedEntries = entries.sort((a, b) => a.name.localeCompare(b.name));

const treeHash = this.createTreeHash(sortedEntries);
this.treeObjects.set(path, sortedEntries);

return treeHash;
}

private createTreeHash(entries: Array<TreeEntry>): string {
const buffers: Array<Buffer> = [];

for (const entry of entries) {
const entryContent = `${entry.mode} ${entry.name}\0${entry.hash}`;
buffers.push(Buffer.from(entryContent));
}

const contentBuffer = Buffer.concat(
buffers.map((buffer) => Uint8Array.from(buffer)),
);
const headerBuffer = Buffer.from(
`tree ${contentBuffer.length.toString()}\0`,
);
const treeBuffer = Buffer.concat([
Uint8Array.from(headerBuffer),
Uint8Array.from(contentBuffer),
]);

return crypto
.createHash("sha1")
.update(Uint8Array.from(treeBuffer))
.digest("hex");
}

private getTreeObject(path: string): Array<TreeEntry> | undefined {
return this.treeObjects.get(path);
}

private async dumpTree(path = ""): Promise<string | undefined> {
const entries = this.getTreeObject(path);
if (!entries) return;

const buffers: Array<Buffer> = [];

for (const entry of entries) {
buffers.push(
Buffer.from(`${entry.mode} ${entry.name}\0`),
Buffer.from(entry.hash, "hex"),
);
}

const contentBuffer = Buffer.concat(
buffers.map((buffer) => Uint8Array.from(buffer)),
);
const headerBuffer = Buffer.from(
`tree ${contentBuffer.length.toString()}\0`,
);
const treeBuffer = Buffer.concat([
Uint8Array.from(headerBuffer),
Uint8Array.from(contentBuffer),
]);

const treeHash = crypto
.createHash("sha1")
.update(Uint8Array.from(treeBuffer))
.digest("hex");

const { dirPath, filePath } = generateObjectPath(treeHash);

if (!(await exists(dirPath))) await mkdir(dirPath, { recursive: true });

const compressedContent = deflateSync(Uint8Array.from(treeBuffer));
await writeFile(filePath, Uint8Array.from(compressedContent));

if (path === "") return treeHash;
}

public async dumpAllTrees(path = ""): Promise<string | undefined> {
const entries = this.getTreeObject(path);
if (!entries || entries.length === 0) return;

const hash = await this.dumpTree(path);

for (const entry of entries) {
if (entry.type === "tree") {
const subPath = path ? `${path}/${entry.name}` : entry.name;
await this.dumpAllTrees(subPath);
}
}

return hash;
}
}

0 comments on commit e725032

Please sign in to comment.