Commit 73e67779 authored by wu-lee's avatar wu-lee

lib/git.js - capture first *and* last commit info,

Order this by dates, don't rely on topology so much to define what is
first and last (except that deletions still delimit a file's history).

Comment code more, include link to originating SO post
parent bc73ce6d
/**
Minimal/custom git functionality.
Basically, just pull, and gather working directory metadata.
*/
'use strict';
const shell = require('shelljs');
shell.config.silent = true;
shell.config.fatal = true;
module.exports = (opts = {}) => {
if (!opts.src)
opts.src = './src';
/** Helper function for accumulateFiles. Indicates when *not* to
* update the working value a when compare to a commit b
*/
const compare = {
first: (a,b) => a < b,
last: (a,b) => a > b,
};
// Reduce accumulator for git-log
//
// @param meta - the accumulator object, expected to be:
// { list: <list>, map: <map> }
// where <list> is an array in which to
// build an ordered list of commit IDs,
// and <map> is a simple object in which
// to map commit IDs to to author and time
// information in this form:
// { authored: <date>, author: <string>
// committed: <date>, committer: <string>}
// @param line - a line from git diff-tree to parse
//
// @returns an updated accumulator object
function accumulateCommits(meta, line) {
// fields are:
// commit ID,
......@@ -17,28 +46,64 @@ module.exports = (opts = {}) => {
var authored = fields[1] && new Date(fields[1]*1000);
var committed = fields[3] && new Date(fields[3]*1000);
meta[fields[0]] = {
meta.map[fields[0]] = {
authored: authored,
author: fields[2],
committed: committed,
committer: fields[4],
committer: fields[4],
};
meta.list.push(fields[0]);
return meta;
}
// Reduce accumulator for git-diff-tree
// Reduce accumulator for git-diff-tree.
//
// @param meta - the accumulator object:
// { commits: <commitlog>, files: <filemap> }
// where <commitlog> is the output from
// accumulateCommits, and <filemap> is an object
// in which to map file paths to information
// about the first and last commits
// touching it (omitting any commits prior to
// a delete):
// { first: <commit>, last: <commit> }
// Where <commit> is an entry in the <commitlog>,
// @param line - a line from git diff-tree to parse
//
// @returns the updated accumulator object with the complete file
// map
function accumulateFiles(meta, line) {
const match = line.match(/([A-Z])\s+(.*)/);
if (match) {
// Parse a file modification
if (match[1] === 'D') {
delete meta.files[match[2]];
delete meta.files[match[2]]; // forget this entry
}
else {
meta.files[match[2]] =
meta.commits[meta.commitid];
const fileinfo = meta.files[match[2]] ||
(meta.files[match[2]] = {first: {}, last: {}});
const commitinfo = meta.commits[meta.commitid];
// A common block of code which compares and updates a
// fileinfo user/date value when appropriate.
const update = (who, what, when) => {
if (fileinfo[when][what] &&
compare[when](fileinfo[when][what],
commitinfo[what]))
return; // nothing to do
// update the records
fileinfo[when][what] = commitinfo[what];
fileinfo[when][who] = commitinfo[who];
};
update('author', 'authored', 'first');
update('author', 'authored', 'last');
update('committer', 'committed', 'first');
update('committer', 'committed', 'last');
}
}
else {
......@@ -70,7 +135,59 @@ module.exports = (opts = {}) => {
};
}
/** gets git metadata */
/** Gets git metadata for all the files in the repo.
*
* Example return format:
*
* { '.gitignore':
* { first:
* { authored: 2019-01-28T21:46:14.000Z,
* author: 'bob',
* committed: 2019-01-28T21:46:14.000Z,
* committer: 'alice' },
* last:
* { authored: 2019-02-10T10:56:29.000Z,
* author: 'alice',
* committed: 2019-02-10T23:32:17.000Z,
* committer: 'bob' },
* span: { authored: 1084215000, committed: 1129563000 } },
* 'package.json':
* { first:
* { authored: 2019-01-28T21:49:24.000Z,
* author: 'alice',
* committed: 2019-01-28T23:31:58.000Z,
* committer: 'alice' },
* last:
* { authored: 2019-02-20T10:45:48.000Z,
* author: 'bob',
* committed: 2019-02-25T17:45:38.000Z,
* committer: 'alice' },
* span: { authored: 1947384000, committed: 2398420000 } },
* 'serve.js':
* { first:
* { authored: 2019-01-28T23:33:30.000Z,
* author: 'alice',
* committed: 2019-01-28T23:37:09.000Z,
* committer: 'bob' },
* last:
* { authored: 2019-02-10T10:54:55.000Z,
* author: 'alice',
* committed: 2019-02-10T23:32:17.000Z,
* committer: 'bob' },
* span: { authored: 1077685000, committed: 1122908000 } },
* // ...
* }
*
* Implementation adapted from:
*
* https://stackoverflow.com/questions/19166483/git-how-to-list-all-files-under-version-control-along-with-their-author-date
*
* Note: first and last dates are more strictly the min and max
* found since the last delete. authorship taken from equivalent
* commits.
*
* Note: does not currently detect renames.
*/
function fileInfoSync() {
const execOpts = {cwd: opts.src, silent: true};
......@@ -87,13 +204,11 @@ module.exports = (opts = {}) => {
// Parse the results into a map of commit IDs to timestamps
const commits = result1.stdout
.split(/\n/)
.reduce(accumulateCommits, {});
const commitids = Object.keys(commits);
.reduce(accumulateCommits, {map:{}, list: []});
// Get the files touched by each commit
const result2 = shell
.ShellString(commitids.join("\n"), {silent: true})
.ShellString(commits.list.join("\n")+"\n", {silent: true})
.exec('git diff-tree -r --root --name-status --stdin',
execOpts);
......@@ -106,8 +221,16 @@ module.exports = (opts = {}) => {
const acc = result2
.stdout
.split(/\n/)
.reduce(accumulateFiles, {commits, files:{}});
.reduce(accumulateFiles, {commits:commits.map, files:{}});
for(const file in acc.files) {
const info = acc.files[file];
info.span = {
authored: info.last.authored - info.first.authored,
committed: info.last.committed - info.first.committed,
};
}
return acc.files;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment