mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-31 22:26:53 +02:00
Merge pull request #5664 from Austin-Fulbright/feature/4401_creating_langium_parser_gitGraph
Feature/4401 creating langium parser git graph
This commit is contained in:
6
.changeset/dirty-mails-watch.md
Normal file
6
.changeset/dirty-mails-watch.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@mermaid-js/parser': minor
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: Migrate git graph to langium, use typescript for internals
|
1322
packages/mermaid/src/diagrams/git/gitGraph.spec.ts
Normal file
1322
packages/mermaid/src/diagrams/git/gitGraph.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,535 +0,0 @@
|
|||||||
import { log } from '../../logger.js';
|
|
||||||
import { random } from '../../utils.js';
|
|
||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
|
||||||
import common from '../common/common.js';
|
|
||||||
import {
|
|
||||||
setAccTitle,
|
|
||||||
getAccTitle,
|
|
||||||
getAccDescription,
|
|
||||||
setAccDescription,
|
|
||||||
clear as commonClear,
|
|
||||||
setDiagramTitle,
|
|
||||||
getDiagramTitle,
|
|
||||||
} from '../common/commonDb.js';
|
|
||||||
|
|
||||||
let { mainBranchName, mainBranchOrder } = getConfig().gitGraph;
|
|
||||||
let commits = new Map();
|
|
||||||
let head = null;
|
|
||||||
let branchesConfig = new Map();
|
|
||||||
branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder });
|
|
||||||
let branches = new Map();
|
|
||||||
branches.set(mainBranchName, head);
|
|
||||||
let curBranch = mainBranchName;
|
|
||||||
let direction = 'LR';
|
|
||||||
let seq = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function getId() {
|
|
||||||
return random({ length: 7 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @param currentCommit
|
|
||||||
// * @param otherCommit
|
|
||||||
// */
|
|
||||||
|
|
||||||
// function isFastForwardable(currentCommit, otherCommit) {
|
|
||||||
// log.debug('Entering isFastForwardable:', currentCommit.id, otherCommit.id);
|
|
||||||
// let cnt = 0;
|
|
||||||
// while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit && cnt < 1000) {
|
|
||||||
// cnt++;
|
|
||||||
// // only if other branch has more commits
|
|
||||||
// if (otherCommit.parent == null) break;
|
|
||||||
// if (Array.isArray(otherCommit.parent)) {
|
|
||||||
// log.debug('In merge commit:', otherCommit.parent);
|
|
||||||
// return (
|
|
||||||
// isFastForwardable(currentCommit, commits.get(otherCommit.parent[0])) ||
|
|
||||||
// isFastForwardable(currentCommit, commits.get(otherCommit.parent[1]))
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// otherCommit = commits.get(otherCommit.parent);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// log.debug(currentCommit.id, otherCommit.id);
|
|
||||||
// return currentCommit.id === otherCommit.id;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param currentCommit
|
|
||||||
* @param otherCommit
|
|
||||||
*/
|
|
||||||
// function isReachableFrom(currentCommit, otherCommit) {
|
|
||||||
// const currentSeq = currentCommit.seq;
|
|
||||||
// const otherSeq = otherCommit.seq;
|
|
||||||
// if (currentSeq > otherSeq) return isFastForwardable(otherCommit, currentCommit);
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list
|
|
||||||
* @param fn
|
|
||||||
*/
|
|
||||||
function uniqBy(list, fn) {
|
|
||||||
const recordMap = Object.create(null);
|
|
||||||
return list.reduce((out, item) => {
|
|
||||||
const key = fn(item);
|
|
||||||
if (!recordMap[key]) {
|
|
||||||
recordMap[key] = true;
|
|
||||||
out.push(item);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setDirection = function (dir) {
|
|
||||||
direction = dir;
|
|
||||||
};
|
|
||||||
let options = {};
|
|
||||||
export const setOptions = function (rawOptString) {
|
|
||||||
log.debug('options str', rawOptString);
|
|
||||||
rawOptString = rawOptString?.trim();
|
|
||||||
rawOptString = rawOptString || '{}';
|
|
||||||
try {
|
|
||||||
options = JSON.parse(rawOptString);
|
|
||||||
} catch (e) {
|
|
||||||
log.error('error while parsing gitGraph options', e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOptions = function () {
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commit = function (msg, id, type, tags) {
|
|
||||||
log.debug('Entering commit:', msg, id, type, tags);
|
|
||||||
const config = getConfig();
|
|
||||||
id = common.sanitizeText(id, config);
|
|
||||||
msg = common.sanitizeText(msg, config);
|
|
||||||
tags = tags?.map((tag) => common.sanitizeText(tag, config));
|
|
||||||
const commit = {
|
|
||||||
id: id ? id : seq + '-' + getId(),
|
|
||||||
message: msg,
|
|
||||||
seq: seq++,
|
|
||||||
type: type ? type : commitType.NORMAL,
|
|
||||||
tags: tags ?? [],
|
|
||||||
parents: head == null ? [] : [head.id],
|
|
||||||
branch: curBranch,
|
|
||||||
};
|
|
||||||
head = commit;
|
|
||||||
commits.set(commit.id, commit);
|
|
||||||
branches.set(curBranch, commit.id);
|
|
||||||
log.debug('in pushCommit ' + commit.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const branch = function (name, order) {
|
|
||||||
name = common.sanitizeText(name, getConfig());
|
|
||||||
if (!branches.has(name)) {
|
|
||||||
branches.set(name, head != null ? head.id : null);
|
|
||||||
branchesConfig.set(name, { name, order: order ? parseInt(order, 10) : null });
|
|
||||||
checkout(name);
|
|
||||||
log.debug('in createBranch');
|
|
||||||
} else {
|
|
||||||
let error = new Error(
|
|
||||||
'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ' +
|
|
||||||
name +
|
|
||||||
'")'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'branch ' + name,
|
|
||||||
token: 'branch ' + name,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['"checkout ' + name + '"'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const merge = function (otherBranch, custom_id, override_type, custom_tags) {
|
|
||||||
const config = getConfig();
|
|
||||||
otherBranch = common.sanitizeText(otherBranch, config);
|
|
||||||
custom_id = common.sanitizeText(custom_id, config);
|
|
||||||
|
|
||||||
const currentCommit = commits.get(branches.get(curBranch));
|
|
||||||
const otherCommit = commits.get(branches.get(otherBranch));
|
|
||||||
if (curBranch === otherBranch) {
|
|
||||||
let error = new Error('Incorrect usage of "merge". Cannot merge a branch to itself');
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch,
|
|
||||||
token: 'merge ' + otherBranch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['branch abc'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else if (currentCommit === undefined || !currentCommit) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch,
|
|
||||||
token: 'merge ' + otherBranch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['commit'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else if (!branches.has(otherBranch)) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch,
|
|
||||||
token: 'merge ' + otherBranch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['branch ' + otherBranch],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else if (otherCommit === undefined || !otherCommit) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch,
|
|
||||||
token: 'merge ' + otherBranch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['"commit"'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else if (currentCommit === otherCommit) {
|
|
||||||
let error = new Error('Incorrect usage of "merge". Both branches have same head');
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch,
|
|
||||||
token: 'merge ' + otherBranch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['branch abc'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else if (custom_id && commits.has(custom_id)) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "merge". Commit with id:' +
|
|
||||||
custom_id +
|
|
||||||
' already exists, use different custom Id'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','),
|
|
||||||
token: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','),
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: [
|
|
||||||
`merge ${otherBranch} ${custom_id}_UNIQUE ${override_type} ${custom_tags?.join(',')}`,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
// if (isReachableFrom(currentCommit, otherCommit)) {
|
|
||||||
// log.debug('Already merged');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (isFastForwardable(currentCommit, otherCommit)) {
|
|
||||||
// branches.set(curBranch, branches.get(otherBranch));
|
|
||||||
// head = commits.get(branches.get(curBranch));
|
|
||||||
// } else {
|
|
||||||
// create merge commit
|
|
||||||
const commit = {
|
|
||||||
id: custom_id ? custom_id : seq + '-' + getId(),
|
|
||||||
message: 'merged branch ' + otherBranch + ' into ' + curBranch,
|
|
||||||
seq: seq++,
|
|
||||||
parents: [head == null ? null : head.id, branches.get(otherBranch)],
|
|
||||||
branch: curBranch,
|
|
||||||
type: commitType.MERGE,
|
|
||||||
customType: override_type,
|
|
||||||
customId: custom_id ? true : false,
|
|
||||||
tags: custom_tags ? custom_tags : [],
|
|
||||||
};
|
|
||||||
head = commit;
|
|
||||||
commits.set(commit.id, commit);
|
|
||||||
branches.set(curBranch, commit.id);
|
|
||||||
// }
|
|
||||||
log.debug(branches);
|
|
||||||
log.debug('in mergeBranch');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
|
|
||||||
log.debug('Entering cherryPick:', sourceId, targetId, tags);
|
|
||||||
const config = getConfig();
|
|
||||||
sourceId = common.sanitizeText(sourceId, config);
|
|
||||||
targetId = common.sanitizeText(targetId, config);
|
|
||||||
tags = tags?.map((tag) => common.sanitizeText(tag, config));
|
|
||||||
parentCommitId = common.sanitizeText(parentCommitId, config);
|
|
||||||
|
|
||||||
if (!sourceId || !commits.has(sourceId)) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "cherryPick". Source commit id should exist and provided'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
token: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['cherry-pick abc'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
let sourceCommit = commits.get(sourceId);
|
|
||||||
let sourceCommitBranch = sourceCommit.branch;
|
|
||||||
if (
|
|
||||||
parentCommitId &&
|
|
||||||
!(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId))
|
|
||||||
) {
|
|
||||||
let error = new Error(
|
|
||||||
'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.'
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (sourceCommit.type === commitType.MERGE && !parentCommitId) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.'
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (!targetId || !commits.has(targetId)) {
|
|
||||||
// cherry-pick source commit to current branch
|
|
||||||
|
|
||||||
if (sourceCommitBranch === curBranch) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "cherryPick". Source commit is already on current branch'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
token: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['cherry-pick abc'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const currentCommit = commits.get(branches.get(curBranch));
|
|
||||||
if (currentCommit === undefined || !currentCommit) {
|
|
||||||
let error = new Error(
|
|
||||||
'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
token: 'cherryPick ' + sourceId + ' ' + targetId,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['cherry-pick abc'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const commit = {
|
|
||||||
id: seq + '-' + getId(),
|
|
||||||
message: 'cherry-picked ' + sourceCommit + ' into ' + curBranch,
|
|
||||||
seq: seq++,
|
|
||||||
parents: [head == null ? null : head.id, sourceCommit.id],
|
|
||||||
branch: curBranch,
|
|
||||||
type: commitType.CHERRY_PICK,
|
|
||||||
tags: tags
|
|
||||||
? tags.filter(Boolean)
|
|
||||||
: [
|
|
||||||
`cherry-pick:${sourceCommit.id}${
|
|
||||||
sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : ''
|
|
||||||
}`,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
head = commit;
|
|
||||||
commits.set(commit.id, commit);
|
|
||||||
branches.set(curBranch, commit.id);
|
|
||||||
log.debug(branches);
|
|
||||||
log.debug('in cherryPick');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const checkout = function (branch) {
|
|
||||||
branch = common.sanitizeText(branch, getConfig());
|
|
||||||
if (!branches.has(branch)) {
|
|
||||||
let error = new Error(
|
|
||||||
'Trying to checkout branch which is not yet created. (Help try using "branch ' + branch + '")'
|
|
||||||
);
|
|
||||||
error.hash = {
|
|
||||||
text: 'checkout ' + branch,
|
|
||||||
token: 'checkout ' + branch,
|
|
||||||
line: '1',
|
|
||||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
|
||||||
expected: ['"branch ' + branch + '"'],
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
curBranch = branch;
|
|
||||||
const id = branches.get(curBranch);
|
|
||||||
head = commits.get(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// export const reset = function (commitRef) {
|
|
||||||
// log.debug('in reset', commitRef);
|
|
||||||
// const ref = commitRef.split(':')[0];
|
|
||||||
// let parentCount = parseInt(commitRef.split(':')[1]);
|
|
||||||
// let commit = ref === 'HEAD' ? head : commits.get(branches.get(ref));
|
|
||||||
// log.debug(commit, parentCount);
|
|
||||||
// while (parentCount > 0) {
|
|
||||||
// commit = commits.get(commit.parent);
|
|
||||||
// parentCount--;
|
|
||||||
// if (!commit) {
|
|
||||||
// const err = 'Critical error - unique parent commit not found during reset';
|
|
||||||
// log.error(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// head = commit;
|
|
||||||
// branches[curBranch] = commit.id;
|
|
||||||
// };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param arr
|
|
||||||
* @param key
|
|
||||||
* @param newVal
|
|
||||||
*/
|
|
||||||
function upsert(arr, key, newVal) {
|
|
||||||
const index = arr.indexOf(key);
|
|
||||||
if (index === -1) {
|
|
||||||
arr.push(newVal);
|
|
||||||
} else {
|
|
||||||
arr.splice(index, 1, newVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param commitArr */
|
|
||||||
function prettyPrintCommitHistory(commitArr) {
|
|
||||||
const commit = commitArr.reduce((out, commit) => {
|
|
||||||
if (out.seq > commit.seq) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
return commit;
|
|
||||||
}, commitArr[0]);
|
|
||||||
let line = '';
|
|
||||||
commitArr.forEach(function (c) {
|
|
||||||
if (c === commit) {
|
|
||||||
line += '\t*';
|
|
||||||
} else {
|
|
||||||
line += '\t|';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const label = [line, commit.id, commit.seq];
|
|
||||||
for (let branch in branches) {
|
|
||||||
if (branches.get(branch) === commit.id) {
|
|
||||||
label.push(branch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.debug(label.join(' '));
|
|
||||||
if (commit.parents && commit.parents.length == 2) {
|
|
||||||
const newCommit = commits.get(commit.parents[0]);
|
|
||||||
upsert(commitArr, commit, newCommit);
|
|
||||||
commitArr.push(commits.get(commit.parents[1]));
|
|
||||||
} else if (commit.parents.length == 0) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const nextCommit = commits.get(commit.parents);
|
|
||||||
upsert(commitArr, commit, nextCommit);
|
|
||||||
}
|
|
||||||
commitArr = uniqBy(commitArr, (c) => c.id);
|
|
||||||
prettyPrintCommitHistory(commitArr);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prettyPrint = function () {
|
|
||||||
log.debug(commits);
|
|
||||||
const node = getCommitsArray()[0];
|
|
||||||
prettyPrintCommitHistory([node]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clear = function () {
|
|
||||||
commits = new Map();
|
|
||||||
head = null;
|
|
||||||
const { mainBranchName, mainBranchOrder } = getConfig().gitGraph;
|
|
||||||
branches = new Map();
|
|
||||||
branches.set(mainBranchName, null);
|
|
||||||
branchesConfig = new Map();
|
|
||||||
branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder });
|
|
||||||
curBranch = mainBranchName;
|
|
||||||
seq = 0;
|
|
||||||
commonClear();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBranchesAsObjArray = function () {
|
|
||||||
const branchesArray = [...branchesConfig.values()]
|
|
||||||
.map((branchConfig, i) => {
|
|
||||||
if (branchConfig.order !== null) {
|
|
||||||
return branchConfig;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...branchConfig,
|
|
||||||
order: parseFloat(`0.${i}`, 10),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.order - b.order)
|
|
||||||
.map(({ name }) => ({ name }));
|
|
||||||
|
|
||||||
return branchesArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBranches = function () {
|
|
||||||
return branches;
|
|
||||||
};
|
|
||||||
export const getCommits = function () {
|
|
||||||
return commits;
|
|
||||||
};
|
|
||||||
export const getCommitsArray = function () {
|
|
||||||
const commitArr = [...commits.values()];
|
|
||||||
commitArr.forEach(function (o) {
|
|
||||||
log.debug(o.id);
|
|
||||||
});
|
|
||||||
commitArr.sort((a, b) => a.seq - b.seq);
|
|
||||||
return commitArr;
|
|
||||||
};
|
|
||||||
export const getCurrentBranch = function () {
|
|
||||||
return curBranch;
|
|
||||||
};
|
|
||||||
export const getDirection = function () {
|
|
||||||
return direction;
|
|
||||||
};
|
|
||||||
export const getHead = function () {
|
|
||||||
return head;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commitType = {
|
|
||||||
NORMAL: 0,
|
|
||||||
REVERSE: 1,
|
|
||||||
HIGHLIGHT: 2,
|
|
||||||
MERGE: 3,
|
|
||||||
CHERRY_PICK: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getConfig: () => getConfig().gitGraph,
|
|
||||||
setDirection,
|
|
||||||
setOptions,
|
|
||||||
getOptions,
|
|
||||||
commit,
|
|
||||||
branch,
|
|
||||||
merge,
|
|
||||||
cherryPick,
|
|
||||||
checkout,
|
|
||||||
//reset,
|
|
||||||
prettyPrint,
|
|
||||||
clear,
|
|
||||||
getBranchesAsObjArray,
|
|
||||||
getBranches,
|
|
||||||
getCommits,
|
|
||||||
getCommitsArray,
|
|
||||||
getCurrentBranch,
|
|
||||||
getDirection,
|
|
||||||
getHead,
|
|
||||||
setAccTitle,
|
|
||||||
getAccTitle,
|
|
||||||
getAccDescription,
|
|
||||||
setAccDescription,
|
|
||||||
setDiagramTitle,
|
|
||||||
getDiagramTitle,
|
|
||||||
commitType,
|
|
||||||
};
|
|
522
packages/mermaid/src/diagrams/git/gitGraphAst.ts
Normal file
522
packages/mermaid/src/diagrams/git/gitGraphAst.ts
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
import { log } from '../../logger.js';
|
||||||
|
import { cleanAndMerge, random } from '../../utils.js';
|
||||||
|
import { getConfig as commonGetConfig } from '../../config.js';
|
||||||
|
import common from '../common/common.js';
|
||||||
|
import {
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
getAccDescription,
|
||||||
|
setAccDescription,
|
||||||
|
clear as commonClear,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
} from '../common/commonDb.js';
|
||||||
|
import type {
|
||||||
|
DiagramOrientation,
|
||||||
|
Commit,
|
||||||
|
GitGraphDB,
|
||||||
|
CommitDB,
|
||||||
|
MergeDB,
|
||||||
|
BranchDB,
|
||||||
|
CherryPickDB,
|
||||||
|
} from './gitGraphTypes.js';
|
||||||
|
import { commitType } from './gitGraphTypes.js';
|
||||||
|
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||||
|
|
||||||
|
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||||
|
|
||||||
|
import type { GitGraphDiagramConfig } from '../../config.type.js';
|
||||||
|
interface GitGraphState {
|
||||||
|
commits: Map<string, Commit>;
|
||||||
|
head: Commit | null;
|
||||||
|
branchConfig: Map<string, { name: string; order: number | undefined }>;
|
||||||
|
branches: Map<string, string | null>;
|
||||||
|
currBranch: string;
|
||||||
|
direction: DiagramOrientation;
|
||||||
|
seq: number;
|
||||||
|
options: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_GITGRAPH_CONFIG: Required<GitGraphDiagramConfig> = DEFAULT_CONFIG.gitGraph;
|
||||||
|
const getConfig = (): Required<GitGraphDiagramConfig> => {
|
||||||
|
const config = cleanAndMerge({
|
||||||
|
...DEFAULT_GITGRAPH_CONFIG,
|
||||||
|
...commonGetConfig().gitGraph,
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = new ImperativeState<GitGraphState>(() => {
|
||||||
|
const config = getConfig();
|
||||||
|
const mainBranchName = config.mainBranchName;
|
||||||
|
const mainBranchOrder = config.mainBranchOrder;
|
||||||
|
return {
|
||||||
|
mainBranchName,
|
||||||
|
commits: new Map(),
|
||||||
|
head: null,
|
||||||
|
branchConfig: new Map([[mainBranchName, { name: mainBranchName, order: mainBranchOrder }]]),
|
||||||
|
branches: new Map([[mainBranchName, null]]),
|
||||||
|
currBranch: mainBranchName,
|
||||||
|
direction: 'LR',
|
||||||
|
seq: 0,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function getID() {
|
||||||
|
return random({ length: 7 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list - list of items
|
||||||
|
* @param fn - function to get the key
|
||||||
|
*/
|
||||||
|
function uniqBy(list: any[], fn: (item: any) => any) {
|
||||||
|
const recordMap = Object.create(null);
|
||||||
|
return list.reduce((out, item) => {
|
||||||
|
const key = fn(item);
|
||||||
|
if (!recordMap[key]) {
|
||||||
|
recordMap[key] = true;
|
||||||
|
out.push(item);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDirection = function (dir: DiagramOrientation) {
|
||||||
|
state.records.direction = dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setOptions = function (rawOptString: string) {
|
||||||
|
log.debug('options str', rawOptString);
|
||||||
|
rawOptString = rawOptString?.trim();
|
||||||
|
rawOptString = rawOptString || '{}';
|
||||||
|
try {
|
||||||
|
state.records.options = JSON.parse(rawOptString);
|
||||||
|
} catch (e: any) {
|
||||||
|
log.error('error while parsing gitGraph options', e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOptions = function () {
|
||||||
|
return state.records.options;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commit = function (commitDB: CommitDB) {
|
||||||
|
let msg = commitDB.msg;
|
||||||
|
let id = commitDB.id;
|
||||||
|
const type = commitDB.type;
|
||||||
|
let tags = commitDB.tags;
|
||||||
|
|
||||||
|
log.info('commit', msg, id, type, tags);
|
||||||
|
log.debug('Entering commit:', msg, id, type, tags);
|
||||||
|
const config = getConfig();
|
||||||
|
id = common.sanitizeText(id, config);
|
||||||
|
msg = common.sanitizeText(msg, config);
|
||||||
|
tags = tags?.map((tag) => common.sanitizeText(tag, config));
|
||||||
|
const newCommit: Commit = {
|
||||||
|
id: id ? id : state.records.seq + '-' + getID(),
|
||||||
|
message: msg,
|
||||||
|
seq: state.records.seq++,
|
||||||
|
type: type ?? commitType.NORMAL,
|
||||||
|
tags: tags ?? [],
|
||||||
|
parents: state.records.head == null ? [] : [state.records.head.id],
|
||||||
|
branch: state.records.currBranch,
|
||||||
|
};
|
||||||
|
state.records.head = newCommit;
|
||||||
|
log.info('main branch', config.mainBranchName);
|
||||||
|
state.records.commits.set(newCommit.id, newCommit);
|
||||||
|
state.records.branches.set(state.records.currBranch, newCommit.id);
|
||||||
|
log.debug('in pushCommit ' + newCommit.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const branch = function (branchDB: BranchDB) {
|
||||||
|
let name = branchDB.name;
|
||||||
|
const order = branchDB.order;
|
||||||
|
name = common.sanitizeText(name, getConfig());
|
||||||
|
if (state.records.branches.has(name)) {
|
||||||
|
throw new Error(
|
||||||
|
`Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${name}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.records.branches.set(name, state.records.head != null ? state.records.head.id : null);
|
||||||
|
state.records.branchConfig.set(name, { name, order });
|
||||||
|
checkout(name);
|
||||||
|
log.debug('in createBranch');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const merge = (mergeDB: MergeDB): void => {
|
||||||
|
let otherBranch = mergeDB.branch;
|
||||||
|
let customId = mergeDB.id;
|
||||||
|
const overrideType = mergeDB.type;
|
||||||
|
const customTags = mergeDB.tags;
|
||||||
|
const config = getConfig();
|
||||||
|
otherBranch = common.sanitizeText(otherBranch, config);
|
||||||
|
if (customId) {
|
||||||
|
customId = common.sanitizeText(customId, config);
|
||||||
|
}
|
||||||
|
const currentBranchCheck = state.records.branches.get(state.records.currBranch);
|
||||||
|
const otherBranchCheck = state.records.branches.get(otherBranch);
|
||||||
|
const currentCommit = currentBranchCheck
|
||||||
|
? state.records.commits.get(currentBranchCheck)
|
||||||
|
: undefined;
|
||||||
|
const otherCommit: Commit | undefined = otherBranchCheck
|
||||||
|
? state.records.commits.get(otherBranchCheck)
|
||||||
|
: undefined;
|
||||||
|
if (currentCommit && otherCommit && currentCommit.branch === otherBranch) {
|
||||||
|
throw new Error(`Cannot merge branch '${otherBranch}' into itself.`);
|
||||||
|
}
|
||||||
|
if (state.records.currBranch === otherBranch) {
|
||||||
|
const error: any = new Error('Incorrect usage of "merge". Cannot merge a branch to itself');
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch}`,
|
||||||
|
token: `merge ${otherBranch}`,
|
||||||
|
expected: ['branch abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (currentCommit === undefined || !currentCommit) {
|
||||||
|
const error: any = new Error(
|
||||||
|
`Incorrect usage of "merge". Current branch (${state.records.currBranch})has no commits`
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch}`,
|
||||||
|
token: `merge ${otherBranch}`,
|
||||||
|
expected: ['commit'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (!state.records.branches.has(otherBranch)) {
|
||||||
|
const error: any = new Error(
|
||||||
|
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist'
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch}`,
|
||||||
|
token: `merge ${otherBranch}`,
|
||||||
|
expected: [`branch ${otherBranch}`],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (otherCommit === undefined || !otherCommit) {
|
||||||
|
const error: any = new Error(
|
||||||
|
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits'
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch}`,
|
||||||
|
token: `merge ${otherBranch}`,
|
||||||
|
expected: ['"commit"'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (currentCommit === otherCommit) {
|
||||||
|
const error: any = new Error('Incorrect usage of "merge". Both branches have same head');
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch}`,
|
||||||
|
token: `merge ${otherBranch}`,
|
||||||
|
expected: ['branch abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (customId && state.records.commits.has(customId)) {
|
||||||
|
const error: any = new Error(
|
||||||
|
'Incorrect usage of "merge". Commit with id:' +
|
||||||
|
customId +
|
||||||
|
' already exists, use different custom Id'
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`,
|
||||||
|
token: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`,
|
||||||
|
expected: [
|
||||||
|
`merge ${otherBranch} ${customId}_UNIQUE ${overrideType} ${customTags?.join(' ')}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this
|
||||||
|
|
||||||
|
const commit = {
|
||||||
|
id: customId || `${state.records.seq}-${getID()}`,
|
||||||
|
message: `merged branch ${otherBranch} into ${state.records.currBranch}`,
|
||||||
|
seq: state.records.seq++,
|
||||||
|
parents: state.records.head == null ? [] : [state.records.head.id, verifiedBranch],
|
||||||
|
branch: state.records.currBranch,
|
||||||
|
type: commitType.MERGE,
|
||||||
|
customType: overrideType,
|
||||||
|
customId: customId ? true : false,
|
||||||
|
tags: customTags ?? [],
|
||||||
|
} satisfies Commit;
|
||||||
|
state.records.head = commit;
|
||||||
|
state.records.commits.set(commit.id, commit);
|
||||||
|
state.records.branches.set(state.records.currBranch, commit.id);
|
||||||
|
log.debug(state.records.branches);
|
||||||
|
log.debug('in mergeBranch');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cherryPick = function (cherryPickDB: CherryPickDB) {
|
||||||
|
let sourceId = cherryPickDB.id;
|
||||||
|
let targetId = cherryPickDB.targetId;
|
||||||
|
let tags = cherryPickDB.tags;
|
||||||
|
let parentCommitId = cherryPickDB.parent;
|
||||||
|
log.debug('Entering cherryPick:', sourceId, targetId, tags);
|
||||||
|
const config = getConfig();
|
||||||
|
sourceId = common.sanitizeText(sourceId, config);
|
||||||
|
targetId = common.sanitizeText(targetId, config);
|
||||||
|
|
||||||
|
tags = tags?.map((tag) => common.sanitizeText(tag, config));
|
||||||
|
|
||||||
|
parentCommitId = common.sanitizeText(parentCommitId, config);
|
||||||
|
|
||||||
|
if (!sourceId || !state.records.commits.has(sourceId)) {
|
||||||
|
const error: any = new Error(
|
||||||
|
'Incorrect usage of "cherryPick". Source commit id should exist and provided'
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
token: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
expected: ['cherry-pick abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCommit = state.records.commits.get(sourceId);
|
||||||
|
if (sourceCommit === undefined || !sourceCommit) {
|
||||||
|
throw new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentCommitId &&
|
||||||
|
!(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId))
|
||||||
|
) {
|
||||||
|
const error = new Error(
|
||||||
|
'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.'
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const sourceCommitBranch = sourceCommit.branch;
|
||||||
|
if (sourceCommit.type === commitType.MERGE && !parentCommitId) {
|
||||||
|
const error = new Error(
|
||||||
|
'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.'
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (!targetId || !state.records.commits.has(targetId)) {
|
||||||
|
// cherry-pick source commit to current branch
|
||||||
|
|
||||||
|
if (sourceCommitBranch === state.records.currBranch) {
|
||||||
|
const error: any = new Error(
|
||||||
|
'Incorrect usage of "cherryPick". Source commit is already on current branch'
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
token: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
expected: ['cherry-pick abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const currentCommitId = state.records.branches.get(state.records.currBranch);
|
||||||
|
if (currentCommitId === undefined || !currentCommitId) {
|
||||||
|
const error: any = new Error(
|
||||||
|
`Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits`
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
token: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
expected: ['cherry-pick abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCommit = state.records.commits.get(currentCommitId);
|
||||||
|
if (currentCommit === undefined || !currentCommit) {
|
||||||
|
const error: any = new Error(
|
||||||
|
`Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits`
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
token: `cherryPick ${sourceId} ${targetId}`,
|
||||||
|
expected: ['cherry-pick abc'],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const commit = {
|
||||||
|
id: state.records.seq + '-' + getID(),
|
||||||
|
message: `cherry-picked ${sourceCommit?.message} into ${state.records.currBranch}`,
|
||||||
|
seq: state.records.seq++,
|
||||||
|
parents: state.records.head == null ? [] : [state.records.head.id, sourceCommit.id],
|
||||||
|
branch: state.records.currBranch,
|
||||||
|
type: commitType.CHERRY_PICK,
|
||||||
|
tags: tags
|
||||||
|
? tags.filter(Boolean)
|
||||||
|
: [
|
||||||
|
`cherry-pick:${sourceCommit.id}${
|
||||||
|
sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : ''
|
||||||
|
}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
state.records.head = commit;
|
||||||
|
state.records.commits.set(commit.id, commit);
|
||||||
|
state.records.branches.set(state.records.currBranch, commit.id);
|
||||||
|
log.debug(state.records.branches);
|
||||||
|
log.debug('in cherryPick');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const checkout = function (branch: string) {
|
||||||
|
branch = common.sanitizeText(branch, getConfig());
|
||||||
|
if (!state.records.branches.has(branch)) {
|
||||||
|
const error: any = new Error(
|
||||||
|
`Trying to checkout branch which is not yet created. (Help try using "branch ${branch}")`
|
||||||
|
);
|
||||||
|
error.hash = {
|
||||||
|
text: `checkout ${branch}`,
|
||||||
|
token: `checkout ${branch}`,
|
||||||
|
expected: [`branch ${branch}`],
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
state.records.currBranch = branch;
|
||||||
|
const id = state.records.branches.get(state.records.currBranch);
|
||||||
|
if (id === undefined || !id) {
|
||||||
|
state.records.head = null;
|
||||||
|
} else {
|
||||||
|
state.records.head = state.records.commits.get(id) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param arr - array
|
||||||
|
* @param key - key
|
||||||
|
* @param newVal - new value
|
||||||
|
*/
|
||||||
|
function upsert(arr: any[], key: any, newVal: any) {
|
||||||
|
const index = arr.indexOf(key);
|
||||||
|
if (index === -1) {
|
||||||
|
arr.push(newVal);
|
||||||
|
} else {
|
||||||
|
arr.splice(index, 1, newVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyPrintCommitHistory(commitArr: Commit[]) {
|
||||||
|
const commit = commitArr.reduce((out, commit) => {
|
||||||
|
if (out.seq > commit.seq) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return commit;
|
||||||
|
}, commitArr[0]);
|
||||||
|
let line = '';
|
||||||
|
commitArr.forEach(function (c) {
|
||||||
|
if (c === commit) {
|
||||||
|
line += '\t*';
|
||||||
|
} else {
|
||||||
|
line += '\t|';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const label = [line, commit.id, commit.seq];
|
||||||
|
for (const branch in state.records.branches) {
|
||||||
|
if (state.records.branches.get(branch) === commit.id) {
|
||||||
|
label.push(branch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug(label.join(' '));
|
||||||
|
if (commit.parents && commit.parents.length == 2 && commit.parents[0] && commit.parents[1]) {
|
||||||
|
const newCommit = state.records.commits.get(commit.parents[0]);
|
||||||
|
upsert(commitArr, commit, newCommit);
|
||||||
|
if (commit.parents[1]) {
|
||||||
|
commitArr.push(state.records.commits.get(commit.parents[1])!);
|
||||||
|
}
|
||||||
|
} else if (commit.parents.length == 0) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (commit.parents[0]) {
|
||||||
|
const newCommit = state.records.commits.get(commit.parents[0]);
|
||||||
|
upsert(commitArr, commit, newCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commitArr = uniqBy(commitArr, (c) => c.id);
|
||||||
|
prettyPrintCommitHistory(commitArr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prettyPrint = function () {
|
||||||
|
log.debug(state.records.commits);
|
||||||
|
const node = getCommitsArray()[0];
|
||||||
|
prettyPrintCommitHistory([node]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clear = function () {
|
||||||
|
state.reset();
|
||||||
|
commonClear();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBranchesAsObjArray = function () {
|
||||||
|
const branchesArray = [...state.records.branchConfig.values()]
|
||||||
|
.map((branchConfig, i) => {
|
||||||
|
if (branchConfig.order !== null && branchConfig.order !== undefined) {
|
||||||
|
return branchConfig;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...branchConfig,
|
||||||
|
order: parseFloat(`0.${i}`),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
|
.map(({ name }) => ({ name }));
|
||||||
|
|
||||||
|
return branchesArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBranches = function () {
|
||||||
|
return state.records.branches;
|
||||||
|
};
|
||||||
|
export const getCommits = function () {
|
||||||
|
return state.records.commits;
|
||||||
|
};
|
||||||
|
export const getCommitsArray = function () {
|
||||||
|
const commitArr = [...state.records.commits.values()];
|
||||||
|
commitArr.forEach(function (o) {
|
||||||
|
log.debug(o.id);
|
||||||
|
});
|
||||||
|
commitArr.sort((a, b) => a.seq - b.seq);
|
||||||
|
return commitArr;
|
||||||
|
};
|
||||||
|
export const getCurrentBranch = function () {
|
||||||
|
return state.records.currBranch;
|
||||||
|
};
|
||||||
|
export const getDirection = function () {
|
||||||
|
return state.records.direction;
|
||||||
|
};
|
||||||
|
export const getHead = function () {
|
||||||
|
return state.records.head;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const db: GitGraphDB = {
|
||||||
|
commitType,
|
||||||
|
getConfig,
|
||||||
|
setDirection,
|
||||||
|
setOptions,
|
||||||
|
getOptions,
|
||||||
|
commit,
|
||||||
|
branch,
|
||||||
|
merge,
|
||||||
|
cherryPick,
|
||||||
|
checkout,
|
||||||
|
//reset,
|
||||||
|
prettyPrint,
|
||||||
|
clear,
|
||||||
|
getBranchesAsObjArray,
|
||||||
|
getBranches,
|
||||||
|
getCommits,
|
||||||
|
getCommitsArray,
|
||||||
|
getCurrentBranch,
|
||||||
|
getDirection,
|
||||||
|
getHead,
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
getAccDescription,
|
||||||
|
setAccDescription,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
};
|
@@ -1,13 +1,13 @@
|
|||||||
// @ts-ignore: JISON doesn't support types
|
// @ts-ignore: JISON doesn't support types
|
||||||
import gitGraphParser from './parser/gitGraph.jison';
|
import { parser } from './gitGraphParser.js';
|
||||||
import gitGraphDb from './gitGraphAst.js';
|
import { db } from './gitGraphAst.js';
|
||||||
import gitGraphRenderer from './gitGraphRenderer.js';
|
import gitGraphRenderer from './gitGraphRenderer.js';
|
||||||
import gitGraphStyles from './styles.js';
|
import gitGraphStyles from './styles.js';
|
||||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
|
|
||||||
export const diagram: DiagramDefinition = {
|
export const diagram: DiagramDefinition = {
|
||||||
parser: gitGraphParser,
|
parser,
|
||||||
db: gitGraphDb,
|
db,
|
||||||
renderer: gitGraphRenderer,
|
renderer: gitGraphRenderer,
|
||||||
styles: gitGraphStyles,
|
styles: gitGraphStyles,
|
||||||
};
|
};
|
||||||
|
@@ -1,272 +0,0 @@
|
|||||||
import gitGraphAst from './gitGraphAst.js';
|
|
||||||
import { parser } from './parser/gitGraph.jison';
|
|
||||||
|
|
||||||
describe('when parsing a gitGraph', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
parser.yy = gitGraphAst;
|
|
||||||
parser.yy.clear();
|
|
||||||
});
|
|
||||||
it('should handle a gitGraph definition', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('LR');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle a gitGraph definition with empty options', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(parser.yy.getOptions()).toEqual({});
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('LR');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle a gitGraph definition with valid options', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(parser.yy.getOptions().key).toBe('value');
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('LR');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not fail on a gitGraph with malformed json', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('LR');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle set direction top to bottom', function () {
|
|
||||||
const str = 'gitGraph TB:\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('TB');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle set direction bottom to top', function () {
|
|
||||||
const str = 'gitGraph BT:\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getDirection()).toBe('BT');
|
|
||||||
expect(parser.yy.getBranches().size).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should checkout a branch', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(0);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('new');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch a branch', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(0);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('new');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add commits to checked out branch', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(2);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('new');
|
|
||||||
const branchCommit = parser.yy.getBranches().get('new');
|
|
||||||
expect(branchCommit).not.toBeNull();
|
|
||||||
expect(commits.get(branchCommit).parent).not.toBeNull();
|
|
||||||
});
|
|
||||||
it('should handle commit with args', function () {
|
|
||||||
const str = 'gitGraph:\n' + 'commit "a commit"\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
|
|
||||||
expect(commits.size).toBe(1);
|
|
||||||
const key = commits.keys().next().value;
|
|
||||||
expect(commits.get(key).message).toBe('a commit');
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset has been commented out in JISON
|
|
||||||
it.skip('should reset a branch', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'reset main\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(3);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
|
||||||
expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main'));
|
|
||||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('reset can take an argument', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'reset main^\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(3);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
|
||||||
const main = commits.get(parser.yy.getBranches().get('main'));
|
|
||||||
expect(parser.yy.getHead().id).toEqual(main.parent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('should handle fast forwardable merges', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'checkout main\n' +
|
|
||||||
'merge newbranch\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(4);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main'));
|
|
||||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle cases when merge is a noop', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'merge main\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(4);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
|
||||||
expect(parser.yy.getBranches().get('newbranch')).not.toEqual(
|
|
||||||
parser.yy.getBranches().get('main')
|
|
||||||
);
|
|
||||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('newbranch'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle merge with 2 parents', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'checkout main\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'merge newbranch\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(5);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
|
||||||
expect(parser.yy.getBranches().get('newbranch')).not.toEqual(
|
|
||||||
parser.yy.getBranches().get('main')
|
|
||||||
);
|
|
||||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('main'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('should handle ff merge when history walk has two parents (merge commit)', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch newbranch\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'checkout main\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'merge newbranch\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'checkout newbranch\n' +
|
|
||||||
'merge main\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
|
|
||||||
const commits = parser.yy.getCommits();
|
|
||||||
expect(commits.size).toBe(7);
|
|
||||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
|
||||||
expect(parser.yy.getBranches().get('newbranch')).toEqual(parser.yy.getBranches().get('main'));
|
|
||||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches().get('main'));
|
|
||||||
|
|
||||||
parser.yy.prettyPrint();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate an array of known branches', function () {
|
|
||||||
const str =
|
|
||||||
'gitGraph:\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch b1\n' +
|
|
||||||
'checkout b1\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'commit\n' +
|
|
||||||
'branch b2\n';
|
|
||||||
|
|
||||||
parser.parse(str);
|
|
||||||
const branches = gitGraphAst.getBranchesAsObjArray();
|
|
||||||
|
|
||||||
expect(branches).toHaveLength(3);
|
|
||||||
expect(branches[0]).toHaveProperty('name', 'main');
|
|
||||||
expect(branches[1]).toHaveProperty('name', 'b1');
|
|
||||||
expect(branches[2]).toHaveProperty('name', 'b2');
|
|
||||||
});
|
|
||||||
});
|
|
243
packages/mermaid/src/diagrams/git/gitGraphParser.ts
Normal file
243
packages/mermaid/src/diagrams/git/gitGraphParser.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import type { GitGraph } from '@mermaid-js/parser';
|
||||||
|
import { parse } from '@mermaid-js/parser';
|
||||||
|
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||||
|
import { db } from './gitGraphAst.js';
|
||||||
|
import { commitType } from './gitGraphTypes.js';
|
||||||
|
import type {
|
||||||
|
CheckoutAst,
|
||||||
|
CherryPickingAst,
|
||||||
|
MergeAst,
|
||||||
|
CommitAst,
|
||||||
|
BranchAst,
|
||||||
|
GitGraphDBParseProvider,
|
||||||
|
CommitDB,
|
||||||
|
BranchDB,
|
||||||
|
MergeDB,
|
||||||
|
CherryPickDB,
|
||||||
|
} from './gitGraphTypes.js';
|
||||||
|
|
||||||
|
const populate = (ast: GitGraph, db: GitGraphDBParseProvider) => {
|
||||||
|
populateCommonDb(ast, db);
|
||||||
|
// @ts-ignore: this wont exist if the direction is not specified
|
||||||
|
if (ast.dir) {
|
||||||
|
// @ts-ignore: this wont exist if the direction is not specified
|
||||||
|
db.setDirection(ast.dir);
|
||||||
|
}
|
||||||
|
for (const statement of ast.statements) {
|
||||||
|
parseStatement(statement, db);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseStatement = (statement: any, db: GitGraphDBParseProvider) => {
|
||||||
|
const parsers: Record<string, (stmt: any) => void> = {
|
||||||
|
Commit: (stmt) => db.commit(parseCommit(stmt)),
|
||||||
|
Branch: (stmt) => db.branch(parseBranch(stmt)),
|
||||||
|
Merge: (stmt) => db.merge(parseMerge(stmt)),
|
||||||
|
Checkout: (stmt) => db.checkout(parseCheckout(stmt)),
|
||||||
|
CherryPicking: (stmt) => db.cherryPick(parseCherryPicking(stmt)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parser = parsers[statement.$type];
|
||||||
|
if (parser) {
|
||||||
|
parser(statement);
|
||||||
|
} else {
|
||||||
|
log.error(`Unknown statement type: ${statement.$type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCommit = (commit: CommitAst): CommitDB => {
|
||||||
|
const commitDB: CommitDB = {
|
||||||
|
id: commit.id,
|
||||||
|
msg: commit.message ?? '',
|
||||||
|
type: commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL,
|
||||||
|
tags: commit.tags ?? undefined,
|
||||||
|
};
|
||||||
|
return commitDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseBranch = (branch: BranchAst): BranchDB => {
|
||||||
|
const branchDB: BranchDB = {
|
||||||
|
name: branch.name,
|
||||||
|
order: branch.order ?? 0,
|
||||||
|
};
|
||||||
|
return branchDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMerge = (merge: MergeAst): MergeDB => {
|
||||||
|
const mergeDB: MergeDB = {
|
||||||
|
branch: merge.branch,
|
||||||
|
id: merge.id ?? '',
|
||||||
|
type: merge.type !== undefined ? commitType[merge.type] : undefined,
|
||||||
|
tags: merge.tags ?? undefined,
|
||||||
|
};
|
||||||
|
return mergeDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCheckout = (checkout: CheckoutAst): string => {
|
||||||
|
const branch = checkout.branch;
|
||||||
|
return branch;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCherryPicking = (cherryPicking: CherryPickingAst): CherryPickDB => {
|
||||||
|
const cherryPickDB: CherryPickDB = {
|
||||||
|
id: cherryPicking.id,
|
||||||
|
targetId: '',
|
||||||
|
tags: cherryPicking.tags?.length === 0 ? undefined : cherryPicking.tags,
|
||||||
|
parent: cherryPicking.parent,
|
||||||
|
};
|
||||||
|
return cherryPickDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parser: ParserDefinition = {
|
||||||
|
parse: async (input: string): Promise<void> => {
|
||||||
|
const ast: GitGraph = await parse('gitGraph', input);
|
||||||
|
log.debug(ast);
|
||||||
|
populate(ast, db);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (import.meta.vitest) {
|
||||||
|
const { it, expect, describe } = import.meta.vitest;
|
||||||
|
|
||||||
|
const mockDB: GitGraphDBParseProvider = {
|
||||||
|
commitType: commitType,
|
||||||
|
setDirection: vi.fn(),
|
||||||
|
commit: vi.fn(),
|
||||||
|
branch: vi.fn(),
|
||||||
|
merge: vi.fn(),
|
||||||
|
cherryPick: vi.fn(),
|
||||||
|
checkout: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('GitGraph Parser', () => {
|
||||||
|
it('should parse a commit statement', () => {
|
||||||
|
const commit = {
|
||||||
|
$type: 'Commit',
|
||||||
|
id: '1',
|
||||||
|
message: 'test',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 'NORMAL',
|
||||||
|
};
|
||||||
|
parseStatement(commit, mockDB);
|
||||||
|
expect(mockDB.commit).toHaveBeenCalledWith({
|
||||||
|
id: '1',
|
||||||
|
msg: 'test',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should parse a branch statement', () => {
|
||||||
|
const branch = {
|
||||||
|
$type: 'Branch',
|
||||||
|
name: 'newBranch',
|
||||||
|
order: 1,
|
||||||
|
};
|
||||||
|
parseStatement(branch, mockDB);
|
||||||
|
expect(mockDB.branch).toHaveBeenCalledWith({ name: 'newBranch', order: 1 });
|
||||||
|
});
|
||||||
|
it('should parse a checkout statement', () => {
|
||||||
|
const checkout = {
|
||||||
|
$type: 'Checkout',
|
||||||
|
branch: 'newBranch',
|
||||||
|
};
|
||||||
|
parseStatement(checkout, mockDB);
|
||||||
|
expect(mockDB.checkout).toHaveBeenCalledWith('newBranch');
|
||||||
|
});
|
||||||
|
it('should parse a merge statement', () => {
|
||||||
|
const merge = {
|
||||||
|
$type: 'Merge',
|
||||||
|
branch: 'newBranch',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 'NORMAL',
|
||||||
|
};
|
||||||
|
parseStatement(merge, mockDB);
|
||||||
|
expect(mockDB.merge).toHaveBeenCalledWith({
|
||||||
|
branch: 'newBranch',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should parse a cherry picking statement', () => {
|
||||||
|
const cherryPick = {
|
||||||
|
$type: 'CherryPicking',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
parent: '2',
|
||||||
|
};
|
||||||
|
parseStatement(cherryPick, mockDB);
|
||||||
|
expect(mockDB.cherryPick).toHaveBeenCalledWith({
|
||||||
|
id: '1',
|
||||||
|
targetId: '',
|
||||||
|
parent: '2',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a langium generated gitGraph ast', () => {
|
||||||
|
const dummy: GitGraph = {
|
||||||
|
$type: 'GitGraph',
|
||||||
|
statements: [],
|
||||||
|
};
|
||||||
|
const gitGraphAst: GitGraph = {
|
||||||
|
$type: 'GitGraph',
|
||||||
|
statements: [
|
||||||
|
{
|
||||||
|
$container: dummy,
|
||||||
|
$type: 'Commit',
|
||||||
|
id: '1',
|
||||||
|
message: 'test',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 'NORMAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$container: dummy,
|
||||||
|
$type: 'Branch',
|
||||||
|
name: 'newBranch',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$container: dummy,
|
||||||
|
$type: 'Merge',
|
||||||
|
branch: 'newBranch',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 'NORMAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$container: dummy,
|
||||||
|
$type: 'Checkout',
|
||||||
|
branch: 'newBranch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$container: dummy,
|
||||||
|
$type: 'CherryPicking',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
parent: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
populate(gitGraphAst, mockDB);
|
||||||
|
|
||||||
|
expect(mockDB.commit).toHaveBeenCalledWith({
|
||||||
|
id: '1',
|
||||||
|
msg: 'test',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
expect(mockDB.branch).toHaveBeenCalledWith({ name: 'newBranch', order: 1 });
|
||||||
|
expect(mockDB.merge).toHaveBeenCalledWith({
|
||||||
|
branch: 'newBranch',
|
||||||
|
id: '1',
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
expect(mockDB.checkout).toHaveBeenCalledWith('newBranch');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,893 +0,0 @@
|
|||||||
import { select } from 'd3';
|
|
||||||
import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI.js';
|
|
||||||
import { log } from '../../logger.js';
|
|
||||||
import utils from '../../utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Map<string, { id: string, message: string, seq: number, type: number, tag: string, parents: string[], branch: string }>} CommitMap
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {CommitMap} */
|
|
||||||
let allCommitsDict = new Map();
|
|
||||||
|
|
||||||
const commitType = {
|
|
||||||
NORMAL: 0,
|
|
||||||
REVERSE: 1,
|
|
||||||
HIGHLIGHT: 2,
|
|
||||||
MERGE: 3,
|
|
||||||
CHERRY_PICK: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const THEME_COLOR_LIMIT = 8;
|
|
||||||
|
|
||||||
let branchPos = {};
|
|
||||||
let commitPos = {};
|
|
||||||
let lanes = [];
|
|
||||||
let maxPos = 0;
|
|
||||||
let dir = 'LR';
|
|
||||||
let defaultPos = 30;
|
|
||||||
const clear = () => {
|
|
||||||
branchPos = new Map();
|
|
||||||
commitPos = new Map();
|
|
||||||
allCommitsDict = new Map();
|
|
||||||
maxPos = 0;
|
|
||||||
lanes = [];
|
|
||||||
dir = 'LR';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a text, used for labels of the branches
|
|
||||||
*
|
|
||||||
* @param {string} txt The text
|
|
||||||
* @returns {SVGElement}
|
|
||||||
*/
|
|
||||||
const drawText = (txt) => {
|
|
||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
||||||
let rows = [];
|
|
||||||
|
|
||||||
// Handling of new lines in the label
|
|
||||||
if (typeof txt === 'string') {
|
|
||||||
rows = txt.split(/\\n|\n|<br\s*\/?>/gi);
|
|
||||||
} else if (Array.isArray(txt)) {
|
|
||||||
rows = txt;
|
|
||||||
} else {
|
|
||||||
rows = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
||||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
||||||
tspan.setAttribute('dy', '1em');
|
|
||||||
tspan.setAttribute('x', '0');
|
|
||||||
tspan.setAttribute('class', 'row');
|
|
||||||
tspan.textContent = row.trim();
|
|
||||||
svgLabel.appendChild(tspan);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param svg
|
|
||||||
* @param selector
|
|
||||||
*/
|
|
||||||
return svgLabel;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for the closest parent from the parents list passed as argument.
|
|
||||||
* The parents list comes from an individual commit. The closest parent is actually
|
|
||||||
* the one farther down the graph, since that means it is closer to its child.
|
|
||||||
*
|
|
||||||
* @param {string[]} parents
|
|
||||||
* @returns {string | undefined}
|
|
||||||
*/
|
|
||||||
const findClosestParent = (parents) => {
|
|
||||||
let closestParent = '';
|
|
||||||
let maxPosition = 0;
|
|
||||||
|
|
||||||
parents.forEach((parent) => {
|
|
||||||
const parentPosition =
|
|
||||||
dir === 'TB' || dir === 'BT' ? commitPos.get(parent).y : commitPos.get(parent).x;
|
|
||||||
if (parentPosition >= maxPosition) {
|
|
||||||
closestParent = parent;
|
|
||||||
maxPosition = parentPosition;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return closestParent || undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for the closest parent from the parents list passed as argument for Bottom-to-Top orientation.
|
|
||||||
* The parents list comes from an individual commit. The closest parent is actually
|
|
||||||
* the one farther down the graph, since that means it is closer to its child.
|
|
||||||
*
|
|
||||||
* @param {string[]} parents
|
|
||||||
* @returns {string | undefined}
|
|
||||||
*/
|
|
||||||
const findClosestParentBT = (parents) => {
|
|
||||||
let closestParent = '';
|
|
||||||
let maxPosition = Infinity;
|
|
||||||
|
|
||||||
parents.forEach((parent) => {
|
|
||||||
const parentPosition = commitPos.get(parent).y;
|
|
||||||
if (parentPosition <= maxPosition) {
|
|
||||||
closestParent = parent;
|
|
||||||
maxPosition = parentPosition;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return closestParent || undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the position of the commit elements when the orientation is set to BT-Parallel.
|
|
||||||
* This is needed to render the chart in Bottom-to-Top mode while keeping the parallel
|
|
||||||
* commits in the correct position. First, it finds the correct position of the root commit
|
|
||||||
* using the findClosestParent method. Then, it uses the findClosestParentBT to set the position
|
|
||||||
* of the remaining commits.
|
|
||||||
*
|
|
||||||
* @param {any} sortedKeys
|
|
||||||
* @param {CommitMap} commits
|
|
||||||
* @param {any} defaultPos
|
|
||||||
* @param {any} commitStep
|
|
||||||
* @param {any} layoutOffset
|
|
||||||
*/
|
|
||||||
const setParallelBTPos = (sortedKeys, commits, defaultPos, commitStep, layoutOffset) => {
|
|
||||||
let curPos = defaultPos;
|
|
||||||
let maxPosition = defaultPos;
|
|
||||||
let roots = [];
|
|
||||||
sortedKeys.forEach((key) => {
|
|
||||||
const commit = commits.get(key);
|
|
||||||
if (commit.parents.length) {
|
|
||||||
const closestParent = findClosestParent(commit.parents);
|
|
||||||
curPos = commitPos.get(closestParent).y + commitStep;
|
|
||||||
if (curPos >= maxPosition) {
|
|
||||||
maxPosition = curPos;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
roots.push(commit);
|
|
||||||
}
|
|
||||||
const x = branchPos.get(commit.branch).pos;
|
|
||||||
const y = curPos + layoutOffset;
|
|
||||||
commitPos.set(commit.id, { x: x, y: y });
|
|
||||||
});
|
|
||||||
curPos = maxPosition;
|
|
||||||
roots.forEach((commit) => {
|
|
||||||
const posWithOffset = curPos + defaultPos;
|
|
||||||
const y = posWithOffset;
|
|
||||||
const x = branchPos.get(commit.branch).pos;
|
|
||||||
commitPos.set(commit.id, { x: x, y: y });
|
|
||||||
});
|
|
||||||
sortedKeys.forEach((key) => {
|
|
||||||
const commit = commits.get(key);
|
|
||||||
if (commit.parents.length) {
|
|
||||||
const closestParent = findClosestParentBT(commit.parents);
|
|
||||||
curPos = commitPos.get(closestParent).y - commitStep;
|
|
||||||
if (curPos <= maxPosition) {
|
|
||||||
maxPosition = curPos;
|
|
||||||
}
|
|
||||||
const x = branchPos.get(commit.branch).pos;
|
|
||||||
const y = curPos - layoutOffset;
|
|
||||||
commitPos.set(commit.id, { x: x, y: y });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the commits with its symbol and labels. The function has two modes, one which only
|
|
||||||
* calculates the positions and one that does the actual drawing. This for a simple way getting the
|
|
||||||
* vertical layering correct in the graph.
|
|
||||||
*
|
|
||||||
* @param {any} svg
|
|
||||||
* @param {CommitMap} commits
|
|
||||||
* @param {any} modifyGraph
|
|
||||||
*/
|
|
||||||
const drawCommits = (svg, commits, modifyGraph) => {
|
|
||||||
const gitGraphConfig = getConfig().gitGraph;
|
|
||||||
const gBullets = svg.append('g').attr('class', 'commit-bullets');
|
|
||||||
const gLabels = svg.append('g').attr('class', 'commit-labels');
|
|
||||||
let pos = 0;
|
|
||||||
|
|
||||||
if (dir === 'TB' || dir === 'BT') {
|
|
||||||
pos = defaultPos;
|
|
||||||
}
|
|
||||||
const keys = [...commits.keys()];
|
|
||||||
const isParallelCommits = gitGraphConfig.parallelCommits;
|
|
||||||
const layoutOffset = 10;
|
|
||||||
const commitStep = 40;
|
|
||||||
let sortedKeys =
|
|
||||||
dir !== 'BT' || (dir === 'BT' && isParallelCommits)
|
|
||||||
? keys.sort((a, b) => {
|
|
||||||
return commits.get(a).seq - commits.get(b).seq;
|
|
||||||
})
|
|
||||||
: keys
|
|
||||||
.sort((a, b) => {
|
|
||||||
return commits.get(a).seq - commits.get(b).seq;
|
|
||||||
})
|
|
||||||
.reverse();
|
|
||||||
|
|
||||||
if (dir === 'BT' && isParallelCommits) {
|
|
||||||
setParallelBTPos(sortedKeys, commits, pos, commitStep, layoutOffset);
|
|
||||||
sortedKeys = sortedKeys.reverse();
|
|
||||||
}
|
|
||||||
sortedKeys.forEach((key) => {
|
|
||||||
const commit = commits.get(key);
|
|
||||||
if (isParallelCommits) {
|
|
||||||
if (commit.parents.length) {
|
|
||||||
const closestParent =
|
|
||||||
dir === 'BT' ? findClosestParentBT(commit.parents) : findClosestParent(commit.parents);
|
|
||||||
if (dir === 'TB') {
|
|
||||||
pos = commitPos.get(closestParent).y + commitStep;
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
pos = commitPos.get(key).y - commitStep;
|
|
||||||
} else {
|
|
||||||
pos = commitPos.get(closestParent).x + commitStep;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (dir === 'TB') {
|
|
||||||
pos = defaultPos;
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
pos = commitPos.get(key).y - commitStep;
|
|
||||||
} else {
|
|
||||||
pos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const posWithOffset = dir === 'BT' && isParallelCommits ? pos : pos + layoutOffset;
|
|
||||||
const y = dir === 'TB' || dir === 'BT' ? posWithOffset : branchPos.get(commit.branch).pos;
|
|
||||||
const x = dir === 'TB' || dir === 'BT' ? branchPos.get(commit.branch).pos : posWithOffset;
|
|
||||||
|
|
||||||
// Don't draw the commits now but calculate the positioning which is used by the branch lines etc.
|
|
||||||
if (modifyGraph) {
|
|
||||||
let typeClass;
|
|
||||||
let commitSymbolType =
|
|
||||||
commit.customType !== undefined && commit.customType !== ''
|
|
||||||
? commit.customType
|
|
||||||
: commit.type;
|
|
||||||
switch (commitSymbolType) {
|
|
||||||
case commitType.NORMAL:
|
|
||||||
typeClass = 'commit-normal';
|
|
||||||
break;
|
|
||||||
case commitType.REVERSE:
|
|
||||||
typeClass = 'commit-reverse';
|
|
||||||
break;
|
|
||||||
case commitType.HIGHLIGHT:
|
|
||||||
typeClass = 'commit-highlight';
|
|
||||||
break;
|
|
||||||
case commitType.MERGE:
|
|
||||||
typeClass = 'commit-merge';
|
|
||||||
break;
|
|
||||||
case commitType.CHERRY_PICK:
|
|
||||||
typeClass = 'commit-cherry-pick';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
typeClass = 'commit-normal';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commitSymbolType === commitType.HIGHLIGHT) {
|
|
||||||
const circle = gBullets.append('rect');
|
|
||||||
circle.attr('x', x - 10);
|
|
||||||
circle.attr('y', y - 10);
|
|
||||||
circle.attr('height', 20);
|
|
||||||
circle.attr('width', 20);
|
|
||||||
circle.attr(
|
|
||||||
'class',
|
|
||||||
`commit ${commit.id} commit-highlight${
|
|
||||||
branchPos.get(commit.branch).index % THEME_COLOR_LIMIT
|
|
||||||
} ${typeClass}-outer`
|
|
||||||
);
|
|
||||||
gBullets
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', x - 6)
|
|
||||||
.attr('y', y - 6)
|
|
||||||
.attr('height', 12)
|
|
||||||
.attr('width', 12)
|
|
||||||
.attr(
|
|
||||||
'class',
|
|
||||||
`commit ${commit.id} commit${
|
|
||||||
branchPos.get(commit.branch).index % THEME_COLOR_LIMIT
|
|
||||||
} ${typeClass}-inner`
|
|
||||||
);
|
|
||||||
} else if (commitSymbolType === commitType.CHERRY_PICK) {
|
|
||||||
gBullets
|
|
||||||
.append('circle')
|
|
||||||
.attr('cx', x)
|
|
||||||
.attr('cy', y)
|
|
||||||
.attr('r', 10)
|
|
||||||
.attr('class', `commit ${commit.id} ${typeClass}`);
|
|
||||||
gBullets
|
|
||||||
.append('circle')
|
|
||||||
.attr('cx', x - 3)
|
|
||||||
.attr('cy', y + 2)
|
|
||||||
.attr('r', 2.75)
|
|
||||||
.attr('fill', '#fff')
|
|
||||||
.attr('class', `commit ${commit.id} ${typeClass}`);
|
|
||||||
gBullets
|
|
||||||
.append('circle')
|
|
||||||
.attr('cx', x + 3)
|
|
||||||
.attr('cy', y + 2)
|
|
||||||
.attr('r', 2.75)
|
|
||||||
.attr('fill', '#fff')
|
|
||||||
.attr('class', `commit ${commit.id} ${typeClass}`);
|
|
||||||
gBullets
|
|
||||||
.append('line')
|
|
||||||
.attr('x1', x + 3)
|
|
||||||
.attr('y1', y + 1)
|
|
||||||
.attr('x2', x)
|
|
||||||
.attr('y2', y - 5)
|
|
||||||
.attr('stroke', '#fff')
|
|
||||||
.attr('class', `commit ${commit.id} ${typeClass}`);
|
|
||||||
gBullets
|
|
||||||
.append('line')
|
|
||||||
.attr('x1', x - 3)
|
|
||||||
.attr('y1', y + 1)
|
|
||||||
.attr('x2', x)
|
|
||||||
.attr('y2', y - 5)
|
|
||||||
.attr('stroke', '#fff')
|
|
||||||
.attr('class', `commit ${commit.id} ${typeClass}`);
|
|
||||||
} else {
|
|
||||||
const circle = gBullets.append('circle');
|
|
||||||
circle.attr('cx', x);
|
|
||||||
circle.attr('cy', y);
|
|
||||||
circle.attr('r', commit.type === commitType.MERGE ? 9 : 10);
|
|
||||||
circle.attr(
|
|
||||||
'class',
|
|
||||||
`commit ${commit.id} commit${branchPos.get(commit.branch).index % THEME_COLOR_LIMIT}`
|
|
||||||
);
|
|
||||||
if (commitSymbolType === commitType.MERGE) {
|
|
||||||
const circle2 = gBullets.append('circle');
|
|
||||||
circle2.attr('cx', x);
|
|
||||||
circle2.attr('cy', y);
|
|
||||||
circle2.attr('r', 6);
|
|
||||||
circle2.attr(
|
|
||||||
'class',
|
|
||||||
`commit ${typeClass} ${commit.id} commit${
|
|
||||||
branchPos.get(commit.branch).index % THEME_COLOR_LIMIT
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (commitSymbolType === commitType.REVERSE) {
|
|
||||||
const cross = gBullets.append('path');
|
|
||||||
cross
|
|
||||||
.attr('d', `M ${x - 5},${y - 5}L${x + 5},${y + 5}M${x - 5},${y + 5}L${x + 5},${y - 5}`)
|
|
||||||
.attr(
|
|
||||||
'class',
|
|
||||||
`commit ${typeClass} ${commit.id} commit${
|
|
||||||
branchPos.get(commit.branch).index % THEME_COLOR_LIMIT
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dir === 'TB' || dir === 'BT') {
|
|
||||||
commitPos.set(commit.id, { x: x, y: posWithOffset });
|
|
||||||
} else {
|
|
||||||
commitPos.set(commit.id, { x: posWithOffset, y: y });
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first iteration over the commits are for positioning purposes, this
|
|
||||||
// is required for drawing the lines. The circles and labels is drawn after the labels
|
|
||||||
// placing them on top of the lines.
|
|
||||||
if (modifyGraph) {
|
|
||||||
const px = 4;
|
|
||||||
const py = 2;
|
|
||||||
// Draw the commit label
|
|
||||||
if (
|
|
||||||
commit.type !== commitType.CHERRY_PICK &&
|
|
||||||
((commit.customId && commit.type === commitType.MERGE) ||
|
|
||||||
commit.type !== commitType.MERGE) &&
|
|
||||||
gitGraphConfig.showCommitLabel
|
|
||||||
) {
|
|
||||||
const wrapper = gLabels.append('g');
|
|
||||||
const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg');
|
|
||||||
|
|
||||||
const text = wrapper
|
|
||||||
.append('text')
|
|
||||||
.attr('x', pos)
|
|
||||||
.attr('y', y + 25)
|
|
||||||
.attr('class', 'commit-label')
|
|
||||||
.text(commit.id);
|
|
||||||
let bbox = text.node().getBBox();
|
|
||||||
|
|
||||||
// Now we have the label, lets position the background
|
|
||||||
labelBkg
|
|
||||||
.attr('x', posWithOffset - bbox.width / 2 - py)
|
|
||||||
.attr('y', y + 13.5)
|
|
||||||
.attr('width', bbox.width + 2 * py)
|
|
||||||
.attr('height', bbox.height + 2 * py);
|
|
||||||
|
|
||||||
if (dir === 'TB' || dir === 'BT') {
|
|
||||||
labelBkg.attr('x', x - (bbox.width + 4 * px + 5)).attr('y', y - 12);
|
|
||||||
text.attr('x', x - (bbox.width + 4 * px)).attr('y', y + bbox.height - 12);
|
|
||||||
} else {
|
|
||||||
text.attr('x', posWithOffset - bbox.width / 2);
|
|
||||||
}
|
|
||||||
if (gitGraphConfig.rotateCommitLabel) {
|
|
||||||
if (dir === 'TB' || dir === 'BT') {
|
|
||||||
text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
|
||||||
labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
|
||||||
} else {
|
|
||||||
let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5;
|
|
||||||
let r_y = 10 + (bbox.width / 25) * 8.5;
|
|
||||||
wrapper.attr(
|
|
||||||
'transform',
|
|
||||||
'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (commit.tags.length > 0) {
|
|
||||||
let yOffset = 0;
|
|
||||||
let maxTagBboxWidth = 0;
|
|
||||||
let maxTagBboxHeight = 0;
|
|
||||||
const tagElements = [];
|
|
||||||
|
|
||||||
for (const tagValue of commit.tags.reverse()) {
|
|
||||||
const rect = gLabels.insert('polygon');
|
|
||||||
const hole = gLabels.append('circle');
|
|
||||||
const tag = gLabels
|
|
||||||
.append('text')
|
|
||||||
// Note that we are delaying setting the x position until we know the width of the text
|
|
||||||
.attr('y', y - 16 - yOffset)
|
|
||||||
.attr('class', 'tag-label')
|
|
||||||
.text(tagValue);
|
|
||||||
let tagBbox = tag.node().getBBox();
|
|
||||||
maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width);
|
|
||||||
maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height);
|
|
||||||
|
|
||||||
// We don't use the max over here to center the text within the tags
|
|
||||||
tag.attr('x', posWithOffset - tagBbox.width / 2);
|
|
||||||
|
|
||||||
tagElements.push({
|
|
||||||
tag,
|
|
||||||
hole,
|
|
||||||
rect,
|
|
||||||
yOffset,
|
|
||||||
});
|
|
||||||
|
|
||||||
yOffset += 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { tag, hole, rect, yOffset } of tagElements) {
|
|
||||||
const h2 = maxTagBboxHeight / 2;
|
|
||||||
const ly = y - 19.2 - yOffset;
|
|
||||||
rect.attr('class', 'tag-label-bkg').attr(
|
|
||||||
'points',
|
|
||||||
`
|
|
||||||
${pos - maxTagBboxWidth / 2 - px / 2},${ly + py}
|
|
||||||
${pos - maxTagBboxWidth / 2 - px / 2},${ly - py}
|
|
||||||
${posWithOffset - maxTagBboxWidth / 2 - px},${ly - h2 - py}
|
|
||||||
${posWithOffset + maxTagBboxWidth / 2 + px},${ly - h2 - py}
|
|
||||||
${posWithOffset + maxTagBboxWidth / 2 + px},${ly + h2 + py}
|
|
||||||
${posWithOffset - maxTagBboxWidth / 2 - px},${ly + h2 + py}`
|
|
||||||
);
|
|
||||||
|
|
||||||
hole
|
|
||||||
.attr('cy', ly)
|
|
||||||
.attr('cx', pos - maxTagBboxWidth / 2 + px / 2)
|
|
||||||
.attr('r', 1.5)
|
|
||||||
.attr('class', 'tag-hole');
|
|
||||||
|
|
||||||
if (dir === 'TB' || dir === 'BT') {
|
|
||||||
const yOrigin = pos + yOffset;
|
|
||||||
|
|
||||||
rect
|
|
||||||
.attr('class', 'tag-label-bkg')
|
|
||||||
.attr(
|
|
||||||
'points',
|
|
||||||
`
|
|
||||||
${x},${yOrigin + py}
|
|
||||||
${x},${yOrigin - py}
|
|
||||||
${x + layoutOffset},${yOrigin - h2 - py}
|
|
||||||
${x + layoutOffset + maxTagBboxWidth + px},${yOrigin - h2 - py}
|
|
||||||
${x + layoutOffset + maxTagBboxWidth + px},${yOrigin + h2 + py}
|
|
||||||
${x + layoutOffset},${yOrigin + h2 + py}`
|
|
||||||
)
|
|
||||||
.attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')');
|
|
||||||
hole
|
|
||||||
.attr('cx', x + px / 2)
|
|
||||||
.attr('cy', yOrigin)
|
|
||||||
.attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')');
|
|
||||||
tag
|
|
||||||
.attr('x', x + 5)
|
|
||||||
.attr('y', yOrigin + 3)
|
|
||||||
.attr('transform', 'translate(14,14) rotate(45, ' + x + ',' + pos + ')');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos = dir === 'BT' && isParallelCommits ? pos + commitStep : pos + commitStep + layoutOffset;
|
|
||||||
if (pos > maxPos) {
|
|
||||||
maxPos = pos;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if there are commits
|
|
||||||
* between commitA's x-position
|
|
||||||
* and commitB's x-position on the
|
|
||||||
* same branch as commitA, where
|
|
||||||
* commitA isn't main
|
|
||||||
*
|
|
||||||
* @param {any} commitA
|
|
||||||
* @param {any} commitB
|
|
||||||
* @param p1
|
|
||||||
* @param p2
|
|
||||||
* @param {CommitMap} allCommits
|
|
||||||
* @returns {boolean}
|
|
||||||
* If there are commits between
|
|
||||||
* commitA's x-position
|
|
||||||
* and commitB's x-position
|
|
||||||
* on the source branch, where
|
|
||||||
* source branch is not main
|
|
||||||
* return true
|
|
||||||
*/
|
|
||||||
const shouldRerouteArrow = (commitA, commitB, p1, p2, allCommits) => {
|
|
||||||
const commitBIsFurthest = dir === 'TB' || dir === 'BT' ? p1.x < p2.x : p1.y < p2.y;
|
|
||||||
const branchToGetCurve = commitBIsFurthest ? commitB.branch : commitA.branch;
|
|
||||||
const isOnBranchToGetCurve = (x) => x.branch === branchToGetCurve;
|
|
||||||
const isBetweenCommits = (x) => x.seq > commitA.seq && x.seq < commitB.seq;
|
|
||||||
return [...allCommits.values()].some((commitX) => {
|
|
||||||
return isBetweenCommits(commitX) && isOnBranchToGetCurve(commitX);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function find a lane in the y-axis that is not overlapping with any other lanes. This is
|
|
||||||
* used for drawing the lines between commits.
|
|
||||||
*
|
|
||||||
* @param {any} y1
|
|
||||||
* @param {any} y2
|
|
||||||
* @param {any} depth
|
|
||||||
* @returns {number} Y value between y1 and y2
|
|
||||||
*/
|
|
||||||
const findLane = (y1, y2, depth = 0) => {
|
|
||||||
const candidate = y1 + Math.abs(y1 - y2) / 2;
|
|
||||||
if (depth > 5) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ok = lanes.every((lane) => Math.abs(lane - candidate) >= 10);
|
|
||||||
if (ok) {
|
|
||||||
lanes.push(candidate);
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
const diff = Math.abs(y1 - y2);
|
|
||||||
return findLane(y1, y2 - diff / 5, depth + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw the lines between the commits. They were arrows initially.
|
|
||||||
*
|
|
||||||
* @param {any} svg
|
|
||||||
* @param {any} commitA
|
|
||||||
* @param {any} commitB
|
|
||||||
* @param {CommitMap} allCommits
|
|
||||||
*/
|
|
||||||
const drawArrow = (svg, commitA, commitB, allCommits) => {
|
|
||||||
const p1 = commitPos.get(commitA.id); // arrowStart
|
|
||||||
const p2 = commitPos.get(commitB.id); // arrowEnd
|
|
||||||
const arrowNeedsRerouting = shouldRerouteArrow(commitA, commitB, p1, p2, allCommits);
|
|
||||||
// log.debug('drawArrow', p1, p2, arrowNeedsRerouting, commitA.id, commitB.id);
|
|
||||||
|
|
||||||
// Lower-right quadrant logic; top-left is 0,0
|
|
||||||
|
|
||||||
let arc = '';
|
|
||||||
let arc2 = '';
|
|
||||||
let radius = 0;
|
|
||||||
let offset = 0;
|
|
||||||
let colorClassNum = branchPos.get(commitB.branch).index;
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
colorClassNum = branchPos.get(commitA.branch).index;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lineDef;
|
|
||||||
if (arrowNeedsRerouting) {
|
|
||||||
arc = 'A 10 10, 0, 0, 0,';
|
|
||||||
arc2 = 'A 10 10, 0, 0, 1,';
|
|
||||||
radius = 10;
|
|
||||||
offset = 10;
|
|
||||||
|
|
||||||
const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y);
|
|
||||||
const lineX = p1.x < p2.x ? findLane(p1.x, p2.x) : findLane(p2.x, p1.x);
|
|
||||||
|
|
||||||
if (dir === 'TB') {
|
|
||||||
if (p1.x < p2.x) {
|
|
||||||
// Source commit is on branch position left of destination commit
|
|
||||||
// so render arrow rightward with colour of destination branch
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc2} ${lineX} ${
|
|
||||||
p1.y + offset
|
|
||||||
} L ${lineX} ${p2.y - radius} ${arc} ${lineX + offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
// Source commit is on branch position right of destination commit
|
|
||||||
// so render arrow leftward with colour of source branch
|
|
||||||
colorClassNum = branchPos.get(commitA.branch).index;
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX + radius} ${p1.y} ${arc} ${lineX} ${
|
|
||||||
p1.y + offset
|
|
||||||
} L ${lineX} ${p2.y - radius} ${arc2} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
if (p1.x < p2.x) {
|
|
||||||
// Source commit is on branch position left of destination commit
|
|
||||||
// so render arrow rightward with colour of destination branch
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc} ${lineX} ${
|
|
||||||
p1.y - offset
|
|
||||||
} L ${lineX} ${p2.y + radius} ${arc2} ${lineX + offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
// Source commit is on branch position right of destination commit
|
|
||||||
// so render arrow leftward with colour of source branch
|
|
||||||
colorClassNum = branchPos.get(commitA.branch).index;
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX + radius} ${p1.y} ${arc2} ${lineX} ${
|
|
||||||
p1.y - offset
|
|
||||||
} L ${lineX} ${p2.y + radius} ${arc} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (p1.y < p2.y) {
|
|
||||||
// Source commit is on branch positioned above destination commit
|
|
||||||
// so render arrow downward with colour of destination branch
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${
|
|
||||||
p1.x + offset
|
|
||||||
} ${lineY} L ${p2.x - radius} ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
// Source commit is on branch positioned below destination commit
|
|
||||||
// so render arrow upward with colour of source branch
|
|
||||||
colorClassNum = branchPos.get(commitA.branch).index;
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${
|
|
||||||
p1.x + offset
|
|
||||||
} ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
arc = 'A 20 20, 0, 0, 0,';
|
|
||||||
arc2 = 'A 20 20, 0, 0, 1,';
|
|
||||||
radius = 20;
|
|
||||||
offset = 20;
|
|
||||||
|
|
||||||
if (dir === 'TB') {
|
|
||||||
if (p1.x < p2.x) {
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${
|
|
||||||
p1.y + offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (p1.x > p2.x) {
|
|
||||||
arc = 'A 20 20, 0, 0, 0,';
|
|
||||||
arc2 = 'A 20 20, 0, 0, 1,';
|
|
||||||
radius = 20;
|
|
||||||
offset = 20;
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x + radius} ${p1.y} ${arc} ${p2.x} ${
|
|
||||||
p1.y + offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p1.x === p2.x) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
if (p1.x < p2.x) {
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
|
||||||
p1.y - offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (p1.x > p2.x) {
|
|
||||||
arc = 'A 20 20, 0, 0, 0,';
|
|
||||||
arc2 = 'A 20 20, 0, 0, 1,';
|
|
||||||
radius = 20;
|
|
||||||
offset = 20;
|
|
||||||
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
|
||||||
p1.y - offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p1.x === p2.x) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (p1.y < p2.y) {
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${
|
|
||||||
p1.y + offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (p1.y > p2.y) {
|
|
||||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
|
||||||
p1.y - offset
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
} else {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${
|
|
||||||
p2.y
|
|
||||||
} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p1.y === p2.y) {
|
|
||||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
svg
|
|
||||||
.append('path')
|
|
||||||
.attr('d', lineDef)
|
|
||||||
.attr('class', 'arrow arrow' + (colorClassNum % THEME_COLOR_LIMIT));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {*} svg
|
|
||||||
* @param {CommitMap} commits
|
|
||||||
*/
|
|
||||||
const drawArrows = (svg, commits) => {
|
|
||||||
const gArrows = svg.append('g').attr('class', 'commit-arrows');
|
|
||||||
[...commits.keys()].forEach((key) => {
|
|
||||||
const commit = commits.get(key);
|
|
||||||
if (commit.parents && commit.parents.length > 0) {
|
|
||||||
commit.parents.forEach((parent) => {
|
|
||||||
drawArrow(gArrows, commits.get(parent), commit, commits);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the branches and the branches' labels to the svg.
|
|
||||||
*
|
|
||||||
* @param svg
|
|
||||||
* @param branches
|
|
||||||
*/
|
|
||||||
const drawBranches = (svg, branches) => {
|
|
||||||
const gitGraphConfig = getConfig().gitGraph;
|
|
||||||
const g = svg.append('g');
|
|
||||||
branches.forEach((branch, index) => {
|
|
||||||
const adjustIndexForTheme = index % THEME_COLOR_LIMIT;
|
|
||||||
|
|
||||||
const pos = branchPos.get(branch.name).pos;
|
|
||||||
const line = g.append('line');
|
|
||||||
line.attr('x1', 0);
|
|
||||||
line.attr('y1', pos);
|
|
||||||
line.attr('x2', maxPos);
|
|
||||||
line.attr('y2', pos);
|
|
||||||
line.attr('class', 'branch branch' + adjustIndexForTheme);
|
|
||||||
|
|
||||||
if (dir === 'TB') {
|
|
||||||
line.attr('y1', defaultPos);
|
|
||||||
line.attr('x1', pos);
|
|
||||||
line.attr('y2', maxPos);
|
|
||||||
line.attr('x2', pos);
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
line.attr('y1', maxPos);
|
|
||||||
line.attr('x1', pos);
|
|
||||||
line.attr('y2', defaultPos);
|
|
||||||
line.attr('x2', pos);
|
|
||||||
}
|
|
||||||
lanes.push(pos);
|
|
||||||
|
|
||||||
let name = branch.name;
|
|
||||||
|
|
||||||
// Create the actual text element
|
|
||||||
const labelElement = drawText(name);
|
|
||||||
// Create outer g, edgeLabel, this will be positioned after graph layout
|
|
||||||
const bkg = g.insert('rect');
|
|
||||||
const branchLabel = g.insert('g').attr('class', 'branchLabel');
|
|
||||||
|
|
||||||
// Create inner g, label, this will be positioned now for centering the text
|
|
||||||
const label = branchLabel.insert('g').attr('class', 'label branch-label' + adjustIndexForTheme);
|
|
||||||
label.node().appendChild(labelElement);
|
|
||||||
let bbox = labelElement.getBBox();
|
|
||||||
bkg
|
|
||||||
.attr('class', 'branchLabelBkg label' + adjustIndexForTheme)
|
|
||||||
.attr('rx', 4)
|
|
||||||
.attr('ry', 4)
|
|
||||||
.attr('x', -bbox.width - 4 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0))
|
|
||||||
.attr('y', -bbox.height / 2 + 8)
|
|
||||||
.attr('width', bbox.width + 18)
|
|
||||||
.attr('height', bbox.height + 4);
|
|
||||||
label.attr(
|
|
||||||
'transform',
|
|
||||||
'translate(' +
|
|
||||||
(-bbox.width - 14 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0)) +
|
|
||||||
', ' +
|
|
||||||
(pos - bbox.height / 2 - 1) +
|
|
||||||
')'
|
|
||||||
);
|
|
||||||
if (dir === 'TB') {
|
|
||||||
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0);
|
|
||||||
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')');
|
|
||||||
} else if (dir === 'BT') {
|
|
||||||
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', maxPos);
|
|
||||||
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + maxPos + ')');
|
|
||||||
} else {
|
|
||||||
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param txt
|
|
||||||
* @param id
|
|
||||||
* @param ver
|
|
||||||
* @param diagObj
|
|
||||||
*/
|
|
||||||
export const draw = function (txt, id, ver, diagObj) {
|
|
||||||
clear();
|
|
||||||
const conf = getConfig();
|
|
||||||
const gitGraphConfig = conf.gitGraph;
|
|
||||||
// try {
|
|
||||||
log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver);
|
|
||||||
|
|
||||||
allCommitsDict = diagObj.db.getCommits();
|
|
||||||
const branches = diagObj.db.getBranchesAsObjArray();
|
|
||||||
dir = diagObj.db.getDirection();
|
|
||||||
const diagram = select(`[id="${id}"]`);
|
|
||||||
// Position branches
|
|
||||||
let pos = 0;
|
|
||||||
branches.forEach((branch, index) => {
|
|
||||||
const labelElement = drawText(branch.name);
|
|
||||||
const g = diagram.append('g');
|
|
||||||
const branchLabel = g.insert('g').attr('class', 'branchLabel');
|
|
||||||
const label = branchLabel.insert('g').attr('class', 'label branch-label');
|
|
||||||
label.node().appendChild(labelElement);
|
|
||||||
let bbox = labelElement.getBBox();
|
|
||||||
|
|
||||||
branchPos.set(branch.name, { pos, index });
|
|
||||||
pos +=
|
|
||||||
50 +
|
|
||||||
(gitGraphConfig.rotateCommitLabel ? 40 : 0) +
|
|
||||||
(dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0);
|
|
||||||
label.remove();
|
|
||||||
branchLabel.remove();
|
|
||||||
g.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
drawCommits(diagram, allCommitsDict, false);
|
|
||||||
if (gitGraphConfig.showBranches) {
|
|
||||||
drawBranches(diagram, branches);
|
|
||||||
}
|
|
||||||
drawArrows(diagram, allCommitsDict);
|
|
||||||
drawCommits(diagram, allCommitsDict, true);
|
|
||||||
utils.insertTitle(
|
|
||||||
diagram,
|
|
||||||
'gitTitleText',
|
|
||||||
gitGraphConfig.titleTopMargin,
|
|
||||||
diagObj.db.getDiagramTitle()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup the view box and size of the svg element
|
|
||||||
setupGraphViewbox(
|
|
||||||
undefined,
|
|
||||||
diagram,
|
|
||||||
gitGraphConfig.diagramPadding,
|
|
||||||
gitGraphConfig.useMaxWidth ?? conf.useMaxWidth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
draw,
|
|
||||||
};
|
|
1350
packages/mermaid/src/diagrams/git/gitGraphRenderer.ts
Normal file
1350
packages/mermaid/src/diagrams/git/gitGraphRenderer.ts
Normal file
File diff suppressed because it is too large
Load Diff
134
packages/mermaid/src/diagrams/git/gitGraphTypes.ts
Normal file
134
packages/mermaid/src/diagrams/git/gitGraphTypes.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import type { GitGraphDiagramConfig } from '../../config.type.js';
|
||||||
|
import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||||
|
|
||||||
|
export const commitType = {
|
||||||
|
NORMAL: 0,
|
||||||
|
REVERSE: 1,
|
||||||
|
HIGHLIGHT: 2,
|
||||||
|
MERGE: 3,
|
||||||
|
CHERRY_PICK: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface CommitDB {
|
||||||
|
msg: string;
|
||||||
|
id: string;
|
||||||
|
type: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchDB {
|
||||||
|
name: string;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MergeDB {
|
||||||
|
branch: string;
|
||||||
|
id: string;
|
||||||
|
type?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CherryPickDB {
|
||||||
|
id: string;
|
||||||
|
targetId: string;
|
||||||
|
parent: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Commit {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
seq: number;
|
||||||
|
type: number;
|
||||||
|
tags: string[];
|
||||||
|
parents: string[];
|
||||||
|
branch: string;
|
||||||
|
customType?: number;
|
||||||
|
customId?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitGraph {
|
||||||
|
statements: Statement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Statement = CommitAst | BranchAst | MergeAst | CheckoutAst | CherryPickingAst;
|
||||||
|
|
||||||
|
export interface CommitAst {
|
||||||
|
$type: 'Commit';
|
||||||
|
id: string;
|
||||||
|
message?: string;
|
||||||
|
tags?: string[];
|
||||||
|
type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchAst {
|
||||||
|
$type: 'Branch';
|
||||||
|
name: string;
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MergeAst {
|
||||||
|
$type: 'Merge';
|
||||||
|
branch: string;
|
||||||
|
id?: string;
|
||||||
|
tags?: string[];
|
||||||
|
type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckoutAst {
|
||||||
|
$type: 'Checkout';
|
||||||
|
branch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CherryPickingAst {
|
||||||
|
$type: 'CherryPicking';
|
||||||
|
id: string;
|
||||||
|
parent: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitGraphDB extends DiagramDBBase<GitGraphDiagramConfig> {
|
||||||
|
commitType: typeof commitType;
|
||||||
|
setDirection: (dir: DiagramOrientation) => void;
|
||||||
|
setOptions: (rawOptString: string) => void;
|
||||||
|
getOptions: () => any;
|
||||||
|
commit: (commitDB: CommitDB) => void;
|
||||||
|
branch: (branchDB: BranchDB) => void;
|
||||||
|
merge: (mergeDB: MergeDB) => void;
|
||||||
|
cherryPick: (cherryPickDB: CherryPickDB) => void;
|
||||||
|
checkout: (branch: string) => void;
|
||||||
|
prettyPrint: () => void;
|
||||||
|
clear: () => void;
|
||||||
|
getBranchesAsObjArray: () => { name: string }[];
|
||||||
|
getBranches: () => Map<string, string | null>;
|
||||||
|
getCommits: () => Map<string, Commit>;
|
||||||
|
getCommitsArray: () => Commit[];
|
||||||
|
getCurrentBranch: () => string;
|
||||||
|
getDirection: () => DiagramOrientation;
|
||||||
|
getHead: () => Commit | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitGraphDBParseProvider extends Partial<GitGraphDB> {
|
||||||
|
commitType: typeof commitType;
|
||||||
|
setDirection: (dir: DiagramOrientation) => void;
|
||||||
|
commit: (commitDB: CommitDB) => void;
|
||||||
|
branch: (branchDB: BranchDB) => void;
|
||||||
|
merge: (mergeDB: MergeDB) => void;
|
||||||
|
cherryPick: (cherryPickDB: CherryPickDB) => void;
|
||||||
|
checkout: (branch: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitGraphDBRenderProvider extends Partial<GitGraphDB> {
|
||||||
|
prettyPrint: () => void;
|
||||||
|
clear: () => void;
|
||||||
|
getBranchesAsObjArray: () => { name: string }[];
|
||||||
|
getBranches: () => Map<string, string | null>;
|
||||||
|
getCommits: () => Map<string, Commit>;
|
||||||
|
getCommitsArray: () => Commit[];
|
||||||
|
getCurrentBranch: () => string;
|
||||||
|
getDirection: () => DiagramOrientation;
|
||||||
|
getHead: () => Commit | null;
|
||||||
|
getDiagramTitle: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiagramOrientation = 'LR' | 'TB' | 'BT';
|
@@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
* Parse following
|
|
||||||
* gitGraph:
|
|
||||||
* commit
|
|
||||||
* commit
|
|
||||||
* branch
|
|
||||||
*/
|
|
||||||
%lex
|
|
||||||
|
|
||||||
%x string
|
|
||||||
%x options
|
|
||||||
%x acc_title
|
|
||||||
%x acc_descr
|
|
||||||
%x acc_descr_multiline
|
|
||||||
%options case-insensitive
|
|
||||||
|
|
||||||
|
|
||||||
%%
|
|
||||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
|
||||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
|
||||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
|
||||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
|
||||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
|
||||||
<acc_descr_multiline>[\}] { this.popState(); }
|
|
||||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
|
||||||
(\r?\n)+ /*{console.log('New line');return 'NL';}*/ return 'NL';
|
|
||||||
\#[^\n]* /* skip comments */
|
|
||||||
\%%[^\n]* /* skip comments */
|
|
||||||
"gitGraph" return 'GG';
|
|
||||||
commit(?=\s|$) return 'COMMIT';
|
|
||||||
"id:" return 'COMMIT_ID';
|
|
||||||
"type:" return 'COMMIT_TYPE';
|
|
||||||
"msg:" return 'COMMIT_MSG';
|
|
||||||
"NORMAL" return 'NORMAL';
|
|
||||||
"REVERSE" return 'REVERSE';
|
|
||||||
"HIGHLIGHT" return 'HIGHLIGHT';
|
|
||||||
"tag:" return 'COMMIT_TAG';
|
|
||||||
branch(?=\s|$) return 'BRANCH';
|
|
||||||
"order:" return 'ORDER';
|
|
||||||
merge(?=\s|$) return 'MERGE';
|
|
||||||
cherry\-pick(?=\s|$) return 'CHERRY_PICK';
|
|
||||||
"parent:" return 'PARENT_COMMIT'
|
|
||||||
// "reset" return 'RESET';
|
|
||||||
\b(checkout|switch)(?=\s|$) return 'CHECKOUT';
|
|
||||||
"LR" return 'DIR';
|
|
||||||
"TB" return 'DIR';
|
|
||||||
"BT" return 'DIR';
|
|
||||||
":" return ':';
|
|
||||||
"^" return 'CARET'
|
|
||||||
"options"\r?\n this.begin("options"); //
|
|
||||||
<options>[ \r\n\t]+"end" this.popState(); // not used anymore in the renderer, fixed for backward compatibility
|
|
||||||
<options>[\s\S]+(?=[ \r\n\t]+"end") return 'OPT'; //
|
|
||||||
["]["] return 'EMPTYSTR';
|
|
||||||
["] this.begin("string");
|
|
||||||
<string>["] this.popState();
|
|
||||||
<string>[^"]* return 'STR';
|
|
||||||
[0-9]+(?=\s|$) return 'NUM';
|
|
||||||
\w([-\./\w]*[-\w])? return 'ID'; // only a subset of https://git-scm.com/docs/git-check-ref-format
|
|
||||||
<<EOF>> return 'EOF';
|
|
||||||
\s+ /* skip all whitespace */ // lowest priority so we can use lookaheads in earlier regex
|
|
||||||
|
|
||||||
/lex
|
|
||||||
|
|
||||||
%left '^'
|
|
||||||
|
|
||||||
%start start
|
|
||||||
|
|
||||||
%% /* language grammar */
|
|
||||||
|
|
||||||
start
|
|
||||||
: eol start
|
|
||||||
| GG document EOF{ return $3; }
|
|
||||||
| GG ':' document EOF{ return $3; }
|
|
||||||
| GG DIR ':' document EOF {yy.setDirection($2); return $4;}
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
document
|
|
||||||
: /*empty*/
|
|
||||||
| options body { yy.setOptions($1); $$ = $2}
|
|
||||||
;
|
|
||||||
|
|
||||||
options
|
|
||||||
: options OPT {$1 +=$2; $$=$1}
|
|
||||||
| NL
|
|
||||||
;
|
|
||||||
body
|
|
||||||
: /*empty*/ {$$ = []}
|
|
||||||
| body line {$1.push($2); $$=$1;}
|
|
||||||
;
|
|
||||||
line
|
|
||||||
: statement eol {$$ =$1}
|
|
||||||
| NL
|
|
||||||
;
|
|
||||||
|
|
||||||
statement
|
|
||||||
: commitStatement
|
|
||||||
| mergeStatement
|
|
||||||
| cherryPickStatement
|
|
||||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
|
||||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
|
||||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
|
|
||||||
| branchStatement
|
|
||||||
| CHECKOUT ref {yy.checkout($2)}
|
|
||||||
// | RESET reset_arg {yy.reset($2)}
|
|
||||||
;
|
|
||||||
branchStatement
|
|
||||||
: BRANCH ref {yy.branch($2)}
|
|
||||||
| BRANCH ref ORDER NUM {yy.branch($2, $4)}
|
|
||||||
;
|
|
||||||
|
|
||||||
cherryPickStatement
|
|
||||||
: CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3, '', undefined)}
|
|
||||||
| CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($3, '', undefined,$5)}
|
|
||||||
| CHERRY_PICK COMMIT_ID STR commitTags {yy.cherryPick($3, '', $4)}
|
|
||||||
| CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR commitTags {yy.cherryPick($3, '', $6,$5)}
|
|
||||||
| CHERRY_PICK COMMIT_ID STR commitTags PARENT_COMMIT STR {yy.cherryPick($3, '', $4,$6)}
|
|
||||||
| CHERRY_PICK commitTags COMMIT_ID STR {yy.cherryPick($4, '', $2)}
|
|
||||||
| CHERRY_PICK commitTags COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($4, '', $2,$6)}
|
|
||||||
;
|
|
||||||
|
|
||||||
mergeStatement
|
|
||||||
: MERGE ref {yy.merge($2,'','', undefined)}
|
|
||||||
| MERGE ref COMMIT_ID STR {yy.merge($2, $4,'', undefined)}
|
|
||||||
| MERGE ref COMMIT_TYPE commitType {yy.merge($2,'', $4, undefined)}
|
|
||||||
| MERGE ref commitTags {yy.merge($2, '','',$3)}
|
|
||||||
| MERGE ref commitTags COMMIT_ID STR {yy.merge($2, $5,'', $3)}
|
|
||||||
| MERGE ref commitTags COMMIT_TYPE commitType {yy.merge($2, '',$5, $3)}
|
|
||||||
| MERGE ref COMMIT_TYPE commitType commitTags {yy.merge($2, '',$4, $5)}
|
|
||||||
| MERGE ref COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $4, $6, undefined)}
|
|
||||||
| MERGE ref COMMIT_ID STR commitTags {yy.merge($2, $4, '', $5)}
|
|
||||||
| MERGE ref COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $6,$4, undefined)}
|
|
||||||
| MERGE ref COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.merge($2, $4, $6, $7)}
|
|
||||||
| MERGE ref COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.merge($2, $7, $4, $5)}
|
|
||||||
| MERGE ref COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.merge($2, $4, $7, $5)}
|
|
||||||
| MERGE ref COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.merge($2, $6, $4, $7)}
|
|
||||||
| MERGE ref commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $7, $5, $3)}
|
|
||||||
| MERGE ref commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $5, $7, $3)}
|
|
||||||
;
|
|
||||||
|
|
||||||
commitStatement
|
|
||||||
: COMMIT commit_arg {yy.commit($2)}
|
|
||||||
| COMMIT commitTags {yy.commit('','',yy.commitType.NORMAL,$2)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType {yy.commit('','',$3, undefined)}
|
|
||||||
| COMMIT commitTags COMMIT_TYPE commitType {yy.commit('','',$4,$2)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType commitTags {yy.commit('','',$3,$4)}
|
|
||||||
| COMMIT COMMIT_ID STR {yy.commit('',$3,yy.commitType.NORMAL, undefined)}
|
|
||||||
| COMMIT COMMIT_ID STR commitTags {yy.commit('',$3,yy.commitType.NORMAL,$4)}
|
|
||||||
| COMMIT commitTags COMMIT_ID STR {yy.commit('',$4,yy.commitType.NORMAL,$2)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_TYPE commitType {yy.commit('',$3,$5, undefined)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_ID STR {yy.commit('',$5,$3, undefined)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.commit('',$3,$5,$6)}
|
|
||||||
| COMMIT COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.commit('',$3,$6,$4)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.commit('',$5,$3,$6)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.commit('',$6,$3,$4)}
|
|
||||||
| COMMIT commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.commit('',$6,$4,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.commit('',$4,$6,$2)}
|
|
||||||
| COMMIT COMMIT_MSG STR {yy.commit($3,'',yy.commitType.NORMAL, undefined)}
|
|
||||||
| COMMIT commitTags COMMIT_MSG STR {yy.commit($4,'',yy.commitType.NORMAL,$2)}
|
|
||||||
| COMMIT COMMIT_MSG STR commitTags {yy.commit($3,'',yy.commitType.NORMAL,$4)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($3,'',$5, undefined)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($5,'',$3, undefined)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_MSG STR {yy.commit($5,$3,yy.commitType.NORMAL, undefined)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_ID STR {yy.commit($3,$5,yy.commitType.NORMAL, undefined)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_TYPE commitType commitTags {yy.commit($3,'',$5,$6)}
|
|
||||||
| COMMIT COMMIT_MSG STR commitTags COMMIT_TYPE commitType {yy.commit($3,'',$6,$4)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_MSG STR commitTags {yy.commit($5,'',$3,$6)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType commitTags COMMIT_MSG STR {yy.commit($6,'',$3,$4)}
|
|
||||||
| COMMIT commitTags COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($6,'',$4,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($4,'',$6,$2)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($3,$7,$5, undefined)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($3,$5,$7, undefined)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR {yy.commit($5,$7,$3, undefined)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR {yy.commit($7,$5,$3, undefined)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($7,$3,$5, undefined)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($5,$3,$7, undefined)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_MSG STR commitTags COMMIT_ID STR {yy.commit($3,$6,yy.commitType.NORMAL,$4)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_ID STR commitTags {yy.commit($3,$5,yy.commitType.NORMAL,$6)}
|
|
||||||
| COMMIT commitTags COMMIT_MSG STR COMMIT_ID STR {yy.commit($4,$6,yy.commitType.NORMAL,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_ID STR COMMIT_MSG STR {yy.commit($6,$4,yy.commitType.NORMAL,$2)}
|
|
||||||
| COMMIT COMMIT_ID STR commitTags COMMIT_MSG STR {yy.commit($6,$3,yy.commitType.NORMAL,$4)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_MSG STR commitTags {yy.commit($5,$3,yy.commitType.NORMAL,$6)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType commitTags {yy.commit($3,$5,$7,$8)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_ID STR commitTags COMMIT_TYPE commitType {yy.commit($3,$5,$8,$6)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR commitTags {yy.commit($3,$7,$5,$8)}
|
|
||||||
| COMMIT COMMIT_MSG STR COMMIT_TYPE commitType commitTags COMMIT_ID STR {yy.commit($3,$8,$5,$6)}
|
|
||||||
| COMMIT COMMIT_MSG STR commitTags COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($3,$6,$8,$4)}
|
|
||||||
| COMMIT COMMIT_MSG STR commitTags COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($3,$8,$6,$4)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType commitTags {yy.commit($5,$3,$7,$8)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_MSG STR commitTags COMMIT_TYPE commitType {yy.commit($5,$3,$8,$6)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR commitTags {yy.commit($7,$3,$5,$8)}
|
|
||||||
| COMMIT COMMIT_ID STR COMMIT_TYPE commitType commitTags COMMIT_MSG STR {yy.commit($8,$3,$5,$6)}
|
|
||||||
| COMMIT COMMIT_ID STR commitTags COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($6,$3,$8,$4)}
|
|
||||||
| COMMIT COMMIT_ID STR commitTags COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($8,$3,$6,$4)}
|
|
||||||
|
|
||||||
| COMMIT commitTags COMMIT_ID STR COMMIT_TYPE commitType COMMIT_MSG STR {yy.commit($8,$4,$6,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_ID STR COMMIT_MSG STR COMMIT_TYPE commitType {yy.commit($6,$4,$8,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR {yy.commit($8,$6,$4,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR {yy.commit($6,$8,$4,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_MSG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.commit($4,$6,$8,$2)}
|
|
||||||
| COMMIT commitTags COMMIT_MSG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.commit($4,$8,$6,$2)}
|
|
||||||
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_ID STR COMMIT_MSG STR commitTags {yy.commit($7,$5,$3,$8)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_ID STR commitTags COMMIT_MSG STR {yy.commit($8,$5,$3,$6)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType commitTags COMMIT_MSG STR COMMIT_ID STR {yy.commit($6,$8,$3,$4)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType commitTags COMMIT_ID STR COMMIT_MSG STR {yy.commit($8,$6,$3,$4)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_MSG STR COMMIT_ID STR commitTags {yy.commit($5,$7,$3,$8)}
|
|
||||||
| COMMIT COMMIT_TYPE commitType COMMIT_MSG STR commitTags COMMIT_ID STR {yy.commit($5,$8,$3,$6)}
|
|
||||||
;
|
|
||||||
commit_arg
|
|
||||||
: /* empty */ {$$ = ""}
|
|
||||||
| STR {$$=$1}
|
|
||||||
;
|
|
||||||
commitType
|
|
||||||
: NORMAL { $$=yy.commitType.NORMAL;}
|
|
||||||
| REVERSE { $$=yy.commitType.REVERSE;}
|
|
||||||
| HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;}
|
|
||||||
;
|
|
||||||
commitTags
|
|
||||||
: COMMIT_TAG STR {$$=[$2]}
|
|
||||||
| COMMIT_TAG EMPTYSTR {$$=['']}
|
|
||||||
| commitTags COMMIT_TAG STR {$commitTags.push($3); $$=$commitTags;}
|
|
||||||
| commitTags COMMIT_TAG EMPTYSTR {$commitTags.push(''); $$=$commitTags;}
|
|
||||||
;
|
|
||||||
|
|
||||||
ref
|
|
||||||
: ID
|
|
||||||
| STR
|
|
||||||
;
|
|
||||||
|
|
||||||
eol
|
|
||||||
: NL
|
|
||||||
| ';'
|
|
||||||
| EOF
|
|
||||||
;
|
|
||||||
// reset_arg
|
|
||||||
// : 'HEAD' reset_parents{$$ = $1+ ":" + $2 }
|
|
||||||
// | ID reset_parents{$$ = $1+ ":" + yy.count; yy.count = 0}
|
|
||||||
// ;
|
|
||||||
// reset_parents
|
|
||||||
// : /* empty */ {yy.count = 0}
|
|
||||||
// | CARET reset_parents { yy.count += 1 }
|
|
||||||
// ;
|
|
@@ -9,5 +9,10 @@
|
|||||||
"$root/*": ["src/*"]
|
"$root/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./package.json"]
|
"include": [
|
||||||
|
"./src/**/*.ts",
|
||||||
|
"./package.json",
|
||||||
|
"src/diagrams/gantt/ganttDb.js",
|
||||||
|
"src/diagrams/git/gitGraphRenderer.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,11 @@
|
|||||||
"id": "pie",
|
"id": "pie",
|
||||||
"grammar": "src/language/pie/pie.langium",
|
"grammar": "src/language/pie/pie.langium",
|
||||||
"fileExtensions": [".mmd", ".mermaid"]
|
"fileExtensions": [".mmd", ".mermaid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gitGraph",
|
||||||
|
"grammar": "src/language/gitGraph/gitGraph.langium",
|
||||||
|
"fileExtensions": [".mmd", ".mermaid"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mode": "production",
|
"mode": "production",
|
||||||
|
87
packages/parser/src/language/gitGraph/gitGraph.langium
Normal file
87
packages/parser/src/language/gitGraph/gitGraph.langium
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
grammar GitGraph
|
||||||
|
|
||||||
|
interface Common {
|
||||||
|
accDescr?: string;
|
||||||
|
accTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment TitleAndAccessibilities:
|
||||||
|
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||||
|
;
|
||||||
|
|
||||||
|
fragment EOL returns string:
|
||||||
|
NEWLINE+ | EOF
|
||||||
|
;
|
||||||
|
|
||||||
|
terminal NEWLINE: /\r?\n/;
|
||||||
|
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
|
||||||
|
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
|
||||||
|
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
|
||||||
|
|
||||||
|
hidden terminal WHITESPACE: /[\t ]+/;
|
||||||
|
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
|
||||||
|
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
|
||||||
|
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;
|
||||||
|
|
||||||
|
entry GitGraph:
|
||||||
|
NEWLINE*
|
||||||
|
('gitGraph' | 'gitGraph' ':' | 'gitGraph:' | ('gitGraph' Direction ':'))
|
||||||
|
NEWLINE*
|
||||||
|
(
|
||||||
|
NEWLINE*
|
||||||
|
(TitleAndAccessibilities |
|
||||||
|
statements+=Statement |
|
||||||
|
NEWLINE)*
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
Statement
|
||||||
|
: Commit
|
||||||
|
| Branch
|
||||||
|
| Merge
|
||||||
|
| Checkout
|
||||||
|
| CherryPicking
|
||||||
|
;
|
||||||
|
|
||||||
|
Direction:
|
||||||
|
dir=('LR' | 'TB' | 'BT');
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
'commit'
|
||||||
|
(
|
||||||
|
'id:' id=STRING
|
||||||
|
|'msg:'? message=STRING
|
||||||
|
|'tag:' tags+=STRING
|
||||||
|
|'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
|
||||||
|
)* EOL;
|
||||||
|
Branch:
|
||||||
|
'branch' name=(ID|STRING)
|
||||||
|
('order:' order=INT)?
|
||||||
|
EOL;
|
||||||
|
|
||||||
|
Merge:
|
||||||
|
'merge' branch=(ID|STRING)
|
||||||
|
(
|
||||||
|
'id:' id=STRING
|
||||||
|
|'tag:' tags+=STRING
|
||||||
|
|'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
|
||||||
|
)* EOL;
|
||||||
|
|
||||||
|
Checkout:
|
||||||
|
('checkout'|'switch') branch=(ID|STRING) EOL;
|
||||||
|
|
||||||
|
CherryPicking:
|
||||||
|
'cherry-pick'
|
||||||
|
(
|
||||||
|
'id:' id=STRING
|
||||||
|
|'tag:' tags+=STRING
|
||||||
|
|'parent:' parent=STRING
|
||||||
|
)* EOL;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
terminal INT returns number: /[0-9]+(?=\s)/;
|
||||||
|
terminal ID returns string: /\w([-\./\w]*[-\w])?/;
|
||||||
|
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||||
|
|
1
packages/parser/src/language/gitGraph/index.ts
Normal file
1
packages/parser/src/language/gitGraph/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './module.js';
|
52
packages/parser/src/language/gitGraph/module.ts
Normal file
52
packages/parser/src/language/gitGraph/module.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type {
|
||||||
|
DefaultSharedCoreModuleContext,
|
||||||
|
LangiumCoreServices,
|
||||||
|
LangiumSharedCoreServices,
|
||||||
|
Module,
|
||||||
|
PartialLangiumCoreServices,
|
||||||
|
} from 'langium';
|
||||||
|
import {
|
||||||
|
inject,
|
||||||
|
createDefaultCoreModule,
|
||||||
|
createDefaultSharedCoreModule,
|
||||||
|
EmptyFileSystem,
|
||||||
|
} from 'langium';
|
||||||
|
import { CommonValueConverter } from '../common/valueConverter.js';
|
||||||
|
import { MermaidGeneratedSharedModule, GitGraphGeneratedModule } from '../generated/module.js';
|
||||||
|
import { GitGraphTokenBuilder } from './tokenBuilder.js';
|
||||||
|
|
||||||
|
interface GitGraphAddedServices {
|
||||||
|
parser: {
|
||||||
|
TokenBuilder: GitGraphTokenBuilder;
|
||||||
|
ValueConverter: CommonValueConverter;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GitGraphServices = LangiumCoreServices & GitGraphAddedServices;
|
||||||
|
|
||||||
|
export const GitGraphModule: Module<
|
||||||
|
GitGraphServices,
|
||||||
|
PartialLangiumCoreServices & GitGraphAddedServices
|
||||||
|
> = {
|
||||||
|
parser: {
|
||||||
|
TokenBuilder: () => new GitGraphTokenBuilder(),
|
||||||
|
ValueConverter: () => new CommonValueConverter(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createGitGraphServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): {
|
||||||
|
shared: LangiumSharedCoreServices;
|
||||||
|
GitGraph: GitGraphServices;
|
||||||
|
} {
|
||||||
|
const shared: LangiumSharedCoreServices = inject(
|
||||||
|
createDefaultSharedCoreModule(context),
|
||||||
|
MermaidGeneratedSharedModule
|
||||||
|
);
|
||||||
|
const GitGraph: GitGraphServices = inject(
|
||||||
|
createDefaultCoreModule({ shared }),
|
||||||
|
GitGraphGeneratedModule,
|
||||||
|
GitGraphModule
|
||||||
|
);
|
||||||
|
shared.ServiceRegistry.register(GitGraph);
|
||||||
|
return { shared, GitGraph };
|
||||||
|
}
|
7
packages/parser/src/language/gitGraph/tokenBuilder.ts
Normal file
7
packages/parser/src/language/gitGraph/tokenBuilder.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { AbstractMermaidTokenBuilder } from '../common/index.js';
|
||||||
|
|
||||||
|
export class GitGraphTokenBuilder extends AbstractMermaidTokenBuilder {
|
||||||
|
public constructor() {
|
||||||
|
super(['gitGraph']);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,20 +5,31 @@ export {
|
|||||||
PacketBlock,
|
PacketBlock,
|
||||||
Pie,
|
Pie,
|
||||||
PieSection,
|
PieSection,
|
||||||
|
GitGraph,
|
||||||
|
Branch,
|
||||||
|
Commit,
|
||||||
|
Merge,
|
||||||
|
Statement,
|
||||||
isCommon,
|
isCommon,
|
||||||
isInfo,
|
isInfo,
|
||||||
isPacket,
|
isPacket,
|
||||||
isPacketBlock,
|
isPacketBlock,
|
||||||
isPie,
|
isPie,
|
||||||
isPieSection,
|
isPieSection,
|
||||||
|
isGitGraph,
|
||||||
|
isBranch,
|
||||||
|
isCommit,
|
||||||
|
isMerge,
|
||||||
} from './generated/ast.js';
|
} from './generated/ast.js';
|
||||||
export {
|
export {
|
||||||
InfoGeneratedModule,
|
InfoGeneratedModule,
|
||||||
MermaidGeneratedSharedModule,
|
MermaidGeneratedSharedModule,
|
||||||
PacketGeneratedModule,
|
PacketGeneratedModule,
|
||||||
PieGeneratedModule,
|
PieGeneratedModule,
|
||||||
|
GitGraphGeneratedModule,
|
||||||
} from './generated/module.js';
|
} from './generated/module.js';
|
||||||
|
|
||||||
|
export * from './gitGraph/index.js';
|
||||||
export * from './common/index.js';
|
export * from './common/index.js';
|
||||||
export * from './info/index.js';
|
export * from './info/index.js';
|
||||||
export * from './packet/index.js';
|
export * from './packet/index.js';
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import type { LangiumParser, ParseResult } from 'langium';
|
import type { LangiumParser, ParseResult } from 'langium';
|
||||||
|
|
||||||
import type { Info, Packet, Pie } from './index.js';
|
import type { Info, Packet, Pie, GitGraph } from './index.js';
|
||||||
|
|
||||||
export type DiagramAST = Info | Packet | Pie;
|
export type DiagramAST = Info | Packet | Pie | GitGraph;
|
||||||
|
|
||||||
const parsers: Record<string, LangiumParser> = {};
|
const parsers: Record<string, LangiumParser> = {};
|
||||||
const initializers = {
|
const initializers = {
|
||||||
@@ -21,11 +21,18 @@ const initializers = {
|
|||||||
const parser = createPieServices().Pie.parser.LangiumParser;
|
const parser = createPieServices().Pie.parser.LangiumParser;
|
||||||
parsers.pie = parser;
|
parsers.pie = parser;
|
||||||
},
|
},
|
||||||
|
gitGraph: async () => {
|
||||||
|
const { createGitGraphServices } = await import('./language/gitGraph/index.js');
|
||||||
|
const parser = createGitGraphServices().GitGraph.parser.LangiumParser;
|
||||||
|
parsers.gitGraph = parser;
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
||||||
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
|
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
|
||||||
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
|
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
|
||||||
|
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
|
||||||
|
|
||||||
export async function parse<T extends DiagramAST>(
|
export async function parse<T extends DiagramAST>(
|
||||||
diagramType: keyof typeof initializers,
|
diagramType: keyof typeof initializers,
|
||||||
text: string
|
text: string
|
||||||
|
207
packages/parser/tests/gitGraph.test.ts
Normal file
207
packages/parser/tests/gitGraph.test.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { Branch, Merge } from '../src/language/index.js';
|
||||||
|
import { gitGraphParse as parse } from './test-util.js';
|
||||||
|
import type { Commit } from '../src/language/index.js';
|
||||||
|
import type { Checkout, CherryPicking } from '../src/language/generated/ast.js';
|
||||||
|
|
||||||
|
describe('Parsing Commit Statements', () => {
|
||||||
|
it('should parse a simple commit', () => {
|
||||||
|
const result = parse(`gitGraph\n commit\n`);
|
||||||
|
expect(result.value.statements[0].$type).toBe('Commit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiple commits', () => {
|
||||||
|
const result = parse(`gitGraph\n commit\n commit\n commit\n`);
|
||||||
|
expect(result.value.statements).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse commits with all properties', () => {
|
||||||
|
const result = parse(`gitGraph\n commit id:"1" msg:"Fix bug" tag:"v1.2" type:NORMAL\n`);
|
||||||
|
const commit = result.value.statements[0] as Commit;
|
||||||
|
expect(commit.$type).toBe('Commit');
|
||||||
|
expect(commit.id).toBe('1');
|
||||||
|
expect(commit.message).toBe('Fix bug');
|
||||||
|
expect(commit.tags).toEqual(['v1.2']);
|
||||||
|
expect(commit.type).toBe('NORMAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commit messages with special characters', () => {
|
||||||
|
const result = parse(`gitGraph\n commit msg:"Fix issue #123: Handle errors"\n`);
|
||||||
|
const commit = result.value.statements[0] as Commit;
|
||||||
|
expect(commit.message).toBe('Fix issue #123: Handle errors');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse commits with only a message and no other properties', () => {
|
||||||
|
const result = parse(`gitGraph\n commit msg:"Initial release"\n`);
|
||||||
|
const commit = result.value.statements[0] as Commit;
|
||||||
|
expect(commit.message).toBe('Initial release');
|
||||||
|
expect(commit.id).toBeUndefined();
|
||||||
|
expect(commit.type).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore malformed properties and not break parsing', () => {
|
||||||
|
const result = parse(`gitGraph\n commit id:"2" msg:"Malformed commit" oops:"ignored"\n`);
|
||||||
|
const commit = result.value.statements[0] as Commit;
|
||||||
|
expect(commit.id).toBe('2');
|
||||||
|
expect(commit.message).toBe('Malformed commit');
|
||||||
|
expect(commit.hasOwnProperty('oops')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiple commits with different types', () => {
|
||||||
|
const result = parse(`gitGraph\n commit type:NORMAL\n commit type:REVERSE\n`);
|
||||||
|
const commit1 = result.value.statements[0] as Commit;
|
||||||
|
const commit2 = result.value.statements[1] as Commit;
|
||||||
|
expect(commit1.type).toBe('NORMAL');
|
||||||
|
expect(commit2.type).toBe('REVERSE');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parsing Branch Statements', () => {
|
||||||
|
it('should parse a branch with a simple name', () => {
|
||||||
|
const result = parse(`gitGraph\n commit\n commit\n branch master\n`);
|
||||||
|
const branch = result.value.statements[2] as Branch;
|
||||||
|
expect(branch.name).toBe('master');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse a branch with an order property', () => {
|
||||||
|
const result = parse(`gitGraph\n commit\n branch feature order:1\n`);
|
||||||
|
const branch = result.value.statements[1] as Branch;
|
||||||
|
expect(branch.name).toBe('feature');
|
||||||
|
expect(branch.order).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle branch names with special characters', () => {
|
||||||
|
const result = parse(`gitGraph\n branch feature/test-branch\n`);
|
||||||
|
const branch = result.value.statements[0] as Branch;
|
||||||
|
expect(branch.name).toBe('feature/test-branch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse branches with hyphens and underscores', () => {
|
||||||
|
const result = parse(`gitGraph\n branch my-feature_branch\n`);
|
||||||
|
const branch = result.value.statements[0] as Branch;
|
||||||
|
expect(branch.name).toBe('my-feature_branch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle branch without order property', () => {
|
||||||
|
const result = parse(`gitGraph\n branch feature\n`);
|
||||||
|
const branch = result.value.statements[0] as Branch;
|
||||||
|
expect(branch.name).toBe('feature');
|
||||||
|
expect(branch.order).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parsing Merge Statements', () => {
|
||||||
|
it('should parse a merge with a branch name', () => {
|
||||||
|
const result = parse(`gitGraph\n merge master\n`);
|
||||||
|
const merge = result.value.statements[0] as Merge;
|
||||||
|
expect(merge.branch).toBe('master');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle merges with additional properties', () => {
|
||||||
|
const result = parse(`gitGraph\n merge feature id:"m1" tag:"release" type:HIGHLIGHT\n`);
|
||||||
|
const merge = result.value.statements[0] as Merge;
|
||||||
|
expect(merge.branch).toBe('feature');
|
||||||
|
expect(merge.id).toBe('m1');
|
||||||
|
expect(merge.tags).toEqual(['release']);
|
||||||
|
expect(merge.type).toBe('HIGHLIGHT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse merge without any properties', () => {
|
||||||
|
const result = parse(`gitGraph\n merge feature\n`);
|
||||||
|
const merge = result.value.statements[0] as Merge;
|
||||||
|
expect(merge.branch).toBe('feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore malformed properties in merge statements', () => {
|
||||||
|
const result = parse(`gitGraph\n merge feature random:"ignored"\n`);
|
||||||
|
const merge = result.value.statements[0] as Merge;
|
||||||
|
expect(merge.branch).toBe('feature');
|
||||||
|
expect(merge.hasOwnProperty('random')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parsing Checkout Statements', () => {
|
||||||
|
it('should parse a checkout to a named branch', () => {
|
||||||
|
const result = parse(
|
||||||
|
`gitGraph\n commit id:"1"\n branch develop\n branch fun\n checkout develop\n`
|
||||||
|
);
|
||||||
|
const checkout = result.value.statements[3] as Checkout;
|
||||||
|
expect(checkout.branch).toBe('develop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse checkout to branches with complex names', () => {
|
||||||
|
const result = parse(`gitGraph\n checkout hotfix-123\n`);
|
||||||
|
const checkout = result.value.statements[0] as Checkout;
|
||||||
|
expect(checkout.branch).toBe('hotfix-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse checkouts with hyphens and numbers', () => {
|
||||||
|
const result = parse(`gitGraph\n checkout release-2021\n`);
|
||||||
|
const checkout = result.value.statements[0] as Checkout;
|
||||||
|
expect(checkout.branch).toBe('release-2021');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parsing CherryPicking Statements', () => {
|
||||||
|
it('should parse cherry-picking with a commit id', () => {
|
||||||
|
const result = parse(`gitGraph\n commit id:"123" commit id:"321" cherry-pick id:"123"\n`);
|
||||||
|
const cherryPick = result.value.statements[2] as CherryPicking;
|
||||||
|
expect(cherryPick.id).toBe('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse cherry-picking with multiple properties', () => {
|
||||||
|
const result = parse(`gitGraph\n cherry-pick id:"123" tag:"urgent" parent:"100"\n`);
|
||||||
|
const cherryPick = result.value.statements[0] as CherryPicking;
|
||||||
|
expect(cherryPick.id).toBe('123');
|
||||||
|
expect(cherryPick.tags).toEqual(['urgent']);
|
||||||
|
expect(cherryPick.parent).toBe('100');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parsing with Accessibility Titles and Descriptions', () => {
|
||||||
|
it('should parse accessibility titles', () => {
|
||||||
|
const result = parse(`gitGraph\n accTitle: Accessible Graph\n commit\n`);
|
||||||
|
expect(result.value.accTitle).toBe('Accessible Graph');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiline accessibility descriptions', () => {
|
||||||
|
const result = parse(
|
||||||
|
`gitGraph\n accDescr {\n Detailed description\n across multiple lines\n }\n commit\n`
|
||||||
|
);
|
||||||
|
expect(result.value.accDescr).toBe('Detailed description\nacross multiple lines');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration Tests', () => {
|
||||||
|
it('should correctly parse a complex graph with various elements', () => {
|
||||||
|
const result = parse(`
|
||||||
|
gitGraph TB:
|
||||||
|
accTitle: Complex Example
|
||||||
|
commit id:"init" type:NORMAL
|
||||||
|
branch feature
|
||||||
|
commit id:"feat1" msg:"Add feature"
|
||||||
|
checkout main
|
||||||
|
merge feature tag:"v1.0"
|
||||||
|
cherry-pick id:"feat1" tag:"critical fix"
|
||||||
|
`);
|
||||||
|
expect(result.value.accTitle).toBe('Complex Example');
|
||||||
|
expect(result.value.statements[0].$type).toBe('Commit');
|
||||||
|
expect(result.value.statements[1].$type).toBe('Branch');
|
||||||
|
expect(result.value.statements[2].$type).toBe('Commit');
|
||||||
|
expect(result.value.statements[3].$type).toBe('Checkout');
|
||||||
|
expect(result.value.statements[4].$type).toBe('Merge');
|
||||||
|
expect(result.value.statements[5].$type).toBe('CherryPicking');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling for Invalid Syntax', () => {
|
||||||
|
it('should report errors for unknown properties in commit', () => {
|
||||||
|
const result = parse(`gitGraph\n commit unknown:"oops"\n`);
|
||||||
|
expect(result.parserErrors).not.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report errors for invalid branch order', () => {
|
||||||
|
const result = parse(`gitGraph\n branch feature order:xyz\n`);
|
||||||
|
expect(result.parserErrors).not.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,7 +1,18 @@
|
|||||||
import type { LangiumParser, ParseResult } from 'langium';
|
import type { LangiumParser, ParseResult } from 'langium';
|
||||||
import { expect, vi } from 'vitest';
|
import { expect, vi } from 'vitest';
|
||||||
import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js';
|
import type {
|
||||||
import { createInfoServices, createPieServices } from '../src/language/index.js';
|
Info,
|
||||||
|
InfoServices,
|
||||||
|
Pie,
|
||||||
|
PieServices,
|
||||||
|
GitGraph,
|
||||||
|
GitGraphServices,
|
||||||
|
} from '../src/language/index.js';
|
||||||
|
import {
|
||||||
|
createInfoServices,
|
||||||
|
createPieServices,
|
||||||
|
createGitGraphServices,
|
||||||
|
} from '../src/language/index.js';
|
||||||
|
|
||||||
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
|
||||||
@@ -40,3 +51,14 @@ export function createPieTestServices() {
|
|||||||
return { services: pieServices, parse };
|
return { services: pieServices, parse };
|
||||||
}
|
}
|
||||||
export const pieParse = createPieTestServices().parse;
|
export const pieParse = createPieTestServices().parse;
|
||||||
|
|
||||||
|
const gitGraphServices: GitGraphServices = createGitGraphServices().GitGraph;
|
||||||
|
const gitGraphParser: LangiumParser = gitGraphServices.parser.LangiumParser;
|
||||||
|
export function createGitGraphTestServices() {
|
||||||
|
const parse = (input: string) => {
|
||||||
|
return gitGraphParser.parse<GitGraph>(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { services: gitGraphServices, parse };
|
||||||
|
}
|
||||||
|
export const gitGraphParse = createGitGraphTestServices().parse;
|
||||||
|
Reference in New Issue
Block a user