Skip to content

Instantly share code, notes, and snippets.

@ukyo
Last active August 30, 2017 13:18
Show Gist options
  • Save ukyo/6ff9ba133eacef2e03cd9cee1b852807 to your computer and use it in GitHub Desktop.
Save ukyo/6ff9ba133eacef2e03cd9cee1b852807 to your computer and use it in GitHub Desktop.
git cat file
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));
}
// 作成者情報をパース
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 };
}
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