Added gitGraphAst as typescript added gitGraphTypes and gitGraphParser

This commit is contained in:
Austin Fulbright
2024-07-22 04:10:36 -04:00
parent 1d0e98dd62
commit 6f7c291512
5 changed files with 312 additions and 235 deletions

View File

@@ -11,16 +11,20 @@ import {
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import defaultConfig from '../../defaultConfig.js';
import type { DiagramOrientation, Commit } from './gitGraphTypes.js';
let { mainBranchName, mainBranchOrder } = getConfig().gitGraph; const mainBranchName = defaultConfig.gitGraph.mainBranchName;
let commits = new Map(); const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder;
let head = null;
let branchesConfig = new Map(); let commits = new Map<string, Commit>();
let head: Commit | null = null;
let branchesConfig = new Map<string, { name: string; order: number }>();
branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder });
let branches = new Map(); let branches = new Map<string, string | null>();
branches.set(mainBranchName, head); branches.set(mainBranchName, null);
let curBranch = mainBranchName; let curBranch = mainBranchName;
let direction = 'LR'; let direction: DiagramOrientation = 'LR';
let seq = 0; let seq = 0;
/** /**
@@ -57,8 +61,8 @@ function getId() {
// } // }
/** /**
* @param currentCommit * @param currentCommit - current commit
* @param otherCommit * @param otherCommit - other commit
*/ */
// function isReachableFrom(currentCommit, otherCommit) { // function isReachableFrom(currentCommit, otherCommit) {
// const currentSeq = currentCommit.seq; // const currentSeq = currentCommit.seq;
@@ -68,10 +72,10 @@ function getId() {
// } // }
/** /**
* @param list * @param list - list of items
* @param fn * @param fn - function to get the key
*/ */
function uniqBy(list, fn) { function uniqBy(list: any[], fn: (item: any) => any) {
const recordMap = Object.create(null); const recordMap = Object.create(null);
return list.reduce((out, item) => { return list.reduce((out, item) => {
const key = fn(item); const key = fn(item);
@@ -83,17 +87,18 @@ function uniqBy(list, fn) {
}, []); }, []);
} }
export const setDirection = function (dir) { export const setDirection = function (dir: DiagramOrientation) {
direction = dir; direction = dir;
}; };
let options = {}; let options = {};
export const setOptions = function (rawOptString) { export const setOptions = function (rawOptString: string) {
log.debug('options str', rawOptString); log.debug('options str', rawOptString);
rawOptString = rawOptString?.trim(); rawOptString = rawOptString?.trim();
rawOptString = rawOptString || '{}'; rawOptString = rawOptString || '{}';
try { try {
options = JSON.parse(rawOptString); options = JSON.parse(rawOptString);
} catch (e) { } catch (e: any) {
log.error('error while parsing gitGraph options', e.message); log.error('error while parsing gitGraph options', e.message);
} }
}; };
@@ -102,60 +107,65 @@ export const getOptions = function () {
return options; return options;
}; };
export const commit = function (msg, id, type, tags) { export const commit = function (msg: string, id: string, type: number, tag: string) {
log.debug('Entering commit:', msg, id, type, tags); log.info('commit', msg, id, type, tag);
const config = getConfig(); log.debug('Entering commit:', msg, id, type, tag);
id = common.sanitizeText(id, config); id = common.sanitizeText(id, getConfig());
msg = common.sanitizeText(msg, config); msg = common.sanitizeText(msg, getConfig());
tags = tags?.map((tag) => common.sanitizeText(tag, config)); tag = common.sanitizeText(tag, getConfig());
const commit = { const newCommit: Commit = {
id: id ? id : seq + '-' + getId(), id: id ? id : seq + '-' + getId(),
message: msg, message: msg,
seq: seq++, seq: seq++,
type: type ? type : commitType.NORMAL, type: type,
tags: tags ?? [], tag: tag ? tag : '',
parents: head == null ? [] : [head.id], parents: head == null ? [] : [head.id],
branch: curBranch, branch: curBranch,
}; };
head = commit; head = newCommit;
commits.set(commit.id, commit); log.info('main branch', mainBranchName);
branches.set(curBranch, commit.id); commits.set(newCommit.id, newCommit);
log.debug('in pushCommit ' + commit.id); branches.set(curBranch, newCommit.id);
log.debug('in pushCommit ' + newCommit.id);
}; };
export const branch = function (name, order) { export const branch = function (name: string, order: number) {
name = common.sanitizeText(name, getConfig()); name = common.sanitizeText(name, getConfig());
if (!branches.has(name)) { if (!branches.has(name)) {
branches.set(name, head != null ? head.id : null); branches.set(name, head != null ? head.id : null);
branchesConfig.set(name, { name, order: order ? parseInt(order, 10) : null }); branchesConfig.set(name, { name, order });
checkout(name); checkout(name);
log.debug('in createBranch'); log.debug('in createBranch');
} else { } else {
let error = new Error( 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 ' + `Trying to create an existing branch: ${name}. Use 'checkout ${name}' instead.`
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) { export const merge = (
const config = getConfig(); otherBranch: string,
otherBranch = common.sanitizeText(otherBranch, config); custom_id?: string,
custom_id = common.sanitizeText(custom_id, config); override_type?: number,
custom_tag?: string
const currentCommit = commits.get(branches.get(curBranch)); ): void => {
const otherCommit = commits.get(branches.get(otherBranch)); otherBranch = common.sanitizeText(otherBranch, getConfig());
if (custom_id) {
custom_id = common.sanitizeText(custom_id, getConfig());
}
const currentBranchCheck: string | null | undefined = branches.get(curBranch);
const otherBranchCheck: string | null | undefined = branches.get(otherBranch);
const currentCommit: Commit | undefined = currentBranchCheck
? commits.get(currentBranchCheck)
: undefined;
const otherCommit: Commit | undefined = otherBranchCheck
? commits.get(otherBranchCheck)
: undefined;
if (currentCommit && otherCommit && currentCommit.branch === otherBranch) {
throw new Error(`Cannot merge branch '${otherBranch}' into itself.`);
}
if (curBranch === otherBranch) { if (curBranch === otherBranch) {
let error = new Error('Incorrect usage of "merge". Cannot merge a branch to itself'); const error: any = new Error('Incorrect usage of "merge". Cannot merge a branch to itself');
error.hash = { error.hash = {
text: 'merge ' + otherBranch, text: 'merge ' + otherBranch,
token: 'merge ' + otherBranch, token: 'merge ' + otherBranch,
@@ -165,7 +175,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
}; };
throw error; throw error;
} else if (currentCommit === undefined || !currentCommit) { } else if (currentCommit === undefined || !currentCommit) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits' 'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits'
); );
error.hash = { error.hash = {
@@ -177,7 +187,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
}; };
throw error; throw error;
} else if (!branches.has(otherBranch)) { } else if (!branches.has(otherBranch)) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist' 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist'
); );
error.hash = { error.hash = {
@@ -189,7 +199,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
}; };
throw error; throw error;
} else if (otherCommit === undefined || !otherCommit) { } else if (otherCommit === undefined || !otherCommit) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits' 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits'
); );
error.hash = { error.hash = {
@@ -201,7 +211,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
}; };
throw error; throw error;
} else if (currentCommit === otherCommit) { } else if (currentCommit === otherCommit) {
let error = new Error('Incorrect usage of "merge". Both branches have same head'); const error: any = new Error('Incorrect usage of "merge". Both branches have same head');
error.hash = { error.hash = {
text: 'merge ' + otherBranch, text: 'merge ' + otherBranch,
token: 'merge ' + otherBranch, token: 'merge ' + otherBranch,
@@ -211,23 +221,24 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
}; };
throw error; throw error;
} else if (custom_id && commits.has(custom_id)) { } else if (custom_id && commits.has(custom_id)) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "merge". Commit with id:' + 'Incorrect usage of "merge". Commit with id:' +
custom_id + custom_id +
' already exists, use different custom Id' ' already exists, use different custom Id'
); );
error.hash = { error.hash = {
text: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','), text: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
token: 'merge ' + otherBranch + custom_id + override_type + custom_tags?.join(','), token: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
line: '1', line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: [ expected: [
`merge ${otherBranch} ${custom_id}_UNIQUE ${override_type} ${custom_tags?.join(',')}`, 'merge ' + otherBranch + ' ' + custom_id + '_UNIQUE ' + override_type + ' ' + custom_tag,
], ],
}; };
throw error; throw error;
} }
// if (isReachableFrom(currentCommit, otherCommit)) { // if (isReachableFrom(currentCommit, otherCommit)) {
// log.debug('Already merged'); // log.debug('Already merged');
// return; // return;
@@ -237,16 +248,19 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
// head = commits.get(branches.get(curBranch)); // head = commits.get(branches.get(curBranch));
// } else { // } else {
// create merge commit // create merge commit
const commit = {
const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this
const commit: Commit = {
id: custom_id ? custom_id : seq + '-' + getId(), id: custom_id ? custom_id : seq + '-' + getId(),
message: 'merged branch ' + otherBranch + ' into ' + curBranch, message: 'merged branch ' + otherBranch + ' into ' + curBranch,
seq: seq++, seq: seq++,
parents: [head == null ? null : head.id, branches.get(otherBranch)], parents: [head == null ? null : head.id, verifiedBranch],
branch: curBranch, branch: curBranch,
type: commitType.MERGE, type: commitType.MERGE,
customType: override_type, customType: override_type, //TODO - need to make customType optional
customId: custom_id ? true : false, customId: custom_id, //TODO - need to make customId optional as well as tag
tags: custom_tags ? custom_tags : [], tag: custom_tag ? custom_tag : '',
}; };
head = commit; head = commit;
commits.set(commit.id, commit); commits.set(commit.id, commit);
@@ -256,16 +270,20 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
log.debug('in mergeBranch'); log.debug('in mergeBranch');
}; };
export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { export const cherryPick = function (
log.debug('Entering cherryPick:', sourceId, targetId, tags); sourceId: string,
const config = getConfig(); targetId: string,
sourceId = common.sanitizeText(sourceId, config); tag: string,
targetId = common.sanitizeText(targetId, config); parentCommitId: string
tags = tags?.map((tag) => common.sanitizeText(tag, config)); ) {
parentCommitId = common.sanitizeText(parentCommitId, config); log.debug('Entering cherryPick:', sourceId, targetId, tag);
sourceId = common.sanitizeText(sourceId, getConfig());
targetId = common.sanitizeText(targetId, getConfig());
tag = common.sanitizeText(tag, getConfig());
parentCommitId = common.sanitizeText(parentCommitId, getConfig());
if (!sourceId || !commits.has(sourceId)) { if (!sourceId || !commits.has(sourceId)) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "cherryPick". Source commit id should exist and provided' 'Incorrect usage of "cherryPick". Source commit id should exist and provided'
); );
error.hash = { error.hash = {
@@ -277,19 +295,22 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
}; };
throw error; throw error;
} }
let sourceCommit = commits.get(sourceId);
let sourceCommitBranch = sourceCommit.branch; const sourceCommit = commits.get(sourceId);
if ( if (
parentCommitId && !sourceCommit ||
!(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) !parentCommitId ||
!Array.isArray(sourceCommit.parents) ||
!sourceCommit.parents.includes(parentCommitId)
) { ) {
let error = new Error( throw new Error(
'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' '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) { if (sourceCommit.type === commitType.MERGE && !parentCommitId) {
let error = new Error( const error = new Error(
'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.'
); );
throw error; throw error;
@@ -298,7 +319,7 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
// cherry-pick source commit to current branch // cherry-pick source commit to current branch
if (sourceCommitBranch === curBranch) { if (sourceCommitBranch === curBranch) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "cherryPick". Source commit is already on current branch' 'Incorrect usage of "cherryPick". Source commit is already on current branch'
); );
error.hash = { error.hash = {
@@ -310,9 +331,24 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
}; };
throw error; throw error;
} }
const currentCommit = commits.get(branches.get(curBranch)); const currentCommitId = branches.get(curBranch);
if (currentCommitId === undefined || !currentCommitId) {
const error: any = 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 currentCommit = commits.get(currentCommitId);
if (currentCommit === undefined || !currentCommit) { if (currentCommit === undefined || !currentCommit) {
let error = new Error( const error: any = new Error(
'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits' 'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits'
); );
error.hash = { error.hash = {
@@ -326,18 +362,16 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
} }
const commit = { const commit = {
id: seq + '-' + getId(), id: seq + '-' + getId(),
message: 'cherry-picked ' + sourceCommit + ' into ' + curBranch, message: 'cherry-picked ' + sourceCommit?.message + ' into ' + curBranch,
seq: seq++, seq: seq++,
parents: [head == null ? null : head.id, sourceCommit.id], parents: [head == null ? null : head.id, sourceCommit.id],
branch: curBranch, branch: curBranch,
type: commitType.CHERRY_PICK, type: commitType.CHERRY_PICK,
tags: tags tag:
? tags.filter(Boolean) tag ??
: [
`cherry-pick:${sourceCommit.id}${ `cherry-pick:${sourceCommit.id}${
sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : ''
}`, }`,
],
}; };
head = commit; head = commit;
commits.set(commit.id, commit); commits.set(commit.id, commit);
@@ -346,10 +380,10 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) {
log.debug('in cherryPick'); log.debug('in cherryPick');
} }
}; };
export const checkout = function (branch) { export const checkout = function (branch: string) {
branch = common.sanitizeText(branch, getConfig()); branch = common.sanitizeText(branch, getConfig());
if (!branches.has(branch)) { if (!branches.has(branch)) {
let error = new Error( const error: any = new Error(
'Trying to checkout branch which is not yet created. (Help try using "branch ' + branch + '")' 'Trying to checkout branch which is not yet created. (Help try using "branch ' + branch + '")'
); );
error.hash = { error.hash = {
@@ -360,10 +394,19 @@ export const checkout = function (branch) {
expected: ['"branch ' + branch + '"'], expected: ['"branch ' + branch + '"'],
}; };
throw error; throw error;
//branches[branch] = head != null ? head.id : null;
//log.debug('in createBranch');
} else { } else {
curBranch = branch; curBranch = branch;
const id = branches.get(curBranch); const id = branches.get(curBranch);
head = commits.get(id);
if (id === null || id === undefined) {
throw new Error('Branch ' + branch + ' has no commits');
}
if (commits.get(id) === undefined) {
throw new Error('Branch ' + branch + ' has no commits');
}
head = commits.get(id) ?? null;
} }
}; };
@@ -387,11 +430,11 @@ export const checkout = function (branch) {
// }; // };
/** /**
* @param arr * @param arr - array
* @param key * @param key - key
* @param newVal * @param newVal - new value
*/ */
function upsert(arr, key, newVal) { function upsert(arr: any[], key: any, newVal: any) {
const index = arr.indexOf(key); const index = arr.indexOf(key);
if (index === -1) { if (index === -1) {
arr.push(newVal); arr.push(newVal);
@@ -400,8 +443,8 @@ function upsert(arr, key, newVal) {
} }
} }
/** @param commitArr */ /** @param commitArr - array */
function prettyPrintCommitHistory(commitArr) { function prettyPrintCommitHistory(commitArr: Commit[]) {
const commit = commitArr.reduce((out, commit) => { const commit = commitArr.reduce((out, commit) => {
if (out.seq > commit.seq) { if (out.seq > commit.seq) {
return out; return out;
@@ -417,21 +460,25 @@ function prettyPrintCommitHistory(commitArr) {
} }
}); });
const label = [line, commit.id, commit.seq]; const label = [line, commit.id, commit.seq];
for (let branch in branches) { for (const branch in branches) {
if (branches.get(branch) === commit.id) { if (branches.get(branch) === commit.id) {
label.push(branch); label.push(branch);
} }
} }
log.debug(label.join(' ')); log.debug(label.join(' '));
if (commit.parents && commit.parents.length == 2) { if (commit.parents && commit.parents.length == 2 && commit.parents[0] && commit.parents[1]) {
const newCommit = commits.get(commit.parents[0]); const newCommit = commits.get(commit.parents[0]);
upsert(commitArr, commit, newCommit); upsert(commitArr, commit, newCommit);
commitArr.push(commits.get(commit.parents[1])); if (commit.parents[1]) {
commitArr.push(commits.get(commit.parents[1])!);
}
} else if (commit.parents.length == 0) { } else if (commit.parents.length == 0) {
return; return;
} else { } else {
const nextCommit = commits.get(commit.parents); if (commit.parents[0]) {
upsert(commitArr, commit, nextCommit); const newCommit = commits.get(commit.parents[0]);
upsert(commitArr, commit, newCommit);
}
} }
commitArr = uniqBy(commitArr, (c) => c.id); commitArr = uniqBy(commitArr, (c) => c.id);
prettyPrintCommitHistory(commitArr); prettyPrintCommitHistory(commitArr);
@@ -446,12 +493,13 @@ export const prettyPrint = function () {
export const clear = function () { export const clear = function () {
commits = new Map(); commits = new Map();
head = null; head = null;
const { mainBranchName, mainBranchOrder } = getConfig().gitGraph; const mainBranch = defaultConfig.gitGraph.mainBranchName;
const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder;
branches = new Map(); branches = new Map();
branches.set(mainBranchName, null); branches.set(mainBranch, null);
branchesConfig = new Map(); branchesConfig = new Map();
branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); branchesConfig.set(mainBranch, { name: mainBranch, order: mainBranchOrder });
curBranch = mainBranchName; curBranch = mainBranch;
seq = 0; seq = 0;
commonClear(); commonClear();
}; };
@@ -464,7 +512,7 @@ export const getBranchesAsObjArray = function () {
} }
return { return {
...branchConfig, ...branchConfig,
order: parseFloat(`0.${i}`, 10), order: parseFloat(`0.${i}`),
}; };
}) })
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
@@ -531,5 +579,4 @@ export default {
setAccDescription, setAccDescription,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
commitType,
}; };

