Merge pull request #565 from eatdrinksleepcode/revision-range

More robust support for revision ranges
This commit is contained in:
Peter Cottle 2019-04-21 12:57:44 -07:00 committed by GitHub
commit 06e0f29acf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 240 additions and 47 deletions

View file

@ -1,3 +1,5 @@
var Q = require('q');
var HeadlessGit = require('../src/js/git/headless').HeadlessGit; var HeadlessGit = require('../src/js/git/headless').HeadlessGit;
var TreeCompare = require('../src/js/graph/treeCompare.js'); var TreeCompare = require('../src/js/graph/treeCompare.js');
@ -95,6 +97,27 @@ var expectLevelSolved = function(levelBlob) {
expectLevelAsync(headless, levelBlob); expectLevelAsync(headless, levelBlob);
}; };
var runCommand = function(command, resultHandler) {
var headless = new HeadlessGit();
var deferred = Q.defer();
var msg = null;
deferred.promise.then(function(commands) {
msg = commands[commands.length - 1].get('error').get('msg');
});
runs(function() {
headless.sendCommand(command, deferred);
});
waitsFor(function() {
if(null == msg) {
return false;
}
resultHandler(msg);
return true;
}, 'commands should be finished', 500);
};
var TIME = 150; var TIME = 150;
// useful for throwing garbage and then expecting one commit // useful for throwing garbage and then expecting one commit
var ONE_COMMIT_TREE = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}'; var ONE_COMMIT_TREE = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';
@ -105,6 +128,7 @@ module.exports = {
TIME: TIME, TIME: TIME,
expectTreeAsync: expectTreeAsync, expectTreeAsync: expectTreeAsync,
expectLevelSolved: expectLevelSolved, expectLevelSolved: expectLevelSolved,
ONE_COMMIT_TREE: ONE_COMMIT_TREE ONE_COMMIT_TREE: ONE_COMMIT_TREE,
runCommand: runCommand
}; };

View file

@ -1,5 +1,6 @@
var base = require('./base'); var base = require('./base');
var expectTreeAsync = base.expectTreeAsync; var expectTreeAsync = base.expectTreeAsync;
var runCommand = base.runCommand;
describe('Git', function() { describe('Git', function() {
it('Commits', function() { it('Commits', function() {
@ -282,4 +283,104 @@ describe('Git', function() {
); );
}); });
describe('RevList', function() {
it('requires at least 1 argument', function() {
runCommand('git rev-list', function(commandMsg) {
expect(commandMsg).toContain('at least 1');
});
});
describe('supports', function() {
var SETUP = 'git co -b left C0; gc; git merge master; git co -b right C0; gc; git merge master; git co -b all left; git merge right; ';
it('single included revision', function() {
runCommand(SETUP + 'git rev-list all', function(commandMsg) {
expect(commandMsg).toBe('C6\nC5\nC4\nC3\nC2\nC1\nC0\n');
});
});
it('single excluded revision', function() {
runCommand(SETUP + 'git rev-list all ^right', function(commandMsg) {
expect(commandMsg).toBe('C6\nC3\nC2\n');
});
});
it('multiple included revisions', function() {
runCommand(SETUP + 'git rev-list right left', function(commandMsg) {
expect(commandMsg).toBe('C5\nC4\nC3\nC2\nC1\nC0\n');
});
});
it('multiple excluded revisions', function() {
runCommand(SETUP + 'git rev-list all ^right ^left', function(commandMsg) {
expect(commandMsg).toBe('C6\n');
});
});
});
});
describe('Log supports', function() {
var SETUP = 'git co -b left C0; gc; git merge master; git co -b right C0; gc; git merge master; git co -b all left; git merge right; ';
it('implied HEAD', function() {
runCommand(SETUP + '; git co right; git log', function(commandMsg) {
expect(commandMsg).toContain('Commit: C0\n');
expect(commandMsg).toContain('Commit: C1\n');
expect(commandMsg).not.toContain('Commit: C2\n');
expect(commandMsg).not.toContain('Commit: C3\n');
expect(commandMsg).toContain('Commit: C4\n');
expect(commandMsg).toContain('Commit: C5\n');
expect(commandMsg).not.toContain('Commit: C6\n');
});
});
it('single included revision', function() {
runCommand(SETUP + 'git log right', function(commandMsg) {
expect(commandMsg).toContain('Commit: C0\n');
expect(commandMsg).toContain('Commit: C1\n');
expect(commandMsg).not.toContain('Commit: C2\n');
expect(commandMsg).not.toContain('Commit: C3\n');
expect(commandMsg).toContain('Commit: C4\n');
expect(commandMsg).toContain('Commit: C5\n');
expect(commandMsg).not.toContain('Commit: C6\n');
});
});
it('single excluded revision', function() {
runCommand(SETUP + 'git log all ^right', function(commandMsg) {
expect(commandMsg).not.toContain('Commit: C0\n');
expect(commandMsg).not.toContain('Commit: C1\n');
expect(commandMsg).toContain('Commit: C2\n');
expect(commandMsg).toContain('Commit: C3\n');
expect(commandMsg).not.toContain('Commit: C4\n');
expect(commandMsg).not.toContain('Commit: C5\n');
expect(commandMsg).toContain('Commit: C6\n');
});
});
it('multiple included revisions', function() {
runCommand(SETUP + 'git log right left', function(commandMsg) {
expect(commandMsg).toContain('Commit: C0\n');
expect(commandMsg).toContain('Commit: C1\n');
expect(commandMsg).toContain('Commit: C2\n');
expect(commandMsg).toContain('Commit: C3\n');
expect(commandMsg).toContain('Commit: C4\n');
expect(commandMsg).toContain('Commit: C5\n');
expect(commandMsg).not.toContain('Commit: C6\n');
});
});
it('multiple excluded revisions', function() {
runCommand(SETUP + 'git log all ^right ^left', function(commandMsg) {
expect(commandMsg).not.toContain('Commit: C0\n');
expect(commandMsg).not.toContain('Commit: C1\n');
expect(commandMsg).not.toContain('Commit: C2\n');
expect(commandMsg).not.toContain('Commit: C3\n');
expect(commandMsg).not.toContain('Commit: C4\n');
expect(commandMsg).not.toContain('Commit: C5\n');
expect(commandMsg).toContain('Commit: C6\n');
});
});
});
}); });

