javascriptでgitをリバースエンジニアリングで実装する(3) ~commit編 後編~
gitをjavascriptで実装する
はじめに
こんにちは!!(@hirokihello)[https://twitter.com/maxyasuda] です!!
今日も引き続きgitを実装していきます!!
前回までの記事はこちら
-
第一回addとindexの解説など
https://zenn.dev/hirokihello/articles/522183114dac569dbe80 -
commitの実装(前編)
https://zenn.dev/hirokihello/articles/bcf4db26df4f88bf3a14
これらの記事の知識を前提で行うので、まだ読まれていない方はぜひ読んでください。
この記事で実装すること
この記事では、git logが動くようにnodejsで実装を行っていきます。
ここまでのコードの復習
さてここまでのコードを復習していきましょう。
作成したファイルは3つです。
- add.js
- commit.js
- tree.js
add.js
const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');
async function add (file) {
const fileObj = await fs.readFile(file)
const content = fileObj.toString()
const header=`blob ${content.length}\0`
const store = header + content;
const shasum = crypto.createHash('sha1');
shasum.update(store);
const sha1 = shasum.digest('hex')
zlib.deflate(store, async function (err, result) {
dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
filePath = dirPath + '/' + sha1.substring(2, 40)
await fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(filePath, result, function (err) {
if (err) throw err;
console.log('Saved!');
})
});
}
async function updateIndex (files) {
const header = Buffer.alloc(12);
const fileNum = files.length
header.write('DIRC', 0);
header.writeUInt32BE(2, 4);
header.writeUInt32BE(fileNum, 8);
const entries = await Promise.all(entriesArray(files))
const content = [header].concat(entries).reduce((accumulator, currentValue) =>{
const length = currentValue.length + accumulator.length
return Buffer.concat([accumulator, currentValue], length)
})
const hash = crypto.createHash('sha1')
hash.update(content);
const sha1 = Buffer.from(hash.digest('hex'), 'hex')
const finalObj = Buffer.concat([content, sha1], content.length + sha1.length)
fs.writeFile(".git/index", finalObj, function (err) {
if (err) throw err;
console.log('Saved!');
})
}
function entriesArray(filePathArray) {
return filePathArray.map(async filePath => {
const statInfo = await fs.stat(filePath, {bigint: true})
const ctime = parseInt((statInfo.ctime.getTime() / 1000 ).toFixed(0))
const ctimeNs = parseInt(statInfo.ctimeNs % 1000000000n) // 下9桁欲しい
const mtime = parseInt((statInfo.mtime.getTime() / 1000 ).toFixed(0))
const mtimeNs = parseInt(statInfo.mtimeNs % 1000000000n)
const dev = parseInt(statInfo.dev)
const ino = parseInt(statInfo.ino)
const mode = parseInt(statInfo.mode)
const uid = parseInt(statInfo.uid)
const gid = parseInt(statInfo.gid)
const size = parseInt(statInfo.size)
const stat = Buffer.alloc(40);
[
ctime,
ctimeNs,
mtime,
mtimeNs,
dev,
ino,
mode,
uid,
gid,
size,
].forEach((attr, idx) => stat.writeUInt32BE(attr, idx * 4))
const sha1String = await genBlobSha1(filePath)
const sha1 = Buffer.from(sha1String, 'hex')
const assumeValid = 0b0 // 1 or 0 default is 0
const extendedFlag = 0b0 // 1 or 0 default is 0
const optionalFlag = (((0b0 | assumeValid) << 1) | extendedFlag) << 14
const flagRes = optionalFlag | filePath.length
const flag = Buffer.alloc(2)
// 16bitなのでこのメソッドを使う。writeIntメソッドもあるがrangeが-32768 < val< 32767で、assumeValid=1になった時flagは最低でも32769となり
// エラーが出るのでwriteUInt16BEを使う。
// ファイル名の制限は一旦なしで。
flag.writeUInt16BE(flagRes)
const fileName = Buffer.from(filePath)
const length = stat.length + sha1.length + flag.length + fileName.length
const paddingCount = 8 - (length % 8)
const padding = Buffer.alloc(paddingCount, '\0');
const entry = Buffer.concat([stat, sha1, flag, fileName, padding], length + paddingCount)
return entry
})
}
async function genBlobSha1 (filePath) {
const file = await fs.readFile(filePath)
const content = file.toString()
const header=`blob ${content.length}\0`
const store = header + content;
const shasum = crypto.createHash('sha1');
shasum.update(store);
const sha1 = shasum.digest('hex')
return sha1
}
async function porcelainAdd () {
if (process.argv.length <= 2) return console.log("error no file was added")
const files = process.argv.slice(2).map(file => file.replace(/^\.\//, ""))
await files.forEach(file => add(file))
await updateIndex(files)
}
porcelainAdd()
commit.js
const { genTree } = require('./tree.js')
const fs = require('fs').promises;
const crypto = require('crypto');
async function porcelainCommit () {
const message = process.argv[2]
const indexFile = await fs.readFile(".git/index")
const header = indexFile.slice(0, 12)
let body = indexFile.slice(12)
const fileNum = parseInt(header.slice(8, 12).toString("hex"))
const fileInfo = []
console.log(fileNum)
for (let i = 0; i < fileNum; i++) {
const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)
const sha1 = body.slice(40, 60).toString('hex')
const flag = body.slice(60, 62)
const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111
const name = body.slice(62, 62+fileLength).toString()
const zeroPadding = 8 - ((62+fileLength) % 8)
fileInfo.push({mode, sha1, name})
body = body.slice(62+fileLength+zeroPadding)
}
const treeHash = await genTree(fileInfo)
genCommitObject(treeHash, message)
}
async function genCommitObject (treeSha1, commitMessage) {
const commitTime = (Date.now() / 1000).toFixed(0)
const content = `tree ${treeSha1}\n` +
`author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
`committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
"\n" +
`${commitMessage}\n`
const header= `commit ${content.length}\0`
const store = header + content
const shasum = crypto.createHash('sha1');
shasum.update(store);
const commitSha1 = shasum.digest('hex')
console.log(commitSha1)
}
porcelainCommit()
tree.js
const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');
async function genTree (fileContents=[]) {
const content = calcContents(fileContents)
const header= Buffer.from(`tree ${content.length}\0`)
const store = Buffer.concat([header, content], header.length + content.length);
const shasum = crypto.createHash('sha1');
shasum.update(store);
const sha1 = shasum.digest('hex')
// zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
// dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
// filePath = dirPath + '/' + sha1.substring(2, 40)
// await fs.mkdir(dirPath, { recursive: true }, (err) => {
// if (err) throw err;
// });
// fs.writeFile(filePath, result, function (err) {
// if (err) throw err;
// console.log('Saved!');
// })
// });
return sha1;
}
function calcContents (fileContents=[]) {
return fileContents.reduce((acc, file) => {
const content = calcContent(file)
return Buffer.concat([acc, content], acc.length + content.length)
}, Buffer.alloc(0))
}
function calcContent (fileContent) {
const fileMode = fileContent.mode //100644
const fileName = fileContent.name // sample.js
const fileHash = fileContent.sha1 // 52679e5d3d185546a06f54ac40c1c652e33d7842
const hash = Buffer.from(fileHash, "hex")
const content = Buffer.from(`${fileMode} ${fileName}\0`) // modeとnameの間に半角スペースを開ける。
const buffer = Buffer.concat([content, hash], hash.length + content.length)
return buffer
}
module.exports = {
genTree
}
logsの実装
さて今回はgit logsが動くように実装を行なっていきます。
logsのドキュメントを読む
まずはlogsのドキュメントを読んでみましょう。
The command takes options applicable to the git rev-list command to control what is shown and how, and options applicable to the git diff-* commands to control how the changes each commit introduces are shown.
引用
さっぱりわかりません。具体的な仕様に関することは書いていなさそうです。
実際の挙動を確認してみましょう。
実際の挙動を確認する
ファイルがどのように更新されるか
commitするとどのファイルが更新されるでしょうか。
前回の(コミットの記事)[https://zenn.dev/hirokihello/articles/bcf4db26df4f88bf3a14] でcommit時に.gitディレクトリの中で下記のものが更新されることを確認しました。
-rw-r--r-- 1 inoue_h staff 13 9 20 16:47 COMMIT_EDITMSG
-rw-r--r-- 1 inoue_h staff 137 9 20 16:47 index
drwxr-xr-x 3 inoue_h staff 96 9 20 16:37 info
drwxr-xr-x 4 inoue_h staff 128 9 20 16:47 logs
drwxr-xr-x 7 inoue_h staff 224 9 20 16:47 objects
indexファイルとobjectsが更新されるのは、最初の記事でやりました。
またrefsの更新も前回の記事で実装しています。
今回はその他のlogsがどのように更新されるかをみていきましょう。
$ ⋊> ~/g/g/h/git_test on master ⨯ rm -rf .git
⋊> ~/g/g/h/git_test on master ⨯ git init
Initialized empty Git repository in /Users/hiroki/git/github.com/hirokihello/git_test/.git/
⋊> ~/g/g/h/git_test on master ⨯ git add hoge.js
⋊> ~/g/g/h/git_test on master ⨯ ls -la .git/
total 32
drwxr-xr-x 10 hiroki staff 320 9 26 21:51 ./
drwxr-xr-x 11 hiroki staff 352 9 26 21:51 ../
-rw-r--r-- 1 hiroki staff 23 9 26 21:51 HEAD
-rw-r--r-- 1 hiroki staff 137 9 26 21:51 config
-rw-r--r-- 1 hiroki staff 73 9 26 21:51 description
drwxr-xr-x 14 hiroki staff 448 9 26 21:51 hooks/
-rw-r--r-- 1 hiroki staff 104 9 26 21:51 index
drwxr-xr-x 3 hiroki staff 96 9 26 21:51 info/
drwxr-xr-x 5 hiroki staff 160 9 26 21:51 objects/
drwxr-xr-x 4 hiroki staff 128 9 26 21:51 refs/
⋊> ~/g/g/h/git_test on master ⨯ git commit -m 'first commit'
[master (root-commit) d39f9e0] first commit
1 file changed, 1 insertion(+)
create mode 100644 hoge.js
⋊> ~/g/g/h/git_test on master ⨯ ls -la .git/
total 40
drwxr-xr-x 12 hiroki staff 384 9 26 22:00 ./
drwxr-xr-x 11 hiroki staff 352 9 26 21:51 ../
-rw-r--r-- 1 hiroki staff 13 9 26 22:00 COMMIT_EDITMSG
-rw-r--r-- 1 hiroki staff 23 9 26 21:51 HEAD
-rw-r--r-- 1 hiroki staff 137 9 26 21:51 config
-rw-r--r-- 1 hiroki staff 73 9 26 21:51 description
drwxr-xr-x 14 hiroki staff 448 9 26 21:51 hooks/
-rw-r--r-- 1 hiroki staff 137 9 26 22:00 index
drwxr-xr-x 3 hiroki staff 96 9 26 21:51 info/
drwxr-xr-x 4 hiroki staff 128 9 26 22:00 logs/
drwxr-xr-x 7 hiroki staff 224 9 26 22:00 objects/
drwxr-xr-x 4 hiroki staff 128 9 26 21:51 refs/
logs
$ ls -la .git/logs
total 8
drwxr-xr-x 4 hiroki staff 128 9 26 22:00 ./
drwxr-xr-x 12 hiroki staff 384 9 26 22:00 ../
-rw-r--r-- 1 hiroki staff 164 9 26 22:00 HEAD
drwxr-xr-x 3 hiroki staff 96 9 26 22:00 refs/
⋊> ~/g/g/h/git_test on master ⨯ ls -la .git/logs/refs/
total 0
drwxr-xr-x 3 hiroki staff 96 9 26 22:00 ./
drwxr-xr-x 4 hiroki staff 128 9 26 22:00 ../
drwxr-xr-x 3 hiroki staff 96 9 26 22:00 heads/
⋊> ~/g/g/h/git_test on master ⨯ ls -la .git/logs/refs/heads/
total 8
drwxr-xr-x 3 hiroki staff 96 9 26 22:00 ./
drwxr-xr-x 3 hiroki staff 96 9 26 22:00 ../
-rw-r--r-- 1 hiroki staff 164 9 26 22:00 master
HEADと、refs以下にmasterというファイルが生まれました。
みてみましょう。
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 d39f9e08a3311fa03083ee2e866e4c34c593e413 hirokihello <iammyeye1@gmail.com> 1601125212 +0900 commit (initial): first commit
$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 d39f9e08a3311fa03083ee2e866e4c34c593e413 hirokihello <iammyeye1@gmail.com> 1601125212 +0900 commit (initial): first commit
全く同じ内容ですね。最初の0000000000000000000000000000000000000000
は謎ですが、d39f9e08a3311fa03083ee2e866e4c34c593e413
はcommitで生まれたhashのようです。
$ git cat-file -p d39f9e08a3311fa03083ee2e866e4c34c593e413
tree f42d359cda8f272ac85e780376812808316beeee
author hirokihello <iammyeye1@gmail.com> 1601125212 +0900
committer hirokihello <iammyeye1@gmail.com> 1601125212 +0900
first commit
d39f9e08a3311fa03083ee2e866e4c34c593e413
はcommitオブジェクトを示しますね。またauthor、mail address、commit時間を表す1601125212
が続き、最後にcommitメッセージが続くことがわかりました。
次に別のファイルであるfuga.jsを追加でcommitしてみましょう。
git add fuga.js
⋊> ~/g/g/h/git_test on master ⨯ git commit -m 'second commit'
[master 3b9f813] second commit
1 file changed, 1 insertion(+)
create mode 100644 fuga.js
⋊> ~/g/g/h/git_test on master ⨯ ls -ls .git/logs/
total 8
8 -rw-r--r-- 1 hiroki staff 319 9 26 22:07 HEAD
0 drwxr-xr-x 3 hiroki staff 96 9 26 22:00 refs/
⋊> ~/g/g/h/git_test on master ⨯ ls -ls .git/logs/refs/heads
total 8
8 -rw-r--r-- 1 hiroki staff 319 9 26 22:07 master
HEADとrefs/masterに更新がかかっています。中を見てみましょう。
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 d39f9e08a3311fa03083ee2e866e4c34c593e413 hirokihello <iammyeye1@gmail.com> 1601125212 +0900 commit (initial): first commit
d39f9e08a3311fa03083ee2e866e4c34c593e413 3b9f813e81da29f44453f8a20df83830b5b07156 hirokihello <iammyeye1@gmail.com> 1601125676 +0900 commit: second commit
2行目に注目してください。
最初のhashは、d39f9e08a3311fa03083ee2e866e4c34c593e413
であり、最初のcommit
で生まれたcommitオブジェクトのhashとなっています。次に現れたものは、おそらく2度目のcommitオブジェクトでしょう。
$ git cat-file -p 3b9f813e81da29f44453f8a20df83830b5b07156
tree c14b28b907d4e1445a21ea86c268f9a5cfc82aa9
parent d39f9e08a3311fa03083ee2e866e4c34c593e413
author hirokihello <iammyeye1@gmail.com> 1601125676 +0900
committer hirokihello <iammyeye1@gmail.com> 1601125676 +0900
second commit
cat-fileできたので、.git/logs/HEADと.git/logs/refs/heads/masterのdiffを確認します。
$ diff <(cat .git/logs/HEAD) <(cat .git/logs/refs/heads/master)
$
HEADファイルと同様にrefs/heads/masterが更新されていることがわかりました。1度目のcommitの0000000000000000000000000000000000000000
は親コミットが存在しない時に記載するものということがわかりました。
ここで前回実装したcommit実装のリファクタリングの必要も出て来ましたね。今の実装では、下記の部分を追記することができないので、追記するようにします。
parent d39f9e08a3311fa03083ee2e866e4c34c593e413
/logsの更新を実装する。
さてここまでで実装することをまとめます。
2. commitの挙動を修正する
3. logs以下を更新するように実装する。
この順番にしたのは、前回の実装のままだと親のcommmitを取得することができないからです。なので、refsを参照して親commitを書き込むようにします。
commitと挙動を修正する
下記は修正後のファイルとなります。addも今の段階では、全て上書きするだけとなっているので、すでにindexに追加されているものがあれば、それを残すようにします。
commit.js
const { genTree } = require('./tree.js')
const fs = require('fs').promises;
const zlib = require('zlib');
...
async function genCommitObject (treeSha1, commitMessage) {
const parentCommitHash = await fs.readFile(".git/refs/heads/master", () => undefined)
const parentInfo = parentCommitHash ? `parent ${parentCommitHash}\n` : undefined
const commitTime = (Date.now() / 1000).toFixed(0)
const content = `tree ${treeSha1}\n` +
`${parentInfo}` +
`author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
`committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
"\n" +
`${commitMessage}\n`
const header= `commit ${content.length}\0`
const store = header + content
const shasum = crypto.createHash('sha1');
shasum.update(store);
const commitSha1 = shasum.digest('hex')
zlib.deflate(store, async function (err, result) {
dirPath = __dirname + '/.git/objects/' + commitSha1.substring(0,2)
filePath = dirPath + '/' + commitSha1.substring(2, 40)
await fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(filePath, result, function (err) {
if (err) throw err;
console.log('Saved!');
})
});
const refDirPath = __dirname + '/.git/refs/heads'
const refFilePath = refDirPath + '/master'
await fs.mkdir(refDirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(refFilePath, commitSha1, function (err) {
if (err) throw err;
console.log('ref Saved!');
})
}
porcelainCommit()
commit.jsの修正点はgenCommitObjectの中身だけです。
具体的には、readFileで.git/refs/heads/masterで読み込み、あればparentとして書き込み、なければ書き込まないようにします。
const parentCommitHash = await fs.readFile(".git/refs/heads/master", () => undefined)
const parentInfo = parentCommitHash ? `parent ${parentCommitHash}\n` : undefined
またいつも通り、zlibでdeflateした物を書き込むようにします。
zlib.deflate(store, async function (err, result) {
dirPath = __dirname + '/.git/objects/' + commitSha1.substring(0,2)
filePath = dirPath + '/' + commitSha1.substring(2, 40)
await fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(filePath, result, function (err) {
if (err) throw err;
console.log('Saved!');
})
});
またtree.jsのコメントアウトも外しておいてください。
async function genTree (fileContents=[]) {
const content = calcContents(fileContents)
const header= Buffer.from(`tree ${content.length}\0`)
const store = Buffer.concat([header, content], header.length + content.length);
const shasum = crypto.createHash('sha1');
shasum.update(store);
const sha1 = shasum.digest('hex')
zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
filePath = dirPath + '/' + sha1.substring(2, 40)
await fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(filePath, result, function (err) {
if (err) throw err;
console.log('Saved!');
})
});
return sha1;
}
日付をfirst commitを1601125212
、second commitを1601125676
固定し、下記が実行できるようにやってみましょう。(ここは各自で取得した日付で検証してみてください)
$ git cat-file -p 3b9f813e81da29f44453f8a20df83830b5b07156
tree c14b28b907d4e1445a21ea86c268f9a5cfc82aa9
parent d39f9e08a3311fa03083ee2e866e4c34c593e413
author hirokihello <iammyeye1@gmail.com> 1601125676 +0900
committer hirokihello <iammyeye1@gmail.com> 1601125676 +0900
second commit
$ rm -rf .git
⋊> ~/g/g/h/git_test on master ⨯ git init
Initialized empty Git repository in /Users/hiroki/git/github.com/hirokihello/git_test/.git/
⋊> ~/g/g/h/git_test on master ⨯ git add hoge.js
⋊> ~/g/g/h/git_test on master ⨯ node commit.js "first commit"
一旦ファイルcommitの時間を1601125676
に固定して、fuga.jsをaddしてcommitします。
⋊> ~/g/g/h/git_test on master ⨯ git add fuga.js
⋊> ~/g/g/h/git_test on master ⨯ node commit.js "second commit"
⋊> ~/g/g/h/git_test on master ⨯ git cat-file -p 3b9f813e81da29f44453f8a20df83830b5b07156
tree c14b28b907d4e1445a21ea86c268f9a5cfc82aa9
parent d39f9e08a3311fa03083ee2e866e4c34c593e413
author hirokihello <iammyeye1@gmail.com> 1601125676 +0900
committer hirokihello <iammyeye1@gmail.com> 1601125676 +0900
second commit
先ほどと同じ3b9f813e81da29f44453f8a20df83830b5b07156
を参照することができたので、無事にcommitが親を参照するようにできました。
logsを実装する
あとは簡単です。commit時に、logsのHEADと、refs以下の情報を更新します。先ほどどのように更新されたかというと、下記のように更新されていました。
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 d39f9e08a3311fa03083ee2e866e4c34c593e413 hirokihello <iammyeye1@gmail.com> 1601125212 +0900 commit (initial): first commit
d39f9e08a3311fa03083ee2e866e4c34c593e413 3b9f813e81da29f44453f8a20df83830b5b07156 hirokihello <iammyeye1@gmail.com> 1601125676 +0900 commit: second commit
やるべきことはrefs/HEADに書かれているcommitオブジェクトをみにいき、そこに書かれている情報を取得し、1行づつ足していくだけです。
完成形のファイル
const { genTree } = require('./tree.js')
const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');
async function porcelainCommit () {
const message = process.argv[2]
const indexFile = await fs.readFile(".git/index")
const header = indexFile.slice(0, 12)
let body = indexFile.slice(12)
const fileNum = parseInt(header.slice(8, 12).toString("hex"))
const fileInfo = []
for (let i = 0; i < fileNum; i++) {
const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)
const sha1 = body.slice(40, 60).toString('hex')
const flag = body.slice(60, 62)
const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111
const name = body.slice(62, 62+fileLength).toString()
const zeroPadding = 8 - ((62+fileLength) % 8)
fileInfo.push({mode, sha1, name})
body = body.slice(62+fileLength+zeroPadding)
}
const treeHash = await genTree(fileInfo)
genCommitObject(treeHash, message)
}
async function updateLogs(commitSha1, commitMessage, commitTime) {
const userName = "hirokihello"
const mailAddress = "iammyeye1@gmail.com"
const refsPath = ".git/refs/heads/master"
const lastCommitHash = fs.readFile(refsPath).catch(() => "0000000000000000000000000000000000000000")
const isInitial = lastCommitHash === "0000000000000000000000000000000000000000"
const line = `${lastCommitHash} ${commitSha1} ${userName} <${mailAddress}> ${commitTime} +0900 commit${isInitial ? " (initial)" : ""}: ${commitMessage}`
const refHeadPath = ".git/refs/heads/master"
const headPath = ".git/HEAD"
fs.appendFile(refHeadPath, line, function (err) {
if (err) throw err;
console.log('Saved!');
})
fs.appendFile(headPath, line, function (err) {
if (err) throw err;
console.log('Saved!');
})
}
async function genCommitObject (treeSha1, commitMessage) {
const parentCommitHash = await fs.readFile(".git/refs/heads/master").catch(() => undefined)
const parentInfo = parentCommitHash ? `parent ${parentCommitHash.toString()}\n` : ""
const commitTime = (Date.now() / 1000).toFixed(0)
const content = `tree ${treeSha1}\n` +
`${parentInfo}` +
`author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
`committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
"\n" +
`${commitMessage}\n`
const header= `commit ${content.length}\0`
const store = header + content
const shasum = crypto.createHash('sha1');
shasum.update(store);
const commitSha1 = shasum.digest('hex')
zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
dirPath = __dirname + '/.git/objects/' + commitSha1.substring(0,2)
filePath = dirPath + '/' + commitSha1.substring(2, 40)
await fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
});
fs.writeFile(filePath, result, function (err) {
if (err) throw err;
console.log('Saved!');
})
});
await updateLogs(commitSha1, commitMessage, commitTime)
const refsPath = ".git/refs/heads/master"
fs.writeFile(refsPath, commitSha1, function (err) {
if (err) throw err;
console.log('Saved!');
})
}
porcelainCommit()
解説
初回のcommitの時のみ挙動が特別なので気をつけます。
まずはlogsをupdateする関数を作成します。
commit.js
async function updateLogs(commitSha1, commitMessage, commitTime) {
const userName = "hirokihello"
const mailAddress = "iammyeye1@gmail.com"
const refsPath = ".git/refs/heads/master"
const lastCommitHash = fs.readFile(refsPath).catch(() => "0000000000000000000000000000000000000000")
const isInitial = lastCommitHash === "0000000000000000000000000000000000000000"
const line = `${lastCommitHash} ${commitSha1} ${userName} <${mailAddress}> ${commitTime} +0900 commit${isInitial ? " (initial)" : ""}: ${commitMessage}`
const refHeadPath = ".git/refs/heads/master"
const headPath = ".git/HEAD"
fs.appendFile(refHeadPath, line, function (err) {
if (err) throw err;
console.log('Saved!');
})
fs.appendFile(headPath, line, function (err) {
if (err) throw err;
console.log('Saved!');
})
}
やっていることは単純です。基本的には同じ文字列をファイルの最後に挿入していきます。
最初のcommitの場合、parent hashを0000000000000000000000000000000000000000
にして、commitの文字列の前にinitialを入れ、他では入れません。
動かしてみる。
実際にaddコミットした後のlogsファイルを見てみて、比較してみます。
git コマンドでcommitした結果。
⋊> ~/g/g/h/git_test on master ⨯ git add hoge.js
⋊> ~/g/g/h/git_test on master ⨯ git commit -m 'first commit'
[master (root-commit) 2bdfd1c] first commit
1 file changed, 1 insertion(+)
create mode 100644 hoge.js
⋊> ~/g/g/h/git_test on master ⨯ cat .git/logs/HEAD
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
⋊> ~/g/g/h/git_test on master ⨯ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
⋊> ~/g/g/h/git_test on master ⨯ git log
commit 2bdfd1c78824b78c6c220cbe499f845b8c144c48 (HEAD -> master)
Author: hirokihello <iammyeye1@gmail.com>
Date: Sun Sep 27 01:45:55 2020 +0900
first commit
⋊> ~/g/g/h/git_test on master ⨯ git add fuga.js
⋊> ~/g/g/h/git_test on master ⨯ git commit -m 'second commit'
[master 6879aae] second commit
1 file changed, 1 insertion(+)
create mode 100644 fuga.js
⋊> ~/g/g/h/git_test on master ⨯ cat .git/logs/HEAD
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
2bdfd1c78824b78c6c220cbe499f845b8c144c48 6879aaef452731d06fb6afe937969c6b950fc817 hirokihello <iammyeye1@gmail.com> 1601138784 +0900 commit: second commit
⋊> ~/g/g/h/git_test on master ⨯ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
2bdfd1c78824b78c6c220cbe499f845b8c144c48 6879aaef452731d06fb6afe937969c6b950fc817 hirokihello <iammyeye1@gmail.com> 1601138784 +0900 commit: second commit
⋊> ~/g/g/h/git_test on master ⨯ git log
commit 6879aaef452731d06fb6afe937969c6b950fc817 (HEAD -> master)
Author: hirokihello <iammyeye1@gmail.com>
Date: Sun Sep 27 01:46:24 2020 +0900
second commit
commit 2bdfd1c78824b78c6c220cbe499f845b8c144c48
Author: hirokihello <iammyeye1@gmail.com>
Date: Sun Sep 27 01:45:55 2020 +0900
first commit
commitのtime、messageを初回を1601138755
とfirst commit
、2回目を1601138784
とsecond commit
に固定してcommitしてみます。
⋊> ~/g/g/h/git_test on master ⨯ rm -rf .git
⋊> ~/g/g/h/git_test on master ⨯ git init
Initialized empty Git repository in /Users/hiroki/git/github.com/hirokihello/git_test/.git/
⋊> ~/g/g/h/git_test on master ⨯ git add hoge.js
⋊> ~/g/g/h/git_test on master ⨯ node commit.js "first commit"
⋊> ~/g/g/h/git_test on master ⨯ git add fuga.js
⋊> ~/g/g/h/git_test on master ⨯ node commit.js "second commit"
⋊> ~/g/g/h/git_test on master ⨯ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
2bdfd1c78824b78c6c220cbe499f845b8c144c48 6879aaef452731d06fb6afe937969c6b950fc817 hirokihello <iammyeye1@gmail.com> 1601138784 +0900 commit: second commit
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 2bdfd1c78824b78c6c220cbe499f845b8c144c48 hirokihello <iammyeye1@gmail.com> 1601138755 +0900 commit (initial): first commit
2bdfd1c78824b78c6c220cbe499f845b8c144c48 6879aaef452731d06fb6afe937969c6b950fc817 hirokihello <iammyeye1@gmail.com> 1601138784 +0900 commit: second commit
$ git log
commit 6879aaef452731d06fb6afe937969c6b950fc817 (HEAD -> master)
Author: hirokihello <iammyeye1@gmail.com>
Date: Sun Sep 27 01:46:24 2020 +0900
second commit
commit 2bdfd1c78824b78c6c220cbe499f845b8c144c48
Author: hirokihello <iammyeye1@gmail.com>
Date: Sun Sep 27 01:45:55 2020 +0900
first commit
同じ出力結果になり、無事に動かすことができました。
Discussion