diff --git a/example b/example new file mode 160000 index 0000000..5fd5ee8 --- /dev/null +++ b/example @@ -0,0 +1 @@ +Subproject commit 5fd5ee806918deb683e2c2738e85600f7f0c023a diff --git a/src/blob.ts b/src/blob.ts new file mode 100644 index 0000000..2154a98 --- /dev/null +++ b/src/blob.ts @@ -0,0 +1,103 @@ +import { createHash } from "crypto"; +import fs from "fs"; +import path from "path"; +import zlib from "zlib"; + +interface GitBlobObject { + type: "blob"; + size: number; + content: string; +} + +/** + * .git/object/ 以下に保存された Git Blob オブジェクトを読み込む。 + * @param hash Git Blob オブジェクトのハッシュ値 + * @returns GitBlobObject + */ +const loadBlob = (hash: string): GitBlobObject => { + const cwd = process.cwd(); + const dirName = hash.slice(0, 2); + const fileName = hash.slice(2); + const objectPath = path.join(cwd, ".git", "objects", dirName, fileName); + + const rawData = fs.readFileSync(objectPath, "binary"); + const dataBuffer = Buffer.from(rawData, "binary"); + const decompressed = zlib.unzipSync(toArrayBuffer(dataBuffer)); + + // Parse the Git object structure: " \0" + const decompressedString = decompressed.toString("utf-8"); + const nullIndex = decompressedString.indexOf("\0"); + if (nullIndex === -1) { + throw new Error("Invalid Git object format: Missing null separator."); + } + + const header = decompressedString.slice(0, nullIndex); + const content = decompressedString.slice(nullIndex + 1); + + const [type, size] = header.split(" "); + if (!type || !size || isNaN(Number(size))) { + throw new Error("Invalid Git object format: Invalid header."); + } + + if (type !== "blob") { + throw new Error("Invalid Git object format: Invalid type. (excepted: blob)"); + } + + return { type, size: Number(size), content }; +}; + +/** + * Git Blob オブジェクトを .git/object/ 以下に保存する。 + * @param blob GitBlobObject + */ +const saveBlob = (blob: GitBlobObject): void => { + const blobBuffer = blob2UInt8Array(blob); + + // Compress the blob data + const compressed = zlib.deflateSync(blobBuffer); + + // Calculate the hash of the blob + const hash = createHash("sha1").update(blobBuffer).digest("hex"); + + // Write the compressed blob to the file + const cwd = process.cwd(); + const dirName = hash.slice(0, 2); + const fileName = hash.slice(2); + const dirPath = path.join(cwd, ".git", "objects", dirName); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + const filePath = path.join(dirPath, fileName); + fs.writeFileSync(filePath, new Uint8Array(compressed)); +}; + + + +const blob2UInt8Array = (blob: GitBlobObject): Uint8Array => { + const contentBuffer = Buffer.from(blob.content, "utf-8"); + const header = `${blob.type} ${contentBuffer.length.toString()}\0`; + const headerBuffer = Buffer.from(header, "utf-8"); + const blobBuffer = Buffer.concat([headerBuffer, contentBuffer].map((b) => new Uint8Array(b))); + return new Uint8Array(blobBuffer); +}; + +const blob2Hash = (blob: GitBlobObject): string => { + const blobBuffer = blob2UInt8Array(blob); + return createHash("sha1").update(blobBuffer).digest("hex"); +}; + +const toArrayBuffer = (buffer: Buffer): ArrayBuffer => { + const ab = new ArrayBuffer(buffer.length); + const view = new Uint8Array(ab); + for (let i = 0; i < buffer.length; ++i) { + // TODO: buffer[i] === undefined の場合に0を入れているが、データ整合静的にはこれでいいのか? + view[i] = buffer[i] ?? 0; + } + return ab; +} + +// test +const data: GitBlobObject = { type: "blob", size: 5, content: "Hello\n" }; +saveBlob(data); +console.log(blob2Hash(data)); +console.log(loadBlob(blob2Hash(data))); \ No newline at end of file diff --git a/src/loader.ts b/src/loader.ts deleted file mode 100644 index 4aaab39..0000000 --- a/src/loader.ts +++ /dev/null @@ -1,53 +0,0 @@ -import fs from "fs"; -import path from "path"; -import zlib from "zlib"; - -type GitBlobObject = { - type: "blob"; - size: number; - content: string; -} - -const loadBlob = (hash: string): GitBlobObject => { - const cwd = process.cwd(); - const dirName = hash.slice(0, 2); - const fileName = hash.slice(2); - const objectPath = path.join(cwd, ".git", "objects", dirName, fileName); - - const rawData = fs.readFileSync(objectPath, "binary"); - const dataBuffer = Buffer.from(rawData, "binary"); - const decompressed = zlib.unzipSync(toArrayBuffer(dataBuffer)); - - // Parse the Git object structure: " \0" - const decompressedString = decompressed.toString("utf-8"); - const nullIndex = decompressedString.indexOf("\0"); - if (nullIndex === -1) { - throw new Error("Invalid Git object format: Missing null separator."); - } - - const header = decompressedString.slice(0, nullIndex); - const content = decompressedString.slice(nullIndex + 1); - - const [type, size] = header.split(" "); - if (!type || !size || isNaN(Number(size))) { - throw new Error("Invalid Git object format: Invalid header."); - } - - if(type !== "blob") { - throw new Error("Invalid Git object format: Invalid type. (excepted: blob)"); - } - - return {type, size: Number(size), content}; -}; - -const toArrayBuffer = (buffer: Buffer): ArrayBuffer => { - const ab = new ArrayBuffer(buffer.length); - const view = new Uint8Array(ab); - for (let i = 0; i < buffer.length; ++i) { - // TODO: buffer[i] === undefined の場合に0を入れているが、データ整合静的にはこれでいいのか? - view[i] = buffer[i] ?? 0; - } - return ab; -} - -console.log(loadBlob("78981922613b2afb6025042ff6bd878ac1994e85")); \ No newline at end of file diff --git a/src/mygit.ts b/src/mygit.ts index e85b721..1bb17ef 100644 --- a/src/mygit.ts +++ b/src/mygit.ts @@ -9,31 +9,23 @@ const __dirname = path.dirname(__filename); export const mygit = async (argv: Array): Promise => { const commandName = argv[2] ?? ""; - - const commandsDirPath = path.join(__dirname, "commands"); - const commandsDir = fs.readdirSync(commandsDirPath) - .filter((file) => file.endsWith(".js")); - - for (const file of commandsDir) { - if(file.replace(".js", "") !== commandName) { - continue; - } - - // * ESM Module まわりが any を使わざるを得ないので、ESLint 側の制限を緩めてます - const commandsPath = path.join(commandsDirPath, file); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const commandModule = await import(commandsPath); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const command: Command = commandModule.default; - - if (typeof command.exec === "function") { - command.exec(argv); - return; - } + + switch (commandName) { + case "log": + break; + + case "add": + break; + + case "commit": + break; + + default: + console.log("サポートされていない引数です ><"); } +} + - console.log("サポートされていない引数です ><"); - - // Avoid eslint error by adding some async operation. - await new Promise((resolve) => setTimeout(resolve, 1000)); +// Avoid eslint error by adding some async operation. +await new Promise((resolve) => setTimeout(resolve, 1000)); };