#931 Reformatting code for gitGraph

This commit is contained in:
knsv
2019-09-12 12:56:54 -07:00
parent 0890ba0fdd
commit a2e3f3d900
3 changed files with 510 additions and 455 deletions

View File

@@ -1,69 +1,71 @@
import _ from 'lodash' import _ from 'lodash';
import { logger } from '../../logger' import { logger } from '../../logger';
let commits = {} let commits = {};
let head = null let head = null;
let branches = { 'master': head } let branches = { master: head };
let curBranch = 'master' let curBranch = 'master';
let direction = 'LR' let direction = 'LR';
let seq = 0 let seq = 0;
function getRandomInt(min, max) { function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min return Math.floor(Math.random() * (max - min)) + min;
} }
function getId() { function getId() {
const pool = '0123456789abcdef' const pool = '0123456789abcdef';
let id = '' let id = '';
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)] id += pool[getRandomInt(0, 16)];
} }
return id return id;
} }
function isfastforwardable(currentCommit, otherCommit) { function isfastforwardable(currentCommit, otherCommit) {
logger.debug('Entering isfastforwardable:', currentCommit.id, otherCommit.id) logger.debug('Entering isfastforwardable:', currentCommit.id, otherCommit.id);
while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit) { while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit) {
// only if other branch has more commits // only if other branch has more commits
if (otherCommit.parent == null) break if (otherCommit.parent == null) break;
if (Array.isArray(otherCommit.parent)) { if (Array.isArray(otherCommit.parent)) {
logger.debug('In merge commit:', otherCommit.parent) logger.debug('In merge commit:', otherCommit.parent);
return isfastforwardable(currentCommit, commits[otherCommit.parent[0]]) || return (
isfastforwardable(currentCommit, commits[otherCommit.parent[0]]) ||
isfastforwardable(currentCommit, commits[otherCommit.parent[1]]) isfastforwardable(currentCommit, commits[otherCommit.parent[1]])
);
} else { } else {
otherCommit = commits[otherCommit.parent] otherCommit = commits[otherCommit.parent];
} }
} }
logger.debug(currentCommit.id, otherCommit.id) logger.debug(currentCommit.id, otherCommit.id);
return currentCommit.id === otherCommit.id return currentCommit.id === otherCommit.id;
} }
function isReachableFrom(currentCommit, otherCommit) { function isReachableFrom(currentCommit, otherCommit) {
const currentSeq = currentCommit.seq const currentSeq = currentCommit.seq;
const otherSeq = otherCommit.seq const otherSeq = otherCommit.seq;
if (currentSeq > otherSeq) return isfastforwardable(otherCommit, currentCommit) if (currentSeq > otherSeq) return isfastforwardable(otherCommit, currentCommit);
return false return false;
} }
export const setDirection = function(dir) { export const setDirection = function(dir) {
direction = dir direction = dir;
} };
let options = {} let options = {};
export const setOptions = function(rawOptString) { export const setOptions = function(rawOptString) {
logger.debug('options str', rawOptString) logger.debug('options str', rawOptString);
rawOptString = rawOptString && rawOptString.trim() rawOptString = rawOptString && rawOptString.trim();
rawOptString = rawOptString || '{}' rawOptString = rawOptString || '{}';
try { try {
options = JSON.parse(rawOptString) options = JSON.parse(rawOptString);
} catch (e) { } catch (e) {
logger.error('error while parsing gitGraph options', e.message) logger.error('error while parsing gitGraph options', e.message);
}
} }
};
export const getOptions = function() { export const getOptions = function() {
return options return options;
} };
export const commit = function(msg) { export const commit = function(msg) {
const commit = { const commit = {
@@ -71,28 +73,28 @@ export const commit = function (msg) {
message: msg, message: msg,
seq: seq++, seq: seq++,
parent: head == null ? null : head.id parent: head == null ? null : head.id
} };
head = commit head = commit;
commits[commit.id] = commit commits[commit.id] = commit;
branches[curBranch] = commit.id branches[curBranch] = commit.id;
logger.debug('in pushCommit ' + commit.id) logger.debug('in pushCommit ' + commit.id);
} };
export const branch = function(name) { export const branch = function(name) {
branches[name] = head != null ? head.id : null branches[name] = head != null ? head.id : null;
logger.debug('in createBranch') logger.debug('in createBranch');
} };
export const merge = function(otherBranch) { export const merge = function(otherBranch) {
const currentCommit = commits[branches[curBranch]] const currentCommit = commits[branches[curBranch]];
const otherCommit = commits[branches[otherBranch]] const otherCommit = commits[branches[otherBranch]];
if (isReachableFrom(currentCommit, otherCommit)) { if (isReachableFrom(currentCommit, otherCommit)) {
logger.debug('Already merged') logger.debug('Already merged');
return return;
} }
if (isfastforwardable(currentCommit, otherCommit)) { if (isfastforwardable(currentCommit, otherCommit)) {
branches[curBranch] = branches[otherBranch] branches[curBranch] = branches[otherBranch];
head = commits[branches[curBranch]] head = commits[branches[curBranch]];
} else { } else {
// create merge commit // create merge commit
const commit = { const commit = {
@@ -100,113 +102,125 @@ export const merge = function (otherBranch) {
message: 'merged branch ' + otherBranch + ' into ' + curBranch, message: 'merged branch ' + otherBranch + ' into ' + curBranch,
seq: seq++, seq: seq++,
parent: [head == null ? null : head.id, branches[otherBranch]] parent: [head == null ? null : head.id, branches[otherBranch]]
};
head = commit;
commits[commit.id] = commit;
branches[curBranch] = commit.id;
} }
head = commit logger.debug(branches);
commits[commit.id] = commit logger.debug('in mergeBranch');
branches[curBranch] = commit.id };
}
logger.debug(branches)
logger.debug('in mergeBranch')
}
export const checkout = function(branch) { export const checkout = function(branch) {
logger.debug('in checkout') logger.debug('in checkout');
curBranch = branch curBranch = branch;
const id = branches[curBranch] const id = branches[curBranch];
head = commits[id] head = commits[id];
} };
export const reset = function(commitRef) { export const reset = function(commitRef) {
logger.debug('in reset', commitRef) logger.debug('in reset', commitRef);
const ref = commitRef.split(':')[0] const ref = commitRef.split(':')[0];
let parentCount = parseInt(commitRef.split(':')[1]) let parentCount = parseInt(commitRef.split(':')[1]);
let commit = ref === 'HEAD' ? head : commits[branches[ref]] let commit = ref === 'HEAD' ? head : commits[branches[ref]];
logger.debug(commit, parentCount) logger.debug(commit, parentCount);
while (parentCount > 0) { while (parentCount > 0) {
commit = commits[commit.parent] commit = commits[commit.parent];
parentCount-- parentCount--;
if (!commit) { if (!commit) {
const err = 'Critical error - unique parent commit not found during reset' const err = 'Critical error - unique parent commit not found during reset';
logger.error(err) logger.error(err);
throw err throw err;
} }
} }
head = commit head = commit;
branches[curBranch] = commit.id branches[curBranch] = commit.id;
} };
function upsert(arr, key, newval) { function upsert(arr, key, newval) {
const index = arr.indexOf(key) const index = arr.indexOf(key);
if (index === -1) { if (index === -1) {
arr.push(newval) arr.push(newval);
} else { } else {
arr.splice(index, 1, newval) arr.splice(index, 1, newval);
} }
} }
function prettyPrintCommitHistory(commitArr) { function prettyPrintCommitHistory(commitArr) {
const commit = _.maxBy(commitArr, 'seq') const commit = _.maxBy(commitArr, 'seq');
let line = '' let line = '';
commitArr.forEach(function(c) { commitArr.forEach(function(c) {
if (c === commit) { if (c === commit) {
line += '\t*' line += '\t*';
} else { } else {
line += '\t|' line += '\t|';
} }
}) });
const label = [line, commit.id, commit.seq] const label = [line, commit.id, commit.seq];
for (let branch in branches) { for (let branch in branches) {
if (branches[branch] === commit.id) label.push(branch) if (branches[branch] === commit.id) label.push(branch);
} }
logger.debug(label.join(' ')) logger.debug(label.join(' '));
if (Array.isArray(commit.parent)) { if (Array.isArray(commit.parent)) {
const newCommit = commits[commit.parent[0]] const newCommit = commits[commit.parent[0]];
upsert(commitArr, commit, newCommit) upsert(commitArr, commit, newCommit);
commitArr.push(commits[commit.parent[1]]) commitArr.push(commits[commit.parent[1]]);
} else if (commit.parent == null) { } else if (commit.parent == null) {
return return;
} else { } else {
const nextCommit = commits[commit.parent] const nextCommit = commits[commit.parent];
upsert(commitArr, commit, nextCommit) upsert(commitArr, commit, nextCommit);
} }
commitArr = _.uniqBy(commitArr, 'id') commitArr = _.uniqBy(commitArr, 'id');
prettyPrintCommitHistory(commitArr) prettyPrintCommitHistory(commitArr);
} }
export const prettyPrint = function() { export const prettyPrint = function() {
logger.debug(commits) logger.debug(commits);
const node = getCommitsArray()[0] const node = getCommitsArray()[0];
prettyPrintCommitHistory([node]) prettyPrintCommitHistory([node]);
} };
export const clear = function() { export const clear = function() {
commits = {} commits = {};
head = null head = null;
branches = { 'master': head } branches = { master: head };
curBranch = 'master' curBranch = 'master';
seq = 0 seq = 0;
} };
export const getBranchesAsObjArray = function() { export const getBranchesAsObjArray = function() {
const branchArr = [] const branchArr = [];
for (let branch in branches) { for (let branch in branches) {
branchArr.push({ name: branch, commit: commits[branches[branch]] }) branchArr.push({ name: branch, commit: commits[branches[branch]] });
}
return branchArr
} }
return branchArr;
};
export const getBranches = function () { return branches } export const getBranches = function() {
export const getCommits = function () { return commits } return branches;
};
export const getCommits = function() {
return commits;
};
export const getCommitsArray = function() { export const getCommitsArray = function() {
const commitArr = Object.keys(commits).map(function(key) { const commitArr = Object.keys(commits).map(function(key) {
return commits[key] return commits[key];
}) });
commitArr.forEach(function (o) { logger.debug(o.id) }) commitArr.forEach(function(o) {
return _.orderBy(commitArr, ['seq'], ['desc']) logger.debug(o.id);
} });
export const getCurrentBranch = function () { return curBranch } return _.orderBy(commitArr, ['seq'], ['desc']);
export const getDirection = function () { return direction } };
export const getHead = function () { return head } export const getCurrentBranch = function() {
return curBranch;
};
export const getDirection = function() {
return direction;
};
export const getHead = function() {
return head;
};
export default { export default {
setDirection, setDirection,
@@ -226,4 +240,4 @@ export default {
getCurrentBranch, getCurrentBranch,
getDirection, getDirection,
getHead getHead
} };

View File

@@ -1,201 +1,186 @@
/* eslint-env jasmine */ /* eslint-env jasmine */
import gitGraphAst from './gitGraphAst' import gitGraphAst from './gitGraphAst';
import { parser } from './parser/gitGraph' import { parser } from './parser/gitGraph';
describe('when parsing a gitGraph', function() { describe('when parsing a gitGraph', function() {
beforeEach(function() { beforeEach(function() {
parser.yy = gitGraphAst parser.yy = gitGraphAst;
parser.yy.clear() parser.yy.clear();
}) });
it('should handle a gitGraph defintion', function() { it('should handle a gitGraph defintion', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'commit\n';
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle a gitGraph defintion with empty options', function() { it('should handle a gitGraph defintion with empty options', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n';
'options\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()).toEqual({}) expect(parser.yy.getOptions()).toEqual({});
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle a gitGraph defintion with valid options', function() { it('should handle a gitGraph defintion with valid options', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
'options\n' +
'{"key": "value"}\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()['key']).toBe('value') expect(parser.yy.getOptions()['key']).toBe('value');
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should not fail on a gitGraph with malformed json', function() { it('should not fail on a gitGraph with malformed json', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
'options\n' +
'{"key": "value"\n' +
'end\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('LR') expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should handle set direction', function() { it('should handle set direction', function() {
const str = 'gitGraph BT:\n' + const str = 'gitGraph BT:\n' + 'commit\n';
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('BT') expect(parser.yy.getDirection()).toBe('BT');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1) expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}) });
it('should checkout a branch', function() { it('should checkout a branch', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
'branch new\n' +
'checkout new\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(0) expect(Object.keys(commits).length).toBe(0);
expect(parser.yy.getCurrentBranch()).toBe('new') expect(parser.yy.getCurrentBranch()).toBe('new');
}) });
it('should add commits to checked out branch', function() { it('should add commits to checked out branch', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
'branch new\n' +
'checkout new\n' +
'commit\n' +
'commit\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(2) expect(Object.keys(commits).length).toBe(2);
expect(parser.yy.getCurrentBranch()).toBe('new') expect(parser.yy.getCurrentBranch()).toBe('new');
const branchCommit = parser.yy.getBranches()['new'] const branchCommit = parser.yy.getBranches()['new'];
expect(branchCommit).not.toBeNull() expect(branchCommit).not.toBeNull();
expect(commits[branchCommit].parent).not.toBeNull() expect(commits[branchCommit].parent).not.toBeNull();
}) });
it('should handle commit with args', function() { it('should handle commit with args', function() {
const str = 'gitGraph:\n' + const str = 'gitGraph:\n' + 'commit "a commit"\n';
'commit "a commit"\n'
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1) expect(Object.keys(commits).length).toBe(1);
const key = Object.keys(commits)[0] const key = Object.keys(commits)[0];
expect(commits[key].message).toBe('a commit') expect(commits[key].message).toBe('a commit');
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
}) });
it('it should reset a branch', function() { it('it should reset a branch', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master\n' 'reset master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('reset can take an argument', function() { it('reset can take an argument', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master^\n' 'reset master^\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
const master = commits[parser.yy.getBranches()['master']] const master = commits[parser.yy.getBranches()['master']];
expect(parser.yy.getHead().id).toEqual(master.parent) expect(parser.yy.getHead().id).toEqual(master.parent);
}) });
it('it should handle fast forwardable merges', function() { it('it should handle fast forwardable merges', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout master\n' +
'merge newbranch\n' 'merge newbranch\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('it should handle cases when merge is a noop', function() { it('it should handle cases when merge is a noop', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'merge master\n' 'merge master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3) expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}) });
it('it should handle merge with 2 parents', function() { it('it should handle merge with 2 parents', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
@@ -203,19 +188,20 @@ describe('when parsing a gitGraph', function () {
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout master\n' +
'commit\n' + 'commit\n' +
'merge newbranch\n' 'merge newbranch\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(5) expect(Object.keys(commits).length).toBe(5);
expect(parser.yy.getCurrentBranch()).toBe('master') expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
}) });
it('it should handle ff merge when history walk has two parents (merge commit)', function() { it('it should handle ff merge when history walk has two parents (merge commit)', function() {
const str = 'gitGraph:\n' + const str =
'gitGraph:\n' +
'commit\n' + 'commit\n' +
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
@@ -226,16 +212,16 @@ describe('when parsing a gitGraph', function () {
'merge newbranch\n' + 'merge newbranch\n' +
'commit\n' + 'commit\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'merge master\n' 'merge master\n';
parser.parse(str) parser.parse(str);
const commits = parser.yy.getCommits() const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(6) expect(Object.keys(commits).length).toBe(6);
expect(parser.yy.getCurrentBranch()).toBe('newbranch') expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']) expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
parser.yy.prettyPrint() parser.yy.prettyPrint();
}) });
}) });

View File

@@ -1,13 +1,13 @@
import * as d3 from 'd3' import * as d3 from 'd3';
import _ from 'lodash' import _ from 'lodash';
import db from './gitGraphAst' import db from './gitGraphAst';
import gitGraphParser from './parser/gitGraph' import gitGraphParser from './parser/gitGraph';
import { logger } from '../../logger' import { logger } from '../../logger';
import { interpolateToCurve } from '../../utils' import { interpolateToCurve } from '../../utils';
let allCommitsDict = {} let allCommitsDict = {};
let branchNum let branchNum;
let config = { let config = {
nodeSpacing: 150, nodeSpacing: 150,
nodeFillColor: 'yellow', nodeFillColor: 'yellow',
@@ -25,11 +25,11 @@ let config = {
x: -25, x: -25,
y: 0 y: 0
} }
} };
let apiConfig = {} let apiConfig = {};
export const setConf = function(c) { export const setConf = function(c) {
apiConfig = c apiConfig = c;
} };
function svgCreateDefs(svg) { function svgCreateDefs(svg) {
svg svg
@@ -39,8 +39,9 @@ function svgCreateDefs (svg) {
.append('circle') .append('circle')
.attr('r', config.nodeRadius) .attr('r', config.nodeRadius)
.attr('cx', 0) .attr('cx', 0)
.attr('cy', 0) .attr('cy', 0);
svg.select('#def-commit') svg
.select('#def-commit')
.append('foreignObject') .append('foreignObject')
.attr('width', config.nodeLabel.width) .attr('width', config.nodeLabel.width)
.attr('height', config.nodeLabel.height) .attr('height', config.nodeLabel.height)
@@ -49,239 +50,293 @@ function svgCreateDefs (svg) {
.attr('class', 'node-label') .attr('class', 'node-label')
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.append('p') .append('p')
.html('') .html('');
} }
function svgDrawLine(svg, points, colorIdx, interpolate) { function svgDrawLine(svg, points, colorIdx, interpolate) {
const curve = interpolateToCurve(interpolate, d3.curveBasis) const curve = interpolateToCurve(interpolate, d3.curveBasis);
const color = config.branchColors[colorIdx % config.branchColors.length] const color = config.branchColors[colorIdx % config.branchColors.length];
const lineGen = d3.line() const lineGen = d3
.line()
.x(function(d) { .x(function(d) {
return Math.round(d.x) return Math.round(d.x);
}) })
.y(function(d) { .y(function(d) {
return Math.round(d.y) return Math.round(d.y);
}) })
.curve(curve) .curve(curve);
svg svg
.append('svg:path') .append('svg:path')
.attr('d', lineGen(points)) .attr('d', lineGen(points))
.style('stroke', color) .style('stroke', color)
.style('stroke-width', config.lineStrokeWidth) .style('stroke-width', config.lineStrokeWidth)
.style('fill', 'none') .style('fill', 'none');
} }
// Pass in the element and its pre-transform coords // Pass in the element and its pre-transform coords
function getElementCoords(element, coords) { function getElementCoords(element, coords) {
coords = coords || element.node().getBBox() coords = coords || element.node().getBBox();
const ctm = element.node().getCTM() const ctm = element.node().getCTM();
const xn = ctm.e + coords.x * ctm.a const xn = ctm.e + coords.x * ctm.a;
const yn = ctm.f + coords.y * ctm.d const yn = ctm.f + coords.y * ctm.d;
return { return {
left: xn, left: xn,
top: yn, top: yn,
width: coords.width, width: coords.width,
height: coords.height height: coords.height
} };
} }
function svgDrawLineForCommits(svg, fromId, toId, direction, color) { function svgDrawLineForCommits(svg, fromId, toId, direction, color) {
logger.debug('svgDrawLineForCommits: ', fromId, toId) logger.debug('svgDrawLineForCommits: ', fromId, toId);
const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle')) const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle'));
const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle')) const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle'));
switch (direction) { switch (direction) {
case 'LR': case 'LR':
// (toBbox) // (toBbox)
// +-------- // +--------
// + (fromBbox) // + (fromBbox)
if (fromBbox.left - toBbox.left > config.nodeSpacing) { if (fromBbox.left - toBbox.left > config.nodeSpacing) {
const lineStart = { x: fromBbox.left - config.nodeSpacing, y: toBbox.top + toBbox.height / 2 } const lineStart = {
const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 } x: fromBbox.left - config.nodeSpacing,
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear') y: toBbox.top + toBbox.height / 2
svgDrawLine(svg, [ };
const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 };
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear');
svgDrawLine(
svg,
[
{ x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 }, { x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 },
{ x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 }, { x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 },
{ x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y }, { x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y },
lineStart], color) lineStart
],
color
);
} else { } else {
svgDrawLine(svg, [{ svgDrawLine(
'x': fromBbox.left, svg,
'y': fromBbox.top + fromBbox.height / 2 [
}, { {
'x': fromBbox.left - config.nodeSpacing / 2, x: fromBbox.left,
'y': fromBbox.top + fromBbox.height / 2 y: fromBbox.top + fromBbox.height / 2
}, { },
'x': fromBbox.left - config.nodeSpacing / 2, {
'y': toBbox.top + toBbox.height / 2 x: fromBbox.left - config.nodeSpacing / 2,
}, { y: fromBbox.top + fromBbox.height / 2
'x': toBbox.left + toBbox.width, },
'y': toBbox.top + toBbox.height / 2 {
}], color) x: fromBbox.left - config.nodeSpacing / 2,
y: toBbox.top + toBbox.height / 2
},
{
x: toBbox.left + toBbox.width,
y: toBbox.top + toBbox.height / 2
} }
break ],
color
);
}
break;
case 'BT': case 'BT':
// + (fromBbox) // + (fromBbox)
// | // |
// | // |
// + (toBbox) // + (toBbox)
if (toBbox.top - fromBbox.top > config.nodeSpacing) { if (toBbox.top - fromBbox.top > config.nodeSpacing) {
const lineStart = { x: toBbox.left + toBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing } const lineStart = {
const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top } x: toBbox.left + toBbox.width / 2,
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear') y: fromBbox.top + fromBbox.height + config.nodeSpacing
svgDrawLine(svg, [ };
const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top };
svgDrawLine(svg, [lineStart, lineEnd], color, 'linear');
svgDrawLine(
svg,
[
{ x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height }, { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height },
{ x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2 }, {
x: fromBbox.left + fromBbox.width / 2,
y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2
},
{ x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 }, { x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 },
lineStart], color) lineStart
],
color
);
} else { } else {
svgDrawLine(svg, [{ svgDrawLine(
'x': fromBbox.left + fromBbox.width / 2, svg,
'y': fromBbox.top + fromBbox.height [
}, { {
'x': fromBbox.left + fromBbox.width / 2, x: fromBbox.left + fromBbox.width / 2,
'y': fromBbox.top + config.nodeSpacing / 2 y: fromBbox.top + fromBbox.height
}, { },
'x': toBbox.left + toBbox.width / 2, {
'y': toBbox.top - config.nodeSpacing / 2 x: fromBbox.left + fromBbox.width / 2,
}, { y: fromBbox.top + config.nodeSpacing / 2
'x': toBbox.left + toBbox.width / 2, },
'y': toBbox.top {
}], color) x: toBbox.left + toBbox.width / 2,
y: toBbox.top - config.nodeSpacing / 2
},
{
x: toBbox.left + toBbox.width / 2,
y: toBbox.top
} }
break ],
color
);
}
break;
} }
} }
function cloneNode(svg, selector) { function cloneNode(svg, selector) {
return svg.select(selector).node().cloneNode(true) return svg
.select(selector)
.node()
.cloneNode(true);
} }
function renderCommitHistory(svg, commitid, branches, direction) { function renderCommitHistory(svg, commitid, branches, direction) {
let commit let commit;
const numCommits = Object.keys(allCommitsDict).length const numCommits = Object.keys(allCommitsDict).length;
if (typeof commitid === 'string') { if (typeof commitid === 'string') {
do { do {
commit = allCommitsDict[commitid] commit = allCommitsDict[commitid];
logger.debug('in renderCommitHistory', commit.id, commit.seq) logger.debug('in renderCommitHistory', commit.id, commit.seq);
if (svg.select('#node-' + commitid).size() > 0) { if (svg.select('#node-' + commitid).size() > 0) {
return return;
} }
svg svg
.append(function() { .append(function() {
return cloneNode(svg, '#def-commit') return cloneNode(svg, '#def-commit');
}) })
.attr('class', 'commit') .attr('class', 'commit')
.attr('id', function() { .attr('id', function() {
return 'node-' + commit.id return 'node-' + commit.id;
}) })
.attr('transform', function() { .attr('transform', function() {
switch (direction) { switch (direction) {
case 'LR': case 'LR':
return 'translate(' + (commit.seq * config.nodeSpacing + config.leftMargin) + ', ' + return (
(branchNum * config.branchOffset) + ')' 'translate(' +
(commit.seq * config.nodeSpacing + config.leftMargin) +
', ' +
branchNum * config.branchOffset +
')'
);
case 'BT': case 'BT':
return 'translate(' + (branchNum * config.branchOffset + config.leftMargin) + ', ' + return (
((numCommits - commit.seq) * config.nodeSpacing) + ')' 'translate(' +
(branchNum * config.branchOffset + config.leftMargin) +
', ' +
(numCommits - commit.seq) * config.nodeSpacing +
')'
);
} }
}) })
.attr('fill', config.nodeFillColor) .attr('fill', config.nodeFillColor)
.attr('stroke', config.nodeStrokeColor) .attr('stroke', config.nodeStrokeColor)
.attr('stroke-width', config.nodeStrokeWidth) .attr('stroke-width', config.nodeStrokeWidth);
let branch let branch;
for (let branchName in branches) { for (let branchName in branches) {
if (branches[branchName].commit === commit) { if (branches[branchName].commit === commit) {
branch = branches[branchName] branch = branches[branchName];
break break;
} }
} }
if (branch) { if (branch) {
logger.debug('found branch ', branch.name) logger.debug('found branch ', branch.name);
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'branch-label') .attr('class', 'branch-label')
.text(branch.name + ', ') .text(branch.name + ', ');
} }
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'commit-id') .attr('class', 'commit-id')
.text(commit.id) .text(commit.id);
if (commit.message !== '' && direction === 'BT') { if (commit.message !== '' && direction === 'BT') {
svg.select('#node-' + commit.id + ' p') svg
.select('#node-' + commit.id + ' p')
.append('xhtml:span') .append('xhtml:span')
.attr('class', 'commit-msg') .attr('class', 'commit-msg')
.text(', ' + commit.message) .text(', ' + commit.message);
} }
commitid = commit.parent commitid = commit.parent;
} while (commitid && allCommitsDict[commitid]) } while (commitid && allCommitsDict[commitid]);
} }
if (Array.isArray(commitid)) { if (Array.isArray(commitid)) {
logger.debug('found merge commmit', commitid) logger.debug('found merge commmit', commitid);
renderCommitHistory(svg, commitid[0], branches, direction) renderCommitHistory(svg, commitid[0], branches, direction);
branchNum++ branchNum++;
renderCommitHistory(svg, commitid[1], branches, direction) renderCommitHistory(svg, commitid[1], branches, direction);
branchNum-- branchNum--;
} }
} }
function renderLines(svg, commit, direction, branchColor) { function renderLines(svg, commit, direction, branchColor) {
branchColor = branchColor || 0 branchColor = branchColor || 0;
while (commit.seq > 0 && !commit.lineDrawn) { while (commit.seq > 0 && !commit.lineDrawn) {
if (typeof commit.parent === 'string') { if (typeof commit.parent === 'string') {
svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor) svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor);
commit.lineDrawn = true commit.lineDrawn = true;
commit = allCommitsDict[commit.parent] commit = allCommitsDict[commit.parent];
} else if (Array.isArray(commit.parent)) { } else if (Array.isArray(commit.parent)) {
svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor) svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor);
svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1) svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1);
renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1) renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1);
commit.lineDrawn = true commit.lineDrawn = true;
commit = allCommitsDict[commit.parent[0]] commit = allCommitsDict[commit.parent[0]];
} }
} }
} }
export const draw = function(txt, id, ver) { export const draw = function(txt, id, ver) {
try { try {
const parser = gitGraphParser.parser const parser = gitGraphParser.parser;
parser.yy = db parser.yy = db;
logger.debug('in gitgraph renderer', txt, id, ver) logger.debug('in gitgraph renderer', txt, id, ver);
// Parse the graph definition // Parse the graph definition
parser.parse(txt + '\n') parser.parse(txt + '\n');
config = _.assign(config, apiConfig, db.getOptions()) config = _.assign(config, apiConfig, db.getOptions());
logger.debug('effective options', config) logger.debug('effective options', config);
const direction = db.getDirection() const direction = db.getDirection();
allCommitsDict = db.getCommits() allCommitsDict = db.getCommits();
const branches = db.getBranchesAsObjArray() const branches = db.getBranchesAsObjArray();
if (direction === 'BT') { if (direction === 'BT') {
config.nodeLabel.x = branches.length * config.branchOffset config.nodeLabel.x = branches.length * config.branchOffset;
config.nodeLabel.width = '100%' config.nodeLabel.width = '100%';
config.nodeLabel.y = -1 * 2 * config.nodeRadius config.nodeLabel.y = -1 * 2 * config.nodeRadius;
} }
const svg = d3.select(`[id="${id}"]`) const svg = d3.select(`[id="${id}"]`);
svgCreateDefs(svg) svgCreateDefs(svg);
branchNum = 1 branchNum = 1;
for (let branch in branches) { for (let branch in branches) {
const v = branches[branch] const v = branches[branch];
renderCommitHistory(svg, v.commit.id, branches, direction) renderCommitHistory(svg, v.commit.id, branches, direction);
renderLines(svg, v.commit, direction) renderLines(svg, v.commit, direction);
branchNum++ branchNum++;
} }
svg.attr('height', function() { svg.attr('height', function() {
if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing;
return (branches.length + 1) * config.branchOffset return (branches.length + 1) * config.branchOffset;
}) });
} catch (e) { } catch (e) {
logger.error('Error while rendering gitgraph') logger.error('Error while rendering gitgraph');
logger.error(e.message) logger.error(e.message);
}
} }
};
export default { export default {
setConf, setConf,
draw draw
} };