View file

@ -574,25 +574,26 @@ var commandConfig = {
} }
}, },
revlist: {
dontCountForGolf: true,
displayName: 'rev-list',
regex: /^git +rev-list($|\s)/,
execute: function(engine, command) {
var generalArgs = command.getGeneralArgs();
command.validateArgBounds(generalArgs, 1);
engine.revlist(generalArgs);
}
},
log: { log: {
dontCountForGolf: true, dontCountForGolf: true,
regex: /^git +log($|\s)/, regex: /^git +log($|\s)/,
execute: function(engine, command) { execute: function(engine, command) {
var generalArgs = command.getGeneralArgs(); var generalArgs = command.getGeneralArgs();
if (generalArgs.length == 2) { command.impliedHead(generalArgs, 0);
// do fancy git log branchA ^branchB engine.log(generalArgs);
if (generalArgs[1][0] == '^') {
engine.logWithout(generalArgs[0], generalArgs[1]);
} else {
throw new GitError({
msg: intl.str('git-error-options')
});
}
}
command.oneArgImpliedHead(generalArgs);
engine.log(generalArgs[0]);
} }
}, },

View file

@ -109,6 +109,8 @@ HeadlessGit.prototype.sendCommand = function(value, entireCommandPromise) {
var chain = deferred.promise; var chain = deferred.promise;
var startTime = new Date().getTime(); var startTime = new Date().getTime();
var commands = [];
util.splitTextCommand(value, function(commandStr) { util.splitTextCommand(value, function(commandStr) {
chain = chain.then(function() { chain = chain.then(function() {
var commandObj = new Command({ var commandObj = new Command({
@ -117,6 +119,7 @@ HeadlessGit.prototype.sendCommand = function(value, entireCommandPromise) {
var thisDeferred = Q.defer(); var thisDeferred = Q.defer();
this.gitEngine.dispatch(commandObj, thisDeferred); this.gitEngine.dispatch(commandObj, thisDeferred);
commands.push(commandObj);
return thisDeferred.promise; return thisDeferred.promise;
}.bind(this)); }.bind(this));
}, this); }, this);
@ -124,7 +127,7 @@ HeadlessGit.prototype.sendCommand = function(value, entireCommandPromise) {
chain.then(function() { chain.then(function() {
var nowTime = new Date().getTime(); var nowTime = new Date().getTime();
if (entireCommandPromise) { if (entireCommandPromise) {
entireCommandPromise.resolve(); entireCommandPromise.resolve(commands);
} }
}); });

View file

@ -2740,37 +2740,26 @@ GitEngine.prototype.logWithout = function(ref, omitBranch) {
this.log(ref, Graph.getUpstreamSet(this, omitBranch)); this.log(ref, Graph.getUpstreamSet(this, omitBranch));
}; };
GitEngine.prototype.log = function(ref, omitSet) { GitEngine.prototype.revlist = function(refs) {
// omit set is for doing stuff like git log branchA ^branchB var range = new RevisionRange(this, refs);
omitSet = omitSet || {};
// first get the commit we referenced
var commit = this.getCommitFromRef(ref);
// then get as many far back as we can from here, order by commit date // now go through and collect ids
var toDump = []; var bigLogStr = range.formatRevisions(function(c) {
var pQueue = [commit]; return c.id + '\n';
});
var seen = {}; throw new CommandResult({
msg: bigLogStr
});
};
while (pQueue.length) { GitEngine.prototype.log = function(refs) {
var popped = pQueue.shift(0); var range = new RevisionRange(this, refs);
if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
continue;
}
seen[popped.get('id')] = true;
toDump.push(popped);
if (popped.get('parents') && popped.get('parents').length) {
pQueue = pQueue.concat(popped.get('parents'));
}
}
// now go through and collect logs // now go through and collect logs
var bigLogStr = ''; var bigLogStr = range.formatRevisions(function(c) {
toDump.forEach(function (c) { return c.getLogEntry();
bigLogStr += c.getLogEntry(); });
}, this);
throw new CommandResult({ throw new CommandResult({
msg: bigLogStr msg: bigLogStr
@ -3079,6 +3068,79 @@ var Tag = Ref.extend({
} }
}); });
function RevisionRange(engine, specifiers) {
this.engine = engine;
this.included = {};
this.excluded = {};
this.revisions = [];
this.processSpecifiers(specifiers);
}
RevisionRange.prototype.isExclusion = function(specifier) {
return specifier.startsWith('^');
};
RevisionRange.prototype.processSpecifiers = function(specifiers) {
var self = this;
var inclusions = [];
var exclusions = [];
specifiers.forEach(function(specifier) {
if(self.isExclusion(specifier)) {
exclusions.push(specifier.slice(1));
} else {
inclusions.push(specifier);
}
});
exclusions.forEach(function(exclusion) {
self.addExcluded(Graph.getUpstreamSet(self.engine, exclusion));
});
inclusions.forEach(function(inclusion) {
self.addIncluded(Graph.getUpstreamSet(self.engine, inclusion));
});
var includedKeys = Array.from(Object.keys(self.included));
self.revisions = includedKeys.map(function(revision) {
return self.engine.resolveStringRef(revision);
});
self.revisions.sort(self.engine.dateSortFunc);
self.revisions.reverse();
};
RevisionRange.prototype.isExcluded = function(revision) {
return this.excluded.hasOwnProperty(revision);
};
RevisionRange.prototype.addExcluded = function(setToExclude) {
var self = this;
Object.keys(setToExclude).forEach(function(toExclude) {
if(!self.isExcluded(toExclude)) {
self.excluded[toExclude] = true;
}
});
};
RevisionRange.prototype.addIncluded = function(setToInclude) {
var self = this;
Object.keys(setToInclude).forEach(function(toInclude) {
if(!self.isExcluded(toInclude)) {
self.included[toInclude] = true;
}
});
};
RevisionRange.prototype.formatRevisions = function(revisionFormatter) {
var output = "";
this.revisions.forEach(function(c) {
output += revisionFormatter(c);
});
return output;
};
exports.GitEngine = GitEngine; exports.GitEngine = GitEngine;
exports.Commit = Commit; exports.Commit = Commit;
exports.Branch = Branch; exports.Branch = Branch;

View file

@ -126,18 +126,14 @@ var Command = Backbone.Model.extend({
oneArgImpliedHead: function(args, option) { oneArgImpliedHead: function(args, option) {
this.validateArgBounds(args, 0, 1, option); this.validateArgBounds(args, 0, 1, option);
// and if it's one, add a HEAD to the back // and if it's one, add a HEAD to the back
if (args.length === 0) { this.impliedHead(args, 0);
args.push('HEAD');
}
}, },
twoArgsImpliedHead: function(args, option) { twoArgsImpliedHead: function(args, option) {
// our args we expect to be between 1 and 2 // our args we expect to be between 1 and 2
this.validateArgBounds(args, 1, 2, option); this.validateArgBounds(args, 1, 2, option);
// and if it's one, add a HEAD to the back // and if it's one, add a HEAD to the back
if (args.length == 1) { this.impliedHead(args, 1);
args.push('HEAD');
}
}, },
oneArgImpliedOrigin: function(args) { oneArgImpliedOrigin: function(args) {
@ -151,6 +147,12 @@ var Command = Backbone.Model.extend({
this.validateArgBounds(args, 0, 2); this.validateArgBounds(args, 0, 2);
}, },
impliedHead: function(args, min) {
if(args.length == min) {
args.push('HEAD');
}
},
// this is a little utility class to help arg validation that happens over and over again // this is a little utility class to help arg validation that happens over and over again
validateArgBounds: function(args, lower, upper, option) { validateArgBounds: function(args, lower, upper, option) {
var what = (option === undefined) ? var what = (option === undefined) ?