View File

@@ -0,0 +1,72 @@
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 type {
Statement,
CommitAst,
Branch,
Merge,
Checkout,
CherryPicking,
} from './gitGraphTypes.js';
const populate = (ast: any) => {
populateCommonDb(ast, db);
for (const statement of ast.statements) {
parseStatement(statement);
}
};
const parseStatement = (statement: Statement) => {
switch (statement.$type) {
case 'Commit':
parseCommit(statement);
break;
case 'Branch':
parseBranch(statement);
break;
case 'Merge':
parseMerge(statement);
break;
case 'Checkout':
parseCheckout(statement);
break;
case 'CherryPicking':
parseCherryPicking(statement);
break;
default:
throw new Error(`Unknown statement type: ${(statement as any).$type}`);
}
};
function parseCommit(commit: CommitAst) {
const message = commit.message ?? '';
db.commit(message, commit.id, commit.tags, commit.type);
}
function parseBranch(branch: Branch) {
db.branch(branch.name, branch.order);
}
function parseMerge(merge: Merge) {
db.merge(merge.branch, merge.id, merge.tags, merge.type);
}
function parseCheckout(checkout: Checkout) {
db.checkout(checkout.branch);
}
function parseCherryPicking(cherryPicking: CherryPicking) {
db.cherryPick(cherryPicking.id, cherryPicking.tags, cherryPicking.parent);
}
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: GitGraph = await parse('gitGraph', input);
log.debug(ast);
populate(ast);
},
};

