From fc4d0ea7d3601e7e889c4a5eb0d88b449a4bc111 Mon Sep 17 00:00:00 2001 From: Keigo Okamoto <107530622+hyphen-o@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:55:56 +0900 Subject: [PATCH] =?UTF-8?q?tree=E3=83=A2=E3=83=87=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E4=BD=9C=E6=88=90=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * treeオブジェクトの作成 * format * リファクタ --- src/commands/commit.ts | 48 ++++++++++++ src/commands/index.ts | 2 + src/constants.ts | 7 ++ src/models/git-index.ts | 12 ++- src/models/tree-object.ts | 157 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/commands/commit.ts create mode 100644 src/models/tree-object.ts diff --git a/src/commands/commit.ts b/src/commands/commit.ts new file mode 100644 index 0000000..a277129 --- /dev/null +++ b/src/commands/commit.ts @@ -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): Promise => { + //ファイル名指定でコミットはできない仕様とする + 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); +}; diff --git a/src/commands/index.ts b/src/commands/index.ts index 634fcae..e13b794 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -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:"); diff --git a/src/constants.ts b/src/constants.ts index 66acc8c..12bfb11 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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", + }, +]; diff --git a/src/models/git-index.ts b/src/models/git-index.ts index 33f4792..0bca7fa 100644 --- a/src/models/git-index.ts +++ b/src/models/git-index.ts @@ -36,8 +36,16 @@ export class GitIndex { this.entries = []; }; - public getFilePaths = (): Array => { - 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 => { diff --git a/src/models/tree-object.ts b/src/models/tree-object.ts new file mode 100644 index 0000000..c005676 --- /dev/null +++ b/src/models/tree-object.ts @@ -0,0 +1,157 @@ +import { createHash } from "crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +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>(); + + constructor(fileData: Array) { + this.buildFileSystem(fileData); + this.createTreeObjects(); + } + + public async dumpAllTrees(path = ""): Promise { + 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 ? join(path, entry.name) : entry.name; + await this.dumpAllTrees(subPath); + } + } + + return hash; + } + + //ファイルパスとハッシュからファイル構造を構築 + private buildFileSystem(fileData: Array): 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 = []; + + 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): string { + const treeBuffer = this.createBufferFromEntries(entries); + + return createHash("sha1").update(Uint8Array.from(treeBuffer)).digest("hex"); + } + + private getTreeObject(path: string): Array | undefined { + return this.treeObjects.get(path); + } + + private createBufferFromEntries = (entries: Array): Buffer => { + const buffers: Array = []; + + 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), + ]); + + return treeBuffer; + }; + + private async dumpTree(path = ""): Promise { + const entries = this.getTreeObject(path); + if (!entries) return; + + const treeBuffer = this.createBufferFromEntries(entries); + + const treeHash = 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; + } +}