From e725032c60cd8496a25631331c45a6aa089408fb Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Thu, 3 Oct 2024 18:53:28 +0900 Subject: [PATCH 1/7] =?UTF-8?q?tree=E3=82=AA=E3=83=96=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 | 170 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 237 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..fa7a642 --- /dev/null +++ b/src/models/tree-object.ts @@ -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>(); + + constructor(fileData: Array) { + this.buildFileSystem(fileData); + this.createTreeObjects(); + } + + //ファイルパスとハッシュからファイル構造を構築 + 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 buffers: Array = []; + + 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 | undefined { + return this.treeObjects.get(path); + } + + private async dumpTree(path = ""): Promise { + const entries = this.getTreeObject(path); + if (!entries) return; + + 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), + ]); + + 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 { + 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; + } +} From 4a58a188ff1c3874d905923c312eaba44d5d632c Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Thu, 3 Oct 2024 19:23:22 +0900 Subject: [PATCH 2/7] format --- src/models/tree-object.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/models/tree-object.ts b/src/models/tree-object.ts index fa7a642..0a4fc4d 100644 --- a/src/models/tree-object.ts +++ b/src/models/tree-object.ts @@ -1,4 +1,4 @@ -import crypto from "crypto"; +import { createHash } from "crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { deflateSync } from "zlib"; @@ -88,8 +88,10 @@ export class TreeObject { const buffers: Array = []; for (const entry of entries) { - const entryContent = `${entry.mode} ${entry.name}\0${entry.hash}`; - buffers.push(Buffer.from(entryContent)); + buffers.push( + Buffer.from(`${entry.mode} ${entry.name}\0`), + Buffer.from(entry.hash, "hex"), + ); } const contentBuffer = Buffer.concat( @@ -103,10 +105,7 @@ export class TreeObject { Uint8Array.from(contentBuffer), ]); - return crypto - .createHash("sha1") - .update(Uint8Array.from(treeBuffer)) - .digest("hex"); + return createHash("sha1").update(Uint8Array.from(treeBuffer)).digest("hex"); } private getTreeObject(path: string): Array | undefined { @@ -137,8 +136,7 @@ export class TreeObject { Uint8Array.from(contentBuffer), ]); - const treeHash = crypto - .createHash("sha1") + const treeHash = createHash("sha1") .update(Uint8Array.from(treeBuffer)) .digest("hex"); From 62008d3bd06d919c73cec088f7fe55fbebcafc7b Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Thu, 3 Oct 2024 22:22:30 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/tree-object.ts | 67 ++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/src/models/tree-object.ts b/src/models/tree-object.ts index 0a4fc4d..c005676 100644 --- a/src/models/tree-object.ts +++ b/src/models/tree-object.ts @@ -1,5 +1,6 @@ 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"; @@ -30,6 +31,22 @@ export class TreeObject { 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 }) => { @@ -85,25 +102,7 @@ export class TreeObject { } private createTreeHash(entries: Array): string { - 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), - ]); + const treeBuffer = this.createBufferFromEntries(entries); return createHash("sha1").update(Uint8Array.from(treeBuffer)).digest("hex"); } @@ -112,10 +111,7 @@ export class TreeObject { return this.treeObjects.get(path); } - private async dumpTree(path = ""): Promise { - const entries = this.getTreeObject(path); - if (!entries) return; - + private createBufferFromEntries = (entries: Array): Buffer => { const buffers: Array = []; for (const entry of entries) { @@ -136,6 +132,15 @@ export class TreeObject { 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"); @@ -149,20 +154,4 @@ export class TreeObject { if (path === "") return treeHash; } - - 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 ? `${path}/${entry.name}` : entry.name; - await this.dumpAllTrees(subPath); - } - } - - return hash; - } } From 345dcfe7ec75ebb274143c682b19bc4bf73569bb Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Fri, 4 Oct 2024 11:27:41 +0900 Subject: [PATCH 4/7] =?UTF-8?q?commit=E3=82=AA=E3=83=96=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=82=92dump=E3=81=99=E3=82=8B=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/commit.ts | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/commands/commit.ts b/src/commands/commit.ts index a277129..53bba6b 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -1,5 +1,10 @@ -import { COMMIT_OPTIONS, GIT_INDEX } from "../constants.js"; +import { readFile, writeFile } from "fs/promises"; +import { join } from "path"; + +import { COMMIT_OPTIONS, GIT_DIR, GIT_HEAD, GIT_INDEX } from "../constants.js"; import { coloredLog } from "../functions/colored-log.js"; +import { extractHeadHash } from "../functions/extract-head-hash.js"; +import { Commit } from "../models/commit.js"; import { GitIndex } from "../models/git-index.js"; import { TreeObject } from "../models/tree-object.js"; @@ -29,6 +34,7 @@ export const commit = async (options: Array): Promise => { }); } + //indexからファイルパスとblobオブジェクトのhashを取得 const gitIndex = new GitIndex(GIT_INDEX); await gitIndex.initialize(); const fileData = gitIndex.getFileData(); @@ -44,5 +50,35 @@ export const commit = async (options: Array): Promise => { return; } - console.log("rootTreeHash: ", rootTreeHash); + const parentHash = await extractHeadHash(); + + //コミットに含める情報をセットしてdump + const commit = new Commit(); + commit.setCommit({ + tree: rootTreeHash, + parent: parentHash, + author: "yamada taro", + email: "yamada@gmail.com", + message: message, + }); + const commitHash = await commit.dumpCommit(); + + //headを最新のコミットhashに更新しておく + const headContent = await readFile(GIT_HEAD, "utf8"); + + if (headContent.startsWith("ref: ")) { + //今回の実装ではmainブランチのみの実装とする + const branchPath = "main"; + + const branchFilePath = join(GIT_DIR, "refs/heads", branchPath); + await writeFile(branchFilePath, commitHash, "utf8"); + + console.log(`Updated ${branchPath} branch with commit hash: ${commitHash}`); + } else { + await writeFile(GIT_HEAD, commitHash, "utf8"); + + console.log( + `Updated ${parentHash ?? ""} branch with commit hash: ${commitHash}`, + ); + } }; From 42f2dea4416093d7bdcaa38cfc8e16bdc9b72666 Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Fri, 4 Oct 2024 11:28:05 +0900 Subject: [PATCH 5/7] =?UTF-8?q?commit=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=A8=E3=81=AE=E7=B9=8B=E3=81=8E=E8=BE=BC=E3=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/log.ts | 34 ++---------- src/constants.ts | 1 + src/functions/extract-head-hash.ts | 27 ++++++++++ src/models/commit.ts | 83 ++++++++++++++++++++++++++---- 4 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 src/functions/extract-head-hash.ts diff --git a/src/commands/log.ts b/src/commands/log.ts index 8340dda..82fa9c2 100644 --- a/src/commands/log.ts +++ b/src/commands/log.ts @@ -1,40 +1,13 @@ -import { readFile } from "node:fs/promises"; -import { join } from "path"; - -import { GIT_DIR } from "../constants.js"; import { coloredLog } from "../functions/colored-log.js"; -import { exists } from "../functions/exists.js"; +import { extractHeadHash } from "../functions/extract-head-hash.js"; import { Commit, CommitFieldType } from "../models/commit.js"; -const extractHeadHash = async (): Promise => { - const headPath = join(GIT_DIR, "HEAD"); - - if (!(await exists(headPath))) { - return; - } - - const headText = await readFile(headPath).then((head) => - head.toString("utf-8"), - ); - - const refPrefix = "ref: "; - //ブランチ名かコミットハッシュのどちらをHEADに持つかを識別して出し分ける - if (headText.startsWith(refPrefix)) { - return await readFile( - join(GIT_DIR, headText.slice(refPrefix.length)).trim(), - "utf-8", - ).then((path) => path.trim()); - } else { - return headText.trim(); - } -}; - const getCommitHistory = async ( hash: string, history: Array = [], ): Promise> => { const commit = new Commit(); - await commit.setCommit(hash); + await commit.setCommitHash(hash); const commitData = commit.getCommit(); const currentHistory = [...history, commitData]; @@ -54,8 +27,7 @@ export const displayCommitHistory = ( text: `commit: ${commit.hash}`, color: "yellow", }); - console.log(`Author: ${commit.author}`); - console.log(`Committer: ${commit.committer}\n`); + console.log(`author: ${commit.author}\n`); console.log(` ${commit.message}\n`); }); }; diff --git a/src/constants.ts b/src/constants.ts index 12bfb11..895d064 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,6 +5,7 @@ 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 GIT_HEAD = join(GIT_DIR, "HEAD"); export const COMMIT_OPTIONS = [ { diff --git a/src/functions/extract-head-hash.ts b/src/functions/extract-head-hash.ts new file mode 100644 index 0000000..f038739 --- /dev/null +++ b/src/functions/extract-head-hash.ts @@ -0,0 +1,27 @@ +import { readFile } from "fs/promises"; +import { join } from "path"; + +import { GIT_DIR } from "../constants.js"; +import { exists } from "./exists.js"; + +export const extractHeadHash = async (): Promise => { + const headPath = join(GIT_DIR, "HEAD"); + + if (!(await exists(headPath))) { + return; + } + + const headText = await readFile(headPath).then((head) => + head.toString("utf-8"), + ); + + const refPrefix = "ref: "; + //ブランチ名かコミットハッシュのどちらをHEADに持つかを識別して出し分ける + if (headText.startsWith(refPrefix)) { + const branchPath = join(GIT_DIR, headText.slice(refPrefix.length)).trim(); + if (!(await exists(branchPath))) return; + return await readFile(branchPath, "utf-8").then((path) => path.trim()); + } else { + return headText.trim(); + } +}; diff --git a/src/models/commit.ts b/src/models/commit.ts index 0cd4bc8..ae5f7fd 100644 --- a/src/models/commit.ts +++ b/src/models/commit.ts @@ -1,13 +1,18 @@ +import { createHash } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { deflateSync } from "node:zlib"; import { GIT_OBJECTS } from "../constants.js"; +import { exists } from "../functions/exists.js"; +import { generateObjectPath } from "../functions/path.js"; import { readGitObject } from "../functions/read-git-object.js"; export interface CommitFieldType { tree: string; parent?: string; author: string; - committer: string; + email: string; hash: string; message: string; } @@ -16,7 +21,7 @@ export class Commit { tree: string; parent?: string; author: string; - committer: string; + email: string; hash: string; message: string; @@ -24,22 +29,30 @@ export class Commit { this.tree = ""; this.author = ""; this.message = ""; - this.committer = ""; + this.email = ""; this.hash = ""; } - public setCommit = async (hash: string): Promise => { + public setCommitHash = async (hash: string): Promise => { const content = await this.getCommitContent(hash); this.parseCommit(hash, content); }; + public setCommit = (commit: Omit): void => { + this.tree = commit.tree; + this.author = commit.author; + this.message = commit.message; + this.email = commit.email; + this.parent = commit.parent; + }; + public getCommit = (): CommitFieldType => { return { tree: this.tree, author: this.author, message: this.message, - committer: this.committer, + email: this.email, hash: this.hash, }; }; @@ -82,13 +95,63 @@ export class Commit { case "parent": this.parent = value; break; - case "author": - this.author = value; - break; - case "committer": - this.committer = value; + case "author": { + const authorRegex = /^(.*?)\s+<([^>]+)>\s+(\d+\s+[+-]\d{4})$/; + const authorMatch = authorRegex.exec(value); + if (authorMatch) { + this.author = authorMatch[1] ? authorMatch[1].trim() : ""; + this.email = authorMatch[2] ?? ""; + } else { + this.author = value; // Keep the original format if parsing fails + } break; + } } } }; + + private formatCommitContent(): string { + let commitContent = `tree ${this.tree}\n`; + + if (this.parent) { + commitContent += `parent ${this.parent}\n`; + } + + commitContent += `author ${this.author} ${this.email} ${Math.floor(Date.now() / 1000).toString()} +0900\n\n`; + commitContent += this.message; + + return commitContent; + } + + // Method to dump the commit object to the .git/objects directory + public async dumpCommit(): Promise { + const content = this.formatCommitContent(); + + // Create the buffer for the commit object + const contentBuffer = Buffer.from(content); + const headerBuffer = Buffer.from( + `commit ${contentBuffer.length.toString()}\0`, + ); + const commitBuffer = Buffer.concat([ + Uint8Array.from(headerBuffer), + Uint8Array.from(contentBuffer), + ]); + + // Create the SHA-1 hash of the commit object + const commitHash = createHash("sha1") + .update(Uint8Array.from(commitBuffer)) + .digest("hex"); + + // Generate the object path and write the compressed content + const { dirPath, filePath } = generateObjectPath(commitHash); + + if (!(await exists(dirPath))) { + await mkdir(dirPath, { recursive: true }); + } + + const compressedContent = deflateSync(Uint8Array.from(commitBuffer)); + await writeFile(filePath, Uint8Array.from(compressedContent)); + + return commitHash; + } } From 8e18d904b7372f55e5d43ba8a1e52a0be67bf6ba Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Fri, 4 Oct 2024 14:15:53 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E8=A4=87=E6=95=B0add=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/add.ts | 48 +++++++++++++++++++++++++----------- src/commands/commit.ts | 6 +++++ src/constants.ts | 4 +++ src/functions/colored-log.ts | 1 + 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/commands/add.ts b/src/commands/add.ts index 13146a4..3d23392 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { readdir, readFile, stat } from "node:fs/promises"; import { GIT_INDEX } from "../constants.js"; import { coloredLog } from "../functions/colored-log.js"; @@ -6,6 +6,34 @@ import { exists } from "../functions/exists.js"; import { isValidPath } from "../functions/is-valid-path.js"; import { BlobObject } from "../models/blob-object.js"; import { GitIndex } from "../models/git-index.js"; +import { join } from "node:path"; + +const processPath = async (filePath: string, gitIndex: GitIndex): Promise => { + const stats = await stat(filePath); + + if (stats.isDirectory()) { + if (filePath === '.git') { + return; + } + const entries = await readdir(filePath); + for (const entry of entries) { + await processPath(join(filePath, entry), gitIndex); + } + } else if (stats.isFile()) { + const content = await readFile(filePath); + const blobObject = new BlobObject(content); + const hash = await blobObject.dumpBlobObject(); + + if (!gitIndex.checkDuplicate(filePath, hash)) { + await gitIndex.pushEntry(filePath, hash); + + coloredLog({ + text: `added '${filePath}'`, + color: 'green' + }) + } + } +} export const add = async (options: Array): Promise => { const filePath = options[0]; @@ -21,7 +49,7 @@ export const add = async (options: Array): Promise => { } //ファイル名が条件を満たしていない場合の処理 - if (!isValidPath(filePath)) { + if (filePath !== '.' && !isValidPath(filePath)) { coloredLog({ text: `fatal: invalid path '${filePath}'`, color: "red", @@ -38,20 +66,10 @@ export const add = async (options: Array): Promise => { return; } - // TODO: ディレクトリを指定した際などに複数回pushEntryする - const content = await readFile(filePath); - - const blobObject = new BlobObject(content); - const hash = await blobObject.dumpBlobObject(); - const gitIndex = new GitIndex(GIT_INDEX); await gitIndex.initialize(); - if (gitIndex.checkDuplicate(filePath, hash)) { - console.log("Nothing has changed."); - return; - } - - await gitIndex.pushEntry(filePath, hash); + await processPath(filePath, gitIndex); + await gitIndex.dumpIndex(); -}; +}; \ No newline at end of file diff --git a/src/commands/commit.ts b/src/commands/commit.ts index 53bba6b..36fc161 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -7,6 +7,7 @@ import { extractHeadHash } from "../functions/extract-head-hash.js"; import { Commit } from "../models/commit.js"; import { GitIndex } from "../models/git-index.js"; import { TreeObject } from "../models/tree-object.js"; +import { add } from "./add.js"; export const commit = async (options: Array): Promise => { //ファイル名指定でコミットはできない仕様とする @@ -34,6 +35,11 @@ export const commit = async (options: Array): Promise => { }); } + //optionが -amだった場合は全てのファイルをaddする + if(option === '-am') { + await add(['.']) + } + //indexからファイルパスとblobオブジェクトのhashを取得 const gitIndex = new GitIndex(GIT_INDEX); await gitIndex.initialize(); diff --git a/src/constants.ts b/src/constants.ts index 895d064..d1858bb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,4 +12,8 @@ export const COMMIT_OPTIONS = [ name: "-m", description: "commit message", }, + { + name: "-am", + description: "add all files and commit message", + }, ]; diff --git a/src/functions/colored-log.ts b/src/functions/colored-log.ts index 87b75cf..510b703 100644 --- a/src/functions/colored-log.ts +++ b/src/functions/colored-log.ts @@ -2,6 +2,7 @@ const colors = { //https://qiita.com/shuhei/items/a61b4324fd5dbc1af79b yellow: "\u001b[33m", red: "\u001b[31m", + green: "\u001b[32m", }; export const coloredLog = ({ From ca9a67cf199adf01c21f7e310e7b43859a88f523 Mon Sep 17 00:00:00 2001 From: hyphen-o Date: Fri, 4 Oct 2024 14:16:57 +0900 Subject: [PATCH 7/7] format --- src/commands/add.ts | 23 +++++++++++++---------- src/commands/commit.ts | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/commands/add.ts b/src/commands/add.ts index 3d23392..02daed9 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -1,4 +1,5 @@ -import { readdir, readFile, stat } from "node:fs/promises"; +import { readFile, readdir, stat } from "node:fs/promises"; +import { join } from "node:path"; import { GIT_INDEX } from "../constants.js"; import { coloredLog } from "../functions/colored-log.js"; @@ -6,13 +7,15 @@ import { exists } from "../functions/exists.js"; import { isValidPath } from "../functions/is-valid-path.js"; import { BlobObject } from "../models/blob-object.js"; import { GitIndex } from "../models/git-index.js"; -import { join } from "node:path"; -const processPath = async (filePath: string, gitIndex: GitIndex): Promise => { +const processPath = async ( + filePath: string, + gitIndex: GitIndex, +): Promise => { const stats = await stat(filePath); if (stats.isDirectory()) { - if (filePath === '.git') { + if (filePath === ".git") { return; } const entries = await readdir(filePath); @@ -29,11 +32,11 @@ const processPath = async (filePath: string, gitIndex: GitIndex): Promise coloredLog({ text: `added '${filePath}'`, - color: 'green' - }) + color: "green", + }); } } -} +}; export const add = async (options: Array): Promise => { const filePath = options[0]; @@ -49,7 +52,7 @@ export const add = async (options: Array): Promise => { } //ファイル名が条件を満たしていない場合の処理 - if (filePath !== '.' && !isValidPath(filePath)) { + if (filePath !== "." && !isValidPath(filePath)) { coloredLog({ text: `fatal: invalid path '${filePath}'`, color: "red", @@ -70,6 +73,6 @@ export const add = async (options: Array): Promise => { await gitIndex.initialize(); await processPath(filePath, gitIndex); - + await gitIndex.dumpIndex(); -}; \ No newline at end of file +}; diff --git a/src/commands/commit.ts b/src/commands/commit.ts index 36fc161..91891d6 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -36,8 +36,8 @@ export const commit = async (options: Array): Promise => { } //optionが -amだった場合は全てのファイルをaddする - if(option === '-am') { - await add(['.']) + if (option === "-am") { + await add(["."]); } //indexからファイルパスとblobオブジェクトのhashを取得