View File

@@ -0,0 +1,55 @@
export type CommitType = 'NORMAL' | 'REVERSE' | 'HIGHLIGHT' | 'MERGE' | 'CHERRY_PICK';
export interface Commit {
id: string;
message: string;
seq: number;
type: number;
tag: string;
parents: (string | null)[];
branch: string;
customType?: number;
customId?: string;
}
export interface GitGraph {
statements: Statement[];
}
export type Statement = CommitAst | Branch | Merge | Checkout | CherryPicking;
export interface CommitAst {
$type: 'Commit';
id: string;
message?: string;
tags?: string[];
type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT';
}
export interface Branch {
$type: 'Branch';
name: string;
order?: number;
}
export interface Merge {
$type: 'Merge';
branch: string;
id?: string;
tags?: string[];
type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT';
}
export interface Checkout {
$type: 'Checkout';
branch: string;
}
export interface CherryPicking {
$type: 'CherryPicking';
id: string;
tags?: string[];
parent: string;
}
export type DiagramOrientation = 'LR' | 'TB';

View File

@@ -1,6 +1,7 @@
grammar GitGraph grammar GitGraph
import "../common/common"; import "../common/common";
entry GitGraph: entry GitGraph:
NEWLINE* NEWLINE*
'gitGraph' Direction? ':'? 'gitGraph' Direction? ':'?
@@ -23,60 +24,43 @@ Statement
; ;
Options:
'options' '{' rawOptions+=STRING* '}' EOL;
Direction: Direction:
dir=('LR' | 'TB' | 'BT') EOL; dir=('LR' | 'TB' | 'BT') EOL;
Options:
'options' '{' rawOptions+=STRING* '}' EOL;
Commit: Commit:
'commit' properties+=CommitProperty* EOL; 'commit'
(
CommitProperty 'id:' id=STRING
: CommitId |'msg:'? message=STRING
| CommitMessage |'tag:' tags=STRING
| Tags |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
| CommitType )* EOL;
;
CommitId:
'id:' id=STRING;
CommitMessage:
'msg:'? message=STRING;
Tags:
'tag:' tags=STRING;
CommitType:
'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT');
Branch: Branch:
'branch' name=(ID|STRING) ('order:' order=INT)? EOL; 'branch' name=(ID|STRING)
('order:' order=INT)?
EOL;
Merge: Merge:
'merge' name=(ID|STRING) properties+=MergeProperties* EOL; 'merge' branch=(ID|STRING)
(
MergeProperties 'id:' id=STRING
: CommitId |'tag:' tags=STRING
| Tags |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
| CommitType )* EOL;
;
Checkout: Checkout:
('checkout'|'switch') id=(ID|STRING) EOL; ('checkout'|'switch') branch=(ID|STRING) EOL;
CherryPicking: CherryPicking:
'cherry-pick' properties+=CherryPickProperties* EOL; 'cherry-pick'
(
CherryPickProperties 'id:' id=STRING
: CommitId |'tag:' tags=STRING
| Tags |'parent:' id=STRING
| ParentCommit )* EOL;
;
ParentCommit:
'parent:' id=STRING;
terminal INT returns number: /[0-9]+(?=\s)/; terminal INT returns number: /[0-9]+(?=\s)/;
terminal ID returns string: /\w([-\./\w]*[-\w])?/; terminal ID returns string: /\w([-\./\w]*[-\w])?/;

