UNPKG

@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
"use strict"; 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; }