diff --git a/.gitignore b/.gitignore
index 381c58f..520b71b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-.idea/
-**/node_modules/
-**/build/
-.vscode/
-coverage/
-logs/
+.idea/**
+**/node_modules/**
+**/build/**
+.vscode/**
+coverage/**
+logs/**
**/*.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 059238b..bb78bb2 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ Checkout my [Notion](https://mohitjain.notion.site/Coding-Challenges-af9b8197a43
23. [Write Your Own Traceroute](src/23/)
24. [Write Your Own Realtime Chat Client and Server - Duplicate of Write Your Own IRC Client](src/16/)
25. [Write Your Own NATS Message Broker](src/25/)
+26. [Write Your Own Git](src/26/)
## Installation
diff --git a/package-lock.json b/package-lock.json
index ddf2f6a..f99dc9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,11 +11,15 @@
"dependencies": {
"@datastructures-js/priority-queue": "^6.3.0",
"axios": "^1.4.0",
+ "commander": "^11.0.0",
"cors": "^2.8.5",
+ "diff": "^5.1.0",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
+ "glob": "^10.3.9",
"helmet": "^7.0.0",
+ "ini": "^4.1.1",
"jsdom": "^22.1.0",
"memcached": "^2.2.2",
"raw-socket": "github:algj/node-raw-socket",
@@ -28,7 +32,10 @@
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-typescript": "^7.22.5",
+ "@types/diff": "^5.0.5",
"@types/express": "^4.17.17",
+ "@types/glob": "^8.1.0",
+ "@types/ini": "^1.3.31",
"@types/jest": "^29.5.3",
"@types/jsdom": "^21.1.2",
"@types/memcached": "^2.2.7",
@@ -2208,6 +2215,95 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -2520,6 +2616,26 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "node_modules/@jest/reporters/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@jest/schemas": {
"version": "29.6.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz",
@@ -2758,6 +2874,15 @@
"node": ">= 8"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@pkgr/utils": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
@@ -2980,6 +3105,12 @@
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
"dev": true
},
+ "node_modules/@types/diff": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.5.tgz",
+ "integrity": "sha512-rt7WqM1bWwKJMRxlB5Rhke56UN21Bqwp1ILER31bafTivcapYdfhtPd5xRWfhf08yjPxoDcfjVkkECdRwFe7EA==",
+ "dev": true
+ },
"node_modules/@types/express": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
@@ -3004,6 +3135,16 @@
"@types/send": "*"
}
},
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -3019,6 +3160,12 @@
"integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==",
"dev": true
},
+ "node_modules/@types/ini": {
+ "version": "1.3.31",
+ "resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz",
+ "integrity": "sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w==",
+ "dev": true
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -3085,6 +3232,12 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true
+ },
"node_modules/@types/node": {
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
@@ -3510,7 +3663,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -3519,7 +3671,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -3727,8 +3878,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/big-integer": {
"version": "1.6.51",
@@ -4097,7 +4247,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -4152,6 +4301,14 @@
"node": ">= 0.8"
}
},
+ "node_modules/commander": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
+ "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -4247,7 +4404,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -4414,9 +4570,9 @@
}
},
"node_modules/diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"engines": {
"node": ">=0.3.1"
}
@@ -4505,6 +4661,11 @@
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4531,8 +4692,7 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/enabled": {
"version": "2.0.0",
@@ -5132,6 +5292,32 @@
}
}
},
+ "node_modules/foreground-child": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+ "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -5263,20 +5449,21 @@
}
},
"node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
+ "version": "10.3.9",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.9.tgz",
+ "integrity": "sha512-2tU/LKevAQvDVuVJ9pg9Yv9xcbSh+TqHuTaXTNbQwf+0kDl9Fm6bMovi4Nm5c8TVvfxo2LLcqCGtmO9KoJaGWg==",
"dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.5",
+ "minimatch": "^9.0.1",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+ "path-scurry": "^1.10.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -5294,6 +5481,28 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globals": {
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
@@ -5566,6 +5775,14 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -5632,7 +5849,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -5741,8 +5957,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.0",
@@ -5827,6 +6042,23 @@
"retry": "0.6.0"
}
},
+ "node_modules/jackspeak": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.5.tgz",
+ "integrity": "sha512-Ratx+B8WeXLAtRJn26hrhY8S1+Jz6pxPMrkrdkgb/NstTNiqMhX0/oFVu5wX+g5n6JlEu2LPsDJmY8nRP4+alw==",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
"node_modules/jest": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.6.1.tgz",
@@ -6065,6 +6297,26 @@
}
}
},
+ "node_modules/jest-config/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/jest-diff": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.1.tgz",
@@ -6345,6 +6597,26 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/jest-runtime/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/jest-snapshot": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.1.tgz",
@@ -6838,6 +7110,14 @@
"node": "*"
}
},
+ "node_modules/minipass": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz",
+ "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -7222,7 +7502,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -7233,6 +7512,29 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
+ "node_modules/path-scurry": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+ "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+ "dependencies": {
+ "lru-cache": "^9.1.1 || ^10.0.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
+ "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -7753,6 +8055,26 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/rrweb-cssom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
@@ -8007,7 +8329,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -8019,7 +8340,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -8183,7 +8503,20 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -8197,7 +8530,18 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -8343,6 +8687,26 @@
"node": ">=8"
}
},
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -8548,6 +8912,14 @@
}
}
},
+ "node_modules/ts-node/node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -8903,7 +9275,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -8976,6 +9347,23 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index 72ffc04..ea4398d 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,10 @@
"lint": "eslint --ext .ts .",
"prettier-format": "prettier --config .prettierrc **/*.ts --write",
"test": "jest",
- "build": "tsc -p .",
+ "build": "tsc -p . && npm run cp:26",
"watch": "tsc -p . --watch",
- "run": "node ./build/index.js"
+ "run": "node ./build/index.js",
+ "cp:26": "mkdir -p build/26/default-files/ && cp -n src/26/default-files/* build/26/default-files/"
},
"keywords": [],
"author": "",
@@ -16,11 +17,15 @@
"dependencies": {
"@datastructures-js/priority-queue": "^6.3.0",
"axios": "^1.4.0",
+ "commander": "^11.0.0",
"cors": "^2.8.5",
+ "diff": "^5.1.0",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
+ "glob": "^10.3.9",
"helmet": "^7.0.0",
+ "ini": "^4.1.1",
"jsdom": "^22.1.0",
"memcached": "^2.2.2",
"raw-socket": "github:algj/node-raw-socket",
@@ -33,7 +38,10 @@
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-typescript": "^7.22.5",
+ "@types/diff": "^5.0.5",
"@types/express": "^4.17.17",
+ "@types/glob": "^8.1.0",
+ "@types/ini": "^1.3.31",
"@types/jest": "^29.5.3",
"@types/jsdom": "^21.1.2",
"@types/memcached": "^2.2.7",
diff --git a/src/26/README.md b/src/26/README.md
new file mode 100644
index 0000000..a8e8542
--- /dev/null
+++ b/src/26/README.md
@@ -0,0 +1,65 @@
+# Challenge 26 - Write Your Own Git
+
+This challenge corresponds to the 26th part of the Coding Challenges series by John Crickett https://codingchallenges.fyi/challenges/challenge-git.
+
+## Description
+
+This is a Node.js implementation for the Git protocol.
+
+The code is somewhat inspired from the Go implementation of the Git Client. https://github.com/go-git/go-git
+
+The client currently supports the following commands. More information provided in [docs](docs/) folder.
+
+- [init](docs/init.md)
+- [hash-object](docs/hash-object.md)
+- [cat-file](docs/cat-file.md)
+- [update-index](docs/update-index.md)
+- [add](docs/update-index.md)
+- [status](docs/status.md)
+- [write-tree](docs/write-tree.md)
+- [commit-tree](docs/commit-tree.md)
+- [commit](docs/commit.md)
+- [diff](docs/diff.md)
+
+Here is a brief description about some of the files/folders in this project:
+
+- [command/](commands/): Each command has its own implementation file present in this folder.
+
+- [docs/](docs/): Documentation about the usage of each command and how it was implemented.
+
+- [objects/](objects/): This directory contains code for encoding and decoding different types of [objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) supported by the Git protocol along with classes and functions to represent those objects and.
+
+- [git.ts](git.ts): The entry point to the Git cli.
+
+- [fileStatus.ts](fileStatus.ts): This file contains the code to find the status of the files present in the Staging and Worktree area.
+
+- [indexParser.ts](indexParser.ts): A class implementation to decode the `.git/index` file. Currently the parser handles all the index entries, and only the TreeExtension. View [this](https://github.com/git/git/blob/867b1c1bf68363bcfd17667d6d4b9031fa6a1300/Documentation/technical/index-format.txt) official index format for more details.
+
+- [jestHelpers.ts](jestHelpers.ts): Some functions used across the testing.
+
+- [utils.ts](utils.ts): This file contains some helper functions and miscellaneous functions used in different commands and functionalities.
+
+_**Note**: The refactoring that you can see is not achieved from the start. As I kept on developing and incorporating more commands, the refactoring automatically came into place. Most of the times, the code that got refactored was already being used somewhere and the new command/functionality needed the same code._
+
+## Usage
+
+You can use the `ts-node` tool to run the Git client as follows:
+
+```bash
+npx ts-node [options] [command]
+```
+
+Use the `--help` option to get more information on how to use the the Git client.
+Or refer to the [docs](docs/) folder.
+
+## Run tests
+
+To run the tests for the Git Client, go to the root directory of this repository and run the following command:
+
+```bash
+npm test src/26/
+```
+
+## TODO
+
+- Complete [Step 6](https://codingchallenges.fyi/challenges/challenge-git/#step-6) of the challenge.
diff --git a/src/26/__tests__/commands/catFile.test.ts b/src/26/__tests__/commands/catFile.test.ts
new file mode 100644
index 0000000..d5ebf14
--- /dev/null
+++ b/src/26/__tests__/commands/catFile.test.ts
@@ -0,0 +1,102 @@
+import {
+ createDummyFile,
+ createTempGitRepo,
+ mockGetSignature
+} from '../../jestHelpers';
+import catFile from '../../commands/catFile';
+import hashObject from '../../commands/hashObject';
+import writeTree from '../../commands/writeTree';
+import updateIndex from '../../commands/updateIndex';
+import { FileMode } from '../../enums';
+import { fileModeString } from '../../utils';
+import commitTree from '../../commands/commitTree';
+
+function hashDummyFile(gitRoot: string): {
+ text: string;
+ filePath: string;
+ objectHash: string;
+} {
+ const { text, filePath, expectedHash } = createDummyFile();
+ const objectHash = hashObject({ gitRoot, write: true, file: filePath });
+ expect(objectHash).toBe(expectedHash);
+ return { text, filePath, objectHash: objectHash };
+}
+
+describe('Testing catFile command', () => {
+ const gitRoot = createTempGitRepo();
+ mockGetSignature();
+
+ let treeHash = '';
+ let commitHash = '';
+
+ it('should output error for invalid args', () => {
+ expect(() => catFile({ gitRoot, object: 'object' })).toThrow();
+ });
+
+ it('should output error for invalid object', () => {
+ expect(() => catFile({ gitRoot, object: 'object', t: true })).toThrow();
+ });
+
+ it('should output correct content - blob', () => {
+ const { text, objectHash } = hashDummyFile(gitRoot);
+ const output = catFile({
+ gitRoot,
+ p: true,
+ object: objectHash
+ });
+ expect(output).toBe(text);
+ });
+
+ it('should output correct type - blob', () => {
+ const { objectHash } = hashDummyFile(gitRoot);
+ const output = catFile({
+ gitRoot,
+ t: true,
+ object: objectHash
+ });
+ expect(output).toBe('blob');
+ });
+
+ it('should output correct content - tree', () => {
+ updateIndex({ gitRoot, files: ['.'] });
+ treeHash = writeTree(gitRoot);
+ const output = catFile({
+ gitRoot,
+ p: true,
+ object: treeHash
+ });
+ expect(output).toContain(fileModeString.get(FileMode.REGULAR));
+ });
+
+ it('should output correct type - tree', () => {
+ updateIndex({ gitRoot, files: ['.'] });
+ const hash = writeTree(gitRoot);
+ const output = catFile({
+ gitRoot,
+ t: true,
+ object: hash
+ });
+ expect(output).toBe('tree');
+ });
+
+ it('should output correct content - commit', () => {
+ const message = 'First commit';
+ commitHash = commitTree({ gitRoot, treeHash, message: message });
+ const output = catFile({
+ gitRoot,
+ p: true,
+ object: commitHash
+ });
+ expect(output).toContain(treeHash);
+ expect(output).toContain(message);
+ });
+
+ it('should output correct type - commit', () => {
+ const output = catFile({
+ gitRoot,
+ t: true,
+ object: commitHash
+ });
+ expect(output).toBe('commit');
+ });
+});
diff --git a/src/26/__tests__/commands/commitTree.test.ts b/src/26/__tests__/commands/commitTree.test.ts
new file mode 100644
index 0000000..c29b7ed
--- /dev/null
+++ b/src/26/__tests__/commands/commitTree.test.ts
@@ -0,0 +1,81 @@
+import { randomBytes } from 'crypto';
+import { createTempGitRepo, mockGetSignature } from '../../jestHelpers';
+import fs from 'fs';
+import updateIndex from '../../commands/updateIndex';
+import path from 'path';
+import writeTree from '../../commands/writeTree';
+import commitTree from '../../commands/commitTree';
+import { decodeCommit } from '../../objects/commit';
+
+describe('Testing commit tree command', () => {
+ const gitRoot = createTempGitRepo();
+ mockGetSignature();
+
+ const files: { name: string; content: Buffer }[] = [
+ { name: 'text1.txt', content: randomBytes(32) },
+ { name: 'dir1/text2.txt', content: randomBytes(32) },
+ { name: 'dir1/dir2/text3.txt', content: randomBytes(32) },
+ { name: 'dir3/text4.txt', content: randomBytes(32) }
+ ];
+ const files2: { name: string; content: Buffer }[] = [
+ { name: 'text5.txt', content: randomBytes(32) }
+ ];
+
+ let firstTreeHash: string;
+ let firstCommitHash: string;
+ const firstMessage = 'First commit';
+
+ let secondTreeHash: string;
+ let secondCommitHash: string;
+ const secondMessage = 'Second commit';
+
+ it('should create the first commit object successfully', () => {
+ // Create files in git repo
+ files.forEach(({ name, content }) => {
+ fs.mkdirSync(path.dirname(name), { recursive: true });
+ fs.writeFileSync(name, content);
+ });
+
+ // perform "git add ." command
+ updateIndex({ gitRoot, files: ['.'] });
+
+ // Perform write tree command
+ firstTreeHash = writeTree(gitRoot);
+
+ firstCommitHash = commitTree({
+ gitRoot,
+ treeHash: firstTreeHash,
+ message: firstMessage
+ });
+ const commitObject = decodeCommit(gitRoot, firstCommitHash);
+ expect(commitObject.message).toBe(firstMessage);
+ expect(commitObject.treeHash).toBe(firstTreeHash);
+ expect(commitObject.parentHashes).toStrictEqual([]);
+ });
+
+ it('should create the second commit object with parent successfully', () => {
+ // Create new files in git repo
+ files2.forEach(({ name, content }) => {
+ fs.mkdirSync(path.dirname(name), { recursive: true });
+ fs.writeFileSync(name, content);
+ });
+
+ // perform "git add ." command
+ updateIndex({ gitRoot, files: ['.'] });
+
+ // Perform write tree command
+ secondTreeHash = writeTree(gitRoot);
+
+ secondCommitHash = commitTree({
+ gitRoot,
+ treeHash: secondTreeHash,
+ parents: [firstCommitHash],
+ message: secondMessage
+ });
+
+ const commitObject = decodeCommit(gitRoot, secondCommitHash);
+ expect(commitObject.message).toBe(secondMessage);
+ expect(commitObject.treeHash).toBe(secondTreeHash);
+ expect(commitObject.parentHashes).toStrictEqual([firstCommitHash]);
+ });
+});
diff --git a/src/26/__tests__/commands/hashObjects.test.ts b/src/26/__tests__/commands/hashObjects.test.ts
new file mode 100644
index 0000000..f215e9b
--- /dev/null
+++ b/src/26/__tests__/commands/hashObjects.test.ts
@@ -0,0 +1,53 @@
+// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
+import path from 'path';
+import fs from 'fs';
+import { createDummyFile, createTempGitRepo } from '../../jestHelpers';
+import hashObject from '../../commands/hashObject';
+import { Readable } from 'stream';
+
+describe('Testing hashObject command', () => {
+ const gitRoot = createTempGitRepo();
+
+ it('should output error on invalid args', () => {
+ expect(() => hashObject({ gitRoot })).toThrow();
+ });
+
+ it('should create hash for file', () => {
+ const { filePath, expectedHash } = createDummyFile();
+
+ const hash = hashObject({ gitRoot, file: filePath });
+ expect(hash.trim()).toBe(expectedHash);
+ });
+
+ it('should handle stdin option', () => {
+ const text = 'what is up, doc?';
+ const expectedHash = 'bd9dbf5aae1a3862dd1526723246b20206e5fc37';
+ const stdinStream = Readable.from(Buffer.from(text));
+
+ const hash = hashObject({
+ gitRoot,
+ stdin: stdinStream,
+ readFromStdin: true
+ });
+ expect(hash.trim()).toBe(expectedHash);
+ stdinStream.destroy();
+ });
+
+ it('should handle write option', () => {
+ const { filePath, expectedHash } = createDummyFile();
+ const pathToBlob = path.join(
+ './.git/objects',
+ expectedHash.substring(0, 2),
+ expectedHash.substring(2, expectedHash.length)
+ );
+
+ const hash = hashObject({
+ gitRoot,
+ file: filePath,
+ write: true
+ });
+
+ expect(hash.trim()).toBe(expectedHash);
+ expect(fs.existsSync(pathToBlob)).toBeTruthy();
+ });
+});
diff --git a/src/26/__tests__/commands/init.test.ts b/src/26/__tests__/commands/init.test.ts
new file mode 100644
index 0000000..2ff8ae1
--- /dev/null
+++ b/src/26/__tests__/commands/init.test.ts
@@ -0,0 +1,50 @@
+import { randomBytes } from 'crypto';
+import os from 'os';
+import path from 'path';
+import fs from 'fs';
+
+import init from '../../commands/init';
+
+describe('Testing init command', () => {
+ let tempPath: string;
+
+ beforeAll(() => {
+ tempPath = path.join(os.tmpdir(), randomBytes(2).toString('hex'));
+ });
+
+ afterAll(() => {
+ fs.rmSync(tempPath, { recursive: true, force: true });
+ });
+
+ it('should initialize empty repository successfully', (done) => {
+ const output = init(tempPath);
+
+ expect(fs.existsSync(path.join(tempPath, '.git'))).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'HEAD'))).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'config'))).toBeTruthy();
+ expect(
+ fs.existsSync(path.join(tempPath, '.git', 'description'))
+ ).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'hooks'))).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'info'))).toBeTruthy();
+ expect(
+ fs.existsSync(path.join(tempPath, '.git', 'info', 'exclude'))
+ ).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'objects'))).toBeTruthy();
+ expect(
+ fs.existsSync(path.join(tempPath, '.git', 'objects', 'info'))
+ ).toBeTruthy();
+ expect(
+ fs.existsSync(path.join(tempPath, '.git', 'objects', 'pack'))
+ ).toBeTruthy();
+ expect(fs.existsSync(path.join(tempPath, '.git', 'refs'))).toBeTruthy();
+
+ expect(output).toContain('Initialized');
+ done();
+ });
+
+ it('should output reinitialized text when git repo is already initialized', () => {
+ const output = init(tempPath);
+ expect(output).toContain('Reinitialized');
+ });
+});
diff --git a/src/26/__tests__/commands/updateIndex.test.ts b/src/26/__tests__/commands/updateIndex.test.ts
new file mode 100644
index 0000000..29bbf84
--- /dev/null
+++ b/src/26/__tests__/commands/updateIndex.test.ts
@@ -0,0 +1,80 @@
+import fs from 'fs';
+import { randomBytes } from 'crypto';
+import updateIndex from '../../commands/updateIndex';
+import { createTempGitRepo } from '../../jestHelpers';
+import IndexParser from '../../indexParser';
+import { RELATIVE_PATH_TO_INDEX_FILE } from '../../constants';
+import path from 'path';
+
+describe('Testing update-index command', () => {
+ const gitRoot = createTempGitRepo();
+
+ it('should throw error on invalid args', () => {
+ expect(() => updateIndex({ gitRoot, files: undefined })).toThrow();
+ expect(() => updateIndex({ gitRoot, files: [] })).toThrow();
+ expect(() =>
+ updateIndex({ gitRoot, files: [randomBytes(2).toString()] })
+ ).toThrow();
+ });
+
+ it('should add file to index and create a new index file', () => {
+ expect(
+ fs.existsSync(path.join(gitRoot, RELATIVE_PATH_TO_INDEX_FILE))
+ ).toBeFalsy();
+
+ const filename = 'cc.txt';
+ fs.closeSync(fs.openSync(filename, 'w'));
+ const expectedStat = fs.statSync(filename);
+
+ updateIndex({ gitRoot, files: [filename] });
+
+ expect(
+ fs.existsSync(path.join(gitRoot, RELATIVE_PATH_TO_INDEX_FILE))
+ ).toBeTruthy();
+
+ const index = new IndexParser(gitRoot).parse();
+
+ expect(index.entries.length).toBe(1);
+
+ const entry = index.getEntry(filename);
+
+ expect(entry).not.toBe(undefined);
+ if (entry) {
+ expect(entry.name).toBe(filename);
+ expect(entry.ino).toBe(expectedStat.ino);
+ expect(entry.dev).toBe(expectedStat.dev);
+ expect(entry.size).toBe(expectedStat.size);
+ expect(entry.gid).toBe(expectedStat.gid);
+ expect(entry.uid).toBe(expectedStat.uid);
+ }
+ });
+
+ it('should add multiple files to index successfully', () => {
+ const files: { name: string; content: Buffer }[] = [
+ { name: 'dir1/text1.txt', content: randomBytes(32) },
+ { name: 'dir2/subdir2/text2.txt', content: randomBytes(64) }
+ ];
+
+ files.forEach(({ name, content }) => {
+ fs.mkdirSync(path.dirname(name), { recursive: true });
+ fs.writeFileSync(name, content);
+ });
+ const fileNames: string[] = files.map(({ name }) => {
+ return name;
+ });
+
+ updateIndex({ gitRoot, files: fileNames });
+
+ const index = new IndexParser(gitRoot).parse();
+
+ files.forEach(({ name, content }) => {
+ const entry1 = index.getEntry(name);
+
+ expect(entry1).not.toBe(undefined);
+ if (entry1) {
+ expect(entry1.name).toBe(name);
+ expect(entry1.size).toBe(content.byteLength);
+ }
+ });
+ });
+});
diff --git a/src/26/__tests__/commands/writeTree.test.ts b/src/26/__tests__/commands/writeTree.test.ts
new file mode 100644
index 0000000..c6f39a4
--- /dev/null
+++ b/src/26/__tests__/commands/writeTree.test.ts
@@ -0,0 +1,65 @@
+import { randomBytes } from 'crypto';
+import { createTempGitRepo } from '../../jestHelpers';
+import fs from 'fs';
+import path from 'path';
+import updateIndex from '../../commands/updateIndex';
+import writeTree from '../../commands/writeTree';
+import { decodeTree } from '../../objects/tree';
+import IndexParser from '../../indexParser';
+import { CachedTree } from '../../objects/cachedTree';
+
+describe('Testing write tree command', () => {
+ const gitRoot = createTempGitRepo();
+ const files: { name: string; content: Buffer }[] = [
+ { name: 'text1.txt', content: randomBytes(32) },
+ { name: 'dir1/text2.txt', content: randomBytes(32) },
+ { name: 'dir1/dir2/text3.txt', content: randomBytes(32) },
+ { name: 'dir3/text4.txt', content: randomBytes(32) }
+ ];
+ const rootNodeChildrenSize = 3;
+ const rootNodeSubtreeCount = 2;
+ const rootNodeEntryCount = 4;
+
+ beforeAll(() => {
+ // Create files in git repo
+ files.forEach(({ name, content }) => {
+ fs.mkdirSync(path.dirname(name), { recursive: true });
+ fs.writeFileSync(name, content);
+ });
+
+ // perform "git add ." command
+ updateIndex({ gitRoot, files: ['.'] });
+ });
+
+ it('should write tree successfully', () => {
+ const treeHash = writeTree(gitRoot);
+ const index = new IndexParser(gitRoot).parse();
+ const tree = decodeTree(gitRoot, treeHash);
+
+ // Check properties of root
+ expect(tree.root.children.size).toBe(rootNodeChildrenSize);
+ expect(tree.root.entryCount).toBe(rootNodeEntryCount);
+ expect(tree.root.subTreeCount).toBe(rootNodeSubtreeCount);
+ expect(tree.root.hash).toBe(treeHash);
+
+ // Check if all the files are present in Tree
+ files.forEach(({ name }) => {
+ const node = tree.getNode(name);
+ const indexEntry = index.getEntry(name);
+
+ expect(node).not.toBe(undefined);
+ expect(indexEntry).not.toBe(undefined);
+ if (node && indexEntry) {
+ expect(node.path).toBe(name);
+ expect(node.hash).toBe(indexEntry.hash);
+ }
+ });
+
+ const cachedTree = new CachedTree();
+ expect(tree.root.calculateHash(gitRoot, false, cachedTree)).toBe(treeHash);
+ cachedTree.entries = cachedTree.entries.sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+ expect(cachedTree).toStrictEqual(index.cache);
+ });
+});
diff --git a/src/26/__tests__/indexParser/indexParser.test.ts b/src/26/__tests__/indexParser/indexParser.test.ts
new file mode 100644
index 0000000..21b6865
--- /dev/null
+++ b/src/26/__tests__/indexParser/indexParser.test.ts
@@ -0,0 +1,30 @@
+import fs from 'fs';
+import IndexParser from '../../indexParser';
+import { createTempGitRepo } from '../../jestHelpers';
+import { RELATIVE_PATH_TO_INDEX_FILE } from '../../constants';
+import path from 'path';
+import { randomBytes } from 'crypto';
+
+describe('Testing indexParser', () => {
+ const dir = path.join(__dirname, 'testFiles');
+ const files = fs.readdirSync(dir);
+
+ const gitRoot = createTempGitRepo();
+
+ it('should throw error when invalid gitRoot is provided', () => {
+ expect(() => new IndexParser(randomBytes(16).toString()).parse()).toThrow();
+ });
+
+ files.forEach((file) => {
+ it(`should parse index file: ${file}`, () => {
+ // Update the index file
+ const contents = fs.readFileSync(path.join(dir, file));
+ fs.writeFileSync(
+ path.join(gitRoot, RELATIVE_PATH_TO_INDEX_FILE),
+ contents
+ );
+
+ expect(() => new IndexParser(gitRoot).parse()).not.toThrow();
+ });
+ });
+});
diff --git a/src/26/__tests__/indexParser/testFiles/index1 b/src/26/__tests__/indexParser/testFiles/index1
new file mode 100644
index 0000000..fa2ce23
Binary files /dev/null and b/src/26/__tests__/indexParser/testFiles/index1 differ
diff --git a/src/26/__tests__/indexParser/testFiles/index2 b/src/26/__tests__/indexParser/testFiles/index2
new file mode 100644
index 0000000..87f868a
Binary files /dev/null and b/src/26/__tests__/indexParser/testFiles/index2 differ
diff --git a/src/26/__tests__/indexParser/testFiles/index3 b/src/26/__tests__/indexParser/testFiles/index3
new file mode 100644
index 0000000..6f59be4
Binary files /dev/null and b/src/26/__tests__/indexParser/testFiles/index3 differ
diff --git a/src/26/__tests__/indexParser/testFiles/index4 b/src/26/__tests__/indexParser/testFiles/index4
new file mode 100644
index 0000000..8f585a7
Binary files /dev/null and b/src/26/__tests__/indexParser/testFiles/index4 differ
diff --git a/src/26/__tests__/signature.test.ts b/src/26/__tests__/signature.test.ts
new file mode 100644
index 0000000..bdc8e1f
--- /dev/null
+++ b/src/26/__tests__/signature.test.ts
@@ -0,0 +1,52 @@
+import {
+ Signature,
+ decodeSignature,
+ getTimeAndTimeZone
+} from '../objects/signature';
+
+describe('Testing Signature related classes and functions', () => {
+ const name = 'John Doe';
+ const email = 'example@gmail.com';
+
+ const date = new Date();
+ const timeInSec = 1696322721;
+ date.setTime(timeInSec * 1000);
+
+ it('should return correct output - toString() method', () => {
+ const signature = new Signature(name, email, date);
+ const str = signature.toString();
+
+ expect(str).toContain(`${name} <${email}> ${timeInSec}`);
+ });
+
+ it('should match timezone format', () => {
+ const date = new Date();
+ const str = getTimeAndTimeZone(date);
+ const regex = /[+-](\d{2})(\d{2})/;
+
+ expect(regex.test(str)).toBeTruthy();
+ });
+
+ it('should decode signature successfully', () => {
+ const str = `${name} <${email}> ${timeInSec} +0530`;
+ const signature = decodeSignature(str);
+
+ expect(signature.name).toBe(name);
+ expect(signature.email).toBe(email);
+ expect(signature.timestamp.getTime()).toBe(date.getTime());
+ });
+
+ it('should convert date object to time and timezone offset', () => {
+ const date = new Date();
+ const seconds = Math.floor(date.getTime() / 1000);
+ const output = getTimeAndTimeZone(date).trim();
+ const [sec, timezone] = output.split(' ');
+
+ expect(parseInt(sec)).toBe(seconds);
+ const timezoneInMin =
+ (timezone[0] === '+' ? -1 : 1) *
+ (parseInt(timezone.substring(1, 3)) * 60 +
+ parseInt(timezone.substring(3, 5)));
+ expect(timezoneInMin).toBe(date.getTimezoneOffset());
+ });
+});
diff --git a/src/26/__tests__/utils.test.ts b/src/26/__tests__/utils.test.ts
new file mode 100644
index 0000000..089b214
--- /dev/null
+++ b/src/26/__tests__/utils.test.ts
@@ -0,0 +1,96 @@
+import path from 'path';
+import { createTempGitRepo, mockGetSignature } from '../jestHelpers';
+import fs from 'fs';
+import { randomBytes, randomInt } from 'crypto';
+import {
+ getCurrentBranchName,
+ getFileStats,
+ getFiles,
+ getIgnoredGlobPatterns,
+ getSignature,
+ isValidSHA1,
+ parseObjectHeader
+} from '../utils';
+import { GitObjectType } from '../types';
+
+describe('Testing utils', () => {
+ const gitRoot = createTempGitRepo();
+ mockGetSignature();
+
+ const gitignoreContent = ['.idea/**', '**/node_modules/**', '**/build/**'];
+ const files: { name: string; content: Buffer }[] = [
+ { name: 'text1.txt', content: randomBytes(32) },
+ { name: 'dir1/text2.txt', content: randomBytes(32) },
+ { name: 'node_modules/module/index.ts', content: randomBytes(32) }
+ ];
+ const names = files.map(({ name }) => {
+ return name;
+ });
+ names.push('.gitignore');
+
+ beforeAll(() => {
+ fs.writeFileSync(
+ path.join(gitRoot, '.gitignore'),
+ gitignoreContent.join('\r\n')
+ );
+ files.forEach(({ name, content }) => {
+ fs.mkdirSync(path.dirname(path.join(gitRoot, name)), { recursive: true });
+ fs.writeFileSync(name, content);
+ });
+ });
+
+ it('should parse .gitignore successfully', () => {
+ const data = getIgnoredGlobPatterns(gitRoot);
+ gitignoreContent.forEach((pattern) => {
+ expect(data.indexOf(pattern)).toBeGreaterThanOrEqual(0);
+ });
+ });
+
+ it('should get files and successfully', () => {
+ const data = getFiles(gitRoot, gitRoot);
+
+ data.forEach((file) => {
+ expect(names.indexOf(file)).toBeGreaterThanOrEqual(0);
+ });
+ });
+
+ it('should get file stats successfully', () => {
+ const data = getFileStats(gitRoot);
+
+ data.forEach((stat, key) => {
+ expect(names.indexOf(key)).toBeGreaterThanOrEqual(0);
+ });
+ });
+
+ it('should throw error when no HEAD file is found', () => {
+ expect(() => getCurrentBranchName(randomBytes(2).toString())).toThrow();
+ });
+
+ it('should get the current branch name successfully', () => {
+ const data = getCurrentBranchName(gitRoot);
+ expect(data.trim()).toBe('master');
+ });
+
+ it('should get signature successfully', () => {
+ const signature = getSignature();
+ expect(signature.email).not.toBe(undefined);
+ expect(signature.name).not.toBe(undefined);
+ });
+
+ it('should parse object header successfully', () => {
+ const objectType: GitObjectType = 'commit';
+ const byteLength = randomInt(100, 10000);
+ const buffer = Buffer.from(`${objectType} ${byteLength}`);
+ const header = parseObjectHeader(buffer);
+
+ expect(header.type).toBe(objectType);
+ expect(header.length).toBe(byteLength);
+ });
+
+ const invalidHashes = ['', '12', '12 ', '12~', 'h'];
+ invalidHashes.forEach((hash) => {
+ it(`should return false for invalid hash ${hash}`, () => {
+ expect(isValidSHA1(hash)).toBeFalsy();
+ });
+ });
+});
diff --git a/src/26/commands/catFile.ts b/src/26/commands/catFile.ts
new file mode 100644
index 0000000..862e7c1
--- /dev/null
+++ b/src/26/commands/catFile.ts
@@ -0,0 +1,103 @@
+import { SPACE, NULL } from '../constants';
+import { fileModeString, fileType, parseObject } from '../utils';
+
+interface CatFileArgs {
+ /**
+ * Absolute path to the root of the Git repo.
+ *
+ * @type {string}
+ */
+ gitRoot: string;
+
+ /**
+ * The hash value corresponding to the object.
+ *
+ * @type {string}
+ */
+ object: string;
+
+ /**
+ * Return the type only.
+ *
+ * @type {?boolean}
+ */
+ t?: boolean;
+
+ /**
+ * Return the content of the given object.
+ *
+ * @type {?boolean}
+ */
+ p?: boolean;
+}
+
+function parseTreeFile(data: Buffer): string {
+ const output: string[] = [];
+
+ for (let i = 0; i < data.length; ) {
+ // Format of each entry:
+ //
+ const modeStartPos = i;
+ while (data[i] !== SPACE) {
+ i++;
+ }
+ const mode = parseInt(data.subarray(modeStartPos, i).toString(), 8);
+ i++;
+
+ const filenameStartPos = i;
+ while (data[i] !== NULL) {
+ i++;
+ }
+ const filename = data.subarray(filenameStartPos, i).toString();
+ i++;
+
+ const hash = data.subarray(i, i + 20).toString('hex');
+ i += 20;
+ output.push(
+ `${fileModeString.get(mode)} ${fileType.get(mode)} ${hash} ${filename}`
+ );
+ }
+
+ return output.join('\r\n');
+}
+
+/**
+ * Main function to perform the cat-file command.
+ * Supported file types:
+ * - blob
+ * - tree
+ *
+ * @param {CatFileArgs} { gitRoot, object, t = false, p = false }
+ * @returns {string}
+ */
+function catFile({
+ gitRoot,
+ object,
+ t = false,
+ p = false
+}: CatFileArgs): string {
+ // Make sure exact one option is provided
+ if ((t && p) || (!t && !p)) {
+ throw new Error('Invalid usage');
+ }
+
+ const gitObject = parseObject(gitRoot, object);
+
+ // The user asked for `type` only
+ if (t) {
+ return gitObject.type;
+ }
+
+ // The user asked for contents of the file
+ switch (gitObject.type) {
+ case 'blob':
+ case 'commit':
+ return gitObject.data.toString();
+ case 'tree':
+ return parseTreeFile(gitObject.data);
+ }
+
+ throw new Error(`File ${gitObject.type} not supported`);
+}
+
+export default catFile;
diff --git a/src/26/commands/commit.ts b/src/26/commands/commit.ts
new file mode 100644
index 0000000..802b1c1
--- /dev/null
+++ b/src/26/commands/commit.ts
@@ -0,0 +1,52 @@
+import path from 'path';
+import { getBranchHeadReference, getCurrentBranchName } from '../utils';
+import commitTree from './commitTree';
+import writeTree from './writeTree';
+import { RELATIVE_PATH_TO_REF_HEADS_DIR } from '../constants';
+import fs from 'fs';
+
+/**
+ * The main function that performs the 'commit' command.
+ *
+ * @param {string} gitRoot
+ * @param {string} message
+ * @returns {string}
+ */
+function commit(gitRoot: string, message: string): string {
+ // Make sure a valid message is provided
+ if (message.length === 0) {
+ throw new Error('Please provide a valid message');
+ }
+
+ // Get current branch name and a ref to the parent hash if present.
+ const branch = getCurrentBranchName(gitRoot);
+ const ref = getBranchHeadReference(gitRoot, branch);
+ const parents: string[] = [];
+ if (ref !== undefined) {
+ parents.push(ref);
+ }
+
+ // Create the tree object from the index
+ const treeHash = writeTree(gitRoot);
+
+ // Create the commit object
+ const hash = commitTree({ gitRoot, treeHash, message, parents });
+
+ // Update the head for the current branch
+ const pathToRef = path.join(gitRoot, RELATIVE_PATH_TO_REF_HEADS_DIR, branch);
+ fs.writeFileSync(pathToRef, hash + '\n');
+
+ // Create the output string
+ let str = '';
+ if (parents.length === 0) {
+ // First commit of this branch
+ str += `[${branch} (root-commit) ${hash.substring(0, 7)} ${message} \r\n`;
+ } else {
+ str += `[${branch} ${hash.substring(0, 7)}] ${message}\r\n`;
+ }
+ // TODO: Show number of files changed, total insertions and deletions
+
+ return str;
+}
+
+export default commit;
diff --git a/src/26/commands/commitTree.ts b/src/26/commands/commitTree.ts
new file mode 100644
index 0000000..d31b9c9
--- /dev/null
+++ b/src/26/commands/commitTree.ts
@@ -0,0 +1,97 @@
+import { createHash } from 'crypto';
+import { Commit } from '../objects/commit';
+import { getSignature, verifyObject } from '../utils';
+import zlib from 'zlib';
+import fs from 'fs';
+import path from 'path';
+import { RELATIVE_PATH_TO_OBJECT_DIR } from '../constants';
+import stream from 'stream';
+
+export interface CommitTreeArgs {
+ /**
+ * The absolute path to the Git repo.
+ *
+ * @type {string}
+ */
+ gitRoot: string;
+
+ /**
+ * The hash of the tree that will be saved with the Commit object.
+ *
+ * @type {string}
+ */
+ treeHash: string;
+
+ /**
+ * List of parent objects.
+ *
+ * @type {?string[]}
+ */
+ parents?: string[];
+
+ /**
+ * Message that will be used as Commit message.
+ *
+ * @type {?string}
+ */
+ message?: string;
+
+ /**
+ * Optionally read from stdin if no message is provided
+ *
+ * @type {?stream.Readable}
+ */
+ stdin?: stream.Readable;
+}
+
+function commitTree({
+ gitRoot,
+ treeHash,
+ parents = [],
+ message,
+ stdin = process.stdin
+}: CommitTreeArgs): string {
+ // Verify the hashes provided by the user
+ treeHash = verifyObject(gitRoot, treeHash, 'tree');
+ for (let i = 0; i < parents.length; i++) {
+ parents[i] = verifyObject(gitRoot, parents[i], 'commit');
+ }
+
+ // If message is not provided in args then read from stdin
+ if (message === undefined) {
+ const buffer = stdin.read() as Buffer;
+ if (buffer === null) {
+ throw new Error('No message provided!');
+ }
+ message = buffer.toString();
+ }
+
+ // Get author and committer details from '~/.gitconfig' file
+ const signature = getSignature();
+
+ const commitObject = new Commit({
+ author: signature,
+ committer: signature,
+ message,
+ treeHash: treeHash,
+ parentHashes: parents
+ });
+
+ // Create hash and store the commit object
+ const store = commitObject.encode();
+ const hash = createHash('sha1').update(store).digest('hex');
+
+ const zlibContent = zlib.deflateSync(store);
+ const pathToBlob = path.join(
+ gitRoot,
+ RELATIVE_PATH_TO_OBJECT_DIR,
+ hash.substring(0, 2),
+ hash.substring(2, hash.length)
+ );
+ fs.mkdirSync(path.dirname(pathToBlob), { recursive: true });
+ fs.writeFileSync(pathToBlob, zlibContent);
+
+ return hash;
+}
+
+export default commitTree;
diff --git a/src/26/commands/diff.ts b/src/26/commands/diff.ts
new file mode 100644
index 0000000..c843bdf
--- /dev/null
+++ b/src/26/commands/diff.ts
@@ -0,0 +1,162 @@
+import { FileMode, FileStatusCode } from '../enums';
+import { getFileStatus } from '../fileStatus';
+import IndexParser from '../indexParser';
+import { fileModeString, getCurrentBranchName, parseObject } from '../utils';
+import fs from 'fs';
+import hashObject from './hashObject';
+import { createTwoFilesPatch } from 'diff';
+import {
+ BoldEnd,
+ BoldStart,
+ ColorReset,
+ FgCyan,
+ FgGreen,
+ FgRed
+} from '../constants';
+
+interface FileInfo {
+ /**
+ * The relative path of the file from the root of the Git repo.
+ *
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * The content of the file.
+ *
+ * @type {string}
+ */
+ content: string;
+
+ /**
+ * The hash of the file as per the hashObject function.
+ *
+ * @type {string}
+ */
+ hash: string;
+}
+
+/**
+ * Calculates the diff between the given FileInfo objects.
+ *
+ * @param {FileInfo} a
+ * @param {FileInfo} b
+ * @param {FileStatusCode} status
+ * @param {FileMode} mode
+ * @returns {string}
+ */
+function diffFile(
+ a: FileInfo,
+ b: FileInfo,
+ status: FileStatusCode,
+ mode: FileMode
+): string {
+ // Create header
+ let str = `${BoldStart}diff --git a/${a.name} b/${b.name}\n`;
+
+ if (status === FileStatusCode.DELETED) {
+ str += `deleted file mode ${fileModeString.get(mode)}\n`;
+ str += `index ${a.hash.substring(0, 7)}..${b.hash.substring(0, 7)}\n`;
+ } else {
+ str += `index ${a.hash.substring(0, 7)}..${b.hash.substring(
+ 0,
+ 7
+ )} ${fileModeString.get(mode)}\n`;
+ }
+
+ // Call the diff package and create the patch using the name and content.
+ const changes = createTwoFilesPatch(
+ `a/${a.name}`,
+ `b/${b.name}`,
+ a.content,
+ b.content
+ );
+
+ // Skip the first line from changes
+ const split = changes.split(/\r\n|\n/).slice(1);
+
+ // End bold lines in header
+ split[1] = `${split[1]}${BoldEnd}`;
+
+ // Add color to lines
+ for (let i = 2; i < split.length; i++) {
+ const line = split[i];
+ if (
+ line.substring(0, 2) === '@@' &&
+ line.substring(line.length - 2, line.length) === '@@'
+ ) {
+ split[i] = `${FgCyan}${line}${ColorReset}`;
+ } else if (line[0] === '+') {
+ split[i] = `${FgGreen}${line}${ColorReset}`;
+ } else if (line[0] === '-') {
+ split[i] = `${FgRed}${line}${ColorReset}`;
+ }
+ }
+
+ return str + split.join('\n');
+}
+
+/**
+ * Main function that performs the 'diff' command
+ *
+ * @export
+ * @param {string} gitRoot
+ * @returns {string}
+ */
+export function gitDiff(gitRoot: string): string {
+ // Get status of files
+ const branch = getCurrentBranchName(gitRoot);
+ const index = new IndexParser(gitRoot).parse();
+ const files = getFileStatus(gitRoot, branch);
+
+ let str = '';
+
+ files.forEach((status) => {
+ switch (status.worktree) {
+ case FileStatusCode.UNMODIFIED:
+ case FileStatusCode.UNTRACKED:
+ case FileStatusCode.ADDED:
+ break;
+ case FileStatusCode.DELETED: {
+ const e = index.getEntry(status.name)!;
+ const gitObject = parseObject(gitRoot, e.hash);
+
+ const a: FileInfo = {
+ name: status.name,
+ content: gitObject.data.toString(),
+ hash: e.hash
+ };
+
+ const b: FileInfo = {
+ name: '/dev/null',
+ content: '',
+ hash: ''.padStart(20, '0')
+ };
+ str += diffFile(a, b, status.worktree, e.mode);
+ break;
+ }
+ case FileStatusCode.MODIFIED: {
+ const e = index.getEntry(status.name)!;
+ const gitObject = parseObject(gitRoot, e.hash);
+
+ const a: FileInfo = {
+ name: status.name,
+ content: gitObject.data.toString(),
+ hash: e.hash
+ };
+
+ const b: FileInfo = {
+ name: status.name,
+ content: fs.readFileSync(status.name, 'utf-8'),
+ hash: hashObject({ gitRoot, file: status.name })
+ };
+
+ str += diffFile(a, b, status.worktree, e.mode);
+ break;
+ }
+ }
+ });
+
+ return str;
+}
diff --git a/src/26/commands/hashObject.ts b/src/26/commands/hashObject.ts
new file mode 100644
index 0000000..f966e4e
--- /dev/null
+++ b/src/26/commands/hashObject.ts
@@ -0,0 +1,87 @@
+// https://git-scm.com/docs/git-hash-object
+import zlib from 'zlib';
+
+import { createHash } from 'crypto';
+import fs from 'fs';
+import path from 'path';
+import { GitObjectType } from '../types';
+import stream from 'stream';
+import { RELATIVE_PATH_TO_OBJECT_DIR } from '../constants';
+
+interface HashObjectArgs {
+ /**
+ * The absolute path to the root of the Git repo.
+ *
+ * @type {string}
+ */
+ gitRoot: string;
+
+ /**
+ * The type of git Object.
+ *
+ * @type {?GitObjectType}
+ */
+ type?: GitObjectType;
+
+ /**
+ * Whether to save the object to the storage.
+ *
+ * @type {?boolean}
+ */
+ write?: boolean;
+
+ /**
+ * Read content of the file from the stdin.
+ *
+ * @type {?boolean}
+ */
+ readFromStdin?: boolean;
+
+ /**
+ * Path to file
+ *
+ * @type {?string}
+ */
+ file?: string;
+ stdin?: stream.Readable;
+}
+
+function hashObject({
+ gitRoot,
+ type = 'blob',
+ write = false,
+ readFromStdin = false,
+ file = undefined,
+ stdin = process.stdin
+}: HashObjectArgs): string {
+ let content: Buffer;
+
+ // Prepare content
+ if (readFromStdin) {
+ content = stdin.read() as Buffer;
+ } else if (file) {
+ content = fs.readFileSync(file);
+ } else {
+ throw new Error('Invalid args. No file provided');
+ }
+
+ const header = Buffer.from(`${type} ${content.byteLength}\0`);
+ const store = Buffer.concat([header, content]);
+ const hash = createHash('sha1').update(store).digest('hex');
+
+ if (write) {
+ const zlibContent = zlib.deflateSync(store);
+ const pathToBlob = path.join(
+ gitRoot,
+ RELATIVE_PATH_TO_OBJECT_DIR,
+ hash.substring(0, 2),
+ hash.substring(2, hash.length)
+ );
+ fs.mkdirSync(path.dirname(pathToBlob), { recursive: true });
+ fs.writeFileSync(pathToBlob, zlibContent);
+ }
+
+ return hash;
+}
+
+export default hashObject;
diff --git a/src/26/commands/init.ts b/src/26/commands/init.ts
new file mode 100644
index 0000000..49e5482
--- /dev/null
+++ b/src/26/commands/init.ts
@@ -0,0 +1,57 @@
+// https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain
+
+import fs from 'fs';
+import path from 'path';
+
+const reinitializeText = 'Reinitialized existing Git repository in ';
+const DEFAULT_CONFIG = fs.readFileSync(
+ path.join(__dirname, '..', 'default-files', 'default-config')
+);
+const DEFAULT_DESCRIPTION = fs.readFileSync(
+ path.join(__dirname, '..', 'default-files', 'default-description')
+);
+const DEFAULT_EXCLUDE = fs.readFileSync(
+ path.join(__dirname, '..', 'default-files', 'default-exclude')
+);
+const DEFAULT_HEAD = fs.readFileSync(
+ path.join(__dirname, '..', 'default-files', 'default-HEAD')
+);
+
+/**
+ * Main function that performs the initialization of a git repo.
+ *
+ * @param {?string} [directory] path where the git repo should be initialized
+ * @returns {string}
+ */
+function init(directory?: string): string {
+ let gitDir = path.join(process.cwd(), '.git');
+
+ if (directory) {
+ if (path.isAbsolute(directory)) {
+ gitDir = path.join(directory, '.git');
+ } else {
+ gitDir = path.join(process.cwd(), directory, '.git');
+ }
+ }
+
+ if (fs.existsSync(gitDir)) {
+ return reinitializeText + gitDir;
+ }
+
+ fs.mkdirSync(gitDir, { recursive: true });
+
+ fs.writeFileSync(path.join(gitDir, 'HEAD'), DEFAULT_HEAD);
+ fs.writeFileSync(path.join(gitDir, 'config'), DEFAULT_CONFIG);
+ fs.writeFileSync(path.join(gitDir, 'description'), DEFAULT_DESCRIPTION);
+ fs.mkdirSync(path.join(gitDir, 'hooks'));
+ fs.mkdirSync(path.join(gitDir, 'info'));
+ fs.writeFileSync(path.join(gitDir, 'info', 'exclude'), DEFAULT_EXCLUDE);
+ fs.mkdirSync(path.join(gitDir, 'objects'));
+ fs.mkdirSync(path.join(gitDir, 'objects', 'info'));
+ fs.mkdirSync(path.join(gitDir, 'objects', 'pack'));
+ fs.mkdirSync(path.join(gitDir, 'refs'));
+
+ return `Initialized empty repository in ${gitDir}`;
+}
+
+export default init;
diff --git a/src/26/commands/status.ts b/src/26/commands/status.ts
new file mode 100644
index 0000000..6e473f6
--- /dev/null
+++ b/src/26/commands/status.ts
@@ -0,0 +1,95 @@
+import { ColorReset, FgGreen, FgRed } from '../constants';
+import path from 'path';
+import { getCurrentBranchName } from '../utils';
+import { FileStatusCode } from '../enums';
+import { getFileStatus } from '../fileStatus';
+import { FileStatus } from '../types';
+
+/**
+ * Prepares a human readable output from a list of FileStatus objects.
+ *
+ * @param {string} gitRoot
+ * @param {string} branch
+ * @param {FileStatus[]} files
+ * @returns {string}
+ */
+function prepareOutput(
+ gitRoot: string,
+ branch: string,
+ files: Map
+): string {
+ const cwd = path.relative(gitRoot, process.cwd());
+ const arr = [...files.values()].sort((a, b) => a.name.localeCompare(b.name));
+ let str = `On branch ${branch}`;
+ let changesToBeCommitted = '';
+ let changesNotStaged = '';
+ let untrackedFiles = '';
+
+ arr.forEach((e) => {
+ const name = path.relative(cwd, e.name);
+
+ switch (e.worktree) {
+ case FileStatusCode.UNMODIFIED:
+ break;
+ case FileStatusCode.UNTRACKED:
+ untrackedFiles += `\t${name}\r\n`;
+ break;
+ case FileStatusCode.DELETED:
+ changesNotStaged += `\tdeleted: ${name}\r\n`;
+ break;
+ case FileStatusCode.MODIFIED:
+ changesNotStaged += `\tmodified: ${name}\r\n`;
+ break;
+ default:
+ throw new Error(`Invalid worktree status ${e}`);
+ }
+
+ switch (e.staging) {
+ case FileStatusCode.ADDED:
+ changesToBeCommitted += `\tnew file: ${name}\r\n`;
+ break;
+ case FileStatusCode.DELETED:
+ changesToBeCommitted += `\tdeleted: ${name}\r\n`;
+ break;
+ case FileStatusCode.MODIFIED:
+ changesToBeCommitted += `\tmodified: ${name}\r\n`;
+ break;
+ case FileStatusCode.UNMODIFIED:
+ case FileStatusCode.UNTRACKED:
+ break;
+ }
+ });
+
+ if (changesToBeCommitted.length > 0) {
+ str += `\r\nChanges to be committed:\r\n${FgGreen}${changesToBeCommitted}${ColorReset}`;
+ }
+ if (changesNotStaged.length > 0) {
+ str += `\r\nChanges not staged for commit:\r\n${FgRed}${changesNotStaged}${ColorReset}`;
+ }
+ if (untrackedFiles.length > 0) {
+ str += `\r\nUntracked files:\r\n${FgRed}${untrackedFiles}${ColorReset}`;
+ }
+
+ if (changesToBeCommitted.length === 0) {
+ if (untrackedFiles.length === 0 && changesNotStaged.length === 0) {
+ str += '\r\nnothing to commit, working tree clean';
+ } else {
+ str += '\r\nno changes added to commit';
+ }
+ }
+ return str;
+}
+
+/**
+ * Main function that handles the `status` command.
+ *
+ * @param {string} gitRoot
+ * @returns {string}
+ */
+function status(gitRoot: string): string {
+ const branch = getCurrentBranchName(gitRoot);
+ const files = getFileStatus(gitRoot, branch);
+ return prepareOutput(gitRoot, branch, files);
+}
+
+export default status;
diff --git a/src/26/commands/updateIndex.ts b/src/26/commands/updateIndex.ts
new file mode 100644
index 0000000..9b5405a
--- /dev/null
+++ b/src/26/commands/updateIndex.ts
@@ -0,0 +1,95 @@
+// https://github.com/git/git/blob/867b1c1bf68363bcfd17667d6d4b9031fa6a1300/Documentation/technical/index-format.txt#L38
+
+import fs from 'fs';
+import { RELATIVE_PATH_TO_INDEX_FILE } from '../constants';
+import { Index, IndexEntry, createIndexEntry } from '../objects';
+import IndexParser from '../indexParser';
+import path from 'path';
+import { getFiles } from '../utils';
+
+interface UpdateIndexArgs {
+ /**
+ * The absolute path to the root of the Git repo.
+ *
+ * @type {string}
+ */
+ gitRoot: string;
+
+ /**
+ * List of files or directories.
+ *
+ * @type {?string[]}
+ */
+ files?: string[];
+}
+
+/**
+ * Main function that performs the 'update-index' command
+ *
+ * @param {UpdateIndexArgs} { gitRoot, files }
+ * @returns {string}
+ */
+function updateIndex({ gitRoot, files }: UpdateIndexArgs): string {
+ // Ensure files or directories are provided
+ if (files === undefined || files.length === 0) {
+ throw new Error('Invalid args');
+ }
+
+ let filesToAdd: string[] = [];
+
+ files.forEach((value) => {
+ const isDir = fs.statSync(value).isDirectory();
+ // If the given path is a directory, then get all the files present inside.
+ if (isDir) {
+ filesToAdd.push(...getFiles(gitRoot, value));
+ return;
+ }
+ filesToAdd.push(value);
+ });
+
+ // Ensuring unique files
+ filesToAdd = [...new Set(filesToAdd)];
+
+ // If index file is already is present
+ if (fs.existsSync(path.join(gitRoot, RELATIVE_PATH_TO_INDEX_FILE))) {
+ const index = new IndexParser(gitRoot).parse();
+
+ // Handle deleted files
+ const paths = index.entries.map((e) => {
+ return e.name;
+ });
+ paths.forEach((value) => {
+ if (!fs.existsSync(path.join(gitRoot, value))) {
+ index.remove(value);
+ }
+ });
+
+ // Handle files that are present in working tree
+ filesToAdd.forEach((file) => {
+ const pathRelativeToGitRoot = path.relative(gitRoot, file);
+ const entry = createIndexEntry(gitRoot, pathRelativeToGitRoot);
+
+ // Remove the previous information, then Add the new information
+ index.remove(entry.name);
+ index.add(entry);
+ });
+
+ // Finally save to disk and return nothing
+ index.saveToDisk();
+ return '';
+ }
+
+ // We are creating a new index file
+ const entries: IndexEntry[] = [];
+ filesToAdd.forEach((file) => {
+ const pathRelativeToGitRoot = path.relative(gitRoot, file);
+ entries.push(createIndexEntry(gitRoot, pathRelativeToGitRoot));
+ });
+
+ const index = new Index({ signature: 'DIRC', version: 2 }, entries);
+
+ index.saveToDisk();
+ return '';
+}
+
+export default updateIndex;
diff --git a/src/26/commands/writeTree.ts b/src/26/commands/writeTree.ts
new file mode 100644
index 0000000..d3576dc
--- /dev/null
+++ b/src/26/commands/writeTree.ts
@@ -0,0 +1,32 @@
+import { CachedTree } from '../objects/cachedTree';
+import IndexParser from '../indexParser';
+import { Tree } from '../objects/tree';
+
+/**
+ * The main function that performs the 'write-tree' command.
+ *
+ * @param {string} gitRoot
+ * @returns {string}
+ */
+function writeTree(gitRoot: string): string {
+ const index = new IndexParser(gitRoot).parse();
+
+ const tree = new Tree();
+ tree.build(index);
+
+ // Change this to false for debugging purposes
+ const writeToDisk = true;
+ const cachedTree = new CachedTree();
+
+ const hash = tree.root.calculateHash(gitRoot, writeToDisk, cachedTree);
+
+ // Ensure entries in cachedTree are sorted by their names.
+ cachedTree.entries.sort((a, b) => a.name.localeCompare(b.name));
+
+ // Invalidate the previous cachedTree and update the index.
+ index.cache = cachedTree;
+ index.saveToDisk();
+ return hash;
+}
+
+export default writeTree;
diff --git a/src/26/constants.ts b/src/26/constants.ts
new file mode 100644
index 0000000..e3bfe9c
--- /dev/null
+++ b/src/26/constants.ts
@@ -0,0 +1,41 @@
+import path from 'path';
+
+export const NULL = Buffer.from('\0')[0];
+export const SPACE = Buffer.from(' ')[0];
+export const LF = Buffer.from('\n')[0];
+
+export const SHA1Regex = /^[a-fA-F0-9]{40}$/;
+
+export const RELATIVE_PATH_TO_INDEX_FILE = '.git/index';
+export const RELATIVE_PATH_TO_HEAD_FILE = '.git/HEAD';
+export const RELATIVE_PATH_TO_OBJECT_DIR = '.git/objects';
+export const RELATIVE_PATH_TO_REF_HEADS_DIR = '.git/refs/heads';
+export const PATH_TO_GIT_CONFIG = path.join(
+ (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) ?? '',
+ '.gitconfig'
+);
+export const DEFAULT_IGNORE_PATTERNS = ['.git/**'];
+
+// Constants used when parsing .git/index file
+export const PREFIX_SIZE = 62;
+export const CTIME_OFFSET = 0;
+export const CTIME_NANO_OFFSET = 4;
+export const MTIME_OFFSET = 8;
+export const MTIME_NANO_OFFSET = 12;
+export const DEV_OFFSET = 16;
+export const INO_OFFSET = 20;
+export const MODE_OFFSET = 24;
+export const UID_OFFSET = 28;
+export const GID_OFFSET = 32;
+export const FILES_SIZE_OFFSET = 36;
+export const HASH_OFFSET = 40;
+export const FLAGS_OFFSET = 60;
+export const NAME_OFFSET = 62;
+
+// Constants for beautify text output to console
+export const FgGreen = '\x1b[32m';
+export const ColorReset = '\x1b[0m';
+export const FgRed = '\x1b[31m';
+export const FgCyan = '\x1b[36m';
+export const BoldStart = '\x1b[1m';
+export const BoldEnd = `\x1b[22m`;
diff --git a/src/26/default-files/default-HEAD b/src/26/default-files/default-HEAD
new file mode 100644
index 0000000..cb089cd
--- /dev/null
+++ b/src/26/default-files/default-HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/src/26/default-files/default-config b/src/26/default-files/default-config
new file mode 100644
index 0000000..d545cda
--- /dev/null
+++ b/src/26/default-files/default-config
@@ -0,0 +1,7 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
diff --git a/src/26/default-files/default-description b/src/26/default-files/default-description
new file mode 100644
index 0000000..498b267
--- /dev/null
+++ b/src/26/default-files/default-description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/src/26/default-files/default-exclude b/src/26/default-files/default-exclude
new file mode 100644
index 0000000..a5196d1
--- /dev/null
+++ b/src/26/default-files/default-exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/src/26/docs/cat-file.md b/src/26/docs/cat-file.md
new file mode 100644
index 0000000..2ec451d
--- /dev/null
+++ b/src/26/docs/cat-file.md
@@ -0,0 +1,39 @@
+## Command: `cat-file`
+
+## Usage
+
+```bash
+# Using ts-node
+npx ts-node cat-file [options]