@git-temporal/git-log-scraper
Version:
Scrapes commit data from the git cli for a file or directory in a repository and returns an array of Javascript objects representing commits with files and lines added and deleted.
161 lines (160 loc) • 5.9 kB
JavaScript
;
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const commons_1 = require("@git-temporal/commons");
const logger_1 = require("@git-temporal/logger");
const { debug } = logger_1.createProxies('git-log-scraper');
const parsedAttributes = {
id: '%H%n',
hash: '%h%n',
authorName: '%an%n',
authorEmail: '%ae%n',
relativeDate: '%cr%n',
authorDate: '%at%n',
message: '%s%n',
body: '%b',
};
function getCommitHistory(path, options = { skip: 0, maxCount: 0 }) {
const { skip, maxCount } = options;
const rawLog = fetchFileHistory(path, skip, maxCount);
const commits = parseGitLogOutput(rawLog)
.sort((a, b) => {
return b.authorDate - a.authorDate;
})
.map((c, i) => (Object.assign({}, c, { index: skip + i })));
const isFile = fs.existsSync(path) && !fs.lstatSync(path).isDirectory();
return {
isFile,
commits,
skip,
maxCount,
path,
};
}
exports.getCommitHistory = getCommitHistory;
function getCommitRange(fileName) {
const gitRoot = commons_1.findGitRoot(fileName);
const logFlags = gitLogFlags({ follow: false });
debug('getCommitRange', { fileName, gitRoot, logFlags });
const cmdFileName = fileName === gitRoot ? '.' : fileName;
const allRevHashes = execGit(gitRoot, `log --pretty="format:%H" --topo-order --date=local -- ${commons_1.escapeForCli(cmdFileName)}`).split('\n');
const firstCommitRaw = execGit(gitRoot, `log ${logFlags} -n1 ${allRevHashes[allRevHashes.length - 1]}`);
const firstCommit = parseGitLogOutput(firstCommitRaw)[0];
const lastCommitRaw = execGit(gitRoot, `log ${logFlags} -n 1 -- ${commons_1.escapeForCli(fileName)}`);
const lastCommit = parseGitLogOutput(lastCommitRaw)[0];
const absoluteFileName = fileName.startsWith(gitRoot)
? fileName
: path.resolve(gitRoot, fileName);
const existsLocally = fs.existsSync(absoluteFileName);
const hasChanges = existsLocally && hasUncommitedChanges(gitRoot, fileName);
return {
gitRoot,
firstCommit,
lastCommit,
existsLocally,
count: allRevHashes.length,
path: fileName,
hasUncommittedChanges: hasChanges,
};
}
exports.getCommitRange = getCommitRange;
function hasUncommitedChanges(gitroot, fileName) {
const statusRaw = execGit(gitroot, `status ${fileName}`);
return statusRaw.match(/(new\sfile|modified|deleted)\:/i) !== null;
}
function fetchFileHistory(fileName, skip, maxCount) {
const gitRoot = commons_1.findGitRoot(fileName);
const flags = gitLogFlags();
const skipFlag = skip ? ` --skip=${skip}` : '';
const countFlag = maxCount ? ` -n ${maxCount}` : '';
return execGit(gitRoot, `log ${flags}${skipFlag}${countFlag} -- ${commons_1.escapeForCli(fileName)}`);
}
function gitLogFlags(options = { follow: true }) {
let format = '';
for (const attr in parsedAttributes) {
format += `${attr}:${parsedAttributes[attr]}`;
}
const follow = false && options.follow ? ' --follow' : '';
return `--pretty=\"format:${format}\" --topo-order --date=local --numstat ${follow}`;
}
function execGit(gitRoot, gitCmd) {
return commons_1.execSync(`git ${gitCmd}`, { cwd: gitRoot, logFn: debug });
}
function parseGitLogOutput(output) {
const logItems = [];
const logLines = output.split(/\n\r?/);
let commitIndex = 0;
let currentlyParsingAttr = null;
let parsedValue = null;
let commitObj = null;
let totalLinesAdded = 0;
let totalLinesDeleted = 0;
const addLogItem = () => {
if (!commitObj) {
return;
}
commitObj.linesAdded = totalLinesAdded;
commitObj.linesDeleted = totalLinesDeleted;
commitObj.index = commitIndex;
logItems.push(commitObj);
totalLinesAdded = 0;
totalLinesDeleted = 0;
commitIndex += 1;
};
for (const line of logLines) {
let matches = line.match(/^id\:(.*)/);
if (matches) {
currentlyParsingAttr = 'id';
addLogItem();
commitObj = {
id: matches[1],
files: [],
body: '',
message: '',
};
continue;
}
matches = line.match(/^([^\:]+):(.*)/);
if (matches) {
let attr;
[, attr, parsedValue] = matches;
if (attr === 'authorDate') {
parsedValue = parseInt(parsedValue, 10);
}
if (Object.keys(parsedAttributes).includes(attr)) {
currentlyParsingAttr = attr;
commitObj[currentlyParsingAttr] = parsedValue;
continue;
}
}
if ((matches = line.match(/^([\d\-]+)\s+([\d\-]+)\s+(.*)/))) {
let [linesAdded, linesDeleted, fileName] = matches.slice(1);
linesAdded = commons_1.safelyParseInt(linesAdded);
linesDeleted = commons_1.safelyParseInt(linesDeleted);
fileName = fileName.trim();
currentlyParsingAttr = 'files';
totalLinesAdded += linesAdded;
totalLinesDeleted += linesDeleted;
commitObj.files.push({
linesAdded,
linesDeleted,
name: fileName,
});
}
else if (currentlyParsingAttr === 'body') {
commitObj.body += `<br>${line}`;
}
}
if (commitObj) {
addLogItem();
}
return logItems;
}