View File

@@ -10,90 +10,9 @@ describe('gitGraph', () => {
expect(result.value.statements).toHaveLength(0); expect(result.value.statements).toHaveLength(0);
}); });
it('should handle multiple commits', () => { it('should handle gitGraph with one statement', () => {
const result = parse(` const result = parse(`gitGraph\n A`);
gitGraph
commit
commit
`);
expect(result.value.$type).toBe(GitGraph); expect(result.value.$type).toBe(GitGraph);
expect(result.value.statements).toHaveLength(2);
expect(
result.value.statements.every((s: { $type: string }) => s.$type === 'Commit')
).toBeTruthy();
});
it('should handle branches and checkouts', () => {
const result = parse(`
gitGraph
branch feature
branch release
checkout feature
`);
expect(result.value.statements).toHaveLength(3);
expect(result.value.statements[0].$type).toBe('Branch');
expect(result.value.statements[0].name).toBe('feature');
expect(result.value.statements[1].$type).toBe('Branch');
expect(result.value.statements[1].name).toBe('release');
expect(result.value.statements[2].$type).toBe('Checkout');
expect(result.value.statements[2].id).toBe('feature');
});
it('should handle merges', () => {
const result = parse(`
gitGraph
branch feature
commit id: "A"
merge feature id: "M"
`);
expect(result.value.statements).toHaveLength(3);
expect(result.value.statements[2].$type).toBe('Merge');
expect(result.value.statements[2].name).toBe('feature');
expect(result.value.statements[2].properties[0].id).toBe('M');
});
it('should handle cherry-picking with tags and parent', () => {
const result = parse(`
gitGraph
branch feature
commit id: "M"
checkout main
cherry-pick id: "M" tag: "v2.1:ZERO" parent:"ZERO"
`);
expect(result.value.statements).toHaveLength(4);
expect(result.value.statements[3].$type).toBe('CherryPicking');
expect(result.value.statements[3].properties.length).toBe(3);
expect(result.value.statements[3].properties[0].id).toBe('M');
expect(result.value.statements[3].properties[1].tags).toBe('v2.1:ZERO');
expect(result.value.statements[3].properties[2].id).toBe('ZERO');
});
it('should parse complex gitGraph interactions', () => {
const result = parse(`
gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
commit id: "C"
cherry-pick id: "M" tag: "v2.1:ZERO" parent:"ZERO"
commit id: "D"
`);
expect(result.value.statements).toHaveLength(12);
expect(result.value.statements[0].$type).toBe('Commit');
expect(result.value.statements[0].properties[0].id).toBe('ZERO');
expect(result.value.statements[1].$type).toBe('Branch');
expect(result.value.statements[6].$type).toBe('Merge');
expect(result.value.statements[10].$type).toBe('CherryPicking');
expect(result.value.statements[10].properties[0].id).toBe('M');
expect(result.value.statements[10].properties[2].id).toBe('ZERO');
expect(result.value.statements[11].$type).toBe('Commit');
expect(result.value.statements[11].properties[0].id).toBe('D');
}); });
}); });
}); });