Last active
August 30, 2017 13:18
-
-
Save ukyo/6ff9ba133eacef2e03cd9cee1b852807 to your computer and use it in GitHub Desktop.
git cat file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs'); | |
const zlib = require('zlib'); | |
const path = require('path'); | |
const { parse } = require('./git-object-parser'); | |
const { Packfile } = require('./packfile'); | |
// hashからGitオブジェクトのパスを作る | |
function getObjectPath(sha1) { | |
return path.resolve( | |
process.cwd(), | |
'.git/objects', | |
sha1.replace(/^(.{2})(.{38})$/, '$1/$2') | |
); | |
} | |
const sha1 = process.argv[2]; | |
const packfile = new Packfile(path.resolve(process.cwd(), '.git')); | |
const buff = packfile.find(sha1); | |
if (buff) { | |
console.log(parse(buff)); | |
} else { | |
const objectPath = getObjectPath(sha1); | |
const buff = zlib.inflateSync(fs.readFileSync(objectPath)); | |
console.log(parse(buff)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 作成者情報をパース | |
function parseActor([name, email, time, tz]) { | |
const [, hour, minute] = tz.match(/([+-]?\d{2})(\d{2})/); | |
return { | |
name, | |
email: email.slice(1, -1), | |
date: new Date(+time * 1000), | |
timezoneOffset: (+hour * 60 + +minute) * 60 * 1000, | |
}; | |
} | |
function parseCommit(body) { | |
const commit = { | |
parents: [], | |
}; | |
const lines = body.toString('utf8').split('\n'); | |
let i; | |
for (i = 0; i < lines.length; i++) { | |
if (!lines[i].length) break; | |
const [type, ...rest] = lines[i].split(/\s/); | |
switch (type) { | |
case 'tree': commit.tree = rest[0]; break; | |
case 'parent': commit.parents.push(rest[0]); break; | |
case 'author': | |
case 'committer': commit[type] = parseActor(rest); break; | |
} | |
} | |
commit.message = lines.slice(i).join('\n').trim(); | |
return commit; | |
} | |
function parseTag(body) { | |
const tag = {}; | |
const lines = body.toString('utf8').split('\n'); | |
let i; | |
for (i = 0; i < lines.length; i++) { | |
if (!lines[i].length) break; | |
const [type, ...rest] = lines[i].split(/\s/); | |
switch (type) { | |
case 'object': | |
case 'type': | |
case 'tag': tag[type] = rest[0]; break; | |
case 'tagger': tag[type] = parseActor(rest); break; | |
} | |
} | |
tag.message = lines.slice(i).join('\n').trim(); | |
return tag; | |
} | |
function parseBlob(body) { | |
return body.toString('utf8'); | |
} | |
const treeChildrenTypes = { | |
040: 'tree', | |
100: 'blob', | |
120: 'symlink', | |
160: 'submodule', | |
}; | |
function parseTree(body) { | |
const children = []; | |
let i = 0; | |
while (i < body.length) { | |
let j = i; | |
while (body[j]) j++; | |
const [, type, mode, name] = body.slice(i, j).toString('utf8').match(/(\d{3})(\d{3}) (.+)/); | |
children.push({ | |
type: treeChildrenTypes[type], | |
mode, | |
name, | |
sha1: body.slice(j += 1, j += 20).toString('hex'), | |
}); | |
i = j; | |
} | |
return children; | |
} | |
const parsers = { | |
commit: parseCommit, | |
tag: parseTag, | |
blob: parseBlob, | |
tree: parseTree, | |
}; | |
module.exports.parse = function parse(buff) { | |
// ヘッダーのパース | |
let index = 0; | |
while (buff[index]) index++; | |
const [type, size] = buff.slice(0, index).toString('utf8').split(' '); | |
// 本体のパース | |
const body = parsers[type](buff.slice(index + 1)); | |
return { type, size, body }; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs'); | |
const zlib = require('zlib'); | |
const path = require('path'); | |
const ObjectTypeEnum = { | |
OBJ_COMMIT: 1, | |
OBJ_TREE: 2, | |
OBJ_BLOB: 3, | |
OBJ_TAG: 4, | |
OBJ_OFS_DELTA: 6, | |
OBJ_REF_DELTA: 7, | |
}; | |
const ObjectTypeStrings = { | |
1: 'commit', | |
2: 'tree', | |
3: 'blob', | |
4: 'tag', | |
}; | |
function inflatePackedObject(fd, offset, size) { | |
const buff = Buffer.alloc(Math.max(size * 2, 128)); | |
fs.read(fd, buff, 0, buff.length, offset); | |
return zlib.inflateSync(buff); | |
} | |
function readDataSize(buff, offset) { | |
let cmd; | |
let size = 0; | |
let x = 1; | |
do { | |
cmd = buff[offset++]; | |
size += (cmd & 0x7f) * x; | |
x *= 128; | |
} while (cmd & 0x80); | |
return [size, offset]; | |
} | |
function patchDelta(src, delta) { | |
let deltaOffset; | |
let srcSize; | |
let dstSize; | |
[srcSize, deltaOffset] = readDataSize(delta, 0); | |
[dstSize, deltaOffset] = readDataSize(delta, deltaOffset); | |
let dstOffset = 0; | |
let cmd; | |
const dst = Buffer.alloc(dstSize); | |
while (deltaOffset < delta.length) { | |
cmd = delta[deltaOffset++]; | |
if (cmd & 0x80) { | |
let offset = 0; | |
let size = 0; | |
if (cmd & 0x01) offset = delta[deltaOffset++]; | |
if (cmd & 0x02) offset |= (delta[deltaOffset++] << 8); | |
if (cmd & 0x04) offset |= (delta[deltaOffset++] << 16); | |
if (cmd & 0x08) offset |= (delta[deltaOffset++] << 24); | |
if (cmd & 0x10) size = delta[deltaOffset++]; | |
if (cmd & 0x20) size |= (delta[deltaOffset++] << 8); | |
if (cmd & 0x40) size |= (delta[deltaOffset++] << 16); | |
if (size === 0) size = 0x10000; | |
dst.set(src.slice(offset, offset + size), dstOffset); | |
dstOffset += size; | |
dstSize -= size; | |
} else if (cmd) { | |
if (cmd > dstSize) { | |
break; | |
} | |
dst.set(delta.slice(deltaOffset, deltaOffset + cmd), dstOffset); | |
dstOffset += cmd; | |
deltaOffset += cmd; | |
dstSize -= cmd; | |
} | |
} | |
return dst; | |
} | |
module.exports.Packfile = class Packfile { | |
constructor(gitDir) { | |
this.packDir = path.join(gitDir, 'objects', 'pack'); | |
this.idx = { | |
objects: {}, | |
packs: [], | |
}; | |
fs.readdirSync(this.packDir) | |
.filter(name => /\.idx$/.test(name)) | |
.map(name => name.match(/(pack-[a-f\d]{40})\.idx/)[1]) | |
.map((name, i) => this._parseIdx(name, i)); | |
} | |
_parseIdx(name, fileIndex) { | |
const buff = fs.readFileSync(path.join(this.packDir, `${name}.idx`)); | |
this.idx.packs.push(`${name}.pack`); | |
if (buff.readUInt32BE(0) === 0xff744f63) { | |
const version = buff.readUInt32BE(4); | |
let index = 8 + 255 * 4; | |
const n = buff.readUInt32BE(index); | |
index += 4; | |
let off32 = index + n * 24; | |
let off64 = off32 + n * 4; | |
for (let i = 0; i < n; i++) { | |
const sha1 = buff.slice(index, index += 20).toString('hex'); | |
let offset = buff.readUInt32BE(off32); | |
off32 += 4; | |
if (offset & 0x80000000) { | |
offset = buff.readUInt32BE(off64 * 4294967296); | |
offset += buff.readUInt32BE(off64 += 4); | |
off64 += 4; | |
} | |
this.idx.objects[sha1] = { offset, fileIndex }; | |
} | |
} else { | |
let index = 255 * 4; | |
const n = buff.readUInt32BE(index); | |
index += 4; | |
for (let i = 0; i < n; i++) { | |
const offset = buff.readUInt32BE(index); | |
const sha1 = buff.slice(index += 4, index += 20).toString('hex'); | |
this.idx.objects[sha1] = { offset, fileIndex }; | |
} | |
} | |
} | |
_findByOffset(fd, offset) { | |
const head = Buffer.alloc(32); | |
fs.readSync(fd, head, 0, head.length, offset); | |
let c = head[0]; | |
const type = (c & 0x7f) >> 4; | |
let size = c & 15; | |
let x = 16; | |
let i = 1; | |
while (c & 0x80) { | |
c = head[i++]; | |
size += (c & 0x7f) * x; | |
x *= 128; // x << 7 | |
} | |
switch (type) { | |
case ObjectTypeEnum.OBJ_COMMIT: | |
case ObjectTypeEnum.OBJ_TREE: | |
case ObjectTypeEnum.OBJ_BLOB: | |
case ObjectTypeEnum.OBJ_TAG: | |
return { type, size, buff: inflatePackedObject(fd, offset + i, size) }; | |
case ObjectTypeEnum.OBJ_OFS_DELTA: | |
case ObjectTypeEnum.OBJ_REF_DELTA: | |
return this._resolveDelta(fd, offset, type, size, head, i); | |
} | |
} | |
_findBySha1(sha1) { | |
if (!this.idx.objects[sha1]) return; | |
const { offset, fileIndex } = this.idx.objects[sha1]; | |
const packFilePath = path.join(this.packDir, this.idx.packs[fileIndex]); | |
const fd = fs.openSync(packFilePath, 'r'); | |
const result = this._findByOffset(fd, offset); | |
fs.closeSync(fd); | |
return result; | |
} | |
find(sha1) { | |
const o = this._findBySha1(sha1); | |
if (!o) return; | |
return Buffer.concat([new Buffer(`${ObjectTypeStrings[o.type]} ${o.size}\x00`), o.buff]); | |
} | |
_resolveDelta(fd, offset, type, size, head, i) { | |
let src; | |
if (type === ObjectTypeEnum.OBJ_OFS_DELTA) { | |
let c = head[i++]; | |
let ofs = c & 7; | |
while (c & 0x80) { | |
ofs++; | |
c = head[i++]; | |
ofs = ofs * 128 + (c & 0x7f); | |
} | |
const baseOffset = offset - ofs; | |
src = this._findByOffset(fd, baseOffset); | |
} else { | |
const sha1 = head.slice(i, i += 20).toString('hex'); | |
src = this._findBySha1(sha1); | |
} | |
const delta = inflatePackedObject(fd, offset + i, size); | |
const buff = patchDelta(src.buff, delta); | |
return { type: src.type, size: buff.length, buff }; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment