tiny-commit-walker
Version:
tiny commit walker
468 lines • 16.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const promisify = require("util.promisify");
const path = require("path");
const commit_1 = require("./commit");
const pack_1 = require("./pack");
const statAsync = promisify(fs.stat);
const readFileAsync = promisify(fs.readFile);
const readDirAsync = promisify(fs.readdir);
class Ref {
constructor(name, _gitDir, _hash, _packs) {
this.name = name;
this._gitDir = _gitDir;
this._hash = _hash;
this._packs = _packs;
}
get commit() {
if (this._commit) {
return this._commit;
}
return this._commit = commit_1.Commit.readCommitSync(this._gitDir, this._hash, this._packs);
}
}
exports.Ref = Ref;
const processings = {};
class Repository {
constructor(gitDir) {
this.gitDir = gitDir;
this._refMaps = {};
this._refsDir = path.join(gitDir, 'refs');
}
/**
* Find a git directory. This function find one from current or parents directories.
*/
static findGitDir(repositoryPath = process.cwd()) {
return __awaiter(this, void 0, void 0, function* () {
try {
const gitDir = path.resolve(repositoryPath, '.git');
const stat = yield statAsync(gitDir);
if (stat.isDirectory()) {
return gitDir;
}
else if (stat.isFile()) {
const gitDirRef = yield readFileAsync(gitDir, 'utf8').trim().split(/\s/).pop();
return path.resolve(repositoryPath, gitDirRef);
}
}
catch (err) {
const parent = path.resolve(repositoryPath, '..');
if (repositoryPath === parent)
return;
return yield Repository.findGitDir(parent);
}
});
}
/**
* Find a git directory sync. This function find one from current or parents directories.
*/
static findGitDirSync(repositoryPath = process.cwd()) {
try {
const gitDir = path.resolve(repositoryPath, '.git');
const stat = fs.statSync(gitDir);
if (stat.isDirectory()) {
return gitDir;
}
else if (stat.isFile()) {
const gitDirRef = fs.readFileSync(gitDir, 'utf8').trim().split(/\s/).pop();
return path.resolve(repositoryPath, gitDirRef);
}
}
catch (err) {
const parent = path.resolve(repositoryPath, '..');
if (repositoryPath === parent)
return;
return Repository.findGitDirSync(parent);
}
}
_initRefs() {
return __awaiter(this, void 0, void 0, function* () {
if (this._refs) {
return;
}
if (processings[this.gitDir]) {
return yield processings[this.gitDir];
}
if (!this._packs) {
this._packs = yield pack_1.Packs.initialize(this.gitDir);
}
const createMap = (dir, prefix = '') => __awaiter(this, void 0, void 0, function* () {
const map = new Map();
try {
const pathes = yield readFilePathes(path.join(this._refsDir, dir));
for (let i = 0; i < pathes.length; i++) {
const p = pathes[i];
if (p === 'HEAD')
continue;
const hash = (yield readFileAsync(path.join(this._refsDir, dir, p), 'utf8')).trim();
map.set(prefix + p, hash);
}
}
catch (e) { }
return map;
});
const readCommits = (map) => __awaiter(this, void 0, void 0, function* () {
const refs = [];
for (const [name, hash] of map.entries()) {
refs[refs.length] = new Ref(name, this.gitDir, hash, this._packs);
}
return refs;
});
const branchMap = yield createMap('heads');
const tagMap = yield createMap('tags');
const remoteBranchMap = new Map();
try {
const dirs = yield readDirAsync(path.join(this._refsDir, 'remotes'));
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i];
const map = yield createMap(path.join('remotes', dir), `${dir}/`);
for (const [key, hash] of map.entries()) {
remoteBranchMap.set(key, hash);
}
}
}
catch (e) { }
try {
const s = yield readFileAsync(path.join(this.gitDir, 'packed-refs'), 'utf8');
addPackedRefs(s, branchMap, tagMap, remoteBranchMap);
}
catch (e) { }
const promise = Promise.all([
readCommits(branchMap),
readCommits(tagMap),
readCommits(remoteBranchMap),
]).then(([heads, tags, remotes]) => {
this._refs = { heads, tags, remotes };
this._initRefMaps();
delete processings[this.gitDir];
});
processings[this.gitDir] = promise;
return yield promise;
});
}
_initRefsSync() {
if (this._refs) {
return;
}
if (!this._packs) {
this._packs = pack_1.Packs.initializeSync(this.gitDir);
}
const createMap = (dir, prefix = '') => {
const map = new Map();
try {
const pathes = readFilePathesSync(path.join(this._refsDir, dir));
for (let i = 0; i < pathes.length; i++) {
const p = pathes[i];
if (p === 'HEAD')
continue;
const hash = fs.readFileSync(path.join(this._refsDir, dir, p), 'utf8').trim();
map.set(prefix + p, hash);
}
}
catch (e) { }
return map;
};
const readCommits = (map) => {
const refs = [];
for (const [name, hash] of map.entries()) {
refs[refs.length] = new Ref(name, this.gitDir, hash, this._packs);
}
return refs;
};
const branchMap = createMap('heads');
const tagMap = createMap('tags');
const remoteBranchMap = new Map();
try {
const dirs = fs.readdirSync(path.join(this._refsDir, 'remotes'));
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i];
const map = createMap(path.join('remotes', dir), `${dir}/`);
for (const [key, hash] of map.entries()) {
remoteBranchMap.set(key, hash);
}
}
}
catch (e) { }
try {
const s = fs.readFileSync(path.join(this.gitDir, 'packed-refs'), 'utf8');
addPackedRefs(s, branchMap, tagMap, remoteBranchMap);
}
catch (e) { }
this._refs = {
heads: readCommits(branchMap),
tags: readCommits(tagMap),
remotes: readCommits(remoteBranchMap),
};
this._initRefMaps();
}
_initRefMaps() {
Object.keys(this._refs).forEach((k) => {
const m = new Map();
this._refs[k].forEach(ref => m.set(ref.name, ref));
this._refMaps[k] = m;
});
}
/**
* Read a infomation of `.git/HEAD`.
*
* ```ts
* const head = repo.readHead();
* // if type is 'branch'
* console.log(head.type === 'branch');
* console.log(head.branch);
* // if type is 'commit'
* console.log(head.type === 'commit);
* console.log(head.branch);
* ```
*/
readHead() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._packs) {
this._packs = yield pack_1.Packs.initialize(this.gitDir);
}
const s = (yield readFileAsync(path.join(this.gitDir, 'HEAD'), 'utf8')).trim();
if (s.startsWith('ref')) {
const name = s.match(/^ref: refs\/heads\/(.+)$/)[1];
return {
type: 'branch',
branch: yield this._readRef('heads', name),
};
}
else {
return {
type: 'commit',
commit: yield commit_1.Commit.readCommit(this.gitDir, s, this._packs),
};
}
});
}
/**
* Read a infomation of `.git/HEAD` sync.
*/
readHeadSync() {
if (!this._packs) {
this._packs = pack_1.Packs.initializeSync(this.gitDir);
}
const s = fs.readFileSync(path.join(this.gitDir, 'HEAD'), 'utf8').trim();
if (s.startsWith('ref')) {
const name = s.match(/^ref: refs\/heads\/(.+)$/)[1];
return {
type: 'branch',
branch: this._readRefSync('heads', name),
};
}
else {
return {
type: 'commit',
commit: commit_1.Commit.readCommitSync(this.gitDir, s, this._packs),
};
}
}
_readRefs(dir) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._refs)
yield this._initRefs();
return this._refs[dir];
});
}
_readRefsSync(dir) {
if (!this._refs)
this._initRefsSync();
return this._refs[dir];
}
_findRef(dir, name) {
const ref = this._refMaps[dir].get(name);
if (!ref)
throw new Error(`refs/${dir}/${name} is not found.`);
return ref;
}
_readRef(dir, name) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._refs)
yield this._initRefs();
return this._findRef(dir, name);
});
}
_readRefSync(dir, name) {
if (!this._refs)
this._initRefsSync();
return this._findRef(dir, name);
}
/**
* Read branches.
*
* ```ts
* const heads = await repo.readBranches(); // or await repo.readBranches('heads');
* const remotes = await repo.readBranches('remotes');
* const allBranches = await repo.readBranches(['heads', 'remotes']);
* console.log(heads[0].name, heads[0].commit);
* ```
*/
readBranches(dirs = ['heads']) {
return __awaiter(this, void 0, void 0, function* () {
if (!Array.isArray(dirs)) {
dirs = [dirs];
}
return yield Promise
.all(dirs.map((dir) => __awaiter(this, void 0, void 0, function* () { return yield this._readRefs(dir); })))
.then(results => Array.prototype.concat.apply([], results));
});
}
/**
* Read branches sync.
*/
readBranchesSync(dirs = ['heads']) {
if (!Array.isArray(dirs)) {
dirs = [dirs];
}
return Array.prototype.concat.apply([], dirs.map(dir => this._readRefsSync(dir)));
}
/**
* Read a commit by branch name.
*
* ```ts
* const masterCommit = await repo.readCommitByBranch('master');
* const originMasterCommit = await repo.readCommitByBranch('origin/master');
* ```
*/
readCommitByBranch(branchName, scope = ['heads', 'remotes']) {
return __awaiter(this, void 0, void 0, function* () {
if (!Array.isArray(scope))
scope = [scope];
let err = null;
for (let i = 0; i < scope.length; i++) {
try {
return (yield this._readRef(scope[i], branchName)).commit;
}
catch (e) {
err = e;
}
}
throw err;
});
}
/**
* Read a commit by branch name sync.
*/
readCommitByBranchSync(branchName, scope = ['heads', 'remotes']) {
if (!Array.isArray(scope))
scope = [scope];
let err = null;
for (let i = 0; i < scope.length; i++) {
try {
return this._readRefSync(scope[i], branchName).commit;
}
catch (e) {
err = e;
}
}
throw err;
}
/**
* Read tags.
*
* ```ts
* const tags = await repo.readTags();
* ```
*/
readTags() {
return __awaiter(this, void 0, void 0, function* () {
return yield this._readRefs('tags');
});
}
/**
* Read tags sync.
*/
readTagsSync() {
return this._readRefsSync('tags');
}
/**
* Read a commit by tag name.
*
* ```ts
* const commit = repo.readCommitByTag('v1.0');
* ```
*/
readCommitByTag(tagName) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this._readRef('tags', tagName)).commit;
});
}
/**
* Read a commit by tag name sync.
*/
readCommitByTagSync(tagName) {
return this._readRefSync('tags', tagName).commit;
}
}
exports.Repository = Repository;
function addPackedRefs(s, branchMap, tagMap, remoteBranchMap) {
const _tagMap = new Map();
const lines = s.trim().split('\n').forEach((line) => {
const m = line.match(/([a-f\d]{40}) refs\/(heads|remotes|tags)\/(.+)/);
if (!m)
return;
const [, hash, type, name] = m;
switch (type) {
case 'heads':
!branchMap.has(name) && branchMap.set(name, hash);
break;
case 'remotes':
!remoteBranchMap.has(name) && remoteBranchMap.set(name, hash);
break;
case 'tags':
!tagMap.has(name) && tagMap.set(name, hash);
break;
}
});
}
function normalizePath(dir, pathes) {
pathes = pathes.map(p => p.slice(dir.length + 1));
return path.sep === '/' ? pathes : pathes.map(p => p.replace(/\\/g, '/'));
}
function readFilePathes(dir) {
return __awaiter(this, void 0, void 0, function* () {
const pathes = [];
function _getFilePathes(dir) {
return __awaiter(this, void 0, void 0, function* () {
let names = yield readDirAsync(dir);
for (let i = 0; i < names.length; i++) {
const p = path.join(dir, names[i]);
const stats = yield statAsync(p);
if (stats.isFile())
pathes.push(p);
if (stats.isDirectory())
yield _getFilePathes(p);
}
});
}
yield _getFilePathes(dir);
return normalizePath(dir, pathes);
});
}
function readFilePathesSync(dir) {
const pathes = [];
function _getFilePathes(dir) {
let names = fs.readdirSync(dir);
for (let i = 0; i < names.length; i++) {
const p = path.join(dir, names[i]);
const stats = fs.statSync(p);
if (stats.isFile())
pathes.push(p);
if (stats.isDirectory())
_getFilePathes(p);
}
}
_getFilePathes(dir);
return normalizePath(dir, pathes);
}
//# sourceMappingURL=repo.js.map