From 1d0e98dd62541f1aeea6b1030a48aff559aae9ae Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sun, 21 Jul 2024 19:50:59 -0400 Subject: [PATCH 01/49] Added the langium module for gitGraph --- .../src/diagrams/git/parser/gitGraph.jison | 1 + packages/parser/langium-config.json | 5 + .../src/language/gitGraph/gitGraph.langium | 84 ++++++++++++++++ .../parser/src/language/gitGraph/index.ts | 1 + .../parser/src/language/gitGraph/module.ts | 53 ++++++++++ .../src/language/gitGraph/tokenBuilder.ts | 7 ++ packages/parser/src/language/index.ts | 10 ++ packages/parser/src/parse.ts | 11 ++- packages/parser/tests/gitGraph.test.ts | 99 +++++++++++++++++++ packages/parser/tests/test-util.ts | 26 ++++- 10 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 packages/parser/src/language/gitGraph/gitGraph.langium create mode 100644 packages/parser/src/language/gitGraph/index.ts create mode 100644 packages/parser/src/language/gitGraph/module.ts create mode 100644 packages/parser/src/language/gitGraph/tokenBuilder.ts create mode 100644 packages/parser/tests/gitGraph.test.ts diff --git a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison index fa2c70586d..56ba1465e7 100644 --- a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison +++ b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison @@ -138,6 +138,7 @@ mergeStatement | 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)} diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index c750f049d5..af8a4cfe6e 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -15,6 +15,11 @@ "id": "pie", "grammar": "src/language/pie/pie.langium", "fileExtensions": [".mmd", ".mermaid"] + }, + { + "id": "gitGraph", + "grammar": "src/language/gitGraph/gitGraph.langium", + "fileExtensions": [".mmd", ".mermaid"] } ], "mode": "production", diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium new file mode 100644 index 0000000000..4956a80a43 --- /dev/null +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -0,0 +1,84 @@ +grammar GitGraph + +import "../common/common"; +entry GitGraph: + NEWLINE* + 'gitGraph' Direction? ':'? + NEWLINE* + ( + Options? + NEWLINE* + (TitleAndAccessibilities | + statements+=Statement | + NEWLINE)* + ) +; + +Statement +: Commit +| Branch +| Merge +| Checkout +| CherryPicking +; + + +Options: + 'options' '{' rawOptions+=STRING* '}' EOL; + +Direction: + dir=('LR' | 'TB' | 'BT') EOL; + +Commit: + 'commit' properties+=CommitProperty* EOL; + +CommitProperty +: CommitId +| CommitMessage +| Tags +| CommitType +; + +CommitId: + 'id:' id=STRING; + +CommitMessage: + 'msg:'? message=STRING; + +Tags: + 'tag:' tags=STRING; + +CommitType: + 'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT'); + +Branch: + 'branch' name=(ID|STRING) ('order:' order=INT)? EOL; + +Merge: + 'merge' name=(ID|STRING) properties+=MergeProperties* EOL; + +MergeProperties +: CommitId +| Tags +| CommitType +; + +Checkout: + ('checkout'|'switch') id=(ID|STRING) EOL; + +CherryPicking: + 'cherry-pick' properties+=CherryPickProperties* EOL; + +CherryPickProperties +: CommitId +| Tags +| ParentCommit +; + +ParentCommit: + 'parent:' id=STRING; + +terminal INT returns number: /[0-9]+(?=\s)/; +terminal ID returns string: /\w([-\./\w]*[-\w])?/; +terminal STRING: /"[^"]*"|'[^']*'/; + diff --git a/packages/parser/src/language/gitGraph/index.ts b/packages/parser/src/language/gitGraph/index.ts new file mode 100644 index 0000000000..fd3c604b08 --- /dev/null +++ b/packages/parser/src/language/gitGraph/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/gitGraph/module.ts b/packages/parser/src/language/gitGraph/module.ts new file mode 100644 index 0000000000..1630377e55 --- /dev/null +++ b/packages/parser/src/language/gitGraph/module.ts @@ -0,0 +1,53 @@ +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 }; +} diff --git a/packages/parser/src/language/gitGraph/tokenBuilder.ts b/packages/parser/src/language/gitGraph/tokenBuilder.ts new file mode 100644 index 0000000000..ccadf1a1f6 --- /dev/null +++ b/packages/parser/src/language/gitGraph/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { AbstractMermaidTokenBuilder } from '../common/index.js'; + +export class GitGraphTokenBuilder extends AbstractMermaidTokenBuilder { + public constructor() { + super(['gitGraph']); + } +} diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index 9f1d92ba8a..3770137951 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -5,20 +5,30 @@ export { PacketBlock, Pie, PieSection, + GitGraph, + Branch, + Commit, + Merge, isCommon, isInfo, isPacket, isPacketBlock, isPie, isPieSection, + isGitGraph, + isBranch, + isCommit, + isMerge, } from './generated/ast.js'; export { InfoGeneratedModule, MermaidGeneratedSharedModule, PacketGeneratedModule, PieGeneratedModule, + GitGraphGeneratedModule, } from './generated/module.js'; +export * from './gitGraph/index.js'; export * from './common/index.js'; export * from './info/index.js'; export * from './packet/index.js'; diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 992b965060..233faed00c 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,8 @@ 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 = {}; const initializers = { @@ -21,11 +21,18 @@ const initializers = { const parser = createPieServices().Pie.parser.LangiumParser; parsers.pie = parser; }, + gitGraph: async () => { + const { createGitGraphServices } = await import('./language/gitGraph/index.js'); + const parser = createGitGraphServices().GitGraph.parser.LangiumParser; + parsers.gitGraph = parser; + }, } as const; export async function parse(diagramType: 'info', text: string): Promise; export async function parse(diagramType: 'packet', text: string): Promise; export async function parse(diagramType: 'pie', text: string): Promise; +export async function parse(diagramType: 'gitGraph', text: string): Promise; + export async function parse( diagramType: keyof typeof initializers, text: string diff --git a/packages/parser/tests/gitGraph.test.ts b/packages/parser/tests/gitGraph.test.ts new file mode 100644 index 0000000000..e00ddfae83 --- /dev/null +++ b/packages/parser/tests/gitGraph.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest'; +import { GitGraph } from '../src/language/index.js'; +import { gitGraphParse as parse } from './test-util.js'; + +describe('gitGraph', () => { + describe('Basic Parsing', () => { + it('should handle empty gitGraph', () => { + const result = parse(`gitGraph`); + expect(result.value.$type).toBe(GitGraph); + expect(result.value.statements).toHaveLength(0); + }); + + it('should handle multiple commits', () => { + const result = parse(` + gitGraph + commit + commit + `); + 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'); + }); + }); +}); diff --git a/packages/parser/tests/test-util.ts b/packages/parser/tests/test-util.ts index 9bdec348a1..5cb487758b 100644 --- a/packages/parser/tests/test-util.ts +++ b/packages/parser/tests/test-util.ts @@ -1,7 +1,18 @@ import type { LangiumParser, ParseResult } from 'langium'; import { expect, vi } from 'vitest'; -import type { Info, InfoServices, Pie, PieServices } from '../src/language/index.js'; -import { createInfoServices, createPieServices } from '../src/language/index.js'; +import type { + 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); @@ -40,3 +51,14 @@ export function createPieTestServices() { return { services: pieServices, 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(input); + }; + + return { services: gitGraphServices, parse }; +} +export const gitGraphParse = createGitGraphTestServices().parse; From 6f7c291512ad54093f2e42beac86126bdf4fbc67 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Mon, 22 Jul 2024 04:10:36 -0400 Subject: [PATCH 02/49] Added gitGraphAst as typescript added gitGraphTypes and gitGraphParser --- .../git/{gitGraphAst.js => gitGraphAst.ts} | 265 +++++++++++------- .../src/diagrams/git/gitGraphParser.ts | 72 +++++ .../mermaid/src/diagrams/git/gitGraphTypes.ts | 55 ++++ .../src/language/gitGraph/gitGraph.langium | 70 ++--- packages/parser/tests/gitGraph.test.ts | 85 +----- 5 files changed, 312 insertions(+), 235 deletions(-) rename packages/mermaid/src/diagrams/git/{gitGraphAst.js => gitGraphAst.ts} (64%) create mode 100644 packages/mermaid/src/diagrams/git/gitGraphParser.ts create mode 100644 packages/mermaid/src/diagrams/git/gitGraphTypes.ts diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.js b/packages/mermaid/src/diagrams/git/gitGraphAst.ts similarity index 64% rename from packages/mermaid/src/diagrams/git/gitGraphAst.js rename to packages/mermaid/src/diagrams/git/gitGraphAst.ts index cebc4fc3ef..b04e78d8c5 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.js +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -11,16 +11,20 @@ import { setDiagramTitle, getDiagramTitle, } from '../common/commonDb.js'; +import defaultConfig from '../../defaultConfig.js'; +import type { DiagramOrientation, Commit } from './gitGraphTypes.js'; -let { mainBranchName, mainBranchOrder } = getConfig().gitGraph; -let commits = new Map(); -let head = null; -let branchesConfig = new Map(); +const mainBranchName = defaultConfig.gitGraph.mainBranchName; +const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; + +let commits = new Map(); +let head: Commit | null = null; +let branchesConfig = new Map(); branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); -let branches = new Map(); -branches.set(mainBranchName, head); +let branches = new Map(); +branches.set(mainBranchName, null); let curBranch = mainBranchName; -let direction = 'LR'; +let direction: DiagramOrientation = 'LR'; let seq = 0; /** @@ -57,8 +61,8 @@ function getId() { // } /** - * @param currentCommit - * @param otherCommit + * @param currentCommit - current commit + * @param otherCommit - other commit */ // function isReachableFrom(currentCommit, otherCommit) { // const currentSeq = currentCommit.seq; @@ -68,10 +72,10 @@ function getId() { // } /** - * @param list - * @param fn + * @param list - list of items + * @param fn - function to get the key */ -function uniqBy(list, fn) { +function uniqBy(list: any[], fn: (item: any) => any) { const recordMap = Object.create(null); return list.reduce((out, 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; }; + let options = {}; -export const setOptions = function (rawOptString) { +export const setOptions = function (rawOptString: string) { log.debug('options str', rawOptString); rawOptString = rawOptString?.trim(); rawOptString = rawOptString || '{}'; try { options = JSON.parse(rawOptString); - } catch (e) { + } catch (e: any) { log.error('error while parsing gitGraph options', e.message); } }; @@ -102,60 +107,65 @@ 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 = { +export const commit = function (msg: string, id: string, type: number, tag: string) { + log.info('commit', msg, id, type, tag); + log.debug('Entering commit:', msg, id, type, tag); + id = common.sanitizeText(id, getConfig()); + msg = common.sanitizeText(msg, getConfig()); + tag = common.sanitizeText(tag, getConfig()); + const newCommit: Commit = { id: id ? id : seq + '-' + getId(), message: msg, seq: seq++, - type: type ? type : commitType.NORMAL, - tags: tags ?? [], + type: type, + tag: tag ? tag : '', 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); + head = newCommit; + log.info('main branch', mainBranchName); + commits.set(newCommit.id, newCommit); + 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()); if (!branches.has(name)) { 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); 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 + - '")' + throw new Error( + `Trying to create an existing branch: ${name}. Use 'checkout ${name}' instead.` ); - 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)); +export const merge = ( + otherBranch: string, + custom_id?: string, + override_type?: number, + custom_tag?: string +): void => { + 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) { - 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 = { text: 'merge ' + otherBranch, token: 'merge ' + otherBranch, @@ -165,7 +175,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag }; throw error; } else if (currentCommit === undefined || !currentCommit) { - let error = new Error( + const error: any = new Error( 'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits' ); error.hash = { @@ -177,7 +187,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag }; throw error; } 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' ); error.hash = { @@ -189,7 +199,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag }; throw error; } 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' ); error.hash = { @@ -201,7 +211,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag }; throw error; } 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 = { text: 'merge ' + otherBranch, token: 'merge ' + otherBranch, @@ -211,23 +221,24 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag }; throw error; } else if (custom_id && commits.has(custom_id)) { - let error = new Error( + const error: any = 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(','), + text: 'merge ' + otherBranch + custom_id + override_type + custom_tag, + token: 'merge ' + otherBranch + custom_id + override_type + custom_tag, 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(',')}`, + 'merge ' + otherBranch + ' ' + custom_id + '_UNIQUE ' + override_type + ' ' + custom_tag, ], }; throw error; } + // if (isReachableFrom(currentCommit, otherCommit)) { // log.debug('Already merged'); // return; @@ -237,16 +248,19 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag // head = commits.get(branches.get(curBranch)); // } else { // 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(), message: 'merged branch ' + otherBranch + ' into ' + curBranch, seq: seq++, - parents: [head == null ? null : head.id, branches.get(otherBranch)], + parents: [head == null ? null : head.id, verifiedBranch], branch: curBranch, type: commitType.MERGE, - customType: override_type, - customId: custom_id ? true : false, - tags: custom_tags ? custom_tags : [], + customType: override_type, //TODO - need to make customType optional + customId: custom_id, //TODO - need to make customId optional as well as tag + tag: custom_tag ? custom_tag : '', }; head = 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'); }; -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); +export const cherryPick = function ( + sourceId: string, + targetId: string, + tag: string, + parentCommitId: string +) { + 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)) { - let error = new Error( + const error: any = new Error( 'Incorrect usage of "cherryPick". Source commit id should exist and provided' ); error.hash = { @@ -277,19 +295,22 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { }; throw error; } - let sourceCommit = commits.get(sourceId); - let sourceCommitBranch = sourceCommit.branch; + + const sourceCommit = commits.get(sourceId); + if ( - parentCommitId && - !(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) + !sourceCommit || + !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.' ); - throw error; } + const sourceCommitBranch = sourceCommit.branch; 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.' ); throw error; @@ -298,7 +319,7 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { // cherry-pick source commit to current branch if (sourceCommitBranch === curBranch) { - let error = new Error( + const error: any = new Error( 'Incorrect usage of "cherryPick". Source commit is already on current branch' ); error.hash = { @@ -310,9 +331,24 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { }; 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) { - let error = new Error( + const error: any = new Error( 'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits' ); error.hash = { @@ -326,18 +362,16 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { } const commit = { id: seq + '-' + getId(), - message: 'cherry-picked ' + sourceCommit + ' into ' + curBranch, + message: 'cherry-picked ' + sourceCommit?.message + ' 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}` : '' - }`, - ], + tag: + tag ?? + `cherry-pick:${sourceCommit.id}${ + sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' + }`, }; head = commit; commits.set(commit.id, commit); @@ -346,10 +380,10 @@ export const cherryPick = function (sourceId, targetId, tags, parentCommitId) { log.debug('in cherryPick'); } }; -export const checkout = function (branch) { +export const checkout = function (branch: string) { branch = common.sanitizeText(branch, getConfig()); 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 + '")' ); error.hash = { @@ -360,10 +394,19 @@ export const checkout = function (branch) { expected: ['"branch ' + branch + '"'], }; throw error; + //branches[branch] = head != null ? head.id : null; + //log.debug('in createBranch'); } else { curBranch = branch; 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 key - * @param newVal + * @param arr - array + * @param key - key + * @param newVal - new value */ -function upsert(arr, key, newVal) { +function upsert(arr: any[], key: any, newVal: any) { const index = arr.indexOf(key); if (index === -1) { arr.push(newVal); @@ -400,8 +443,8 @@ function upsert(arr, key, newVal) { } } -/** @param commitArr */ -function prettyPrintCommitHistory(commitArr) { +/** @param commitArr - array */ +function prettyPrintCommitHistory(commitArr: Commit[]) { const commit = commitArr.reduce((out, commit) => { if (out.seq > commit.seq) { return out; @@ -417,21 +460,25 @@ function prettyPrintCommitHistory(commitArr) { } }); const label = [line, commit.id, commit.seq]; - for (let branch in branches) { + for (const branch in branches) { if (branches.get(branch) === commit.id) { label.push(branch); } } 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]); 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) { return; } else { - const nextCommit = commits.get(commit.parents); - upsert(commitArr, commit, nextCommit); + if (commit.parents[0]) { + const newCommit = commits.get(commit.parents[0]); + upsert(commitArr, commit, newCommit); + } } commitArr = uniqBy(commitArr, (c) => c.id); prettyPrintCommitHistory(commitArr); @@ -446,12 +493,13 @@ export const prettyPrint = function () { export const clear = function () { commits = new Map(); head = null; - const { mainBranchName, mainBranchOrder } = getConfig().gitGraph; + const mainBranch = defaultConfig.gitGraph.mainBranchName; + const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; branches = new Map(); - branches.set(mainBranchName, null); + branches.set(mainBranch, null); branchesConfig = new Map(); - branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); - curBranch = mainBranchName; + branchesConfig.set(mainBranch, { name: mainBranch, order: mainBranchOrder }); + curBranch = mainBranch; seq = 0; commonClear(); }; @@ -464,7 +512,7 @@ export const getBranchesAsObjArray = function () { } return { ...branchConfig, - order: parseFloat(`0.${i}`, 10), + order: parseFloat(`0.${i}`), }; }) .sort((a, b) => a.order - b.order) @@ -531,5 +579,4 @@ export default { setAccDescription, setDiagramTitle, getDiagramTitle, - commitType, }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts new file mode 100644 index 0000000000..713992e0fa --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -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 => { + const ast: GitGraph = await parse('gitGraph', input); + log.debug(ast); + populate(ast); + }, +}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts new file mode 100644 index 0000000000..da7567b8d5 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -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'; diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 4956a80a43..88adaf3f71 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -1,6 +1,7 @@ grammar GitGraph import "../common/common"; + entry GitGraph: NEWLINE* 'gitGraph' Direction? ':'? @@ -23,60 +24,43 @@ Statement ; -Options: - 'options' '{' rawOptions+=STRING* '}' EOL; - Direction: dir=('LR' | 'TB' | 'BT') EOL; -Commit: - 'commit' properties+=CommitProperty* EOL; - -CommitProperty -: CommitId -| CommitMessage -| Tags -| CommitType -; - -CommitId: - 'id:' id=STRING; - -CommitMessage: - 'msg:'? message=STRING; - -Tags: - 'tag:' tags=STRING; - -CommitType: - 'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT'); +Options: + 'options' '{' rawOptions+=STRING* '}' EOL; +Commit: + 'commit' + ( + 'id:' id=STRING + |'msg:'? message=STRING + |'tag:' tags=STRING + |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + )* EOL; Branch: - 'branch' name=(ID|STRING) ('order:' order=INT)? EOL; + 'branch' name=(ID|STRING) + ('order:' order=INT)? + EOL; Merge: - 'merge' name=(ID|STRING) properties+=MergeProperties* EOL; - -MergeProperties -: CommitId -| Tags -| CommitType -; + 'merge' branch=(ID|STRING) + ( + 'id:' id=STRING + |'tag:' tags=STRING + |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + )* EOL; Checkout: - ('checkout'|'switch') id=(ID|STRING) EOL; + ('checkout'|'switch') branch=(ID|STRING) EOL; CherryPicking: - 'cherry-pick' properties+=CherryPickProperties* EOL; - -CherryPickProperties -: CommitId -| Tags -| ParentCommit -; - -ParentCommit: - 'parent:' id=STRING; + 'cherry-pick' + ( + 'id:' id=STRING + |'tag:' tags=STRING + |'parent:' id=STRING + )* EOL; terminal INT returns number: /[0-9]+(?=\s)/; terminal ID returns string: /\w([-\./\w]*[-\w])?/; diff --git a/packages/parser/tests/gitGraph.test.ts b/packages/parser/tests/gitGraph.test.ts index e00ddfae83..aff69977aa 100644 --- a/packages/parser/tests/gitGraph.test.ts +++ b/packages/parser/tests/gitGraph.test.ts @@ -10,90 +10,9 @@ describe('gitGraph', () => { expect(result.value.statements).toHaveLength(0); }); - it('should handle multiple commits', () => { - const result = parse(` - gitGraph - commit - commit - `); + it('should handle gitGraph with one statement', () => { + const result = parse(`gitGraph\n A`); 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'); }); }); }); From 5460bc0a0c46dcd6dda345357d7ac418c9c20a74 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Wed, 24 Jul 2024 07:13:10 -0400 Subject: [PATCH 03/49] fixed checkout branch with no commits --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index b04e78d8c5..9a26d32f7d 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -399,14 +399,11 @@ export const checkout = function (branch: string) { } else { curBranch = branch; const id = branches.get(curBranch); - - 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'); + if (id === undefined || !id) { + head = null; + } else { + head = commits.get(id) ?? null; } - head = commits.get(id) ?? null; } }; From d2e2017907913210e5b4c5ff39f518926dc71df1 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Wed, 24 Jul 2024 09:48:46 -0400 Subject: [PATCH 04/49] fixed undefined for type errors --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 64 +++++++++++-------- .../src/diagrams/git/gitGraphParser.ts | 53 +-------------- .../mermaid/src/diagrams/git/gitGraphTypes.ts | 4 +- 3 files changed, 41 insertions(+), 80 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 9a26d32f7d..8148e02492 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -19,7 +19,7 @@ const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; let commits = new Map(); let head: Commit | null = null; -let branchesConfig = new Map(); +let branchesConfig = new Map(); branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); let branches = new Map(); branches.set(mainBranchName, null); @@ -107,18 +107,19 @@ export const getOptions = function () { return options; }; -export const commit = function (msg: string, id: string, type: number, tag: string) { - log.info('commit', msg, id, type, tag); - log.debug('Entering commit:', msg, id, type, tag); +export const commit = function (msg: string, id: string, type: number, tags: string[] | undefined) { + log.info('commit', msg, id, type, tags); + log.debug('Entering commit:', msg, id, type, tags); id = common.sanitizeText(id, getConfig()); msg = common.sanitizeText(msg, getConfig()); - tag = common.sanitizeText(tag, getConfig()); + const config = getConfig(); + tags = tags?.map((tag) => common.sanitizeText(tag, config)); const newCommit: Commit = { id: id ? id : seq + '-' + getId(), message: msg, seq: seq++, - type: type, - tag: tag ? tag : '', + type: type ? type : commitType.NORMAL, + tags: tags ? tags : [], parents: head == null ? [] : [head.id], branch: curBranch, }; @@ -129,7 +130,7 @@ export const commit = function (msg: string, id: string, type: number, tag: stri log.debug('in pushCommit ' + newCommit.id); }; -export const branch = function (name: string, order: number) { +export const branch = function (name: string, order: number | undefined) { name = common.sanitizeText(name, getConfig()); if (!branches.has(name)) { branches.set(name, head != null ? head.id : null); @@ -138,7 +139,7 @@ export const branch = function (name: string, order: number) { log.debug('in createBranch'); } else { throw new Error( - `Trying to create an existing branch: ${name}. Use 'checkout ${name}' instead.` + `Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${name}" to switch to an existing branch)` ); } }; @@ -147,7 +148,7 @@ export const merge = ( otherBranch: string, custom_id?: string, override_type?: number, - custom_tag?: string + custom_tags?: string[] ): void => { otherBranch = common.sanitizeText(otherBranch, getConfig()); if (custom_id) { @@ -227,12 +228,19 @@ export const merge = ( ' already exists, use different custom Id' ); error.hash = { - text: 'merge ' + otherBranch + custom_id + override_type + custom_tag, - token: 'merge ' + otherBranch + custom_id + override_type + custom_tag, + 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_tag, + 'merge ' + + otherBranch + + ' ' + + custom_id + + '_UNIQUE ' + + override_type + + ' ' + + custom_tags?.join(' '), ], }; @@ -258,9 +266,9 @@ export const merge = ( parents: [head == null ? null : head.id, verifiedBranch], branch: curBranch, type: commitType.MERGE, - customType: override_type, //TODO - need to make customType optional - customId: custom_id, //TODO - need to make customId optional as well as tag - tag: custom_tag ? custom_tag : '', + customType: override_type, + customId: custom_id ? true : false, + tags: custom_tags ? custom_tags : [], }; head = commit; commits.set(commit.id, commit); @@ -273,13 +281,14 @@ export const merge = ( export const cherryPick = function ( sourceId: string, targetId: string, - tag: string, + tags: string[], parentCommitId: string ) { - log.debug('Entering cherryPick:', sourceId, targetId, tag); + log.debug('Entering cherryPick:', sourceId, targetId, tags); sourceId = common.sanitizeText(sourceId, getConfig()); targetId = common.sanitizeText(targetId, getConfig()); - tag = common.sanitizeText(tag, getConfig()); + const config = getConfig(); + tags = tags?.map((tag) => common.sanitizeText(tag, config)); parentCommitId = common.sanitizeText(parentCommitId, getConfig()); if (!sourceId || !commits.has(sourceId)) { @@ -367,11 +376,13 @@ export const cherryPick = function ( parents: [head == null ? null : head.id, sourceCommit.id], branch: curBranch, type: commitType.CHERRY_PICK, - tag: - tag ?? - `cherry-pick:${sourceCommit.id}${ - sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' - }`, + tags: tags + ? tags.filter(Boolean) + : [ + `cherry-pick:${sourceCommit.id}${ + sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' + }`, + ], }; head = commit; commits.set(commit.id, commit); @@ -504,7 +515,7 @@ export const clear = function () { export const getBranchesAsObjArray = function () { const branchesArray = [...branchesConfig.values()] .map((branchConfig, i) => { - if (branchConfig.order !== null) { + if (branchConfig.order !== null && branchConfig.order !== undefined) { return branchConfig; } return { @@ -512,7 +523,7 @@ export const getBranchesAsObjArray = function () { order: parseFloat(`0.${i}`), }; }) - .sort((a, b) => a.order - b.order) + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) .map(({ name }) => ({ name })); return branchesArray; @@ -551,6 +562,7 @@ export const commitType = { }; export default { + commitType, getConfig: () => getConfig().gitGraph, setDirection, setOptions, diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 713992e0fa..d193109b47 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -4,65 +4,14 @@ 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); + log.debug(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 => { const ast: GitGraph = await parse('gitGraph', input); diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index da7567b8d5..92bc3617a5 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -5,11 +5,11 @@ export interface Commit { message: string; seq: number; type: number; - tag: string; + tags: string[] | undefined; parents: (string | null)[]; branch: string; customType?: number; - customId?: string; + customId?: boolean; } export interface GitGraph { From 0b67cffdfa5af394361376a3c028b063aee140e0 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Wed, 24 Jul 2024 10:37:21 -0400 Subject: [PATCH 05/49] fixed cherrypicking tests --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 8148e02492..e12e58f7db 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -139,7 +139,7 @@ export const branch = function (name: string, order: number | undefined) { log.debug('in createBranch'); } else { 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}" to switch to an existing branch)` + `Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${name}")` ); } }; @@ -306,16 +306,17 @@ export const cherryPick = function ( } const sourceCommit = commits.get(sourceId); - + if (sourceCommit === undefined || !sourceCommit) { + throw new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided'); + } if ( - !sourceCommit || - !parentCommitId || - !Array.isArray(sourceCommit.parents) || - !sourceCommit.parents.includes(parentCommitId) + parentCommitId && + !(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) ) { - throw new Error( + 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) { From 1a95d4885282b475ec898e111e031133eda83a5e Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Wed, 24 Jul 2024 13:53:00 -0400 Subject: [PATCH 06/49] added Imperative state --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 265 +++++++++--------- 1 file changed, 127 insertions(+), 138 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index e12e58f7db..13e54ddf40 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -13,19 +13,32 @@ import { } from '../common/commonDb.js'; import defaultConfig from '../../defaultConfig.js'; import type { DiagramOrientation, Commit } from './gitGraphTypes.js'; +import { ImperativeState } from '../../utils/imperativeState.js'; + +interface GitGraphState { + commits: Map; + head: Commit | null; + branchConfig: Map; + branches: Map; + currBranch: string; + direction: DiagramOrientation; + seq: number; + options: any; +} const mainBranchName = defaultConfig.gitGraph.mainBranchName; const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; -let commits = new Map(); -let head: Commit | null = null; -let branchesConfig = new Map(); -branchesConfig.set(mainBranchName, { name: mainBranchName, order: mainBranchOrder }); -let branches = new Map(); -branches.set(mainBranchName, null); -let curBranch = mainBranchName; -let direction: DiagramOrientation = 'LR'; -let seq = 0; +const state = new ImperativeState(() => ({ + 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: {}, +})); /** * @@ -34,11 +47,6 @@ 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; @@ -60,10 +68,6 @@ function getId() { // return currentCommit.id === otherCommit.id; // } -/** - * @param currentCommit - current commit - * @param otherCommit - other commit - */ // function isReachableFrom(currentCommit, otherCommit) { // const currentSeq = currentCommit.seq; // const otherSeq = otherCommit.seq; @@ -88,23 +92,22 @@ function uniqBy(list: any[], fn: (item: any) => any) { } export const setDirection = function (dir: DiagramOrientation) { - direction = dir; + state.records.direction = dir; }; -let options = {}; export const setOptions = function (rawOptString: string) { log.debug('options str', rawOptString); rawOptString = rawOptString?.trim(); rawOptString = rawOptString || '{}'; try { - options = JSON.parse(rawOptString); + state.records.options = JSON.parse(rawOptString); } catch (e: any) { log.error('error while parsing gitGraph options', e.message); } }; export const getOptions = function () { - return options; + return state.records.options; }; export const commit = function (msg: string, id: string, type: number, tags: string[] | undefined) { @@ -115,26 +118,26 @@ export const commit = function (msg: string, id: string, type: number, tags: str const config = getConfig(); tags = tags?.map((tag) => common.sanitizeText(tag, config)); const newCommit: Commit = { - id: id ? id : seq + '-' + getId(), + id: id ? id : state.records.seq + '-' + getId(), message: msg, - seq: seq++, - type: type ? type : commitType.NORMAL, - tags: tags ? tags : [], - parents: head == null ? [] : [head.id], - branch: curBranch, + seq: state.records.seq++, + type: type ?? commitType.NORMAL, + tags: tags ?? [], + parents: state.records.head == null ? [] : [state.records.head.id], + branch: state.records.currBranch, }; - head = newCommit; + state.records.head = newCommit; log.info('main branch', mainBranchName); - commits.set(newCommit.id, newCommit); - branches.set(curBranch, newCommit.id); + 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 (name: string, order: number | undefined) { name = common.sanitizeText(name, getConfig()); - if (!branches.has(name)) { - branches.set(name, head != null ? head.id : null); - branchesConfig.set(name, { name, order }); + if (!state.records.branches.has(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'); } else { @@ -146,30 +149,32 @@ export const branch = function (name: string, order: number | undefined) { export const merge = ( otherBranch: string, - custom_id?: string, - override_type?: number, - custom_tags?: string[] + customId?: string, + overrideType?: number, + customTags?: string[] ): void => { otherBranch = common.sanitizeText(otherBranch, getConfig()); - if (custom_id) { - custom_id = common.sanitizeText(custom_id, getConfig()); + if (customId) { + customId = common.sanitizeText(customId, getConfig()); } - const currentBranchCheck: string | null | undefined = branches.get(curBranch); - const otherBranchCheck: string | null | undefined = branches.get(otherBranch); + const currentBranchCheck: string | null | undefined = state.records.branches.get( + state.records.currBranch + ); + const otherBranchCheck: string | null | undefined = state.records.branches.get(otherBranch); const currentCommit: Commit | undefined = currentBranchCheck - ? commits.get(currentBranchCheck) + ? state.records.commits.get(currentBranchCheck) : undefined; const otherCommit: Commit | undefined = otherBranchCheck - ? commits.get(otherBranchCheck) + ? state.records.commits.get(otherBranchCheck) : undefined; if (currentCommit && otherCommit && currentCommit.branch === otherBranch) { throw new Error(`Cannot merge branch '${otherBranch}' into itself.`); } - if (curBranch === otherBranch) { + 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, + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, line: '1', loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, expected: ['branch abc'], @@ -177,26 +182,26 @@ export const merge = ( throw error; } else if (currentCommit === undefined || !currentCommit) { const error: any = new Error( - 'Incorrect usage of "merge". Current branch (' + curBranch + ')has no commits' + `Incorrect usage of "merge". Current branch (${state.records.currBranch})has no commits` ); error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, + 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)) { + } else 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, + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, line: '1', loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['branch ' + otherBranch], + expected: [`branch ${otherBranch}`], }; throw error; } else if (otherCommit === undefined || !otherCommit) { @@ -204,8 +209,8 @@ export const merge = ( 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits' ); error.hash = { - text: 'merge ' + otherBranch, - token: 'merge ' + otherBranch, + text: `merge ${otherBranch}`, + token: `merge ${otherBranch}`, line: '1', loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, expected: ['"commit"'], @@ -214,33 +219,26 @@ export const merge = ( } else 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, + 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)) { + } else if (customId && state.records.commits.has(customId)) { const error: any = new Error( 'Incorrect usage of "merge". Commit with id:' + - custom_id + + customId + ' 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(' '), + text: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`, + token: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.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(' '), + `merge ${otherBranch} ${customId}_UNIQUE ${overrideType} ${customTags?.join(' ')}`, ], }; @@ -260,21 +258,21 @@ export const merge = ( const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this const commit: Commit = { - id: custom_id ? custom_id : seq + '-' + getId(), - message: 'merged branch ' + otherBranch + ' into ' + curBranch, - seq: seq++, - parents: [head == null ? null : head.id, verifiedBranch], - branch: curBranch, + id: customId ? customId : state.records.seq + '-' + getId(), + message: `merged branch ${otherBranch} into ${state.records.currBranch}`, + seq: state.records.seq++, + parents: [state.records.head == null ? null : state.records.head.id, verifiedBranch], + branch: state.records.currBranch, type: commitType.MERGE, - customType: override_type, - customId: custom_id ? true : false, - tags: custom_tags ? custom_tags : [], + customType: overrideType, + customId: customId ? true : false, + tags: customTags ? customTags : [], }; - head = commit; - commits.set(commit.id, commit); - branches.set(curBranch, commit.id); + state.records.head = commit; + state.records.commits.set(commit.id, commit); + state.records.branches.set(state.records.currBranch, commit.id); // } - log.debug(branches); + log.debug(state.records.branches); log.debug('in mergeBranch'); }; @@ -291,13 +289,13 @@ export const cherryPick = function ( tags = tags?.map((tag) => common.sanitizeText(tag, config)); parentCommitId = common.sanitizeText(parentCommitId, getConfig()); - if (!sourceId || !commits.has(sourceId)) { + 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, + 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'], @@ -305,7 +303,7 @@ export const cherryPick = function ( throw error; } - const sourceCommit = commits.get(sourceId); + 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'); } @@ -325,30 +323,30 @@ export const cherryPick = function ( ); throw error; } - if (!targetId || !commits.has(targetId)) { + if (!targetId || !state.records.commits.has(targetId)) { // cherry-pick source commit to current branch - if (sourceCommitBranch === curBranch) { + 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, + 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 currentCommitId = branches.get(curBranch); + 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 (' + curBranch + ')has no commits' + `Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits` ); error.hash = { - text: 'cherryPick ' + sourceId + ' ' + targetId, - token: 'cherryPick ' + sourceId + ' ' + targetId, + 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'], @@ -356,14 +354,14 @@ export const cherryPick = function ( throw error; } - const currentCommit = commits.get(currentCommitId); + const currentCommit = state.records.commits.get(currentCommitId); if (currentCommit === undefined || !currentCommit) { const error: any = new Error( - 'Incorrect usage of "cherry-pick". Current branch (' + curBranch + ')has no commits' + `Incorrect usage of "cherry-pick". Current branch (${state.records.currBranch})has no commits` ); error.hash = { - text: 'cherryPick ' + sourceId + ' ' + targetId, - token: 'cherryPick ' + sourceId + ' ' + targetId, + 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'], @@ -371,11 +369,11 @@ export const cherryPick = function ( throw error; } const commit = { - id: seq + '-' + getId(), - message: 'cherry-picked ' + sourceCommit?.message + ' into ' + curBranch, - seq: seq++, - parents: [head == null ? null : head.id, sourceCommit.id], - branch: curBranch, + id: state.records.seq + '-' + getId(), + message: `cherry-picked ${sourceCommit?.message} into ${state.records.currBranch}`, + seq: state.records.seq++, + parents: [state.records.head == null ? null : state.records.head.id, sourceCommit.id], + branch: state.records.currBranch, type: commitType.CHERRY_PICK, tags: tags ? tags.filter(Boolean) @@ -385,36 +383,36 @@ export const cherryPick = function ( }`, ], }; - head = commit; - commits.set(commit.id, commit); - branches.set(curBranch, commit.id); - log.debug(branches); + 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 (!branches.has(branch)) { + 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 + '")' + `Trying to checkout branch which is not yet created. (Help try using "branch ${branch}")` ); error.hash = { - text: 'checkout ' + branch, - token: 'checkout ' + branch, + text: `checkout ${branch}`, + token: `checkout ${branch}`, line: '1', loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, - expected: ['"branch ' + branch + '"'], + expected: [`branch ${branch}`], }; throw error; //branches[branch] = head != null ? head.id : null; //log.debug('in createBranch'); } else { - curBranch = branch; - const id = branches.get(curBranch); + state.records.currBranch = branch; + const id = state.records.branches.get(state.records.currBranch); if (id === undefined || !id) { - head = null; + state.records.head = null; } else { - head = commits.get(id) ?? null; + state.records.head = state.records.commits.get(id) ?? null; } } }; @@ -469,23 +467,23 @@ function prettyPrintCommitHistory(commitArr: Commit[]) { } }); const label = [line, commit.id, commit.seq]; - for (const branch in branches) { - if (branches.get(branch) === commit.id) { + 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 = commits.get(commit.parents[0]); + const newCommit = state.records.commits.get(commit.parents[0]); upsert(commitArr, commit, newCommit); if (commit.parents[1]) { - commitArr.push(commits.get(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 = commits.get(commit.parents[0]); + const newCommit = state.records.commits.get(commit.parents[0]); upsert(commitArr, commit, newCommit); } } @@ -494,27 +492,18 @@ function prettyPrintCommitHistory(commitArr: Commit[]) { } export const prettyPrint = function () { - log.debug(commits); + log.debug(state.records.commits); const node = getCommitsArray()[0]; prettyPrintCommitHistory([node]); }; export const clear = function () { - commits = new Map(); - head = null; - const mainBranch = defaultConfig.gitGraph.mainBranchName; - const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; - branches = new Map(); - branches.set(mainBranch, null); - branchesConfig = new Map(); - branchesConfig.set(mainBranch, { name: mainBranch, order: mainBranchOrder }); - curBranch = mainBranch; - seq = 0; + state.reset(); commonClear(); }; export const getBranchesAsObjArray = function () { - const branchesArray = [...branchesConfig.values()] + const branchesArray = [...state.records.branchConfig.values()] .map((branchConfig, i) => { if (branchConfig.order !== null && branchConfig.order !== undefined) { return branchConfig; @@ -531,13 +520,13 @@ export const getBranchesAsObjArray = function () { }; export const getBranches = function () { - return branches; + return state.records.branches; }; export const getCommits = function () { - return commits; + return state.records.commits; }; export const getCommitsArray = function () { - const commitArr = [...commits.values()]; + const commitArr = [...state.records.commits.values()]; commitArr.forEach(function (o) { log.debug(o.id); }); @@ -545,13 +534,13 @@ export const getCommitsArray = function () { return commitArr; }; export const getCurrentBranch = function () { - return curBranch; + return state.records.currBranch; }; export const getDirection = function () { - return direction; + return state.records.direction; }; export const getHead = function () { - return head; + return state.records.head; }; export const commitType = { From d0eadebb991e5f481360ac8b45766ffbd27aae6a Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Thu, 25 Jul 2024 05:25:19 -0400 Subject: [PATCH 07/49] added parser --- .../src/diagrams/git/gitGraphDiagram.ts | 4 +- .../src/diagrams/git/gitGraphParser.ts | 68 ++++++++++++++++++- .../mermaid/src/diagrams/git/gitGraphTypes.ts | 10 +-- .../src/language/gitGraph/gitGraph.langium | 26 ++++++- packages/parser/tests/gitGraph.test.ts | 14 +++- 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts index 2a9efdb59c..e5534140b1 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts @@ -1,12 +1,12 @@ // @ts-ignore: JISON doesn't support types -import gitGraphParser from './parser/gitGraph.jison'; +import { parser } from './gitGraphParser.js'; import gitGraphDb from './gitGraphAst.js'; import gitGraphRenderer from './gitGraphRenderer.js'; import gitGraphStyles from './styles.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { - parser: gitGraphParser, + parser: parser, db: gitGraphDb, renderer: gitGraphRenderer, styles: gitGraphStyles, diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index d193109b47..c9488173dd 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -4,14 +4,78 @@ 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 './gitGraphAst.js'; +import type { + CheckoutAst, + CherryPickingAst, + MergeAst, + CommitAst, + BranchAst, +} from './gitGraphTypes.js'; -const populate = (ast: any) => { +const populate = (ast: GitGraph) => { populateCommonDb(ast, db); for (const statement of ast.statements) { - log.debug(statement); + parseStatement(statement); } }; +const parseStatement = (statement: any) => { + 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: + log.warn(`Unknown statement type`); + } +}; + +const parseCommit = (commit: CommitAst) => { + const id = commit.id; + const message = commit.message ?? ''; + const tags = commit.tags ?? []; + const type = commit.type !== undefined ? commitType[commit.type] : 0; + db.commit(message, id, type, tags); +}; + +const parseBranch = (branch: BranchAst) => { + const name = branch.name; + const order = branch.order ?? 0; + db.branch(name, order); +}; + +const parseMerge = (merge: MergeAst) => { + const branch = merge.branch; + const id = merge.id ?? ''; + const tags = merge.tags ?? []; + const type = merge.type !== undefined ? commitType[merge.type] : 0; + db.merge(branch, id, type, tags); +}; + +const parseCheckout = (checkout: CheckoutAst) => { + const branch = checkout.branch; + db.checkout(branch); +}; + +const parseCherryPicking = (cherryPicking: CherryPickingAst) => { + const id = cherryPicking.id; + const tags = cherryPicking.tags ?? []; + const parent = cherryPicking.parent; + db.cherryPick(id, '', tags, parent); +}; + export const parser: ParserDefinition = { parse: async (input: string): Promise => { const ast: GitGraph = await parse('gitGraph', input); diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 92bc3617a5..36d88fd974 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -16,7 +16,7 @@ export interface GitGraph { statements: Statement[]; } -export type Statement = CommitAst | Branch | Merge | Checkout | CherryPicking; +export type Statement = CommitAst | BranchAst | MergeAst | CheckoutAst | CherryPickingAst; export interface CommitAst { $type: 'Commit'; @@ -26,13 +26,13 @@ export interface CommitAst { type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT'; } -export interface Branch { +export interface BranchAst { $type: 'Branch'; name: string; order?: number; } -export interface Merge { +export interface MergeAst { $type: 'Merge'; branch: string; id?: string; @@ -40,12 +40,12 @@ export interface Merge { type?: 'NORMAL' | 'REVERSE' | 'HIGHLIGHT'; } -export interface Checkout { +export interface CheckoutAst { $type: 'Checkout'; branch: string; } -export interface CherryPicking { +export interface CherryPickingAst { $type: 'CherryPicking'; id: string; tags?: string[]; diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 88adaf3f71..8751dccc15 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -1,6 +1,28 @@ grammar GitGraph -import "../common/common"; +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* @@ -62,6 +84,8 @@ CherryPicking: |'parent:' id=STRING )* EOL; + + terminal INT returns number: /[0-9]+(?=\s)/; terminal ID returns string: /\w([-\./\w]*[-\w])?/; terminal STRING: /"[^"]*"|'[^']*'/; diff --git a/packages/parser/tests/gitGraph.test.ts b/packages/parser/tests/gitGraph.test.ts index aff69977aa..850b34bf9a 100644 --- a/packages/parser/tests/gitGraph.test.ts +++ b/packages/parser/tests/gitGraph.test.ts @@ -8,11 +8,23 @@ describe('gitGraph', () => { const result = parse(`gitGraph`); expect(result.value.$type).toBe(GitGraph); expect(result.value.statements).toHaveLength(0); + expect(result.lexerErrors).toHaveLength(0); + expect(result.parserErrors).toHaveLength(0); }); it('should handle gitGraph with one statement', () => { - const result = parse(`gitGraph\n A`); + const result = parse(`gitGraph\n commit\n`); expect(result.value.$type).toBe(GitGraph); + expect(result.lexerErrors).toHaveLength(0); + expect(result.parserErrors).toHaveLength(0); + expect(result.value.statements).toHaveLength(1); + }); + + it('should handle gitGraph with multiple statements and use accTitle', () => { + const result = parse(`gitGraph\n commit\n commit\n accTitle: title\n commit\n`); + expect(result.value.$type).toBe(GitGraph); + expect(result.lexerErrors).toHaveLength(0); + expect(result.parserErrors).toHaveLength(0); }); }); }); From 1af90946bc1fd7e22169beb65b220565f41f2b4b Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Thu, 25 Jul 2024 06:22:37 -0400 Subject: [PATCH 08/49] fixed options e2e test --- packages/parser/src/language/gitGraph/gitGraph.langium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 8751dccc15..9d08faf639 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -47,7 +47,7 @@ Statement Direction: - dir=('LR' | 'TB' | 'BT') EOL; + dir=('LR' | 'TB' | 'BT'); Options: 'options' '{' rawOptions+=STRING* '}' EOL; From a386bd0b74f7313ca9d33d30be70834c5c4f06c1 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Fri, 26 Jul 2024 22:55:40 -0400 Subject: [PATCH 09/49] fixed tags for gitGraph --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 7 ++++--- packages/mermaid/src/diagrams/git/gitGraphTypes.ts | 2 +- packages/parser/src/language/gitGraph/gitGraph.langium | 7 +++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 13e54ddf40..23cd20d805 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -279,7 +279,7 @@ export const merge = ( export const cherryPick = function ( sourceId: string, targetId: string, - tags: string[], + tags: string[] | undefined, parentCommitId: string ) { log.debug('Entering cherryPick:', sourceId, targetId, tags); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index c9488173dd..ff046a456c 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -45,8 +45,9 @@ const parseStatement = (statement: any) => { const parseCommit = (commit: CommitAst) => { const id = commit.id; const message = commit.message ?? ''; - const tags = commit.tags ?? []; + const tags = commit.tags ?? undefined; const type = commit.type !== undefined ? commitType[commit.type] : 0; + log.info(`Commit: ${id} ${message} ${type}`); db.commit(message, id, type, tags); }; @@ -59,7 +60,7 @@ const parseBranch = (branch: BranchAst) => { const parseMerge = (merge: MergeAst) => { const branch = merge.branch; const id = merge.id ?? ''; - const tags = merge.tags ?? []; + const tags = merge.tags ?? undefined; const type = merge.type !== undefined ? commitType[merge.type] : 0; db.merge(branch, id, type, tags); }; @@ -71,7 +72,7 @@ const parseCheckout = (checkout: CheckoutAst) => { const parseCherryPicking = (cherryPicking: CherryPickingAst) => { const id = cherryPicking.id; - const tags = cherryPicking.tags ?? []; + const tags = cherryPicking.tags ?? undefined; const parent = cherryPicking.parent; db.cherryPick(id, '', tags, parent); }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 36d88fd974..29e2781a97 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -5,7 +5,7 @@ export interface Commit { message: string; seq: number; type: number; - tags: string[] | undefined; + tags: string[]; parents: (string | null)[]; branch: string; customType?: number; diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 9d08faf639..6824762602 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -45,7 +45,6 @@ Statement | CherryPicking ; - Direction: dir=('LR' | 'TB' | 'BT'); @@ -57,7 +56,7 @@ Commit: ( 'id:' id=STRING |'msg:'? message=STRING - |'tag:' tags=STRING + |'tag:' tags+=STRING |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') )* EOL; Branch: @@ -69,7 +68,7 @@ Merge: 'merge' branch=(ID|STRING) ( 'id:' id=STRING - |'tag:' tags=STRING + |'tag:' tags+=STRING |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') )* EOL; @@ -80,7 +79,7 @@ CherryPicking: 'cherry-pick' ( 'id:' id=STRING - |'tag:' tags=STRING + |'tag:' tags+=STRING |'parent:' id=STRING )* EOL; From 887e5803d8e6f01c82be21faca4d2f3cd86607ea Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Fri, 26 Jul 2024 23:28:07 -0400 Subject: [PATCH 10/49] fixed some features and added propper default direction --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 1 + packages/parser/src/language/gitGraph/gitGraph.langium | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 23cd20d805..b0a5ffd317 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -35,7 +35,7 @@ const state = new ImperativeState(() => ({ branchConfig: new Map([[mainBranchName, { name: mainBranchName, order: mainBranchOrder }]]), branches: new Map([[mainBranchName, null]]), currBranch: mainBranchName, - direction: 'LR', + direction: 'TB', seq: 0, options: {}, })); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index ff046a456c..8116d7d4cd 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -46,6 +46,7 @@ const parseCommit = (commit: CommitAst) => { const id = commit.id; const message = commit.message ?? ''; const tags = commit.tags ?? undefined; + log.info(`Commit type`, commit.type); const type = commit.type !== undefined ? commitType[commit.type] : 0; log.info(`Commit: ${id} ${message} ${type}`); db.commit(message, id, type, tags); diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 6824762602..9c97e6e84a 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -57,7 +57,7 @@ Commit: 'id:' id=STRING |'msg:'? message=STRING |'tag:' tags+=STRING - |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + |'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') )* EOL; Branch: 'branch' name=(ID|STRING) From 3168084cf5987384c9adcd3d96a4f49d1b15c831 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 27 Jul 2024 00:03:59 -0400 Subject: [PATCH 11/49] fixed rendering --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 7 +++++-- packages/parser/src/language/gitGraph/gitGraph.langium | 4 ---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index b0a5ffd317..23cd20d805 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -35,7 +35,7 @@ const state = new ImperativeState(() => ({ branchConfig: new Map([[mainBranchName, { name: mainBranchName, order: mainBranchOrder }]]), branches: new Map([[mainBranchName, null]]), currBranch: mainBranchName, - direction: 'TB', + direction: 'LR', seq: 0, options: {}, })); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 8116d7d4cd..b657b94b83 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -15,6 +15,11 @@ import type { const populate = (ast: GitGraph) => { 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); } @@ -46,9 +51,7 @@ const parseCommit = (commit: CommitAst) => { const id = commit.id; const message = commit.message ?? ''; const tags = commit.tags ?? undefined; - log.info(`Commit type`, commit.type); const type = commit.type !== undefined ? commitType[commit.type] : 0; - log.info(`Commit: ${id} ${message} ${type}`); db.commit(message, id, type, tags); }; diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 9c97e6e84a..176e6ee7ef 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -29,7 +29,6 @@ entry GitGraph: 'gitGraph' Direction? ':'? NEWLINE* ( - Options? NEWLINE* (TitleAndAccessibilities | statements+=Statement | @@ -48,9 +47,6 @@ Statement Direction: dir=('LR' | 'TB' | 'BT'); -Options: - 'options' '{' rawOptions+=STRING* '}' EOL; - Commit: 'commit' ( From 9f6a7b79ac31f7577ce9fb9c8c727b064eb9b658 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:49:07 -0400 Subject: [PATCH 12/49] allows for custom merge type --- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index b657b94b83..9ef5ae0630 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -65,7 +65,7 @@ const parseMerge = (merge: MergeAst) => { const branch = merge.branch; const id = merge.id ?? ''; const tags = merge.tags ?? undefined; - const type = merge.type !== undefined ? commitType[merge.type] : 0; + const type = merge.type !== undefined ? commitType[merge.type] : undefined; db.merge(branch, id, type, tags); }; From 275dbe9b2edf29504d240afce7adfb3612961d19 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 27 Jul 2024 02:02:12 -0400 Subject: [PATCH 13/49] fixed all rendering differences --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 ++ packages/mermaid/src/diagrams/git/gitGraphParser.ts | 2 +- packages/parser/src/language/gitGraph/gitGraph.langium | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 23cd20d805..f31689452e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -287,6 +287,7 @@ export const cherryPick = function ( targetId = common.sanitizeText(targetId, getConfig()); const config = getConfig(); tags = tags?.map((tag) => common.sanitizeText(tag, config)); + parentCommitId = common.sanitizeText(parentCommitId, getConfig()); if (!sourceId || !state.records.commits.has(sourceId)) { @@ -383,6 +384,7 @@ export const cherryPick = function ( }`, ], }; + state.records.head = commit; state.records.commits.set(commit.id, commit); state.records.branches.set(state.records.currBranch, commit.id); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 9ef5ae0630..9a5cbcb2bd 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -76,7 +76,7 @@ const parseCheckout = (checkout: CheckoutAst) => { const parseCherryPicking = (cherryPicking: CherryPickingAst) => { const id = cherryPicking.id; - const tags = cherryPicking.tags ?? undefined; + const tags = cherryPicking.tags?.length === 0 ? undefined : cherryPicking.tags; const parent = cherryPicking.parent; db.cherryPick(id, '', tags, parent); }; diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 176e6ee7ef..807f9382b7 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -65,7 +65,7 @@ Merge: ( 'id:' id=STRING |'tag:' tags+=STRING - |'type:' name=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') + |'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT') )* EOL; Checkout: @@ -76,7 +76,7 @@ CherryPicking: ( 'id:' id=STRING |'tag:' tags+=STRING - |'parent:' id=STRING + |'parent:' parent=STRING )* EOL; From 0d4c3e5f72ba63d883776ca491a9b6683c018e4d Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 27 Jul 2024 03:51:28 -0400 Subject: [PATCH 14/49] added unit tests for gitGraph parser --- packages/parser/src/language/index.ts | 1 + packages/parser/tests/gitGraph.test.ts | 217 ++++++++++++++++++++++--- 2 files changed, 198 insertions(+), 20 deletions(-) diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index 3770137951..8e8dbce4f7 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -9,6 +9,7 @@ export { Branch, Commit, Merge, + Statement, isCommon, isInfo, isPacket, diff --git a/packages/parser/tests/gitGraph.test.ts b/packages/parser/tests/gitGraph.test.ts index 850b34bf9a..2d7c21bbe8 100644 --- a/packages/parser/tests/gitGraph.test.ts +++ b/packages/parser/tests/gitGraph.test.ts @@ -1,30 +1,207 @@ import { describe, expect, it } from 'vitest'; -import { GitGraph } from '../src/language/index.js'; +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('gitGraph', () => { - describe('Basic Parsing', () => { - it('should handle empty gitGraph', () => { - const result = parse(`gitGraph`); - expect(result.value.$type).toBe(GitGraph); - expect(result.value.statements).toHaveLength(0); - expect(result.lexerErrors).toHaveLength(0); - expect(result.parserErrors).toHaveLength(0); +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 handle gitGraph with one statement', () => { - const result = parse(`gitGraph\n commit\n`); - expect(result.value.$type).toBe(GitGraph); - expect(result.lexerErrors).toHaveLength(0); - expect(result.parserErrors).toHaveLength(0); - expect(result.value.statements).toHaveLength(1); + 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 handle gitGraph with multiple statements and use accTitle', () => { - const result = parse(`gitGraph\n commit\n commit\n accTitle: title\n commit\n`); - expect(result.value.$type).toBe(GitGraph); - expect(result.lexerErrors).toHaveLength(0); - expect(result.parserErrors).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); }); }); }); From e57fee1f37ec82e082e147e0d258346dc8c5fd6d Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:02:49 -0400 Subject: [PATCH 15/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index f31689452e..8ce891261a 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -40,10 +40,7 @@ const state = new ImperativeState(() => ({ options: {}, })); -/** - * - */ -function getId() { +function getID() { return random({ length: 7 }); } From 62757c529ff65f9694252855f3655e86c07fb107 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:02:56 -0400 Subject: [PATCH 16/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 8ce891261a..2f29a45319 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -44,33 +44,6 @@ function getID() { return random({ length: 7 }); } -// 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; -// } - -// function isReachableFrom(currentCommit, otherCommit) { -// const currentSeq = currentCommit.seq; -// const otherSeq = otherCommit.seq; -// if (currentSeq > otherSeq) return isFastForwardable(otherCommit, currentCommit); -// return false; -// } /** * @param list - list of items From 281064f714cd7da9413a8a08c3a967a269c9bb6d Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:03:02 -0400 Subject: [PATCH 17/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 2f29a45319..48689d9c52 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -103,7 +103,7 @@ export const commit = function (msg: string, id: string, type: number, tags: str log.debug('in pushCommit ' + newCommit.id); }; -export const branch = function (name: string, order: number | undefined) { +export const branch = function (name: string, order?: number) { name = common.sanitizeText(name, getConfig()); if (!state.records.branches.has(name)) { state.records.branches.set(name, state.records.head != null ? state.records.head.id : null); From ec2d9c9a087eaf08387d6260519737b833710754 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:03:08 -0400 Subject: [PATCH 18/49] Update packages/mermaid/src/diagrams/git/gitGraphParser.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 9a5cbcb2bd..b7522996ad 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -43,7 +43,7 @@ const parseStatement = (statement: any) => { parseCherryPicking(statement); break; default: - log.warn(`Unknown statement type`); + log.error(`Unknown statement type`); } }; From 5dfc94e6f5064b767fbe6225def31c92a340c86d Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:03:14 -0400 Subject: [PATCH 19/49] Update packages/mermaid/src/diagrams/git/gitGraphParser.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index b7522996ad..5598062b4f 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -51,7 +51,7 @@ const parseCommit = (commit: CommitAst) => { const id = commit.id; const message = commit.message ?? ''; const tags = commit.tags ?? undefined; - const type = commit.type !== undefined ? commitType[commit.type] : 0; + const type = commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL; db.commit(message, id, type, tags); }; From 871f0478c691ba3e5168fa90f583975704ded3c0 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:03:19 -0400 Subject: [PATCH 20/49] Update packages/parser/src/language/gitGraph/module.ts Co-authored-by: Sidharth Vinod --- packages/parser/src/language/gitGraph/module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/parser/src/language/gitGraph/module.ts b/packages/parser/src/language/gitGraph/module.ts index 1630377e55..e2d45c8fa6 100644 --- a/packages/parser/src/language/gitGraph/module.ts +++ b/packages/parser/src/language/gitGraph/module.ts @@ -11,7 +11,6 @@ import { createDefaultSharedCoreModule, EmptyFileSystem, } from 'langium'; - import { CommonValueConverter } from '../common/valueConverter.js'; import { MermaidGeneratedSharedModule, GitGraphGeneratedModule } from '../generated/module.js'; import { GitGraphTokenBuilder } from './tokenBuilder.js'; From 6e5e5f9c6188407a06e83ea043f8f54368a135b6 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:03:25 -0400 Subject: [PATCH 21/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 48689d9c52..577aefc3cf 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -215,16 +215,6 @@ export const merge = ( 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 verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this const commit: Commit = { From 346efdd384e4970e545d00cea63ea05a34fbd6f5 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:04:16 -0400 Subject: [PATCH 22/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 577aefc3cf..e2c77b708f 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -83,9 +83,9 @@ export const getOptions = function () { export const commit = function (msg: string, id: string, type: number, tags: string[] | undefined) { log.info('commit', msg, id, type, tags); log.debug('Entering commit:', msg, id, type, tags); - id = common.sanitizeText(id, getConfig()); - msg = common.sanitizeText(msg, getConfig()); 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(), From a0207f9195b31d185dc3060654fff361f32f6631 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:04:23 -0400 Subject: [PATCH 23/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index e2c77b708f..f7a732b3f7 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -80,7 +80,7 @@ export const getOptions = function () { return state.records.options; }; -export const commit = function (msg: string, id: string, type: number, tags: string[] | undefined) { +export const commit = function (msg: string, id: string, type: number, tags?: string[]) { log.info('commit', msg, id, type, tags); log.debug('Entering commit:', msg, id, type, tags); const config = getConfig(); From 6c1e5aae9222fc3db9db775923d0d3b34591b736 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:05:34 -0400 Subject: [PATCH 24/49] Delete packages/mermaid/src/diagrams/git/parser/gitGraph.jison --- .../src/diagrams/git/parser/gitGraph.jison | 249 ------------------ 1 file changed, 249 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/git/parser/gitGraph.jison diff --git a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison deleted file mode 100644 index 56ba1465e7..0000000000 --- a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison +++ /dev/null @@ -1,249 +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'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } -accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } -(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; } -accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} -[\}] { this.popState(); } -[^\}]* 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"); // -[ \r\n\t]+"end" this.popState(); // not used anymore in the renderer, fixed for backward compatibility -[\s\S]+(?=[ \r\n\t]+"end") return 'OPT'; // -["]["] return 'EMPTYSTR'; -["] this.begin("string"); -["] this.popState(); -[^"]* 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 -<> 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 } -// ; From ef25160b8ed36b465f825f50251fa4917d78353a Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:06:09 -0400 Subject: [PATCH 25/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index f7a732b3f7..c12c646a40 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -105,16 +105,16 @@ export const commit = function (msg: string, id: string, type: number, tags?: st export const branch = function (name: string, order?: number) { name = common.sanitizeText(name, getConfig()); - if (!state.records.branches.has(name)) { + 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'); - } else { - 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}")` - ); - } }; export const merge = ( From f30085c47e90a8cdeb94b2da281310a613f2e703 Mon Sep 17 00:00:00 2001 From: Austin-Fulbright <53443958+Austin-Fulbright@users.noreply.github.com> Date: Sat, 27 Jul 2024 04:06:43 -0400 Subject: [PATCH 26/49] Update packages/mermaid/src/diagrams/git/gitGraphAst.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index c12c646a40..3641853df7 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -231,7 +231,6 @@ export const merge = ( 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'); }; From 2a38d46fd901df29e6e2a55d3759adf9dde6796c Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Mon, 5 Aug 2024 13:53:51 -0400 Subject: [PATCH 27/49] fixed the rest of the concerns, refactored portions of the gitGraphParser test to handle async actions --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 72 +-- .../src/diagrams/git/gitGraphParser.spec.js | 199 +++---- .../src/diagrams/git/gitGraphParserV2.spec.js | 550 +++++++++--------- .../src/language/gitGraph/gitGraph.langium | 2 +- 4 files changed, 377 insertions(+), 446 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 3641853df7..008e7923f5 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -44,7 +44,6 @@ function getID() { return random({ length: 7 }); } - /** * @param list - list of items * @param fn - function to get the key @@ -88,7 +87,7 @@ export const commit = function (msg: string, id: string, type: number, tags?: st msg = common.sanitizeText(msg, config); tags = tags?.map((tag) => common.sanitizeText(tag, config)); const newCommit: Commit = { - id: id ? id : state.records.seq + '-' + getId(), + id: id ? id : state.records.seq + '-' + getID(), message: msg, seq: state.records.seq++, type: type ?? commitType.NORMAL, @@ -109,12 +108,12 @@ export const branch = function (name: string, order?: number) { 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'); + } + + 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 = ( @@ -123,9 +122,10 @@ export const merge = ( overrideType?: number, customTags?: string[] ): void => { - otherBranch = common.sanitizeText(otherBranch, getConfig()); + const config = getConfig(); + otherBranch = common.sanitizeText(otherBranch, config); if (customId) { - customId = common.sanitizeText(customId, getConfig()); + customId = common.sanitizeText(customId, config); } const currentBranchCheck: string | null | undefined = state.records.branches.get( state.records.currBranch @@ -150,7 +150,8 @@ export const merge = ( expected: ['branch abc'], }; throw error; - } else if (currentCommit === undefined || !currentCommit) { + } + if (currentCommit === undefined || !currentCommit) { const error: any = new Error( `Incorrect usage of "merge". Current branch (${state.records.currBranch})has no commits` ); @@ -162,7 +163,8 @@ export const merge = ( expected: ['commit'], }; throw error; - } else if (!state.records.branches.has(otherBranch)) { + } + if (!state.records.branches.has(otherBranch)) { const error: any = new Error( 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') does not exist' ); @@ -174,7 +176,8 @@ export const merge = ( expected: [`branch ${otherBranch}`], }; throw error; - } else if (otherCommit === undefined || !otherCommit) { + } + if (otherCommit === undefined || !otherCommit) { const error: any = new Error( 'Incorrect usage of "merge". Branch to be merged (' + otherBranch + ') has no commits' ); @@ -186,7 +189,8 @@ export const merge = ( expected: ['"commit"'], }; throw error; - } else if (currentCommit === otherCommit) { + } + if (currentCommit === otherCommit) { const error: any = new Error('Incorrect usage of "merge". Both branches have same head'); error.hash = { text: `merge ${otherBranch}`, @@ -196,7 +200,8 @@ export const merge = ( expected: ['branch abc'], }; throw error; - } else if (customId && state.records.commits.has(customId)) { + } + if (customId && state.records.commits.has(customId)) { const error: any = new Error( 'Incorrect usage of "merge". Commit with id:' + customId + @@ -218,7 +223,7 @@ export const merge = ( const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this const commit: Commit = { - id: customId ? customId : state.records.seq + '-' + getId(), + id: customId ? customId : state.records.seq + '-' + getID(), message: `merged branch ${otherBranch} into ${state.records.currBranch}`, seq: state.records.seq++, parents: [state.records.head == null ? null : state.records.head.id, verifiedBranch], @@ -242,12 +247,13 @@ export const cherryPick = function ( parentCommitId: string ) { log.debug('Entering cherryPick:', sourceId, targetId, tags); - sourceId = common.sanitizeText(sourceId, getConfig()); - targetId = common.sanitizeText(targetId, getConfig()); 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, getConfig()); + parentCommitId = common.sanitizeText(parentCommitId, config); if (!sourceId || !state.records.commits.has(sourceId)) { const error: any = new Error( @@ -293,8 +299,6 @@ export const cherryPick = function ( 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; @@ -307,8 +311,6 @@ export const cherryPick = function ( 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; @@ -323,13 +325,12 @@ export const cherryPick = function ( 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: state.records.seq + '-' + getId(), + id: state.records.seq + '-' + getID(), message: `cherry-picked ${sourceCommit?.message} into ${state.records.currBranch}`, seq: state.records.seq++, parents: [state.records.head == null ? null : state.records.head.id, sourceCommit.id], @@ -365,8 +366,6 @@ export const checkout = function (branch: string) { expected: [`branch ${branch}`], }; throw error; - //branches[branch] = head != null ? head.id : null; - //log.debug('in createBranch'); } else { state.records.currBranch = branch; const id = state.records.branches.get(state.records.currBranch); @@ -378,25 +377,6 @@ export const checkout = function (branch: string) { } }; -// 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 - array * @param key - key diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js index d498577fe0..460d039aaa 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js @@ -1,129 +1,92 @@ -import gitGraphAst from './gitGraphAst.js'; -import { parser } from './parser/gitGraph.jison'; +import db from './gitGraphAst.js'; +import { parser } from './gitGraphParser.js'; describe('when parsing a gitGraph', function () { beforeEach(function () { - parser.yy = gitGraphAst; - parser.yy.clear(); + db.clear(); }); - it('should handle a gitGraph definition', function () { - const str = 'gitGraph:\n' + 'commit\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + it('should handle a gitGraph definition', async () => { + const str = `gitGraph:\n commit\n`; - 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'; + await parser.parse(str); + const commits = db.getCommits(); - 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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); }); - it('should handle set direction top to bottom', function () { + it('should handle set direction top to bottom', async () => { const str = 'gitGraph TB:\n' + 'commit\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('TB'); + expect(db.getBranches().size).toBe(1); }); - it('should handle set direction bottom to top', function () { + it('should handle set direction bottom to top', async () => { const str = 'gitGraph BT:\n' + 'commit\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('BT'); + expect(db.getBranches().size).toBe(1); }); - it('should checkout a branch', function () { + it('should checkout a branch', async () => { const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(0); - expect(parser.yy.getCurrentBranch()).toBe('new'); + expect(db.getCurrentBranch()).toBe('new'); }); - it('should switch a branch', function () { + it('should switch a branch', async () => { const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(0); - expect(parser.yy.getCurrentBranch()).toBe('new'); + expect(db.getCurrentBranch()).toBe('new'); }); - it('should add commits to checked out branch', function () { + it('should add commits to checked out branch', async () => { const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('new'); - const branchCommit = parser.yy.getBranches().get('new'); + expect(db.getCurrentBranch()).toBe('new'); + const branchCommit = db.getBranches().get('new'); expect(branchCommit).not.toBeNull(); expect(commits.get(branchCommit).parent).not.toBeNull(); }); - it('should handle commit with args', function () { + it('should handle commit with args', async () => { const str = 'gitGraph:\n' + 'commit "a commit"\n'; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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'); + expect(db.getCurrentBranch()).toBe('main'); }); - // Reset has been commented out in JISON - it.skip('should reset a branch', function () { + it.skip('should reset a branch', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -133,16 +96,16 @@ describe('when parsing a gitGraph', function () { 'commit\n' + 'reset main\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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')); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); }); - it.skip('reset can take an argument', function () { + it.skip('reset can take an argument', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -152,16 +115,16 @@ describe('when parsing a gitGraph', function () { 'commit\n' + 'reset main^\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('newbranch'); + const main = commits.get(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(main.parent); }); - it.skip('should handle fast forwardable merges', function () { + it.skip('should handle fast forwardable merges', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -172,16 +135,16 @@ describe('when parsing a gitGraph', function () { 'checkout main\n' + 'merge newbranch\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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')); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); }); - it('should handle cases when merge is a noop', function () { + it('should handle cases when merge is a noop', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -191,18 +154,16 @@ describe('when parsing a gitGraph', function () { 'commit\n' + 'merge main\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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')); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); }); - it('should handle merge with 2 parents', function () { + it('should handle merge with 2 parents', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -214,18 +175,16 @@ describe('when parsing a gitGraph', function () { 'commit\n' + 'merge newbranch\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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')); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(db.getBranches().get('main')); }); - it.skip('should handle ff merge when history walk has two parents (merge commit)', function () { + it.skip('should handle ff merge when history walk has two parents (merge commit)', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -240,18 +199,18 @@ describe('when parsing a gitGraph', function () { 'checkout newbranch\n' + 'merge main\n'; - parser.parse(str); + await parser.parse(str); - const commits = parser.yy.getCommits(); + const commits = db.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')); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead().id).toEqual(db.getBranches().get('main')); - parser.yy.prettyPrint(); + db.prettyPrint(); }); - it('should generate an array of known branches', function () { + it('should generate an array of known branches', async () => { const str = 'gitGraph:\n' + 'commit\n' + @@ -261,8 +220,8 @@ describe('when parsing a gitGraph', function () { 'commit\n' + 'branch b2\n'; - parser.parse(str); - const branches = gitGraphAst.getBranchesAsObjArray(); + await parser.parse(str); + const branches = db.getBranchesAsObjArray(); expect(branches).toHaveLength(3); expect(branches[0]).toHaveProperty('name', 'main'); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js index 1fb64a5c43..e1e95551be 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js +++ b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js @@ -1,22 +1,21 @@ -import gitGraphAst from './gitGraphAst.js'; -import { parser } from './parser/gitGraph.jison'; +import db from './gitGraphAst.js'; +import { parser } from './gitGraphParser.js'; describe('when parsing a gitGraph', function () { beforeEach(function () { - parser.yy = gitGraphAst; - parser.yy.clear(); + db.clear(); }); - it('should handle a gitGraph commit with NO pararms, get auto-generated reandom ID', function () { + it('should handle a gitGraph commit with NO params, get auto-generated read-only ID', async () => { const str = `gitGraph: commit `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); //console.info(commits); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -24,16 +23,16 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit id only', function () { + it('should handle a gitGraph commit with custom commit id only', async () => { const str = `gitGraph: commit id:"1111" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).toBe('1111'); @@ -41,17 +40,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit tag only', function () { + it('should handle a gitGraph commit with custom commit tag only', async () => { const str = `gitGraph: commit tag:"test" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -59,17 +58,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', function () { + it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', async () => { const str = `gitGraph: commit type: HIGHLIGHT `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -77,17 +76,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(2); }); - it('should handle a gitGraph commit with custom commit type REVERSE only', function () { + it('should handle a gitGraph commit with custom commit type REVERSE only', async () => { const str = `gitGraph: commit type: REVERSE `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -95,17 +94,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle a gitGraph commit with custom commit type NORMAL only', function () { + it('should handle a gitGraph commit with custom commit type NORMAL only', async () => { const str = `gitGraph: commit type: NORMAL `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -113,17 +112,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit msg only', function () { + it('should handle a gitGraph commit with custom commit msg only', async () => { const str = `gitGraph: commit "test commit" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test commit'); expect(commits.get(key).id).not.toBeNull(); @@ -131,17 +130,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit "msg:" key only', function () { + it('should handle a gitGraph commit with custom commit "msg:" key only', async () => { const str = `gitGraph: commit msg: "test commit" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test commit'); expect(commits.get(key).id).not.toBeNull(); @@ -149,17 +148,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit id, tag only', function () { + it('should handle a gitGraph commit with custom commit id, tag only', async () => { const str = `gitGraph: commit id:"1111" tag: "test tag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).toBe('1111'); @@ -167,17 +166,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(0); }); - it('should handle a gitGraph commit with custom commit type, tag only', function () { + it('should handle a gitGraph commit with custom commit type, tag only', async () => { const str = `gitGraph: commit type:HIGHLIGHT tag: "test tag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -185,17 +184,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(2); }); - it('should handle a gitGraph commit with custom commit tag and type only', function () { + it('should handle a gitGraph commit with custom commit tag and type only', async () => { const str = `gitGraph: commit tag: "test tag" type:HIGHLIGHT `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).not.toBeNull(); @@ -203,17 +202,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(2); }); - it('should handle a gitGraph commit with custom commit id, type and tag only', function () { + it('should handle a gitGraph commit with custom commit id, type and tag only', async () => { const str = `gitGraph: commit id:"1111" type:REVERSE tag: "test tag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe(''); expect(commits.get(key).id).toBe('1111'); @@ -221,17 +220,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle a gitGraph commit with custom commit id, type, tag and msg', function () { + it('should handle a gitGraph commit with custom commit id, type, tag and msg', async () => { const str = `gitGraph: commit id:"1111" type:REVERSE tag: "test tag" msg:"test msg" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test msg'); expect(commits.get(key).id).toBe('1111'); @@ -239,18 +238,18 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle a gitGraph commit with custom type,tag, msg, commit id,', function () { + it('should handle a gitGraph commit with custom type,tag, msg, commit id,', async () => { const str = `gitGraph: commit type:REVERSE tag: "test tag" msg: "test msg" id: "1111" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test msg'); expect(commits.get(key).id).toBe('1111'); @@ -258,17 +257,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle a gitGraph commit with custom tag, msg, commit id, type,', function () { + it('should handle a gitGraph commit with custom tag, msg, commit id, type,', async () => { const str = `gitGraph: commit tag: "test tag" msg:"test msg" id:"1111" type:REVERSE `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test msg'); expect(commits.get(key).id).toBe('1111'); @@ -276,17 +275,17 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle a gitGraph commit with custom msg, commit id, type,tag', function () { + it('should handle a gitGraph commit with custom msg, commit id, type,tag', async () => { const str = `gitGraph: commit msg:"test msg" id:"1111" type:REVERSE tag: "test tag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.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); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); const key = commits.keys().next().value; expect(commits.get(key).message).toBe('test msg'); expect(commits.get(key).id).toBe('1111'); @@ -294,36 +293,36 @@ describe('when parsing a gitGraph', function () { expect(commits.get(key).type).toBe(1); }); - it('should handle 3 straight commits', function () { + it('should handle 3 straight commits', async () => { const str = `gitGraph: commit commit commit `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); }); - it('should handle new branch creation', function () { + it('should handle new branch creation', async () => { const str = `gitGraph: commit branch testBranch `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); }); - it('should allow quoted branch names', function () { + it('should allow quoted branch names', async () => { const str = `gitGraph: commit branch "branch" @@ -333,49 +332,49 @@ describe('when parsing a gitGraph', function () { merge "branch" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2, commit3] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit2).branch).toBe('branch'); expect(commits.get(commit3).branch).toBe('main'); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); }); - it('should allow _-./ characters in branch names', function () { + it('should allow _-./ characters in branch names', async () => { const str = `gitGraph: commit branch azAZ_-./test `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('azAZ_-./test'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('azAZ_-./test'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); }); - it('should allow branch names starting with numbers', function () { + it('should allow branch names starting with numbers', async () => { const str = `gitGraph: commit %% branch names starting with numbers are not recommended, but are supported by git branch 1.0.1 `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('1.0.1'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('1.0.1'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); }); - it('should allow branch names starting with unusual prefixes', function () { + it('should allow branch names starting with unusual prefixes', async () => { const str = `gitGraph: commit %% branch names starting with numbers are not recommended, but are supported by git @@ -388,13 +387,13 @@ describe('when parsing a gitGraph', function () { branch A `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('A'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(7); - expect([...parser.yy.getBranches().keys()]).toEqual( + expect(db.getCurrentBranch()).toBe('A'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(7); + expect([...db.getBranches().keys()]).toEqual( expect.arrayContaining([ 'branch01', 'checkout02', @@ -406,21 +405,21 @@ describe('when parsing a gitGraph', function () { ); }); - it('should handle new branch checkout', function () { + it('should handle new branch checkout', async () => { const str = `gitGraph: commit branch testBranch checkout testBranch `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); }); - it('should handle new branch checkout with order', function () { + it('should handle new branch checkout with order', async () => { const str = `gitGraph: commit branch test1 order: 3 @@ -428,19 +427,19 @@ describe('when parsing a gitGraph', function () { branch test3 order: 1 `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('test3'); - expect(parser.yy.getBranches().size).toBe(4); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ { name: 'main' }, { name: 'test3' }, { name: 'test2' }, { name: 'test1' }, ]); }); - it('should handle new branch checkout with and without order', function () { + it('should handle new branch checkout with and without order', async () => { const str = `gitGraph: commit branch test1 order: 1 @@ -448,12 +447,12 @@ describe('when parsing a gitGraph', function () { branch test3 `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('test3'); - expect(parser.yy.getBranches().size).toBe(4); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ { name: 'main' }, { name: 'test2' }, { name: 'test3' }, @@ -461,7 +460,7 @@ describe('when parsing a gitGraph', function () { ]); }); - it('should handle new branch checkout & commit', function () { + it('should handle new branch checkout & commit', async () => { const str = `gitGraph: commit branch testBranch @@ -469,12 +468,12 @@ describe('when parsing a gitGraph', function () { commit `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit1).parents).toStrictEqual([]); @@ -482,7 +481,7 @@ describe('when parsing a gitGraph', function () { expect(commits.get(commit2).parents).toStrictEqual([commit1]); }); - it('should handle new branch checkout & commit and merge', function () { + it('should handle new branch checkout & commit and merge', async () => { const str = `gitGraph: commit branch testBranch @@ -493,12 +492,12 @@ describe('when parsing a gitGraph', function () { merge testBranch `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2, commit3, commit4] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit1).parents).toStrictEqual([]); @@ -511,28 +510,25 @@ describe('when parsing a gitGraph', function () { commits.get(commit1).id, commits.get(commit3).id, ]); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); }); - it('should handle new branch switch', function () { + it('should handle new branch switch', async () => { const str = `gitGraph: commit branch testBranch switch testBranch `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(1); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); }); - it('should handle new branch switch & commit', function () { + it('should handle new branch switch & commit', async () => { const str = `gitGraph: commit branch testBranch @@ -540,12 +536,12 @@ describe('when parsing a gitGraph', function () { commit `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(2); - expect(parser.yy.getCurrentBranch()).toBe('testBranch'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit1).parents).toStrictEqual([]); @@ -553,7 +549,7 @@ describe('when parsing a gitGraph', function () { expect(commits.get(commit2).parents).toStrictEqual([commit1]); }); - it('should handle new branch switch & commit and merge', function () { + it('should handle new branch switch & commit and merge', async () => { const str = `gitGraph: commit branch testBranch @@ -564,12 +560,12 @@ describe('when parsing a gitGraph', function () { merge testBranch `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(4); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2, commit3, commit4] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit1).parents).toStrictEqual([]); @@ -582,13 +578,10 @@ describe('when parsing a gitGraph', function () { commits.get(commit1).id, commits.get(commit3).id, ]); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); }); - it('should handle merge tags', function () { + it('should handle merge tags', async () => { const str = `gitGraph: commit branch testBranch @@ -598,12 +591,12 @@ describe('when parsing a gitGraph', function () { merge testBranch tag: "merge-tag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(3); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); - expect(parser.yy.getBranches().size).toBe(2); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); const [commit1, commit2, commit3] = commits.keys(); expect(commits.get(commit1).branch).toBe('main'); expect(commits.get(commit1).parents).toStrictEqual([]); @@ -617,13 +610,10 @@ describe('when parsing a gitGraph', function () { commits.get(commit2).id, ]); expect(commits.get(commit3).tags).toStrictEqual(['merge-tag']); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); }); - it('should handle merge with custom ids, tags and typr', function () { + it('should handle merge with custom ids, tags and type', async () => { const str = `gitGraph: commit branch testBranch @@ -646,11 +636,11 @@ describe('when parsing a gitGraph', function () { merge testBranch3 id: "6-666" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(7); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getDirection()).toBe('LR'); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); // The order of these commits is in alphabetical order of IDs const [ @@ -685,7 +675,7 @@ describe('when parsing a gitGraph', function () { expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]); expect(testBranch3Merge.id).toBe('6-666'); - expect(parser.yy.getBranchesAsObjArray()).toStrictEqual([ + expect(db.getBranchesAsObjArray()).toStrictEqual([ { name: 'main' }, { name: 'testBranch' }, { name: 'testBranch2' }, @@ -693,7 +683,7 @@ describe('when parsing a gitGraph', function () { ]); }); - it('should support cherry-picking commits', function () { + it('should support cherry-picking commits', async () => { const str = `gitGraph commit id: "ZERO" branch develop @@ -702,14 +692,14 @@ describe('when parsing a gitGraph', function () { cherry-pick id:"A" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][2]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:A']); expect(commits.get(cherryPickCommitID).branch).toBe('main'); }); - it('should support cherry-picking commits with custom tag', function () { + it('should support cherry-picking commits with custom tag', async () => { const str = `gitGraph commit id: "ZERO" branch develop @@ -718,14 +708,14 @@ describe('when parsing a gitGraph', function () { cherry-pick id:"A" tag:"MyTag" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][2]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['MyTag']); expect(commits.get(cherryPickCommitID).branch).toBe('main'); }); - it('should support cherry-picking commits with no tag', function () { + it('should support cherry-picking commits with no tag', async () => { const str = `gitGraph commit id: "ZERO" branch develop @@ -734,14 +724,14 @@ describe('when parsing a gitGraph', function () { cherry-pick id:"A" tag:"" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][2]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); expect(commits.get(cherryPickCommitID).branch).toBe('main'); }); - it('should support cherry-picking of merge commits', function () { + it('should support cherry-picking of merge commits', async () => { const str = `gitGraph commit id: "ZERO" branch feature @@ -755,14 +745,14 @@ describe('when parsing a gitGraph', function () { cherry-pick id: "M" parent:"B" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][4]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:M|parent:B']); expect(commits.get(cherryPickCommitID).branch).toBe('release'); }); - it('should support cherry-picking of merge commits with tag', function () { + it('should support cherry-picking of merge commits with tag', async () => { const str = `gitGraph commit id: "ZERO" branch feature @@ -776,14 +766,14 @@ describe('when parsing a gitGraph', function () { cherry-pick id: "M" parent:"ZERO" tag: "v1.0" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][4]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v1.0']); expect(commits.get(cherryPickCommitID).branch).toBe('release'); }); - it('should support cherry-picking of merge commits with additional commit', function () { + it('should support cherry-picking of merge commits with additional commit', async () => { const str = `gitGraph commit id: "ZERO" branch feature @@ -799,14 +789,14 @@ describe('when parsing a gitGraph', function () { commit id: "D" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][5]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v2.1:ZERO']); expect(commits.get(cherryPickCommitID).branch).toBe('release'); }); - it('should support cherry-picking of merge commits with empty tag', function () { + it('should support cherry-picking of merge commits with empty tag', async () => { const str = `gitGraph commit id: "ZERO" branch feature @@ -823,8 +813,8 @@ describe('when parsing a gitGraph', function () { cherry-pick id:"M" tag:"" parent: "B" `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); const cherryPickCommitID = [...commits.keys()][5]; const cherryPickCommitID2 = [...commits.keys()][7]; expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); @@ -833,10 +823,11 @@ describe('when parsing a gitGraph', function () { }); it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', function () { - expect(() => - parser - .parse( - `gitGraph + expect( + async () => + await parser + .parse( + `gitGraph commit id: "ZERO" branch feature branch release @@ -849,18 +840,19 @@ describe('when parsing a gitGraph', function () { commit id: "C" cherry-pick id:"M" ` - ) - .toThrow( - 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' - ) + ) + .toThrow( + 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' + ) ); }); it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', function () { - expect(() => - parser - .parse( - `gitGraph + expect( + async () => + await parser + .parse( + `gitGraph commit id: "ZERO" branch feature branch release @@ -873,14 +865,14 @@ describe('when parsing a gitGraph', function () { commit id: "C" cherry-pick id:"M" parent: "A" ` - ) - .toThrow( - 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' - ) + ) + .toThrow( + 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' + ) ); }); - it('should throw error when try to branch existing branch: main', function () { + it('should throw error when try to branch existing branch: main', async () => { const str = `gitGraph commit branch testBranch @@ -892,7 +884,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -901,7 +893,7 @@ describe('when parsing a gitGraph', function () { ); } }); - it('should throw error when try to branch existing branch: testBranch', function () { + it('should throw error when try to branch existing branch: testBranch', async () => { const str = `gitGraph commit branch testBranch @@ -913,7 +905,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -922,7 +914,7 @@ describe('when parsing a gitGraph', function () { ); } }); - it('should throw error when try to checkout unknown branch: testBranch', function () { + it('should throw error when try to checkout unknown branch: testBranch', async () => { const str = `gitGraph commit checkout testBranch @@ -934,7 +926,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -943,7 +935,7 @@ describe('when parsing a gitGraph', function () { ); } }); - it('should throw error when trying to merge, when current branch has no commits', function () { + it('should throw error when trying to merge, when current branch has no commits', async () => { const str = `gitGraph merge testBranch commit @@ -956,14 +948,14 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { expect(e.message).toBe('Incorrect usage of "merge". Current branch (main)has no commits'); } }); - it('should throw error when trying to merge unknown branch', function () { + it('should throw error when trying to merge unknown branch', async () => { const str = `gitGraph commit merge testBranch @@ -977,7 +969,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -986,7 +978,7 @@ describe('when parsing a gitGraph', function () { ); } }); - it('should throw error when trying to merge branch to itself', function () { + it('should throw error when trying to merge branch to itself', async () => { const str = `gitGraph commit branch testBranch @@ -994,7 +986,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -1002,7 +994,7 @@ describe('when parsing a gitGraph', function () { } }); - it('should throw error when using existing id as merge ID', function () { + it('should throw error when using existing id as merge ID', async () => { const str = `gitGraph commit id: "1-111" branch testBranch @@ -1013,7 +1005,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -1022,7 +1014,7 @@ describe('when parsing a gitGraph', function () { ); } }); - it('should throw error when trying to merge branches having same heads', function () { + it('should throw error when trying to merge branches having same heads', async () => { const str = `gitGraph commit branch testBranch @@ -1031,14 +1023,14 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head'); } }); - it('should throw error when trying to merge branch which has no commits', function () { + it('should throw error when trying to merge branch which has no commits', async () => { const str = `gitGraph branch test1 @@ -1048,7 +1040,7 @@ describe('when parsing a gitGraph', function () { `; try { - parser.parse(str); + await parser.parse(str); // Fail test if above expression doesn't throw anything. expect(true).toBe(false); } catch (e) { @@ -1058,17 +1050,17 @@ describe('when parsing a gitGraph', function () { } }); describe('accessibility', () => { - it('should handle a title and a description (accDescr)', () => { + it('should handle a title and a description (accDescr)', async () => { const str = `gitGraph: accTitle: This is a title accDescr: This is a description commit `; - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('This is a title'); - expect(parser.yy.getAccDescription()).toBe('This is a description'); + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description'); }); - it('should handle a title and a multiline description (accDescr)', () => { + it('should handle a title and a multiline description (accDescr)', async () => { const str = `gitGraph: accTitle: This is a title accDescr { @@ -1077,15 +1069,15 @@ describe('when parsing a gitGraph', function () { } commit `; - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('This is a title'); - expect(parser.yy.getAccDescription()).toBe('This is a description\nusing multiple lines'); + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description\nusing multiple lines'); }); }); describe('unsafe properties', () => { for (const prop of ['__proto__', 'constructor']) { - it(`should work with custom commit id or branch name ${prop}`, () => { + it(`should work with custom commit id or branch name ${prop}`, async () => { const str = `gitGraph commit id:"${prop}" branch ${prop} @@ -1094,13 +1086,13 @@ describe('when parsing a gitGraph', function () { checkout main merge ${prop} `; - parser.parse(str); - const commits = parser.yy.getCommits(); + await parser.parse(str); + const commits = db.getCommits(); expect(commits.size).toBe(3); expect(commits.keys().next().value).toBe(prop); - expect(parser.yy.getCurrentBranch()).toBe('main'); - expect(parser.yy.getBranches().size).toBe(2); - expect(parser.yy.getBranchesAsObjArray()[1].name).toBe(prop); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().size).toBe(2); + expect(db.getBranchesAsObjArray()[1].name).toBe(prop); }); } }); diff --git a/packages/parser/src/language/gitGraph/gitGraph.langium b/packages/parser/src/language/gitGraph/gitGraph.langium index 807f9382b7..1571ebba8a 100644 --- a/packages/parser/src/language/gitGraph/gitGraph.langium +++ b/packages/parser/src/language/gitGraph/gitGraph.langium @@ -26,7 +26,7 @@ hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/; entry GitGraph: NEWLINE* - 'gitGraph' Direction? ':'? + ('gitGraph' | 'gitGraph' ':' | 'gitGraph:' | ('gitGraph' Direction ':')) NEWLINE* ( NEWLINE* From 38e048b94e6cb945627f10cf0cebea9932680b68 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Mon, 5 Aug 2024 14:08:42 -0400 Subject: [PATCH 28/49] fixed hash error loc & line properties --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 008e7923f5..31302133a6 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -145,8 +145,6 @@ export const merge = ( 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; @@ -158,8 +156,6 @@ export const merge = ( 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; @@ -171,8 +167,6 @@ export const merge = ( 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; @@ -184,8 +178,6 @@ export const merge = ( 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; @@ -195,8 +187,6 @@ export const merge = ( 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; @@ -210,8 +200,6 @@ export const merge = ( error.hash = { text: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`, token: `merge ${otherBranch} ${customId} ${overrideType} ${customTags?.join(' ')}`, - line: '1', - loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, expected: [ `merge ${otherBranch} ${customId}_UNIQUE ${overrideType} ${customTags?.join(' ')}`, ], @@ -262,8 +250,6 @@ export const cherryPick = function ( 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; @@ -324,7 +310,6 @@ export const cherryPick = function ( error.hash = { text: `cherryPick ${sourceId} ${targetId}`, token: `cherryPick ${sourceId} ${targetId}`, - line: '1', expected: ['cherry-pick abc'], }; throw error; @@ -361,8 +346,6 @@ export const checkout = function (branch: string) { 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; From 8fe0ed1d037ac9eae3913b2f27d6af02aee0b79c Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 6 Aug 2024 19:26:13 -0400 Subject: [PATCH 29/49] added parser test and combined the two gitGraph tests --- .../src/diagrams/git/gitGraph.parser.spec.ts | 75 + .../mermaid/src/diagrams/git/gitGraph.spec.ts | 1322 +++++++++++++++++ .../src/diagrams/git/gitGraphParser.spec.js | 231 --- .../src/diagrams/git/gitGraphParser.ts | 5 +- .../src/diagrams/git/gitGraphParserV2.spec.js | 1099 -------------- packages/mermaid/tsconfig.json | 2 +- 6 files changed, 1401 insertions(+), 1333 deletions(-) create mode 100644 packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts create mode 100644 packages/mermaid/src/diagrams/git/gitGraph.spec.ts delete mode 100644 packages/mermaid/src/diagrams/git/gitGraphParser.spec.js delete mode 100644 packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js diff --git a/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts new file mode 100644 index 0000000000..2c86d7c7b0 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { parser } from './gitGraphParser.js'; +import db from './gitGraphAst.js'; +import { parse } from 'path'; + +const parseInput = async (input: string) => { + await parser.parse(input); +}; + +const spyOn = vi.spyOn; + +describe('GitGraph Parsing', function () { + beforeEach(() => { + db.clear(); + }); + it('should parse a default commit statement', async () => { + const input = `gitGraph: + commit + `; + const commitSpy = spyOn(db, 'commit'); + await parseInput(input); + + expect(commitSpy).toHaveBeenCalledWith('', undefined, 0, []); + commitSpy.mockRestore(); + }); + + it('should parse a basic branch statement with just a name', async () => { + const input = `gitGraph: + branch newBranch + `; + const branchSpy = spyOn(db, 'branch'); + await parseInput(input); + expect(branchSpy).toHaveBeenCalledWith('newBranch', 0); + branchSpy.mockRestore(); + }); + + it('should parse a basic checkout statement', async () => { + const input = `gitGraph: + branch newBranch + checkout newBranch + `; + const checkoutSpy = spyOn(db, 'checkout'); + await parseInput(input); + expect(checkoutSpy).toHaveBeenCalledWith('newBranch'); + checkoutSpy.mockRestore(); + }); + + it('should parse a basic merge statement', async () => { + const input = `gitGraph: + commit + branch newBranch + checkout newBranch + commit + checkout main + merge newBranch`; + const mergeSpy = spyOn(db, 'merge'); + await parseInput(input); + expect(mergeSpy).toHaveBeenCalledWith('newBranch', '', undefined, []); + mergeSpy.mockRestore(); + }); + + it('should parse cherry-picking', async () => { + const input = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" + `; + const cherryPickSpy = spyOn(db, 'cherryPick'); + await parseInput(input); + expect(cherryPickSpy).toHaveBeenCalledWith('A', '', undefined, undefined); + cherryPickSpy.mockRestore(); + }); +}); diff --git a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts new file mode 100644 index 0000000000..ab315a82ac --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts @@ -0,0 +1,1322 @@ +import { rejects } from 'assert'; +import db from './gitGraphAst.js'; +import { parser } from './gitGraphParser.js'; + +describe('when parsing a gitGraph', function () { + beforeEach(function () { + db.clear(); + }); + describe('when parsing basic gitGraph', function () { + it('should handle a gitGraph definition', async () => { + const str = `gitGraph:\n commit\n`; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle set direction top to bottom', async () => { + const str = 'gitGraph TB:\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('TB'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle set direction bottom to top', async () => { + const str = 'gitGraph BT:\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('BT'); + expect(db.getBranches().size).toBe(1); + }); + + it('should checkout a branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(0); + expect(db.getCurrentBranch()).toBe('new'); + }); + + it('should switch a branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(0); + expect(db.getCurrentBranch()).toBe('new'); + }); + + it('should add commits to checked out branch', async () => { + const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('new'); + const branchCommit = db.getBranches().get('new'); + expect(branchCommit).not.toBeNull(); + if (branchCommit) { + expect(commits.get(branchCommit)?.parents).not.toBeNull(); + } + }); + it('should handle commit with args', async () => { + const str = 'gitGraph:\n' + 'commit "a commit"\n'; + + await parser.parse(str); + const commits = db.getCommits(); + + expect(commits.size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('a commit'); + expect(db.getCurrentBranch()).toBe('main'); + }); + + it.skip('should reset a branch', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'reset main\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it.skip('reset can take an argument', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'reset main^\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('newbranch'); + const branch = db.getBranches().get('main'); + const main = commits.get(branch ?? ''); + expect(db.getHead()?.id).toEqual(main?.parents); + }); + + it.skip('should handle fast forwardable merges', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'checkout main\n' + + 'merge newbranch\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it('should handle cases when merge is a noop', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'merge main\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('newbranch')); + }); + + it('should handle merge with 2 parents', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch newbranch\n' + + 'checkout newbranch\n' + + 'commit\n' + + 'commit\n' + + 'checkout main\n' + + 'commit\n' + + 'merge newbranch\n'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(5); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('main')); + }); + + it.skip('should handle ff merge when history walk has two parents (merge commit)', async () => { + 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'; + + await parser.parse(str); + + const commits = db.getCommits(); + expect(commits.size).toBe(7); + expect(db.getCurrentBranch()).toBe('newbranch'); + expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); + expect(db.getHead()?.id).toEqual(db.getBranches().get('main')); + + db.prettyPrint(); + }); + + it('should generate an array of known branches', async () => { + const str = + 'gitGraph:\n' + + 'commit\n' + + 'branch b1\n' + + 'checkout b1\n' + + 'commit\n' + + 'commit\n' + + 'branch b2\n'; + + await parser.parse(str); + const branches = db.getBranchesAsObjArray(); + + expect(branches).toHaveLength(3); + expect(branches[0]).toHaveProperty('name', 'main'); + expect(branches[1]).toHaveProperty('name', 'b1'); + expect(branches[2]).toHaveProperty('name', 'b2'); + }); + }); + + describe('when parsing more advanced gitGraphs', () => { + it('should handle a gitGraph commit with NO params, get auto-generated read-only ID', async () => { + const str = `gitGraph: + commit + `; + await parser.parse(str); + const commits = db.getCommits(); + //console.info(commits); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit id only', async () => { + const str = `gitGraph: + commit id:"1111" + `; + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit tag only', async () => { + const str = `gitGraph: + commit tag:"test" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test']); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', async () => { + const str = `gitGraph: + commit type: HIGHLIGHT + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit type REVERSE only', async () => { + const str = `gitGraph: + commit type: REVERSE + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom commit type NORMAL only', async () => { + const str = `gitGraph: + commit type: NORMAL + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit msg only', async () => { + const str = `gitGraph: + commit "test commit" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test commit'); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit "msg:" key only', async () => { + const str = `gitGraph: + commit msg: "test commit" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test commit'); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual([]); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit id, tag only', async () => { + const str = `gitGraph: + commit id:"1111" tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(0); + }); + + it('should handle a gitGraph commit with custom commit type, tag only', async () => { + const str = `gitGraph: + commit type:HIGHLIGHT tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit tag and type only', async () => { + const str = `gitGraph: + commit tag: "test tag" type:HIGHLIGHT + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).not.toBeNull(); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(2); + }); + + it('should handle a gitGraph commit with custom commit id, type and tag only', async () => { + const str = `gitGraph: + commit id:"1111" type:REVERSE tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe(''); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom commit id, type, tag and msg', async () => { + const str = `gitGraph: + commit id:"1111" type:REVERSE tag: "test tag" msg:"test msg" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom type,tag, msg, commit id,', async () => { + const str = `gitGraph: + commit type:REVERSE tag: "test tag" msg: "test msg" id: "1111" + + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom tag, msg, commit id, type,', async () => { + const str = `gitGraph: + commit tag: "test tag" msg:"test msg" id:"1111" type:REVERSE + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle a gitGraph commit with custom msg, commit id, type,tag', async () => { + const str = `gitGraph: + commit msg:"test msg" id:"1111" type:REVERSE tag: "test tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + const key = commits.keys().next().value; + expect(commits.get(key)?.message).toBe('test msg'); + expect(commits.get(key)?.id).toBe('1111'); + expect(commits.get(key)?.tags).toStrictEqual(['test tag']); + expect(commits.get(key)?.type).toBe(1); + }); + + it('should handle 3 straight commits', async () => { + const str = `gitGraph: + commit + commit + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(1); + }); + + it('should handle new branch creation', async () => { + const str = `gitGraph: + commit + branch testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow quoted branch names', async () => { + const str = `gitGraph: + commit + branch "branch" + checkout "branch" + commit + checkout main + merge "branch" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit2)?.branch).toBe('branch'); + expect(commits.get(commit3)?.branch).toBe('main'); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); + }); + + it('should allow _-./ characters in branch names', async () => { + const str = `gitGraph: + commit + branch azAZ_-./test + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('azAZ_-./test'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow branch names starting with numbers', async () => { + const str = `gitGraph: + commit + %% branch names starting with numbers are not recommended, but are supported by git + branch 1.0.1 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('1.0.1'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should allow branch names starting with unusual prefixes', async () => { + const str = `gitGraph: + commit + %% branch names starting with numbers are not recommended, but are supported by git + branch branch01 + branch checkout02 + branch cherry-pick03 + branch branch/example-branch + branch merge/test_merge + %% single character branch name + branch A + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('A'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(7); + expect([...db.getBranches().keys()]).toEqual( + expect.arrayContaining([ + 'branch01', + 'checkout02', + 'cherry-pick03', + 'branch/example-branch', + 'merge/test_merge', + 'A', + ]) + ); + }); + + it('should handle new branch checkout', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + it('should handle new branch checkout with order', async () => { + const str = `gitGraph: + commit + branch test1 order: 3 + branch test2 order: 2 + branch test3 order: 1 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'test3' }, + { name: 'test2' }, + { name: 'test1' }, + ]); + }); + it('should handle new branch checkout with and without order', async () => { + const str = `gitGraph: + commit + branch test1 order: 1 + branch test2 + branch test3 + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('test3'); + expect(db.getBranches().size).toBe(4); + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'test2' }, + { name: 'test3' }, + { name: 'test1' }, + ]); + }); + + it('should handle new branch checkout & commit', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commit1]); + }); + + it('should handle new branch checkout & commit and merge', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + commit + checkout main + merge testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3, commit4] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + expect(commits.get(commit3)?.branch).toBe('testBranch'); + expect(commits.get(commit3)?.parents).toStrictEqual([commits.get(commit2)?.id]); + expect(commits.get(commit4)?.branch).toBe('main'); + expect(commits.get(commit4)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit3)?.id, + ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle new branch switch', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(1); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + }); + + it('should handle new branch switch & commit', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + commit + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(2); + expect(db.getCurrentBranch()).toBe('testBranch'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commit1]); + }); + + it('should handle new branch switch & commit and merge', async () => { + const str = `gitGraph: + commit + branch testBranch + switch testBranch + commit + commit + switch main + merge testBranch + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(4); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3, commit4] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + expect(commits.get(commit3)?.branch).toBe('testBranch'); + expect(commits.get(commit3)?.parents).toStrictEqual([commits.get(commit2)?.id]); + expect(commits.get(commit4)?.branch).toBe('main'); + expect(commits.get(commit4)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit3)?.id, + ]); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle merge tags', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + checkout main + merge testBranch tag: "merge-tag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + expect(db.getBranches().size).toBe(2); + const [commit1, commit2, commit3] = commits.keys(); + expect(commits.get(commit1)?.branch).toBe('main'); + expect(commits.get(commit1)?.parents).toStrictEqual([]); + + expect(commits.get(commit2)?.branch).toBe('testBranch'); + expect(commits.get(commit2)?.parents).toStrictEqual([commits.get(commit1)?.id]); + + expect(commits.get(commit3)?.branch).toBe('main'); + expect(commits.get(commit3)?.parents).toStrictEqual([ + commits.get(commit1)?.id, + commits.get(commit2)?.id, + ]); + expect(commits.get(commit3)?.tags).toStrictEqual(['merge-tag']); + expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); + }); + + it('should handle merge with custom ids, tags and type', async () => { + const str = `gitGraph: + commit + branch testBranch + checkout testBranch + commit + checkout main + %% Merge Tag and ID + merge testBranch tag: "merge-tag" id: "2-222" + branch testBranch2 + checkout testBranch2 + commit + checkout main + %% Merge ID and Tag (reverse order) + merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT + branch testBranch3 + checkout testBranch3 + commit + checkout main + %% just Merge ID + merge testBranch3 id: "6-666" + `; + + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(7); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getDirection()).toBe('LR'); + + // The order of these commits is in alphabetical order of IDs + const [ + mainCommit, + testBranchCommit, + testBranchMerge, + testBranch2Commit, + testBranch2Merge, + testBranch3Commit, + testBranch3Merge, + ] = [...commits.values()]; + + expect(mainCommit.branch).toBe('main'); + expect(mainCommit.parents).toStrictEqual([]); + + expect(testBranchCommit.branch).toBe('testBranch'); + expect(testBranchCommit.parents).toStrictEqual([mainCommit.id]); + + expect(testBranchMerge.branch).toBe('main'); + expect(testBranchMerge.parents).toStrictEqual([mainCommit.id, testBranchCommit.id]); + expect(testBranchMerge.tags).toStrictEqual(['merge-tag']); + expect(testBranchMerge.id).toBe('2-222'); + + expect(testBranch2Merge.branch).toBe('main'); + expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]); + expect(testBranch2Merge.tags).toStrictEqual(['merge-tag2']); + expect(testBranch2Merge.id).toBe('4-444'); + expect(testBranch2Merge.customType).toBe(2); + expect(testBranch2Merge.customId).toBe(true); + + expect(testBranch3Merge.branch).toBe('main'); + expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]); + expect(testBranch3Merge.id).toBe('6-666'); + + expect(db.getBranchesAsObjArray()).toStrictEqual([ + { name: 'main' }, + { name: 'testBranch' }, + { name: 'testBranch2' }, + { name: 'testBranch3' }, + ]); + }); + + it('should support cherry-picking commits', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['cherry-pick:A']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking commits with custom tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" tag:"MyTag" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['MyTag']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking commits with no tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch develop + commit id:"A" + checkout main + cherry-pick id:"A" tag:"" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][2]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID)?.branch).toBe('main'); + }); + + it('should support cherry-picking of merge commits', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + cherry-pick id: "M" parent:"B" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][4]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['cherry-pick:M|parent:B']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with tag', async () => { + const str = `gitGraph + commit id: "ZERO" + branch feature + branch release + checkout feature + commit id: "A" + commit id: "B" + checkout main + merge feature id: "M" + checkout release + cherry-pick id: "M" parent:"ZERO" tag: "v1.0" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][4]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['v1.0']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with additional commit', async () => { + const str = `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" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][5]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual(['v2.1:ZERO']); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with empty tag', async () => { + const str = `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" parent: "ZERO" tag:"" + commit id: "D" + cherry-pick id:"M" tag:"" parent: "B" + `; + + await parser.parse(str); + const commits = db.getCommits(); + const cherryPickCommitID = [...commits.keys()][5]; + const cherryPickCommitID2 = [...commits.keys()][7]; + expect(commits.get(cherryPickCommitID)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID2)?.tags).toStrictEqual([]); + expect(commits.get(cherryPickCommitID)?.branch).toBe('release'); + }); + + it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', async () => { + await expect( + parser.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" + ` + ) + ).rejects.toThrow( + 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' + ); + }); + + it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', async () => { + await expect( + parser.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" parent: "A" + ` + ) + ).rejects.toThrow( + 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' + ); + }); + + it('should throw error when try to branch existing branch: main', async () => { + const str = `gitGraph + commit + branch testBranch + commit + branch main + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout main")' + ); + } + }); + it('should throw error when try to branch existing branch: testBranch', async () => { + const str = `gitGraph + commit + branch testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout testBranch")' + ); + } + }); + it('should throw error when try to checkout unknown branch: testBranch', async () => { + const str = `gitGraph + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Trying to checkout branch which is not yet created. (Help try using "branch testBranch")' + ); + } + }); + it('should throw error when trying to merge, when current branch has no commits', async () => { + const str = `gitGraph + merge testBranch + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Current branch (main)has no commits'); + } + }); + it('should throw error when trying to merge unknown branch', async () => { + const str = `gitGraph + commit + merge testBranch + commit + checkout testBranch + commit + branch testBranch + commit + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Branch to be merged (testBranch) does not exist' + ); + } + }); + it('should throw error when trying to merge branch to itself', async () => { + const str = `gitGraph + commit + branch testBranch + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Cannot merge a branch to itself'); + } + }); + + it('should throw error when using existing id as merge ID', async () => { + const str = `gitGraph + commit id: "1-111" + branch testBranch + commit id: "2-222" + commit id: "3-333" + checkout main + merge testBranch id: "1-111" + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Commit with id:1-111 already exists, use different custom Id' + ); + } + }); + it('should throw error when trying to merge branches having same heads', async () => { + const str = `gitGraph + commit + branch testBranch + checkout main + merge testBranch + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head'); + } + }); + it('should throw error when trying to merge branch which has no commits', async () => { + const str = `gitGraph + branch test1 + + checkout main + commit + merge test1 + `; + + try { + await parser.parse(str); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + 'Incorrect usage of "merge". Branch to be merged (test1) has no commits' + ); + } + }); + describe('accessibility', () => { + it('should handle a title and a description (accDescr)', async () => { + const str = `gitGraph: + accTitle: This is a title + accDescr: This is a description + commit + `; + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description'); + }); + it('should handle a title and a multiline description (accDescr)', async () => { + const str = `gitGraph: + accTitle: This is a title + accDescr { + This is a description + using multiple lines + } + commit + `; + await parser.parse(str); + expect(db.getAccTitle()).toBe('This is a title'); + expect(db.getAccDescription()).toBe('This is a description\nusing multiple lines'); + }); + }); + + describe('unsafe properties', () => { + for (const prop of ['__proto__', 'constructor']) { + it(`should work with custom commit id or branch name ${prop}`, async () => { + const str = `gitGraph + commit id:"${prop}" + branch ${prop} + checkout ${prop} + commit + checkout main + merge ${prop} + `; + await parser.parse(str); + const commits = db.getCommits(); + expect(commits.size).toBe(3); + expect(commits.keys().next().value).toBe(prop); + expect(db.getCurrentBranch()).toBe('main'); + expect(db.getBranches().size).toBe(2); + expect(db.getBranchesAsObjArray()[1].name).toBe(prop); + }); + } + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js deleted file mode 100644 index 460d039aaa..0000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.spec.js +++ /dev/null @@ -1,231 +0,0 @@ -import db from './gitGraphAst.js'; -import { parser } from './gitGraphParser.js'; - -describe('when parsing a gitGraph', function () { - beforeEach(function () { - db.clear(); - }); - - it('should handle a gitGraph definition', async () => { - const str = `gitGraph:\n commit\n`; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - }); - - it('should handle set direction top to bottom', async () => { - const str = 'gitGraph TB:\n' + 'commit\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('TB'); - expect(db.getBranches().size).toBe(1); - }); - - it('should handle set direction bottom to top', async () => { - const str = 'gitGraph BT:\n' + 'commit\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('BT'); - expect(db.getBranches().size).toBe(1); - }); - - it('should checkout a branch', async () => { - const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(0); - expect(db.getCurrentBranch()).toBe('new'); - }); - - it('should switch a branch', async () => { - const str = 'gitGraph:\n' + 'branch new\n' + 'switch new\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(0); - expect(db.getCurrentBranch()).toBe('new'); - }); - - it('should add commits to checked out branch', async () => { - const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(2); - expect(db.getCurrentBranch()).toBe('new'); - const branchCommit = db.getBranches().get('new'); - expect(branchCommit).not.toBeNull(); - expect(commits.get(branchCommit).parent).not.toBeNull(); - }); - it('should handle commit with args', async () => { - const str = 'gitGraph:\n' + 'commit "a commit"\n'; - - await parser.parse(str); - const commits = db.getCommits(); - - expect(commits.size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('a commit'); - expect(db.getCurrentBranch()).toBe('main'); - }); - - it.skip('should reset a branch', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'reset main\n'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(db.getCurrentBranch()).toBe('newbranch'); - expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); - }); - - it.skip('reset can take an argument', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'reset main^\n'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(db.getCurrentBranch()).toBe('newbranch'); - const main = commits.get(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(main.parent); - }); - - it.skip('should handle fast forwardable merges', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'checkout main\n' + - 'merge newbranch\n'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(4); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); - }); - - it('should handle cases when merge is a noop', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'merge main\n'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(4); - expect(db.getCurrentBranch()).toBe('newbranch'); - expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(db.getBranches().get('newbranch')); - }); - - it('should handle merge with 2 parents', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch newbranch\n' + - 'checkout newbranch\n' + - 'commit\n' + - 'commit\n' + - 'checkout main\n' + - 'commit\n' + - 'merge newbranch\n'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(5); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getBranches().get('newbranch')).not.toEqual(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(db.getBranches().get('main')); - }); - - it.skip('should handle ff merge when history walk has two parents (merge commit)', async () => { - 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'; - - await parser.parse(str); - - const commits = db.getCommits(); - expect(commits.size).toBe(7); - expect(db.getCurrentBranch()).toBe('newbranch'); - expect(db.getBranches().get('newbranch')).toEqual(db.getBranches().get('main')); - expect(db.getHead().id).toEqual(db.getBranches().get('main')); - - db.prettyPrint(); - }); - - it('should generate an array of known branches', async () => { - const str = - 'gitGraph:\n' + - 'commit\n' + - 'branch b1\n' + - 'checkout b1\n' + - 'commit\n' + - 'commit\n' + - 'branch b2\n'; - - await parser.parse(str); - const branches = db.getBranchesAsObjArray(); - - expect(branches).toHaveLength(3); - expect(branches[0]).toHaveProperty('name', 'main'); - expect(branches[1]).toHaveProperty('name', 'b1'); - expect(branches[2]).toHaveProperty('name', 'b2'); - }); -}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 5598062b4f..93a671993c 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -50,8 +50,9 @@ const parseStatement = (statement: any) => { const parseCommit = (commit: CommitAst) => { const id = commit.id; const message = commit.message ?? ''; - const tags = commit.tags ?? undefined; const type = commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL; + const tags = commit.tags ?? undefined; + db.commit(message, id, type, tags); }; @@ -64,8 +65,8 @@ const parseBranch = (branch: BranchAst) => { const parseMerge = (merge: MergeAst) => { const branch = merge.branch; const id = merge.id ?? ''; - const tags = merge.tags ?? undefined; const type = merge.type !== undefined ? commitType[merge.type] : undefined; + const tags = merge.tags ?? undefined; db.merge(branch, id, type, tags); }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js deleted file mode 100644 index e1e95551be..0000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js +++ /dev/null @@ -1,1099 +0,0 @@ -import db from './gitGraphAst.js'; -import { parser } from './gitGraphParser.js'; - -describe('when parsing a gitGraph', function () { - beforeEach(function () { - db.clear(); - }); - it('should handle a gitGraph commit with NO params, get auto-generated read-only ID', async () => { - const str = `gitGraph: - commit - `; - await parser.parse(str); - const commits = db.getCommits(); - //console.info(commits); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit id only', async () => { - const str = `gitGraph: - commit id:"1111" - `; - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit tag only', async () => { - const str = `gitGraph: - commit tag:"test" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test']); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit type HIGHLIGHT only', async () => { - const str = `gitGraph: - commit type: HIGHLIGHT - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit type REVERSE only', async () => { - const str = `gitGraph: - commit type: REVERSE - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom commit type NORMAL only', async () => { - const str = `gitGraph: - commit type: NORMAL - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit msg only', async () => { - const str = `gitGraph: - commit "test commit" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test commit'); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit "msg:" key only', async () => { - const str = `gitGraph: - commit msg: "test commit" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test commit'); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual([]); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit id, tag only', async () => { - const str = `gitGraph: - commit id:"1111" tag: "test tag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(0); - }); - - it('should handle a gitGraph commit with custom commit type, tag only', async () => { - const str = `gitGraph: - commit type:HIGHLIGHT tag: "test tag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit tag and type only', async () => { - const str = `gitGraph: - commit tag: "test tag" type:HIGHLIGHT - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).not.toBeNull(); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(2); - }); - - it('should handle a gitGraph commit with custom commit id, type and tag only', async () => { - const str = `gitGraph: - commit id:"1111" type:REVERSE tag: "test tag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe(''); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom commit id, type, tag and msg', async () => { - const str = `gitGraph: - commit id:"1111" type:REVERSE tag: "test tag" msg:"test msg" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom type,tag, msg, commit id,', async () => { - const str = `gitGraph: - commit type:REVERSE tag: "test tag" msg: "test msg" id: "1111" - - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom tag, msg, commit id, type,', async () => { - const str = `gitGraph: - commit tag: "test tag" msg:"test msg" id:"1111" type:REVERSE - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle a gitGraph commit with custom msg, commit id, type,tag', async () => { - const str = `gitGraph: - commit msg:"test msg" id:"1111" type:REVERSE tag: "test tag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - const key = commits.keys().next().value; - expect(commits.get(key).message).toBe('test msg'); - expect(commits.get(key).id).toBe('1111'); - expect(commits.get(key).tags).toStrictEqual(['test tag']); - expect(commits.get(key).type).toBe(1); - }); - - it('should handle 3 straight commits', async () => { - const str = `gitGraph: - commit - commit - commit - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(1); - }); - - it('should handle new branch creation', async () => { - const str = `gitGraph: - commit - branch testBranch - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('testBranch'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - }); - - it('should allow quoted branch names', async () => { - const str = `gitGraph: - commit - branch "branch" - checkout "branch" - commit - checkout main - merge "branch" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2, commit3] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit2).branch).toBe('branch'); - expect(commits.get(commit3).branch).toBe('main'); - expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'branch' }]); - }); - - it('should allow _-./ characters in branch names', async () => { - const str = `gitGraph: - commit - branch azAZ_-./test - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('azAZ_-./test'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - }); - - it('should allow branch names starting with numbers', async () => { - const str = `gitGraph: - commit - %% branch names starting with numbers are not recommended, but are supported by git - branch 1.0.1 - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('1.0.1'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - }); - - it('should allow branch names starting with unusual prefixes', async () => { - const str = `gitGraph: - commit - %% branch names starting with numbers are not recommended, but are supported by git - branch branch01 - branch checkout02 - branch cherry-pick03 - branch branch/example-branch - branch merge/test_merge - %% single character branch name - branch A - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('A'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(7); - expect([...db.getBranches().keys()]).toEqual( - expect.arrayContaining([ - 'branch01', - 'checkout02', - 'cherry-pick03', - 'branch/example-branch', - 'merge/test_merge', - 'A', - ]) - ); - }); - - it('should handle new branch checkout', async () => { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('testBranch'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - }); - it('should handle new branch checkout with order', async () => { - const str = `gitGraph: - commit - branch test1 order: 3 - branch test2 order: 2 - branch test3 order: 1 - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('test3'); - expect(db.getBranches().size).toBe(4); - expect(db.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'test3' }, - { name: 'test2' }, - { name: 'test1' }, - ]); - }); - it('should handle new branch checkout with and without order', async () => { - const str = `gitGraph: - commit - branch test1 order: 1 - branch test2 - branch test3 - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('test3'); - expect(db.getBranches().size).toBe(4); - expect(db.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'test2' }, - { name: 'test3' }, - { name: 'test1' }, - ]); - }); - - it('should handle new branch checkout & commit', async () => { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(2); - expect(db.getCurrentBranch()).toBe('testBranch'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commit1]); - }); - - it('should handle new branch checkout & commit and merge', async () => { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - commit - checkout main - merge testBranch - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(4); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2, commit3, commit4] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - expect(commits.get(commit3).branch).toBe('testBranch'); - expect(commits.get(commit3).parents).toStrictEqual([commits.get(commit2).id]); - expect(commits.get(commit4).branch).toBe('main'); - expect(commits.get(commit4).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit3).id, - ]); - expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); - }); - - it('should handle new branch switch', async () => { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(1); - expect(db.getCurrentBranch()).toBe('testBranch'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - }); - - it('should handle new branch switch & commit', async () => { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - commit - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(2); - expect(db.getCurrentBranch()).toBe('testBranch'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commit1]); - }); - - it('should handle new branch switch & commit and merge', async () => { - const str = `gitGraph: - commit - branch testBranch - switch testBranch - commit - commit - switch main - merge testBranch - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(4); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2, commit3, commit4] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - expect(commits.get(commit3).branch).toBe('testBranch'); - expect(commits.get(commit3).parents).toStrictEqual([commits.get(commit2).id]); - expect(commits.get(commit4).branch).toBe('main'); - expect(commits.get(commit4).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit3).id, - ]); - expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); - }); - - it('should handle merge tags', async () => { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - checkout main - merge testBranch tag: "merge-tag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - expect(db.getBranches().size).toBe(2); - const [commit1, commit2, commit3] = commits.keys(); - expect(commits.get(commit1).branch).toBe('main'); - expect(commits.get(commit1).parents).toStrictEqual([]); - - expect(commits.get(commit2).branch).toBe('testBranch'); - expect(commits.get(commit2).parents).toStrictEqual([commits.get(commit1).id]); - - expect(commits.get(commit3).branch).toBe('main'); - expect(commits.get(commit3).parents).toStrictEqual([ - commits.get(commit1).id, - commits.get(commit2).id, - ]); - expect(commits.get(commit3).tags).toStrictEqual(['merge-tag']); - expect(db.getBranchesAsObjArray()).toStrictEqual([{ name: 'main' }, { name: 'testBranch' }]); - }); - - it('should handle merge with custom ids, tags and type', async () => { - const str = `gitGraph: - commit - branch testBranch - checkout testBranch - commit - checkout main - %% Merge Tag and ID - merge testBranch tag: "merge-tag" id: "2-222" - branch testBranch2 - checkout testBranch2 - commit - checkout main - %% Merge ID and Tag (reverse order) - merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT - branch testBranch3 - checkout testBranch3 - commit - checkout main - %% just Merge ID - merge testBranch3 id: "6-666" - `; - - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(7); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getDirection()).toBe('LR'); - - // The order of these commits is in alphabetical order of IDs - const [ - mainCommit, - testBranchCommit, - testBranchMerge, - testBranch2Commit, - testBranch2Merge, - testBranch3Commit, - testBranch3Merge, - ] = [...commits.values()]; - - expect(mainCommit.branch).toBe('main'); - expect(mainCommit.parents).toStrictEqual([]); - - expect(testBranchCommit.branch).toBe('testBranch'); - expect(testBranchCommit.parents).toStrictEqual([mainCommit.id]); - - expect(testBranchMerge.branch).toBe('main'); - expect(testBranchMerge.parents).toStrictEqual([mainCommit.id, testBranchCommit.id]); - expect(testBranchMerge.tags).toStrictEqual(['merge-tag']); - expect(testBranchMerge.id).toBe('2-222'); - - expect(testBranch2Merge.branch).toBe('main'); - expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]); - expect(testBranch2Merge.tags).toStrictEqual(['merge-tag2']); - expect(testBranch2Merge.id).toBe('4-444'); - expect(testBranch2Merge.customType).toBe(2); - expect(testBranch2Merge.customId).toBe(true); - - expect(testBranch3Merge.branch).toBe('main'); - expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]); - expect(testBranch3Merge.id).toBe('6-666'); - - expect(db.getBranchesAsObjArray()).toStrictEqual([ - { name: 'main' }, - { name: 'testBranch' }, - { name: 'testBranch2' }, - { name: 'testBranch3' }, - ]); - }); - - it('should support cherry-picking commits', async () => { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:A']); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking commits with custom tag', async () => { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" tag:"MyTag" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['MyTag']); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking commits with no tag', async () => { - const str = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" tag:"" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][2]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID).branch).toBe('main'); - }); - - it('should support cherry-picking of merge commits', async () => { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - cherry-pick id: "M" parent:"B" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][4]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['cherry-pick:M|parent:B']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with tag', async () => { - const str = `gitGraph - commit id: "ZERO" - branch feature - branch release - checkout feature - commit id: "A" - commit id: "B" - checkout main - merge feature id: "M" - checkout release - cherry-pick id: "M" parent:"ZERO" tag: "v1.0" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][4]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v1.0']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with additional commit', async () => { - const str = `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" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][5]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual(['v2.1:ZERO']); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should support cherry-picking of merge commits with empty tag', async () => { - const str = `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" parent: "ZERO" tag:"" - commit id: "D" - cherry-pick id:"M" tag:"" parent: "B" - `; - - await parser.parse(str); - const commits = db.getCommits(); - const cherryPickCommitID = [...commits.keys()][5]; - const cherryPickCommitID2 = [...commits.keys()][7]; - expect(commits.get(cherryPickCommitID).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID2).tags).toStrictEqual([]); - expect(commits.get(cherryPickCommitID).branch).toBe('release'); - }); - - it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', function () { - expect( - async () => - await parser - .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" - ` - ) - .toThrow( - 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' - ) - ); - }); - - it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', function () { - expect( - async () => - await parser - .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" parent: "A" - ` - ) - .toThrow( - 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' - ) - ); - }); - - it('should throw error when try to branch existing branch: main', async () => { - const str = `gitGraph - commit - branch testBranch - commit - branch main - commit - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout main")' - ); - } - }); - it('should throw error when try to branch existing branch: testBranch', async () => { - const str = `gitGraph - commit - branch testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout testBranch")' - ); - } - }); - it('should throw error when try to checkout unknown branch: testBranch', async () => { - const str = `gitGraph - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Trying to checkout branch which is not yet created. (Help try using "branch testBranch")' - ); - } - }); - it('should throw error when trying to merge, when current branch has no commits', async () => { - const str = `gitGraph - merge testBranch - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Current branch (main)has no commits'); - } - }); - it('should throw error when trying to merge unknown branch', async () => { - const str = `gitGraph - commit - merge testBranch - commit - checkout testBranch - commit - branch testBranch - commit - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Branch to be merged (testBranch) does not exist' - ); - } - }); - it('should throw error when trying to merge branch to itself', async () => { - const str = `gitGraph - commit - branch testBranch - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Cannot merge a branch to itself'); - } - }); - - it('should throw error when using existing id as merge ID', async () => { - const str = `gitGraph - commit id: "1-111" - branch testBranch - commit id: "2-222" - commit id: "3-333" - checkout main - merge testBranch id: "1-111" - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Commit with id:1-111 already exists, use different custom Id' - ); - } - }); - it('should throw error when trying to merge branches having same heads', async () => { - const str = `gitGraph - commit - branch testBranch - checkout main - merge testBranch - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Incorrect usage of "merge". Both branches have same head'); - } - }); - it('should throw error when trying to merge branch which has no commits', async () => { - const str = `gitGraph - branch test1 - - checkout main - commit - merge test1 - `; - - try { - await parser.parse(str); - // Fail test if above expression doesn't throw anything. - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Incorrect usage of "merge". Branch to be merged (test1) has no commits' - ); - } - }); - describe('accessibility', () => { - it('should handle a title and a description (accDescr)', async () => { - const str = `gitGraph: - accTitle: This is a title - accDescr: This is a description - commit - `; - await parser.parse(str); - expect(db.getAccTitle()).toBe('This is a title'); - expect(db.getAccDescription()).toBe('This is a description'); - }); - it('should handle a title and a multiline description (accDescr)', async () => { - const str = `gitGraph: - accTitle: This is a title - accDescr { - This is a description - using multiple lines - } - commit - `; - await parser.parse(str); - expect(db.getAccTitle()).toBe('This is a title'); - expect(db.getAccDescription()).toBe('This is a description\nusing multiple lines'); - }); - }); - - describe('unsafe properties', () => { - for (const prop of ['__proto__', 'constructor']) { - it(`should work with custom commit id or branch name ${prop}`, async () => { - const str = `gitGraph - commit id:"${prop}" - branch ${prop} - checkout ${prop} - commit - checkout main - merge ${prop} - `; - await parser.parse(str); - const commits = db.getCommits(); - expect(commits.size).toBe(3); - expect(commits.keys().next().value).toBe(prop); - expect(db.getCurrentBranch()).toBe('main'); - expect(db.getBranches().size).toBe(2); - expect(db.getBranchesAsObjArray()[1].name).toBe(prop); - }); - } - }); -}); diff --git a/packages/mermaid/tsconfig.json b/packages/mermaid/tsconfig.json index 78e3cf2de5..f4451225ed 100644 --- a/packages/mermaid/tsconfig.json +++ b/packages/mermaid/tsconfig.json @@ -5,5 +5,5 @@ "outDir": "./dist", "types": ["vitest/importMeta", "vitest/globals"] }, - "include": ["./src/**/*.ts", "./package.json"] + "include": ["./src/**/*.ts", "./package.json", "src/diagrams/gantt/ganttDb.js"] } From 00603e7bac5ea7eb8e7a91f6dd668343584e98b3 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Thu, 8 Aug 2024 18:47:34 -0400 Subject: [PATCH 30/49] fix ts-ignore errors refactored large functions to use helpers --- .../src/diagrams/git/gitGraph.parser.spec.ts | 3 +- .../mermaid/src/diagrams/git/gitGraph.spec.ts | 2 +- .../mermaid/src/diagrams/git/gitGraphAst.ts | 7 +- .../src/diagrams/git/gitGraphDiagram.ts | 4 +- .../src/diagrams/git/gitGraphParser.ts | 2 +- .../src/diagrams/git/gitGraphRenderer.js | 893 ---------------- .../src/diagrams/git/gitGraphRenderer.ts | 968 ++++++++++++++++++ .../mermaid/src/diagrams/git/gitGraphTypes.ts | 59 +- packages/mermaid/tsconfig.json | 7 +- 9 files changed, 1040 insertions(+), 905 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/git/gitGraphRenderer.js create mode 100644 packages/mermaid/src/diagrams/git/gitGraphRenderer.ts diff --git a/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts index 2c86d7c7b0..1a38a8217a 100644 --- a/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts +++ b/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { parser } from './gitGraphParser.js'; -import db from './gitGraphAst.js'; -import { parse } from 'path'; +import { db } from './gitGraphAst.js'; const parseInput = async (input: string) => { await parser.parse(input); diff --git a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts index ab315a82ac..9b3236f907 100644 --- a/packages/mermaid/src/diagrams/git/gitGraph.spec.ts +++ b/packages/mermaid/src/diagrams/git/gitGraph.spec.ts @@ -1,5 +1,5 @@ import { rejects } from 'assert'; -import db from './gitGraphAst.js'; +import { db } from './gitGraphAst.js'; import { parser } from './gitGraphParser.js'; describe('when parsing a gitGraph', function () { diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 31302133a6..53c361e3ed 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -12,9 +12,8 @@ import { getDiagramTitle, } from '../common/commonDb.js'; import defaultConfig from '../../defaultConfig.js'; -import type { DiagramOrientation, Commit } from './gitGraphTypes.js'; +import type { DiagramOrientation, Commit, GitGraphDB, CommitType } from './gitGraphTypes.js'; import { ImperativeState } from '../../utils/imperativeState.js'; - interface GitGraphState { commits: Map; head: Commit | null; @@ -467,7 +466,7 @@ export const getHead = function () { return state.records.head; }; -export const commitType = { +export const commitType: CommitType = { NORMAL: 0, REVERSE: 1, HIGHLIGHT: 2, @@ -475,7 +474,7 @@ export const commitType = { CHERRY_PICK: 4, }; -export default { +export const db: GitGraphDB = { commitType, getConfig: () => getConfig().gitGraph, setDirection, diff --git a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts index e5534140b1..01537e5511 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts @@ -1,13 +1,13 @@ // @ts-ignore: JISON doesn't support types import { parser } from './gitGraphParser.js'; -import gitGraphDb from './gitGraphAst.js'; +import { db } from './gitGraphAst.js'; import gitGraphRenderer from './gitGraphRenderer.js'; import gitGraphStyles from './styles.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { parser: parser, - db: gitGraphDb, + db: db, renderer: gitGraphRenderer, styles: gitGraphStyles, }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 93a671993c..b014d7e574 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -3,7 +3,7 @@ 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 { db } from './gitGraphAst.js'; import { commitType } from './gitGraphAst.js'; import type { CheckoutAst, diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js deleted file mode 100644 index b8b13e0899..0000000000 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js +++ /dev/null @@ -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} 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|/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, -}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts new file mode 100644 index 0000000000..ec9a39f2a3 --- /dev/null +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -0,0 +1,968 @@ +import { select } from 'd3'; +import { getConfig, setupGraphViewbox } from '../../diagram-api/diagramAPI.js'; +import { log } from '../../logger.js'; +import utils from '../../utils.js'; +import type { DrawDefinition } from '../../diagram-api/types.js'; +import type d3 from 'd3'; +import type { CommitType, Commit, GitGraphDB, DiagramOrientation } from './gitGraphTypes.js'; +import type { GitGraphDiagramConfig } from '../../config.type.js'; + +let allCommitsDict = new Map(); + +const commitType: CommitType = { + NORMAL: 0, + REVERSE: 1, + HIGHLIGHT: 2, + MERGE: 3, + CHERRY_PICK: 4, +}; + +const THEME_COLOR_LIMIT = 8; + +interface BranchPosition { + pos: number; + index: number; +} + +interface CommitPosition { + x: number; + y: number; +} + +const branchPos = new Map(); +const commitPos = new Map(); +let lanes: number[] = []; +let maxPos = 0; +let dir: DiagramOrientation = 'LR'; +const defaultPos = 30; + +const clear = () => { + branchPos.clear(); + commitPos.clear(); + allCommitsDict.clear(); + maxPos = 0; + lanes = []; + dir = 'LR'; +}; + +const drawText = (txt: string | string[]) => { + const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const rows = typeof txt === 'string' ? txt.split(/\\n|\n|/gi) : txt; + + rows.forEach((row) => { + 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); + }); + + return svgLabel; +}; + +const findClosestParent = (parents: string[], useBTLogic = false): string | undefined => { + let closestParent: string | undefined; + let comparisonFunc; + let targetPosition: number; + + if (dir === 'BT' || useBTLogic) { + comparisonFunc = (a: number, b: number) => a <= b; + targetPosition = Infinity; + } else { + comparisonFunc = (a: number, b: number) => a >= b; + targetPosition = 0; + } + + parents.forEach((parent) => { + const parentPosition = + dir === 'TB' || dir == 'BT' || useBTLogic + ? commitPos.get(parent)?.y + : commitPos.get(parent)?.x; + + if (parentPosition !== undefined && comparisonFunc(parentPosition, targetPosition)) { + closestParent = parent; + targetPosition = parentPosition; + } + }); + + return closestParent; +}; + +const setParallelBTPos = ( + sortedKeys: string[], + commits: Map, + defaultPos: number, + commitStep: number, + layoutOffset: number +) => { + let curPos = defaultPos; + let maxPosition = defaultPos; + const roots: Commit[] = []; + + sortedKeys.forEach((key) => { + const commit = commits.get(key); + if (!commit) { + throw new Error(`Commit not found for key ${key}`); + } + + if (hasParents(commit)) { + curPos = calculateCommitPosition(commit, commitStep, maxPosition); + maxPosition = Math.max(curPos, maxPosition); + } else { + roots.push(commit); + } + setCommitPosition(commit, curPos, layoutOffset); + }); + + curPos = maxPosition; + roots.forEach((commit) => { + setRootPosition(commit, curPos, defaultPos); + }); +}; + +const hasParents = (commit: Commit): boolean => commit.parents?.length > 0; + +const findClosestParentPos = (commit: Commit): number => { + const closestParent = findClosestParent(commit.parents.filter((p) => p !== null) as string[]); + if (!closestParent) { + throw new Error(`Closest parent not found for commit ${commit.id}`); + } + + const closestParentPos = commitPos.get(closestParent)?.y; + if (closestParentPos === undefined) { + throw new Error(`Closest parent position not found for commit ${commit.id}`); + } + return closestParentPos; +}; + +const calculateCommitPosition = (commit: Commit, commitStep: number): number => { + const closestParentPos = findClosestParentPos(commit); + return closestParentPos + commitStep; +}; + +const setCommitPosition = (commit: Commit, curPos: number, layoutOffset: number) => { + const branch = branchPos.get(commit.branch); + if (!branch) { + throw new Error(`Branch not found for commit ${commit.id}`); + } + + const x = branch.pos; + const y = curPos + layoutOffset; + commitPos.set(commit.id, { x, y }); +}; + +const setRootPosition = (commit: Commit, curPos: number, defaultPos: number) => { + const branch = branchPos.get(commit.branch); + if (!branch) { + throw new Error(`Branch not found for commit ${commit.id}`); + } + + const y = curPos + defaultPos; + const x = branch.pos; + commitPos.set(commit.id, { x, y }); +}; + +const drawCommitBullet = ( + gBullets: d3.Selection, + commit: Commit, + x: number, + y: number, + typeClass: string, + branchIndex: number, + commitSymbolType: number +) => { + if (commitSymbolType === commitType.HIGHLIGHT) { + gBullets + .append('rect') + .attr('x', x - 10) + .attr('y', y - 10) + .attr('width', 20) + .attr('height', 20) + .attr( + 'class', + `commit ${commit.id} commit-highlights${branchIndex % THEME_COLOR_LIMIT} ${typeClass}-outer` + ); + gBullets + .append('rect') + .attr('x', x - 6) + .attr('y', y - 6) + .attr('width', 12) + .attr('height', 12) + .attr( + 'class', + `commit ${commit.id} commit${branchIndex % 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${branchIndex % THEME_COLOR_LIMIT}`); + if (commit.type === 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${branchIndex % 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${branchIndex % THEME_COLOR_LIMIT}`); + } + } +}; + +const drawCommitLabel = ( + gLabels: d3.Selection, + commit: Commit, + x: number, + y: number, + pos: number, + posWithOffset: number, + gitGraphConfig: GitGraphDiagramConfig +) => { + 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', x) + .attr('y', y + 25) + .attr('class', 'commit-label') + .text(commit.id); + const bbox = text.node()?.getBBox(); + + if (bbox) { + labelBkg + .attr('x', posWithOffset - bbox.width / 2 - 2) + .attr('y', y + 13.5) + .attr('width', bbox.width + 4) + .attr('height', bbox.height + 4); + + if (dir === 'TB' || dir === 'BT') { + labelBkg.attr('x', x - (bbox.width + 4)).attr('y', y - 12); + text.attr('x', x - (bbox.width + 2)).attr('y', y + bbox.height - 12); + } + + if (gitGraphConfig.rotateCommitLabel) { + if (dir === 'TB' || dir === 'BT') { + text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); + labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); + } else { + const r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5; + const r_y = 10 + (bbox.width / 25) * 8.5; + wrapper.attr( + 'transform', + 'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')' + ); + } + } + } + } +}; + +const drawCommitTags = ( + gLabels: d3.Selection, + commit: Commit, + x: number, + y: number, + pos: number, + posWithOffset: number, + layoutOffset: number +) => { + 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') + .attr('y', y - 16 - yOffset) + .attr('class', 'tag-label') + .text(tagValue); + const tagBbox = tag.node()?.getBBox(); + if (!tagBbox) { + throw new Error('Tag bbox not found'); + } + maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width); + maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height); + + 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 - 2},${ly + 2} + ${pos - maxTagBboxWidth / 2 - 2},${ly - 2} + ${posWithOffset - maxTagBboxWidth / 2 - 4},${ly - h2 - 2} + ${posWithOffset + maxTagBboxWidth / 2 + 4},${ly - h2 - 2} + ${posWithOffset + maxTagBboxWidth / 2 + 4},${ly + h2 + 2} + ${posWithOffset - maxTagBboxWidth / 2 - 4},${ly + h2 + 2}` + ); + + hole + .attr('cy', ly) + .attr('cx', pos - maxTagBboxWidth / 2 + 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 + 2} + ${x},${yOrigin - 2} + ${x + layoutOffset},${yOrigin - h2 - 2} + ${x + layoutOffset + maxTagBboxWidth + 4},${yOrigin - h2 - 2} + ${x + layoutOffset + maxTagBboxWidth + 4},${yOrigin + h2 + 2} + ${x + layoutOffset},${yOrigin + h2 + 2}` + ) + .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); + hole + .attr('cx', x + 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 + ')'); + } + } + } +}; + +const getCommitClassType = (commit: Commit): string => { + const commitSymbolType = commit.customType ?? commit.type; + switch (commitSymbolType) { + case commitType.NORMAL: + return 'commit-normal'; + case commitType.REVERSE: + return 'commit-reverse'; + case commitType.HIGHLIGHT: + return 'commit-highlight'; + case commitType.MERGE: + return 'commit-merge'; + case commitType.CHERRY_PICK: + return 'commit-cherry-pick'; + default: + return 'commit-normal'; + } +}; + +const calculatePosition = ( + commit: Commit, + dir: string, + isParallelCommits: boolean, + pos: number, + commitStep: number, + layoutOffset: number, + commitPos: Map +): number => { + const defaultCommitPosition = { x: 0, y: defaultPos }; // Default position if commit is not found + + if (isParallelCommits) { + if (commit.parents.length > 0) { + const closestParent = + dir === 'BT' ? findClosestParent(commit.parents) : findClosestParent(commit.parents); + + // Check if closestParent is defined + if (closestParent) { + const parentPosition = commitPos.get(closestParent) ?? defaultCommitPosition; + + if (dir === 'TB') { + return parentPosition.y + commitStep; + } else if (dir === 'BT') { + const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; + return currentPosition.y - commitStep; + } else { + return parentPosition.x + commitStep; + } + } + } else { + if (dir === 'TB') { + return defaultPos; + } else if (dir === 'BT') { + const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; + return currentPosition.y - commitStep; + } else { + return 0; + } + } + } + + return dir === 'TB' && isParallelCommits ? pos : pos + layoutOffset; +}; + +const drawCommits = ( + svg: d3.Selection, + commits: Map, + modifyGraph: boolean +) => { + const gitGraphConfig = getConfig().gitGraph; + if (!gitGraphConfig) { + throw new Error('GitGraph config not found'); + } + const gBullets = svg.append('g').attr('class', 'commit-bullets'); + const gLabels = svg.append('g').attr('class', 'commit-labels'); + let pos = dir === 'TB' || dir === 'BT' ? defaultPos : 0; + const keys = [...commits.keys()]; + const isParallelCommits = gitGraphConfig?.parallelCommits ?? false; + const layoutOffset = 10; + const commitStep = 40; + + const sortKeys = (a: string, b: string) => { + const seqA = commits.get(a)?.seq; + const seqB = commits.get(b)?.seq; + return seqA !== undefined && seqB !== undefined ? seqA - seqB : 0; + }; + + let sortedKeys = keys.sort(sortKeys); + + if (dir === 'BT' && !isParallelCommits) { + sortedKeys = sortedKeys.reverse(); + } + + if (dir === 'BT' && isParallelCommits) { + setParallelBTPos(sortedKeys, commits, pos, commitStep, layoutOffset); + sortedKeys = sortedKeys.reverse(); + } + + sortedKeys.forEach((key) => { + const commit = commits.get(key); + if (!commit) { + throw new Error(`Commit not found for key ${key}`); + } else { + pos = calculatePosition( + commit, + dir, + isParallelCommits, + pos, + commitStep, + layoutOffset, + commitPos + ); + 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; + if (x === undefined || y === undefined) { + throw new Error(`Position were undefined for commit ${commit.id}`); + } + // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. + if (modifyGraph) { + const typeClass = getCommitClassType(commit); + const commitSymbolType = commit.customType ?? commit.type; + const branchIndex = branchPos.get(commit.branch)?.index ?? 0; + drawCommitBullet(gBullets, commit, x, y, typeClass, branchIndex, commitSymbolType); + drawCommitLabel(gLabels, commit, x, y, pos, posWithOffset, gitGraphConfig); + drawCommitTags(gLabels, commit, x, y, pos, posWithOffset, layoutOffset); + } + if (dir === 'TB' || dir === 'BT') { + commitPos.set(commit.id, { x: x, y: posWithOffset }); + } else { + commitPos.set(commit.id, { x: posWithOffset, y: y }); + } + pos = dir === 'BT' && isParallelCommits ? pos + commitStep : pos + commitStep + layoutOffset; + if (pos > maxPos) { + maxPos = pos; + } + } + }); +}; + +const shouldRerouteArrow = ( + commitA: Commit, + commitB: Commit, + p1: CommitPosition, + p2: CommitPosition, + allCommits: Map +) => { + const commitBIsFurthest = dir === 'TB' || dir === 'BT' ? p1.x < p2.x : p1.y < p2.y; + const branchToGetCurve = commitBIsFurthest ? commitB.branch : commitA.branch; + const isOnBranchToGetCurve = (x: Commit) => x.branch === branchToGetCurve; + const isBetweenCommits = (x: Commit) => x.seq > commitA.seq && x.seq < commitB.seq; + return [...allCommits.values()].some((commitX) => { + return isBetweenCommits(commitX) && isOnBranchToGetCurve(commitX); + }); +}; + +const findLane = (y1: number, y2: number, depth = 0): number => { + const candidate = y1 + Math.abs(y1 - y2) / 2; + if (depth > 5) { + return candidate; + } + + const 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); +}; + +const drawArrow = ( + svg: d3.Selection, + commitA: Commit, + commitB: Commit, + allCommits: Map +) => { + const p1 = commitPos.get(commitA.id); // arrowStart + const p2 = commitPos.get(commitB.id); // arrowEnd + // @ts-ignore: TODO Fix ts errors + 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; + // @ts-ignore: TODO Fix ts errors + let colorClassNum = branchPos.get(commitB.branch).index; + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + // @ts-ignore: TODO Fix ts errors + 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; + // @ts-ignore: TODO Fix ts errors + const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y); + // @ts-ignore: TODO Fix ts errors + const lineX = p1.x < p2.x ? findLane(p1.x, p2.x) : findLane(p2.x, p1.x); + + if (dir === 'TB') { + // @ts-ignore: TODO Fix ts errors + if (p1.x < p2.x) { + // Source commit is on branch position left of destination commit + // so render arrow rightward with colour of destination branch + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc2} ${lineX} ${ + // @ts-ignore: TODO Fix ts errors + p1.y + offset + // @ts-ignore: TODO Fix ts errors + } 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 + // @ts-ignore: TODO Fix ts errors + colorClassNum = branchPos.get(commitA.branch).index; + // @ts-ignore: TODO Fix ts errors + 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') { + // @ts-ignore: TODO Fix ts errors + if (p1.x < p2.x) { + // Source commit is on branch position left of destination commit + // so render arrow rightward with colour of destination branch + // @ts-ignore: TODO Fix ts errors + 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 + // @ts-ignore: TODO Fix ts errors + colorClassNum = branchPos.get(commitA.branch).index; + // @ts-ignore: TODO Fix ts errors + 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 { + // @ts-ignore: TODO Fix ts errors + if (p1.y < p2.y) { + // Source commit is on branch positioned above destination commit + // so render arrow downward with colour of destination branch + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${ + // @ts-ignore: TODO Fix ts errors + p1.x + offset + // @ts-ignore: TODO Fix ts errors + } ${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 + // @ts-ignore: TODO Fix ts errors + colorClassNum = branchPos.get(commitA.branch).index; + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${ + // @ts-ignore: TODO Fix ts errors + p1.x + offset + // @ts-ignore: TODO Fix ts errors + } ${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') { + // @ts-ignore: TODO Fix ts errors + if (p1.x < p2.x) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y + offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } + // @ts-ignore: TODO Fix ts errors + 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]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x + radius} ${p1.y} ${arc} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y + offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } + // @ts-ignore: TODO Fix ts errors + if (p1.x === p2.x) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } else if (dir === 'BT') { + // @ts-ignore: TODO Fix ts errors + if (p1.x < p2.x) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y - offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } + // @ts-ignore: TODO Fix ts errors + 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]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y - offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } + // @ts-ignore: TODO Fix ts errors + if (p1.x === p2.x) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } else { + // @ts-ignore: TODO Fix ts errors + if (p1.y < p2.y) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y + offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } // @ts-ignore: TODO Fix ts errors + if (p1.y > p2.y) { + if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ + // @ts-ignore: TODO Fix ts errors + p1.y - offset + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } else { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ + // @ts-ignore: TODO Fix ts errors + p2.y + // @ts-ignore: TODO Fix ts errors + } L ${p2.x} ${p2.y}`; + } + } + // @ts-ignore: TODO Fix ts errors + if (p1.y === p2.y) { + // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; + } + } + } + svg + .append('path') + // @ts-ignore: TODO Fix ts errors + .attr('d', lineDef) + .attr('class', 'arrow arrow' + (colorClassNum % THEME_COLOR_LIMIT)); +}; + +const drawArrows = ( + svg: d3.Selection, + commits: Map +) => { + const gArrows = svg.append('g').attr('class', 'commit-arrows'); + [...commits.keys()].forEach((key) => { + const commit = commits.get(key); + // @ts-ignore: TODO Fix ts errors + if (commit.parents && commit.parents.length > 0) { + // @ts-ignore: TODO Fix ts errors + commit.parents.forEach((parent) => { + // @ts-ignore: TODO Fix ts errors + drawArrow(gArrows, commits.get(parent), commit, commits); + }); + } + }); +}; + +const drawBranches = ( + svg: d3.Selection, + branches: { name: string }[] +) => { + const gitGraphConfig = getConfig().gitGraph; + const g = svg.append('g'); + branches.forEach((branch, index) => { + // @ts-ignore: TODO Fix ts errors + const adjustIndexForTheme = index % THEME_COLOR_LIMIT; + // @ts-ignore: TODO Fix ts errors + 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); + + const 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); + // @ts-ignore: TODO Fix ts errors + label.node().appendChild(labelElement); + const bbox = labelElement.getBBox(); + bkg + .attr('class', 'branchLabelBkg label' + adjustIndexForTheme) + .attr('rx', 4) + .attr('ry', 4) + // @ts-ignore: TODO Fix ts errors + .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(' + + // @ts-ignore: TODO Fix ts errors + (-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) + ')'); + } + }); +}; + +export const draw: DrawDefinition = function (txt, id, ver, diagObj) { + clear(); + const conf = getConfig(); + const gitGraphConfig = conf.gitGraph; + // try { + log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); + const db = diagObj.db as GitGraphDB; + allCommitsDict = db.getCommits(); + const branches = db.getBranchesAsObjArray(); + dir = 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'); + // @ts-ignore: TODO Fix ts errors + label.node().appendChild(labelElement); + const bbox = labelElement.getBBox(); + + branchPos.set(branch.name, { pos, index }); + pos += + 50 + + // @ts-ignore: TODO Fix ts errors + (gitGraphConfig.rotateCommitLabel ? 40 : 0) + + (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); + label.remove(); + branchLabel.remove(); + g.remove(); + }); + + drawCommits(diagram, allCommitsDict, false); + // @ts-ignore: TODO Fix ts errors + if (gitGraphConfig.showBranches) { + drawBranches(diagram, branches); + } + drawArrows(diagram, allCommitsDict); + drawCommits(diagram, allCommitsDict, true); + utils.insertTitle( + diagram, + 'gitTitleText', + // @ts-ignore: TODO Fix ts errors + gitGraphConfig.titleTopMargin, + db.getDiagramTitle() + ); + + // Setup the view box and size of the svg element + setupGraphViewbox( + undefined, + diagram, + // @ts-ignore: TODO Fix ts errors + gitGraphConfig.diagramPadding, + // @ts-ignore: TODO Fix ts errors + gitGraphConfig.useMaxWidth ?? conf.useMaxWidth + ); +}; + +export default { + draw, +}; diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 29e2781a97..0e929666f1 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -1,4 +1,13 @@ -export type CommitType = 'NORMAL' | 'REVERSE' | 'HIGHLIGHT' | 'MERGE' | 'CHERRY_PICK'; +import type { DiagramDB } from '../../diagram-api/types.js'; +import type { GitGraphDiagramConfig } from '../../config.type.js'; + +export interface CommitType { + NORMAL: number; + REVERSE: number; + HIGHLIGHT: number; + MERGE: number; + CHERRY_PICK: number; +} export interface Commit { id: string; @@ -16,6 +25,11 @@ export interface GitGraph { statements: Statement[]; } +export interface Position { + x: number; + y: number; +} + export type Statement = CommitAst | BranchAst | MergeAst | CheckoutAst | CherryPickingAst; export interface CommitAst { @@ -52,4 +66,47 @@ export interface CherryPickingAst { parent: string; } +export interface GitGraphDB extends DiagramDB { + // config + getConfig: () => GitGraphDiagramConfig | undefined; + + // common db + clear: () => void; + setDiagramTitle: (title: string) => void; + getDiagramTitle: () => string; + setAccTitle: (title: string) => void; + getAccTitle: () => string; + setAccDescription: (description: string) => void; + getAccDescription: () => string; + + // diagram db + commitType: CommitType; + setDirection: (direction: DiagramOrientation) => void; + getDirection: () => DiagramOrientation; + setOptions: (options: string) => void; + getOptions: () => string; + commit: (msg: string, id: string, type: number, tags?: string[] | undefined) => void; + branch: (name: string, order: number) => void; + merge: ( + otherBranch: string, + customId?: string, + overrideType?: number, + customTags?: string[] + ) => void; + cherryPick: ( + sourceId: string, + targetId: string, + tags: string[] | undefined, + parentCommitId: string + ) => void; + checkout: (branch: string) => void; + prettyPrint: () => void; + getBranchesAsObjArray: () => { name: string }[]; + getBranches: () => Map; + getCommits: () => Map; + getCommitsArray: () => Commit[]; + getCurrentBranch: () => string; + getHead: () => Commit | null; +} + export type DiagramOrientation = 'LR' | 'TB'; diff --git a/packages/mermaid/tsconfig.json b/packages/mermaid/tsconfig.json index f4451225ed..66c6600f63 100644 --- a/packages/mermaid/tsconfig.json +++ b/packages/mermaid/tsconfig.json @@ -5,5 +5,10 @@ "outDir": "./dist", "types": ["vitest/importMeta", "vitest/globals"] }, - "include": ["./src/**/*.ts", "./package.json", "src/diagrams/gantt/ganttDb.js"] + "include": [ + "./src/**/*.ts", + "./package.json", + "src/diagrams/gantt/ganttDb.js", + "src/diagrams/git/gitGraphRenderer.js" + ] } From 2218929416b905a78c465041c1b0f73b4ad9594f Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Thu, 8 Aug 2024 19:49:09 -0400 Subject: [PATCH 31/49] fixed types in gitGraphTypes --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 4 ++-- packages/mermaid/src/diagrams/git/gitGraphTypes.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 53c361e3ed..1a40450d9e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -213,7 +213,7 @@ export const merge = ( id: customId ? customId : state.records.seq + '-' + getID(), message: `merged branch ${otherBranch} into ${state.records.currBranch}`, seq: state.records.seq++, - parents: [state.records.head == null ? null : state.records.head.id, verifiedBranch], + parents: state.records.head == null ? [] : [state.records.head.id, verifiedBranch], branch: state.records.currBranch, type: commitType.MERGE, customType: overrideType, @@ -317,7 +317,7 @@ export const cherryPick = function ( id: state.records.seq + '-' + getID(), message: `cherry-picked ${sourceCommit?.message} into ${state.records.currBranch}`, seq: state.records.seq++, - parents: [state.records.head == null ? null : state.records.head.id, sourceCommit.id], + parents: state.records.head == null ? [] : [state.records.head.id, sourceCommit.id], branch: state.records.currBranch, type: commitType.CHERRY_PICK, tags: tags diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 0e929666f1..b473dd8749 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -15,7 +15,7 @@ export interface Commit { seq: number; type: number; tags: string[]; - parents: (string | null)[]; + parents: string[]; branch: string; customType?: number; customId?: boolean; @@ -109,4 +109,4 @@ export interface GitGraphDB extends DiagramDB { getHead: () => Commit | null; } -export type DiagramOrientation = 'LR' | 'TB'; +export type DiagramOrientation = 'LR' | 'TB' | 'BT'; From a93b8324ad882f6ff085f02a2f550a5a7f2ed8a7 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 06:08:57 -0400 Subject: [PATCH 32/49] made draw commit more readable, included more helper functions and interfaces, added in-source test suite to renderer --- .../src/diagrams/git/gitGraphRenderer.ts | 536 ++++++++++++------ 1 file changed, 375 insertions(+), 161 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index ec9a39f2a3..3493fa9953 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -9,6 +9,9 @@ import type { GitGraphDiagramConfig } from '../../config.type.js'; let allCommitsDict = new Map(); +const LAYOUT_OFFSET = 10; +const COMMIT_STEP = 40; + const commitType: CommitType = { NORMAL: 0, REVERSE: 1, @@ -29,6 +32,10 @@ interface CommitPosition { y: number; } +interface CommitPositionOffset extends CommitPosition { + posWithOffset: number; +} + const branchPos = new Map(); const commitPos = new Map(); let lanes: number[] = []; @@ -93,9 +100,7 @@ const findClosestParent = (parents: string[], useBTLogic = false): string | unde const setParallelBTPos = ( sortedKeys: string[], commits: Map, - defaultPos: number, - commitStep: number, - layoutOffset: number + defaultPos: number ) => { let curPos = defaultPos; let maxPosition = defaultPos; @@ -108,12 +113,12 @@ const setParallelBTPos = ( } if (hasParents(commit)) { - curPos = calculateCommitPosition(commit, commitStep, maxPosition); + curPos = calculateCommitPosition(commit); maxPosition = Math.max(curPos, maxPosition); } else { roots.push(commit); } - setCommitPosition(commit, curPos, layoutOffset); + setCommitPosition(commit, curPos); }); curPos = maxPosition; @@ -125,7 +130,7 @@ const setParallelBTPos = ( const hasParents = (commit: Commit): boolean => commit.parents?.length > 0; const findClosestParentPos = (commit: Commit): number => { - const closestParent = findClosestParent(commit.parents.filter((p) => p !== null) as string[]); + const closestParent = findClosestParent(commit.parents.filter((p) => p !== null)); if (!closestParent) { throw new Error(`Closest parent not found for commit ${commit.id}`); } @@ -137,19 +142,19 @@ const findClosestParentPos = (commit: Commit): number => { return closestParentPos; }; -const calculateCommitPosition = (commit: Commit, commitStep: number): number => { +const calculateCommitPosition = (commit: Commit): number => { const closestParentPos = findClosestParentPos(commit); - return closestParentPos + commitStep; + return closestParentPos + COMMIT_STEP; }; -const setCommitPosition = (commit: Commit, curPos: number, layoutOffset: number) => { +const setCommitPosition = (commit: Commit, curPos: number) => { const branch = branchPos.get(commit.branch); if (!branch) { throw new Error(`Branch not found for commit ${commit.id}`); } const x = branch.pos; - const y = curPos + layoutOffset; + const y = curPos + LAYOUT_OFFSET; commitPos.set(commit.id, { x, y }); }; @@ -167,8 +172,7 @@ const setRootPosition = (commit: Commit, curPos: number, defaultPos: number) => const drawCommitBullet = ( gBullets: d3.Selection, commit: Commit, - x: number, - y: number, + commitPosition: CommitPositionOffset, typeClass: string, branchIndex: number, commitSymbolType: number @@ -176,8 +180,8 @@ const drawCommitBullet = ( if (commitSymbolType === commitType.HIGHLIGHT) { gBullets .append('rect') - .attr('x', x - 10) - .attr('y', y - 10) + .attr('x', commitPosition.x - 10) + .attr('y', commitPosition.y - 10) .attr('width', 20) .attr('height', 20) .attr( @@ -186,8 +190,8 @@ const drawCommitBullet = ( ); gBullets .append('rect') - .attr('x', x - 6) - .attr('y', y - 6) + .attr('x', commitPosition.x - 6) + .attr('y', commitPosition.y - 6) .attr('width', 12) .attr('height', 12) .attr( @@ -197,50 +201,50 @@ const drawCommitBullet = ( } else if (commitSymbolType === commitType.CHERRY_PICK) { gBullets .append('circle') - .attr('cx', x) - .attr('cy', y) + .attr('cx', commitPosition.x) + .attr('cy', commitPosition.y) .attr('r', 10) .attr('class', `commit ${commit.id} ${typeClass}`); gBullets .append('circle') - .attr('cx', x - 3) - .attr('cy', y + 2) + .attr('cx', commitPosition.x - 3) + .attr('cy', commitPosition.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('cx', commitPosition.x + 3) + .attr('cy', commitPosition.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('x1', commitPosition.x + 3) + .attr('y1', commitPosition.y + 1) + .attr('x2', commitPosition.x) + .attr('y2', commitPosition.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('x1', commitPosition.x - 3) + .attr('y1', commitPosition.y + 1) + .attr('x2', commitPosition.x) + .attr('y2', commitPosition.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('cx', commitPosition.x); + circle.attr('cy', commitPosition.y); circle.attr('r', commit.type === commitType.MERGE ? 9 : 10); circle.attr('class', `commit ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); if (commit.type === commitType.MERGE) { const circle2 = gBullets.append('circle'); - circle2.attr('cx', x); - circle2.attr('cy', y); + circle2.attr('cx', commitPosition.x); + circle2.attr('cy', commitPosition.y); circle2.attr('r', 6); circle2.attr( 'class', @@ -250,7 +254,10 @@ const drawCommitBullet = ( 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( + 'd', + `M ${commitPosition.x - 5},${commitPosition.y - 5}L${commitPosition.x + 5},${commitPosition.y + 5}M${commitPosition.x - 5},${commitPosition.y + 5}L${commitPosition.x + 5},${commitPosition.y - 5}` + ) .attr('class', `commit ${typeClass} ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); } } @@ -259,10 +266,8 @@ const drawCommitBullet = ( const drawCommitLabel = ( gLabels: d3.Selection, commit: Commit, - x: number, - y: number, + commitPosition: CommitPositionOffset, pos: number, - posWithOffset: number, gitGraphConfig: GitGraphDiagramConfig ) => { if ( @@ -274,34 +279,52 @@ const drawCommitLabel = ( const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); const text = wrapper .append('text') - .attr('x', x) - .attr('y', y + 25) + .attr('x', commitPosition.x) + .attr('y', commitPosition.y + 25) .attr('class', 'commit-label') .text(commit.id); const bbox = text.node()?.getBBox(); if (bbox) { labelBkg - .attr('x', posWithOffset - bbox.width / 2 - 2) - .attr('y', y + 13.5) + .attr('x', commitPosition.posWithOffset - bbox.width / 2 - 2) + .attr('y', commitPosition.y + 13.5) .attr('width', bbox.width + 4) .attr('height', bbox.height + 4); if (dir === 'TB' || dir === 'BT') { - labelBkg.attr('x', x - (bbox.width + 4)).attr('y', y - 12); - text.attr('x', x - (bbox.width + 2)).attr('y', y + bbox.height - 12); + labelBkg.attr('x', commitPosition.x - (bbox.width + 4)).attr('y', commitPosition.y - 12); + text + .attr('x', commitPosition.x - (bbox.width + 2)) + .attr('y', commitPosition.y + bbox.height - 12); } if (gitGraphConfig.rotateCommitLabel) { if (dir === 'TB' || dir === 'BT') { - text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); - labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')'); + text.attr( + 'transform', + 'rotate(' + -45 + ', ' + commitPosition.x + ', ' + commitPosition.y + ')' + ); + labelBkg.attr( + 'transform', + 'rotate(' + -45 + ', ' + commitPosition.x + ', ' + commitPosition.y + ')' + ); } else { const r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5; const r_y = 10 + (bbox.width / 25) * 8.5; wrapper.attr( 'transform', - 'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')' + 'translate(' + + r_x + + ', ' + + r_y + + ') rotate(' + + -45 + + ', ' + + pos + + ', ' + + commitPosition.y + + ')' ); } } @@ -312,11 +335,8 @@ const drawCommitLabel = ( const drawCommitTags = ( gLabels: d3.Selection, commit: Commit, - x: number, - y: number, - pos: number, - posWithOffset: number, - layoutOffset: number + commitPosition: CommitPositionOffset, + pos: number ) => { if (commit.tags.length > 0) { let yOffset = 0; @@ -329,7 +349,7 @@ const drawCommitTags = ( const hole = gLabels.append('circle'); const tag = gLabels .append('text') - .attr('y', y - 16 - yOffset) + .attr('y', commitPosition.y - 16 - yOffset) .attr('class', 'tag-label') .text(tagValue); const tagBbox = tag.node()?.getBBox(); @@ -339,7 +359,7 @@ const drawCommitTags = ( maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width); maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height); - tag.attr('x', posWithOffset - tagBbox.width / 2); + tag.attr('x', commitPosition.posWithOffset - tagBbox.width / 2); tagElements.push({ tag, @@ -353,16 +373,16 @@ const drawCommitTags = ( for (const { tag, hole, rect, yOffset } of tagElements) { const h2 = maxTagBboxHeight / 2; - const ly = y - 19.2 - yOffset; + const ly = commitPosition.y - 19.2 - yOffset; rect.attr('class', 'tag-label-bkg').attr( 'points', ` ${pos - maxTagBboxWidth / 2 - 2},${ly + 2} ${pos - maxTagBboxWidth / 2 - 2},${ly - 2} - ${posWithOffset - maxTagBboxWidth / 2 - 4},${ly - h2 - 2} - ${posWithOffset + maxTagBboxWidth / 2 + 4},${ly - h2 - 2} - ${posWithOffset + maxTagBboxWidth / 2 + 4},${ly + h2 + 2} - ${posWithOffset - maxTagBboxWidth / 2 - 4},${ly + h2 + 2}` + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - 4},${ly - h2 - 2} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + 4},${ly - h2 - 2} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + 4},${ly + h2 + 2} + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - 4},${ly + h2 + 2}` ); hole @@ -379,22 +399,22 @@ const drawCommitTags = ( .attr( 'points', ` - ${x},${yOrigin + 2} - ${x},${yOrigin - 2} - ${x + layoutOffset},${yOrigin - h2 - 2} - ${x + layoutOffset + maxTagBboxWidth + 4},${yOrigin - h2 - 2} - ${x + layoutOffset + maxTagBboxWidth + 4},${yOrigin + h2 + 2} - ${x + layoutOffset},${yOrigin + h2 + 2}` + ${commitPosition.x},${yOrigin + 2} + ${commitPosition.x},${yOrigin - 2} + ${commitPosition.x + LAYOUT_OFFSET},${yOrigin - h2 - 2} + ${commitPosition.x + LAYOUT_OFFSET + maxTagBboxWidth + 4},${yOrigin - h2 - 2} + ${commitPosition.x + LAYOUT_OFFSET + maxTagBboxWidth + 4},${yOrigin + h2 + 2} + ${commitPosition.x + LAYOUT_OFFSET},${yOrigin + h2 + 2}` ) - .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); + .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); hole - .attr('cx', x + 2) + .attr('cx', commitPosition.x + 2) .attr('cy', yOrigin) - .attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')'); + .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); tag - .attr('x', x + 5) + .attr('x', commitPosition.x + 5) .attr('y', yOrigin + 3) - .attr('transform', 'translate(14,14) rotate(45, ' + x + ',' + pos + ')'); + .attr('transform', 'translate(14,14) rotate(45, ' + commitPosition.x + ',' + pos + ')'); } } } @@ -421,45 +441,50 @@ const getCommitClassType = (commit: Commit): string => { const calculatePosition = ( commit: Commit, dir: string, - isParallelCommits: boolean, pos: number, - commitStep: number, - layoutOffset: number, commitPos: Map ): number => { - const defaultCommitPosition = { x: 0, y: defaultPos }; // Default position if commit is not found - - if (isParallelCommits) { - if (commit.parents.length > 0) { - const closestParent = - dir === 'BT' ? findClosestParent(commit.parents) : findClosestParent(commit.parents); - - // Check if closestParent is defined - if (closestParent) { - const parentPosition = commitPos.get(closestParent) ?? defaultCommitPosition; - - if (dir === 'TB') { - return parentPosition.y + commitStep; - } else if (dir === 'BT') { - const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; - return currentPosition.y - commitStep; - } else { - return parentPosition.x + commitStep; - } - } - } else { + const defaultCommitPosition = { x: 0, y: 0 }; // Default position if commit is not found + + if (commit.parents.length > 0) { + const closestParent = findClosestParent(commit.parents); + if (closestParent) { + const parentPosition = commitPos.get(closestParent) ?? defaultCommitPosition; + if (dir === 'TB') { - return defaultPos; + return parentPosition.y + COMMIT_STEP; } else if (dir === 'BT') { const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; - return currentPosition.y - commitStep; + return currentPosition.y - COMMIT_STEP; } else { - return 0; + return parentPosition.x + COMMIT_STEP; } } + } else { + if (dir === 'TB') { + return defaultPos; + } else if (dir === 'BT') { + const currentPosition = commitPos.get(commit.id) ?? defaultCommitPosition; + return currentPosition.y - COMMIT_STEP; + } else { + return 0; + } } + return 0; +}; - return dir === 'TB' && isParallelCommits ? pos : pos + layoutOffset; +const getCommitPosition = ( + commit: Commit, + pos: number, + isParallelCommits: boolean +): CommitPositionOffset => { + const posWithOffset = dir === 'BT' && isParallelCommits ? pos : pos + LAYOUT_OFFSET; + const y = dir === 'TB' || dir === 'BT' ? posWithOffset : branchPos.get(commit.branch)?.pos; + const x = dir === 'TB' || dir === 'BT' ? branchPos.get(commit.branch)?.pos : posWithOffset; + if (x === undefined || y === undefined) { + throw new Error(`Position were undefined for commit ${commit.id}`); + } + return { x, y, posWithOffset }; }; const drawCommits = ( @@ -476,8 +501,6 @@ const drawCommits = ( let pos = dir === 'TB' || dir === 'BT' ? defaultPos : 0; const keys = [...commits.keys()]; const isParallelCommits = gitGraphConfig?.parallelCommits ?? false; - const layoutOffset = 10; - const commitStep = 40; const sortKeys = (a: string, b: string) => { const seqA = commits.get(a)?.seq; @@ -487,12 +510,10 @@ const drawCommits = ( let sortedKeys = keys.sort(sortKeys); - if (dir === 'BT' && !isParallelCommits) { - sortedKeys = sortedKeys.reverse(); - } - - if (dir === 'BT' && isParallelCommits) { - setParallelBTPos(sortedKeys, commits, pos, commitStep, layoutOffset); + if (dir === 'BT') { + if (isParallelCommits) { + setParallelBTPos(sortedKeys, commits, pos); + } sortedKeys = sortedKeys.reverse(); } @@ -500,40 +521,29 @@ const drawCommits = ( const commit = commits.get(key); if (!commit) { throw new Error(`Commit not found for key ${key}`); + } + if (isParallelCommits) { + pos = calculatePosition(commit, dir, pos, commitPos); + } + + const commitPosition = getCommitPosition(commit, pos, isParallelCommits); + // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. + if (modifyGraph) { + const typeClass = getCommitClassType(commit); + const commitSymbolType = commit.customType ?? commit.type; + const branchIndex = branchPos.get(commit.branch)?.index ?? 0; + drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commitSymbolType); + drawCommitLabel(gLabels, commit, commitPosition, pos, gitGraphConfig); + drawCommitTags(gLabels, commit, commitPosition, pos); + } + if (dir === 'TB' || dir === 'BT') { + commitPos.set(commit.id, { x: commitPosition.x, y: commitPosition.posWithOffset }); } else { - pos = calculatePosition( - commit, - dir, - isParallelCommits, - pos, - commitStep, - layoutOffset, - commitPos - ); - 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; - if (x === undefined || y === undefined) { - throw new Error(`Position were undefined for commit ${commit.id}`); - } - // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. - if (modifyGraph) { - const typeClass = getCommitClassType(commit); - const commitSymbolType = commit.customType ?? commit.type; - const branchIndex = branchPos.get(commit.branch)?.index ?? 0; - drawCommitBullet(gBullets, commit, x, y, typeClass, branchIndex, commitSymbolType); - drawCommitLabel(gLabels, commit, x, y, pos, posWithOffset, gitGraphConfig); - drawCommitTags(gLabels, commit, x, y, pos, posWithOffset, layoutOffset); - } - if (dir === 'TB' || dir === 'BT') { - commitPos.set(commit.id, { x: x, y: posWithOffset }); - } else { - commitPos.set(commit.id, { x: posWithOffset, y: y }); - } - pos = dir === 'BT' && isParallelCommits ? pos + commitStep : pos + commitStep + layoutOffset; - if (pos > maxPos) { - maxPos = pos; - } + commitPos.set(commit.id, { x: commitPosition.posWithOffset, y: commitPosition.y }); + } + pos = dir === 'BT' && isParallelCommits ? pos + COMMIT_STEP : pos + COMMIT_STEP + LAYOUT_OFFSET; + if (pos > maxPos) { + maxPos = pos; } }); }; @@ -904,65 +914,269 @@ const drawBranches = ( }); }; +const setBranchPosition = function ( + name: string, + pos: number, + index: number, + bbox: DOMRect, + rotateCommitLabel: boolean +): number { + branchPos.set(name, { pos, index }); + pos += 50 + (rotateCommitLabel ? 40 : 0) + (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); + return pos; +}; + export const draw: DrawDefinition = function (txt, id, ver, diagObj) { clear(); const conf = getConfig(); const gitGraphConfig = conf.gitGraph; - // try { + log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); + if (!gitGraphConfig) { + throw new Error('GitGraph config not found'); + } + const rotateCommitLabel = gitGraphConfig.rotateCommitLabel ?? false; const db = diagObj.db as GitGraphDB; allCommitsDict = db.getCommits(); const branches = db.getBranchesAsObjArray(); dir = 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'); - // @ts-ignore: TODO Fix ts errors - label.node().appendChild(labelElement); const bbox = labelElement.getBBox(); - - branchPos.set(branch.name, { pos, index }); - pos += - 50 + - // @ts-ignore: TODO Fix ts errors - (gitGraphConfig.rotateCommitLabel ? 40 : 0) + - (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); + pos = setBranchPosition(branch.name, pos, index, bbox, rotateCommitLabel); label.remove(); branchLabel.remove(); g.remove(); }); drawCommits(diagram, allCommitsDict, false); - // @ts-ignore: TODO Fix ts errors if (gitGraphConfig.showBranches) { drawBranches(diagram, branches); } drawArrows(diagram, allCommitsDict); drawCommits(diagram, allCommitsDict, true); + utils.insertTitle( diagram, 'gitTitleText', - // @ts-ignore: TODO Fix ts errors - gitGraphConfig.titleTopMargin, + gitGraphConfig.titleTopMargin ?? 0, db.getDiagramTitle() ); // Setup the view box and size of the svg element - setupGraphViewbox( - undefined, - diagram, - // @ts-ignore: TODO Fix ts errors - gitGraphConfig.diagramPadding, - // @ts-ignore: TODO Fix ts errors - gitGraphConfig.useMaxWidth ?? conf.useMaxWidth - ); + setupGraphViewbox(undefined, diagram, gitGraphConfig.diagramPadding, gitGraphConfig.useMaxWidth); }; export default { draw, }; + +if (import.meta.vitest) { + const { it, expect, describe } = import.meta.vitest; + + describe('drawText', () => { + it('should drawText', () => { + const svgLabel = drawText('main'); + expect(svgLabel).toBeDefined(); + expect(svgLabel.children[0].innerHTML).toBe('main'); + }); + }); + + describe('drawBranchPositions', () => { + const bbox: DOMRect = { + x: 0, + y: 0, + width: 10, + height: 10, + top: 0, + right: 0, + bottom: 0, + left: 0, + toJSON: () => '', + }; + + it('should setBranchPositions LR with two branches', () => { + dir = 'LR'; + + const pos = setBranchPosition('main', 0, 0, bbox, true); + expect(pos).toBe(90); + expect(branchPos.get('main')).toEqual({ pos: 0, index: 0 }); + const posNext = setBranchPosition('develop', pos, 1, bbox, true); + expect(posNext).toBe(180); + expect(branchPos.get('develop')).toEqual({ pos: pos, index: 1 }); + }); + + it('should setBranchPositions TB with two branches', () => { + dir = 'TB'; + bbox.width = 34.9921875; + + const pos = setBranchPosition('main', 0, 0, bbox, true); + expect(pos).toBe(107.49609375); + expect(branchPos.get('main')).toEqual({ pos: 0, index: 0 }); + + bbox.width = 56.421875; + const posNext = setBranchPosition('develop', pos, 1, bbox, true); + expect(posNext).toBe(225.70703125); + expect(branchPos.get('develop')).toEqual({ pos: pos, index: 1 }); + }); + }); + /* + describe('drawCommits', () => { + dir = 'TB'; + const commits = new Map([ + [ + 'commitZero', + { + id: 'ZERO', + message: '', + seq: 0, + type: commitType.NORMAL, + tags: [], + parents: [], + branch: 'main', + }, + ], + [ + 'commitA', + { + id: 'A', + message: '', + seq: 1, + type: commitType.NORMAL, + tags: [], + parents: ['ZERO'], + branch: 'feature', + }, + ], + [ + 'commitB', + { + id: 'B', + message: '', + seq: 2, + type: commitType.NORMAL, + tags: [], + parents: ['A'], + branch: 'feature', + }, + ], + [ + 'commitM', + { + id: 'M', + message: 'merged branch feature into main', + seq: 3, + type: commitType.MERGE, + tags: [], + parents: ['ZERO', 'B'], + branch: 'main', + customId: true, + }, + ], + [ + 'commitC', + { + id: 'C', + message: '', + seq: 4, + type: commitType.NORMAL, + tags: [], + parents: ['ZERO'], + branch: 'release', + }, + ], + [ + 'commit5_8928ea0', + { + id: '5-8928ea0', + message: 'cherry-picked [object Object] into release', + seq: 5, + type: commitType.CHERRY_PICK, + tags: [], + parents: ['C', 'M'], + branch: 'release', + }, + ], + [ + 'commitD', + { + id: 'D', + message: '', + seq: 6, + type: commitType.NORMAL, + tags: [], + parents: ['5-8928ea0'], + branch: 'release', + }, + ], + [ + 'commit7_ed848ba', + { + id: '7-ed848ba', + message: 'cherry-picked [object Object] into release', + seq: 7, + type: commitType.CHERRY_PICK, + tags: [], + parents: ['D', 'M'], + branch: 'release', + }, + ], + ]); + + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('feature', { pos: 107.49609375, index: 1 }); + branchPos.set('release', { pos: 224.03515625, index: 2 }); + + commits.forEach((commit) => { + it(`should draw commit ${commit.id}`, () => { + const commitPosition = getCommitPosition(commit, 0, false); + expect(commitPosition).toBeDefined(); + }); + it(`should draw commit ${commit.id} with position`, () => { + const commitPosition = getCommitPosition(commit, 0, false); + expect(commitPosition.x).toBeDefined(); + expect(commitPosition.y).toBeDefined(); + expect(commitPosition.posWithOffset).toBeDefined(); + } + it(`should draw commit ${commit.id} bullet`, () => { + const gBullets = svg.append('g').attr('class', 'commit-bullets'); + const typeClass = getCommitClassType(commit); + const branchIndex = branchPos.get(commit.branch)?.index ?? 0; + drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commit.type); + } + it(`should draw commit ${commit.id} label`, () => { + const gLabels = svg.append('g').attr('class', 'commit-labels'); + drawCommitLabel(gLabels, commit, commitPosition, 0, gitGraphConfig); + } + }); +*/ + describe('drawBranches', () => { + it('should drawBranches', () => { + expect(true).toBe(true); + }); + }); + + describe('drawArrows', () => { + it('should drawArrows', () => { + expect(true).toBe(true); + }); + }); + + it('add', () => { + commitPos.set('parent1', { x: 1, y: 1 }); + commitPos.set('parent2', { x: 2, y: 2 }); + commitPos.set('parent3', { x: 3, y: 3 }); + dir = 'LR'; + const parents = ['parent1', 'parent2', 'parent3']; + const closestParent = findClosestParent(parents); + + expect(closestParent).toBe('parent3'); + commitPos.clear(); + }); +} From 62950c31a455ac35cb65883f05b9d89fd712f184 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 07:46:10 -0400 Subject: [PATCH 33/49] finished gitGraphRenderer.ts --- .../src/diagrams/git/gitGraphRenderer.ts | 223 +++++++----------- 1 file changed, 81 insertions(+), 142 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index 3493fa9953..0408baa230 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -11,6 +11,8 @@ let allCommitsDict = new Map(); const LAYOUT_OFFSET = 10; const COMMIT_STEP = 40; +const PX = 4; +const PY = 2; const commitType: CommitType = { NORMAL: 0, @@ -279,7 +281,7 @@ const drawCommitLabel = ( const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); const text = wrapper .append('text') - .attr('x', commitPosition.x) + .attr('x', pos) .attr('y', commitPosition.y + 25) .attr('class', 'commit-label') .text(commit.id); @@ -287,16 +289,20 @@ const drawCommitLabel = ( if (bbox) { labelBkg - .attr('x', commitPosition.posWithOffset - bbox.width / 2 - 2) + .attr('x', commitPosition.posWithOffset - bbox.width / 2 - PY) .attr('y', commitPosition.y + 13.5) - .attr('width', bbox.width + 4) - .attr('height', bbox.height + 4); + .attr('width', bbox.width + 2 * PY) + .attr('height', bbox.height + 2 * PY); if (dir === 'TB' || dir === 'BT') { - labelBkg.attr('x', commitPosition.x - (bbox.width + 4)).attr('y', commitPosition.y - 12); + labelBkg + .attr('x', commitPosition.x - (bbox.width + 4 * PX + 5)) + .attr('y', commitPosition.y - 12); text - .attr('x', commitPosition.x - (bbox.width + 2)) + .attr('x', commitPosition.x - (bbox.width + 4 * PX)) .attr('y', commitPosition.y + bbox.height - 12); + } else { + text.attr('x', commitPosition.posWithOffset - bbox.width / 2); } if (gitGraphConfig.rotateCommitLabel) { @@ -356,6 +362,7 @@ const drawCommitTags = ( if (!tagBbox) { throw new Error('Tag bbox not found'); } + maxTagBboxWidth = Math.max(maxTagBboxWidth, tagBbox.width); maxTagBboxHeight = Math.max(maxTagBboxHeight, tagBbox.height); @@ -377,17 +384,17 @@ const drawCommitTags = ( rect.attr('class', 'tag-label-bkg').attr( 'points', ` - ${pos - maxTagBboxWidth / 2 - 2},${ly + 2} - ${pos - maxTagBboxWidth / 2 - 2},${ly - 2} - ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - 4},${ly - h2 - 2} - ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + 4},${ly - h2 - 2} - ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + 4},${ly + h2 + 2} - ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - 4},${ly + h2 + 2}` + ${pos - maxTagBboxWidth / 2 - PX / 2},${ly + PY} + ${pos - maxTagBboxWidth / 2 - PX / 2},${ly - PY} + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - PX},${ly - h2 - PY} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + PX},${ly - h2 - PY} + ${commitPosition.posWithOffset + maxTagBboxWidth / 2 + PX},${ly + h2 + PY} + ${commitPosition.posWithOffset - maxTagBboxWidth / 2 - PX},${ly + h2 + PY}` ); hole .attr('cy', ly) - .attr('cx', pos - maxTagBboxWidth / 2 + 2) + .attr('cx', pos - maxTagBboxWidth / 2 + PX / 2) .attr('r', 1.5) .attr('class', 'tag-hole'); @@ -408,7 +415,7 @@ const drawCommitTags = ( ) .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); hole - .attr('cx', commitPosition.x + 2) + .attr('cx', commitPosition.x + PX / 2) .attr('cy', yOrigin) .attr('transform', 'translate(12,12) rotate(45, ' + commitPosition.x + ',' + pos + ')'); tag @@ -580,14 +587,16 @@ const findLane = (y1: number, y2: number, depth = 0): number => { }; const drawArrow = ( - svg: d3.Selection, + svg: d3.Selection, commitA: Commit, commitB: Commit, allCommits: Map ) => { const p1 = commitPos.get(commitA.id); // arrowStart const p2 = commitPos.get(commitB.id); // arrowEnd - // @ts-ignore: TODO Fix ts errors + if (p1 === undefined || p2 === undefined) { + throw new Error(`Commit positions not found for commits ${commitA.id} and ${commitB.id}`); + } const arrowNeedsRerouting = shouldRerouteArrow(commitA, commitB, p1, p2, allCommits); // log.debug('drawArrow', p1, p2, arrowNeedsRerouting, commitA.id, commitB.id); @@ -597,11 +606,10 @@ const drawArrow = ( let arc2 = ''; let radius = 0; let offset = 0; - // @ts-ignore: TODO Fix ts errors - let colorClassNum = branchPos.get(commitB.branch).index; + + let colorClassNum = branchPos.get(commitB.branch)?.index; if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors - colorClassNum = branchPos.get(commitA.branch).index; + colorClassNum = branchPos.get(commitA.branch)?.index; } let lineDef; @@ -610,66 +618,57 @@ const drawArrow = ( arc2 = 'A 10 10, 0, 0, 1,'; radius = 10; offset = 10; - // @ts-ignore: TODO Fix ts errors + const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y); - // @ts-ignore: TODO Fix ts errors + const lineX = p1.x < p2.x ? findLane(p1.x, p2.x) : findLane(p2.x, p1.x); if (dir === 'TB') { - // @ts-ignore: TODO Fix ts errors if (p1.x < p2.x) { // Source commit is on branch position left of destination commit // so render arrow rightward with colour of destination branch - // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc2} ${lineX} ${ - // @ts-ignore: TODO Fix ts errors p1.y + offset - // @ts-ignore: TODO Fix ts errors } 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 - // @ts-ignore: TODO Fix ts errors - colorClassNum = branchPos.get(commitA.branch).index; - // @ts-ignore: TODO Fix ts errors + + 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') { - // @ts-ignore: TODO Fix ts errors if (p1.x < p2.x) { // Source commit is on branch position left of destination commit // so render arrow rightward with colour of destination branch - // @ts-ignore: TODO Fix ts errors + 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 - // @ts-ignore: TODO Fix ts errors - colorClassNum = branchPos.get(commitA.branch).index; - // @ts-ignore: TODO Fix ts errors + + 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 { - // @ts-ignore: TODO Fix ts errors if (p1.y < p2.y) { // Source commit is on branch positioned above destination commit // so render arrow downward with colour of destination branch - // @ts-ignore: TODO Fix ts errors + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${ - // @ts-ignore: TODO Fix ts errors p1.x + offset - // @ts-ignore: TODO Fix ts errors } ${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 - // @ts-ignore: TODO Fix ts errors - colorClassNum = branchPos.get(commitA.branch).index; - // @ts-ignore: TODO Fix ts errors + + colorClassNum = branchPos.get(commitA.branch)?.index; + lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${ - // @ts-ignore: TODO Fix ts errors p1.x + offset - // @ts-ignore: TODO Fix ts errors } ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`; } } @@ -680,71 +679,48 @@ const drawArrow = ( offset = 20; if (dir === 'TB') { - // @ts-ignore: TODO Fix ts errors if (p1.x < p2.x) { if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y + offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } } - // @ts-ignore: TODO Fix ts errors + 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]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x + radius} ${p1.y} ${arc} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y + offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } } - // @ts-ignore: TODO Fix ts errors if (p1.x === p2.x) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; } } else if (dir === 'BT') { - // @ts-ignore: TODO Fix ts errors if (p1.x < p2.x) { if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y - offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } } - // @ts-ignore: TODO Fix ts errors if (p1.x > p2.x) { arc = 'A 20 20, 0, 0, 0,'; arc2 = 'A 20 20, 0, 0, 1,'; @@ -752,74 +728,53 @@ const drawArrow = ( offset = 20; if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y - offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } } - // @ts-ignore: TODO Fix ts errors + if (p1.x === p2.x) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; } } else { - // @ts-ignore: TODO Fix ts errors if (p1.y < p2.y) { if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y + offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } - } // @ts-ignore: TODO Fix ts errors + } if (p1.y > p2.y) { if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${ - // @ts-ignore: TODO Fix ts errors p1.y - offset - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } else { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${ - // @ts-ignore: TODO Fix ts errors p2.y - // @ts-ignore: TODO Fix ts errors } L ${p2.x} ${p2.y}`; } } - // @ts-ignore: TODO Fix ts errors + if (p1.y === p2.y) { - // @ts-ignore: TODO Fix ts errors lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`; } } } + svg .append('path') - // @ts-ignore: TODO Fix ts errors .attr('d', lineDef) - .attr('class', 'arrow arrow' + (colorClassNum % THEME_COLOR_LIMIT)); + .attr('class', 'arrow arrow' + (colorClassNum! % THEME_COLOR_LIMIT)); }; const drawArrows = ( @@ -829,12 +784,10 @@ const drawArrows = ( const gArrows = svg.append('g').attr('class', 'commit-arrows'); [...commits.keys()].forEach((key) => { const commit = commits.get(key); - // @ts-ignore: TODO Fix ts errors - if (commit.parents && commit.parents.length > 0) { - // @ts-ignore: TODO Fix ts errors - commit.parents.forEach((parent) => { - // @ts-ignore: TODO Fix ts errors - drawArrow(gArrows, commits.get(parent), commit, commits); + + if (commit!.parents && commit!.parents.length > 0) { + commit!.parents.forEach((parent) => { + drawArrow(gArrows, commits.get(parent)!, commit!, commits); }); } }); @@ -847,10 +800,12 @@ const drawBranches = ( const gitGraphConfig = getConfig().gitGraph; const g = svg.append('g'); branches.forEach((branch, index) => { - // @ts-ignore: TODO Fix ts errors const adjustIndexForTheme = index % THEME_COLOR_LIMIT; - // @ts-ignore: TODO Fix ts errors - const pos = branchPos.get(branch.name).pos; + + const pos = branchPos.get(branch.name)?.pos; + if (pos === undefined) { + throw new Error(`Position not found for branch ${branch.name}`); + } const line = g.append('line'); line.attr('x1', 0); line.attr('y1', pos); @@ -881,23 +836,21 @@ const drawBranches = ( // Create inner g, label, this will be positioned now for centering the text const label = branchLabel.insert('g').attr('class', 'label branch-label' + adjustIndexForTheme); - // @ts-ignore: TODO Fix ts errors - label.node().appendChild(labelElement); + + label.node()!.appendChild(labelElement); const bbox = labelElement.getBBox(); bkg .attr('class', 'branchLabelBkg label' + adjustIndexForTheme) .attr('rx', 4) .attr('ry', 4) - // @ts-ignore: TODO Fix ts errors - .attr('x', -bbox.width - 4 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0)) + .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(' + - // @ts-ignore: TODO Fix ts errors - (-bbox.width - 14 - (gitGraphConfig.rotateCommitLabel === true ? 30 : 0)) + + (-bbox.width - 14 - (gitGraphConfig?.rotateCommitLabel === true ? 30 : 0)) + ', ' + (pos - bbox.height / 2 - 1) + ')' @@ -988,7 +941,7 @@ if (import.meta.vitest) { }); }); - describe('drawBranchPositions', () => { + describe('branchPosition', () => { const bbox: DOMRect = { x: 0, y: 0, @@ -1026,8 +979,8 @@ if (import.meta.vitest) { expect(branchPos.get('develop')).toEqual({ pos: pos, index: 1 }); }); }); - /* - describe('drawCommits', () => { + + describe('commitPosition', () => { dir = 'TB'; const commits = new Map([ [ @@ -1129,42 +1082,28 @@ if (import.meta.vitest) { ], ]); + const expectedCommitPos = new Map([ + ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], + ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], + ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], + ['commitM', { x: 0, y: 190, posWithOffset: 190 }], + ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], + ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], + ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], + ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], + ]); + branchPos.set('main', { pos: 0, index: 0 }); branchPos.set('feature', { pos: 107.49609375, index: 1 }); branchPos.set('release', { pos: 224.03515625, index: 2 }); - commits.forEach((commit) => { - it(`should draw commit ${commit.id}`, () => { - const commitPosition = getCommitPosition(commit, 0, false); - expect(commitPosition).toBeDefined(); + let pos = 30; + commits.forEach((commit, key) => { + it(`should give the correct position for commit ${key}`, () => { + const position = getCommitPosition(commit, pos, false); + expect(position).toEqual(expectedCommitPos.get(key)); + pos += 50; }); - it(`should draw commit ${commit.id} with position`, () => { - const commitPosition = getCommitPosition(commit, 0, false); - expect(commitPosition.x).toBeDefined(); - expect(commitPosition.y).toBeDefined(); - expect(commitPosition.posWithOffset).toBeDefined(); - } - it(`should draw commit ${commit.id} bullet`, () => { - const gBullets = svg.append('g').attr('class', 'commit-bullets'); - const typeClass = getCommitClassType(commit); - const branchIndex = branchPos.get(commit.branch)?.index ?? 0; - drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commit.type); - } - it(`should draw commit ${commit.id} label`, () => { - const gLabels = svg.append('g').attr('class', 'commit-labels'); - drawCommitLabel(gLabels, commit, commitPosition, 0, gitGraphConfig); - } - }); -*/ - describe('drawBranches', () => { - it('should drawBranches', () => { - expect(true).toBe(true); - }); - }); - - describe('drawArrows', () => { - it('should drawArrows', () => { - expect(true).toBe(true); }); }); From 269284c6d7f070e2f0c42a561333821fd0bc9a07 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 10:37:24 -0400 Subject: [PATCH 34/49] added parser unit tests and organized config in gitGraphAst.ts --- .../src/diagrams/git/gitGraph.parser.spec.ts | 74 ------------- .../src/diagrams/git/gitGraphParser.ts | 101 ++++++++++++------ .../src/diagrams/git/gitGraphRenderer.ts | 13 ++- .../mermaid/src/diagrams/git/gitGraphTypes.ts | 59 ++++++---- 4 files changed, 119 insertions(+), 128 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts diff --git a/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts b/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts deleted file mode 100644 index 1a38a8217a..0000000000 --- a/packages/mermaid/src/diagrams/git/gitGraph.parser.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { parser } from './gitGraphParser.js'; -import { db } from './gitGraphAst.js'; - -const parseInput = async (input: string) => { - await parser.parse(input); -}; - -const spyOn = vi.spyOn; - -describe('GitGraph Parsing', function () { - beforeEach(() => { - db.clear(); - }); - it('should parse a default commit statement', async () => { - const input = `gitGraph: - commit - `; - const commitSpy = spyOn(db, 'commit'); - await parseInput(input); - - expect(commitSpy).toHaveBeenCalledWith('', undefined, 0, []); - commitSpy.mockRestore(); - }); - - it('should parse a basic branch statement with just a name', async () => { - const input = `gitGraph: - branch newBranch - `; - const branchSpy = spyOn(db, 'branch'); - await parseInput(input); - expect(branchSpy).toHaveBeenCalledWith('newBranch', 0); - branchSpy.mockRestore(); - }); - - it('should parse a basic checkout statement', async () => { - const input = `gitGraph: - branch newBranch - checkout newBranch - `; - const checkoutSpy = spyOn(db, 'checkout'); - await parseInput(input); - expect(checkoutSpy).toHaveBeenCalledWith('newBranch'); - checkoutSpy.mockRestore(); - }); - - it('should parse a basic merge statement', async () => { - const input = `gitGraph: - commit - branch newBranch - checkout newBranch - commit - checkout main - merge newBranch`; - const mergeSpy = spyOn(db, 'merge'); - await parseInput(input); - expect(mergeSpy).toHaveBeenCalledWith('newBranch', '', undefined, []); - mergeSpy.mockRestore(); - }); - - it('should parse cherry-picking', async () => { - const input = `gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - cherry-pick id:"A" - `; - const cherryPickSpy = spyOn(db, 'cherryPick'); - await parseInput(input); - expect(cherryPickSpy).toHaveBeenCalledWith('A', '', undefined, undefined); - cherryPickSpy.mockRestore(); - }); -}); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index b014d7e574..1ca863329e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -11,9 +11,10 @@ import type { MergeAst, CommitAst, BranchAst, + GitGraphDBProvider, } from './gitGraphTypes.js'; -const populate = (ast: GitGraph) => { +const populate = (ast: GitGraph, db: GitGraphDBProvider) => { populateCommonDb(ast, db); // @ts-ignore: this wont exist if the direction is not specified if (ast.dir) { @@ -21,71 +22,107 @@ const populate = (ast: GitGraph) => { db.setDirection(ast.dir); } for (const statement of ast.statements) { - parseStatement(statement); + parseStatement(statement, db); } }; -const parseStatement = (statement: any) => { - 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: - log.error(`Unknown statement type`); +const parseStatement = (statement: any, db: GitGraphDBProvider) => { + const parsers: Record 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) => { +const parseCommit = (commit: CommitAst): [string, string, number, string[] | undefined] => { const id = commit.id; const message = commit.message ?? ''; const type = commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL; const tags = commit.tags ?? undefined; - db.commit(message, id, type, tags); + return [message, id, type, tags]; }; -const parseBranch = (branch: BranchAst) => { +const parseBranch = (branch: BranchAst): [string, number] => { const name = branch.name; const order = branch.order ?? 0; - db.branch(name, order); + return [name, order]; }; -const parseMerge = (merge: MergeAst) => { +const parseMerge = ( + merge: MergeAst +): [string, string, number | undefined, string[] | undefined] => { const branch = merge.branch; const id = merge.id ?? ''; const type = merge.type !== undefined ? commitType[merge.type] : undefined; const tags = merge.tags ?? undefined; - db.merge(branch, id, type, tags); + return [branch, id, type, tags]; }; -const parseCheckout = (checkout: CheckoutAst) => { +const parseCheckout = (checkout: CheckoutAst): string => { const branch = checkout.branch; - db.checkout(branch); + return branch; }; -const parseCherryPicking = (cherryPicking: CherryPickingAst) => { +const parseCherryPicking = ( + cherryPicking: CherryPickingAst +): [string, string, string[] | undefined, string] => { const id = cherryPicking.id; const tags = cherryPicking.tags?.length === 0 ? undefined : cherryPicking.tags; const parent = cherryPicking.parent; - db.cherryPick(id, '', tags, parent); + return [id, '', tags, parent]; }; export const parser: ParserDefinition = { parse: async (input: string): Promise => { const ast: GitGraph = await parse('gitGraph', input); log.debug(ast); - populate(ast); + populate(ast, db); }, }; + +if (import.meta.vitest) { + const { it, expect, describe } = import.meta.vitest; + + const mockDB: GitGraphDBProvider = { + 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('test', '1', 0, ['tag1', 'tag2']); + }); + it('should parse a branch statement', () => { + const branch = { + $type: 'Branch', + name: 'newBranch', + order: 1, + }; + parseStatement(branch, mockDB); + expect(mockDB.branch).toHaveBeenCalledWith('newBranch', 1); + }); + }); +} diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index 0408baa230..c3862e5817 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -4,7 +4,12 @@ import { log } from '../../logger.js'; import utils from '../../utils.js'; import type { DrawDefinition } from '../../diagram-api/types.js'; import type d3 from 'd3'; -import type { CommitType, Commit, GitGraphDB, DiagramOrientation } from './gitGraphTypes.js'; +import type { + CommitType, + Commit, + GitGraphDBRenderProvider, + DiagramOrientation, +} from './gitGraphTypes.js'; import type { GitGraphDiagramConfig } from '../../config.type.js'; let allCommitsDict = new Map(); @@ -770,7 +775,9 @@ const drawArrow = ( } } } - + if (lineDef === undefined) { + throw new Error('Line definition not found'); + } svg .append('path') .attr('d', lineDef) @@ -889,7 +896,7 @@ export const draw: DrawDefinition = function (txt, id, ver, diagObj) { throw new Error('GitGraph config not found'); } const rotateCommitLabel = gitGraphConfig.rotateCommitLabel ?? false; - const db = diagObj.db as GitGraphDB; + const db = diagObj.db as GitGraphDBRenderProvider; allCommitsDict = db.getCommits(); const branches = db.getBranchesAsObjArray(); dir = db.getDirection(); diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index b473dd8749..b888cc297e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -1,5 +1,5 @@ -import type { DiagramDB } from '../../diagram-api/types.js'; import type { GitGraphDiagramConfig } from '../../config.type.js'; +import type { DiagramDBBase } from '../../diagram-api/types.js'; export interface CommitType { NORMAL: number; @@ -66,27 +66,42 @@ export interface CherryPickingAst { parent: string; } -export interface GitGraphDB extends DiagramDB { - // config - getConfig: () => GitGraphDiagramConfig | undefined; - - // common db +export interface GitGraphDB extends DiagramDBBase { + commitType: CommitType; + setDirection: (dir: DiagramOrientation) => void; + setOptions: (rawOptString: string) => void; + getOptions: () => any; + commit: (msg: string, id: string, type: number, tags?: string[]) => void; + branch: (name: string, order?: number) => void; + merge: ( + otherBranch: string, + customId?: string, + overrideType?: number, + customTags?: string[] + ) => void; + cherryPick: ( + sourceId: string, + targetId: string, + tags: string[] | undefined, + parentCommitId: string + ) => void; + checkout: (branch: string) => void; + prettyPrint: () => void; clear: () => void; - setDiagramTitle: (title: string) => void; - getDiagramTitle: () => string; - setAccTitle: (title: string) => void; - getAccTitle: () => string; - setAccDescription: (description: string) => void; - getAccDescription: () => string; + getBranchesAsObjArray: () => { name: string }[]; + getBranches: () => Map; + getCommits: () => Map; + getCommitsArray: () => Commit[]; + getCurrentBranch: () => string; + getDirection: () => DiagramOrientation; + getHead: () => Commit | null; +} - // diagram db +export interface GitGraphDBParseProvider extends Partial { commitType: CommitType; - setDirection: (direction: DiagramOrientation) => void; - getDirection: () => DiagramOrientation; - setOptions: (options: string) => void; - getOptions: () => string; - commit: (msg: string, id: string, type: number, tags?: string[] | undefined) => void; - branch: (name: string, order: number) => void; + setDirection: (dir: DiagramOrientation) => void; + commit: (msg: string, id: string, type: number, tags?: string[]) => void; + branch: (name: string, order?: number) => void; merge: ( otherBranch: string, customId?: string, @@ -100,13 +115,19 @@ export interface GitGraphDB extends DiagramDB { parentCommitId: string ) => void; checkout: (branch: string) => void; +} + +export interface GitGraphDBRenderProvider extends Partial { prettyPrint: () => void; + clear: () => void; getBranchesAsObjArray: () => { name: string }[]; getBranches: () => Map; getCommits: () => Map; getCommitsArray: () => Commit[]; getCurrentBranch: () => string; + getDirection: () => DiagramOrientation; getHead: () => Commit | null; + getDiagramTitle: () => string; } export type DiagramOrientation = 'LR' | 'TB' | 'BT'; From d684e0d92450dace5f93ccf47225b401fd9f5333 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 11:22:22 -0400 Subject: [PATCH 35/49] added more unit tests to gitGraphParser.ts and gitGraphRenderer.ts --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 16 ++-- .../src/diagrams/git/gitGraphParser.ts | 90 ++++++++++++++++++- .../src/diagrams/git/gitGraphRenderer.ts | 78 ++++++++++++---- 3 files changed, 156 insertions(+), 28 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 1a40450d9e..b47809aac4 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -1,6 +1,5 @@ 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, @@ -11,9 +10,11 @@ import { setDiagramTitle, getDiagramTitle, } from '../common/commonDb.js'; -import defaultConfig from '../../defaultConfig.js'; import type { DiagramOrientation, Commit, GitGraphDB, 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; head: Commit | null; @@ -25,8 +26,13 @@ interface GitGraphState { options: any; } -const mainBranchName = defaultConfig.gitGraph.mainBranchName; -const mainBranchOrder = defaultConfig.gitGraph.mainBranchOrder; +const DEFAULT_GITGRAPH_CONFIG: Required = DEFAULT_CONFIG.gitGraph; + +const mainBranchName = DEFAULT_GITGRAPH_CONFIG.mainBranchName; +const mainBranchOrder = DEFAULT_GITGRAPH_CONFIG.mainBranchOrder; +const config: Required = structuredClone(DEFAULT_GITGRAPH_CONFIG); + +const getConfig = (): Required => structuredClone(config); const state = new ImperativeState(() => ({ commits: new Map(), @@ -476,7 +482,7 @@ export const commitType: CommitType = { export const db: GitGraphDB = { commitType, - getConfig: () => getConfig().gitGraph, + getConfig, setDirection, setOptions, getOptions, diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 1ca863329e..404f0c202b 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -11,10 +11,10 @@ import type { MergeAst, CommitAst, BranchAst, - GitGraphDBProvider, + GitGraphDBParseProvider, } from './gitGraphTypes.js'; -const populate = (ast: GitGraph, db: GitGraphDBProvider) => { +const populate = (ast: GitGraph, db: GitGraphDBParseProvider) => { populateCommonDb(ast, db); // @ts-ignore: this wont exist if the direction is not specified if (ast.dir) { @@ -26,7 +26,7 @@ const populate = (ast: GitGraph, db: GitGraphDBProvider) => { } }; -const parseStatement = (statement: any, db: GitGraphDBProvider) => { +const parseStatement = (statement: any, db: GitGraphDBParseProvider) => { const parsers: Record void> = { Commit: (stmt) => db.commit(...parseCommit(stmt)), Branch: (stmt) => db.branch(...parseBranch(stmt)), @@ -93,7 +93,7 @@ export const parser: ParserDefinition = { if (import.meta.vitest) { const { it, expect, describe } = import.meta.vitest; - const mockDB: GitGraphDBProvider = { + const mockDB: GitGraphDBParseProvider = { commitType: commitType, setDirection: vi.fn(), commit: vi.fn(), @@ -124,5 +124,87 @@ if (import.meta.vitest) { parseStatement(branch, mockDB); expect(mockDB.branch).toHaveBeenCalledWith('newBranch', 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('newBranch', '1', 0, ['tag1', 'tag2']); + }); + 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('1', '', ['tag1', 'tag2'], '2'); + }); + + 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('test', '1', 0, ['tag1', 'tag2']); + expect(mockDB.branch).toHaveBeenCalledWith('newBranch', 1); + expect(mockDB.merge).toHaveBeenCalledWith('newBranch', '1', 0, ['tag1', 'tag2']); + expect(mockDB.checkout).toHaveBeenCalledWith('newBranch'); + }); }); } diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index c3862e5817..dde171a628 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -988,7 +988,6 @@ if (import.meta.vitest) { }); describe('commitPosition', () => { - dir = 'TB'; const commits = new Map([ [ 'commitZero', @@ -1088,28 +1087,69 @@ if (import.meta.vitest) { }, ], ]); - - const expectedCommitPos = new Map([ - ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], - ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], - ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], - ['commitM', { x: 0, y: 190, posWithOffset: 190 }], - ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], - ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], - ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], - ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], - ]); - + let pos = 0; branchPos.set('main', { pos: 0, index: 0 }); branchPos.set('feature', { pos: 107.49609375, index: 1 }); branchPos.set('release', { pos: 224.03515625, index: 2 }); - let pos = 30; - commits.forEach((commit, key) => { - it(`should give the correct position for commit ${key}`, () => { - const position = getCommitPosition(commit, pos, false); - expect(position).toEqual(expectedCommitPos.get(key)); - pos += 50; + describe('TB', () => { + pos = 30; + dir = 'TB'; + const expectedCommitPositionTB = new Map([ + ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], + ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], + ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], + ['commitM', { x: 0, y: 190, posWithOffset: 190 }], + ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], + ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], + ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], + ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], + ]); + commits.forEach((commit, key) => { + it(`should give the correct position for commit ${key}`, () => { + const position = getCommitPosition(commit, pos, false); + expect(position).toEqual(expectedCommitPositionTB.get(key)); + pos += 50; + }); + }); + }); + describe('LR', () => { + let pos = 30; + dir = 'LR'; + const expectedCommitPositionLR = new Map([ + ['commitZero', { x: 0, y: 40, posWithOffset: 40 }], + ['commitA', { x: 107.49609375, y: 90, posWithOffset: 90 }], + ['commitB', { x: 107.49609375, y: 140, posWithOffset: 140 }], + ['commitM', { x: 0, y: 190, posWithOffset: 190 }], + ['commitC', { x: 224.03515625, y: 240, posWithOffset: 240 }], + ['commit5_8928ea0', { x: 224.03515625, y: 290, posWithOffset: 290 }], + ['commitD', { x: 224.03515625, y: 340, posWithOffset: 340 }], + ['commit7_ed848ba', { x: 224.03515625, y: 390, posWithOffset: 390 }], + ]); + commits.forEach((commit, key) => { + it(`should give the correct position for commit ${key}`, () => { + const position = getCommitPosition(commit, pos, false); + expect(position).toEqual(expectedCommitPositionLR.get(key)); + pos += 50; + }); + }); + }); + describe('getCommitClassType', () => { + const expectedCommitClassType = new Map([ + ['commitZero', 'commit-normal'], + ['commitA', 'commit-normal'], + ['commitB', 'commit-normal'], + ['commitM', 'commit-merge'], + ['commitC', 'commit-normal'], + ['commit5_8928ea0', 'commit-cherry-pick'], + ['commitD', 'commit-normal'], + ['commit7_ed848ba', 'commit-cherry-pick'], + ]); + commits.forEach((commit, key) => { + it(`should give the correct class type for commit ${key}`, () => { + const classType = getCommitClassType(commit); + expect(classType).toBe(expectedCommitClassType.get(key)); + }); }); }); }); From c49a1bf60c5502021a2a97aabed4b950ef11b101 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 12:28:24 -0400 Subject: [PATCH 36/49] fixed custom type REVERSE for merge, fixed branch spacing for TB --- packages/mermaid/src/diagrams/git/gitGraphRenderer.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index dde171a628..ecf0f07670 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -248,7 +248,7 @@ const drawCommitBullet = ( circle.attr('cy', commitPosition.y); circle.attr('r', commit.type === commitType.MERGE ? 9 : 10); circle.attr('class', `commit ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); - if (commit.type === commitType.MERGE) { + if (commitSymbolType === commitType.MERGE) { const circle2 = gBullets.append('circle'); circle2.attr('cx', commitPosition.x); circle2.attr('cy', commitPosition.y); @@ -266,6 +266,7 @@ const drawCommitBullet = ( `M ${commitPosition.x - 5},${commitPosition.y - 5}L${commitPosition.x + 5},${commitPosition.y + 5}M${commitPosition.x - 5},${commitPosition.y + 5}L${commitPosition.x + 5},${commitPosition.y - 5}` ) .attr('class', `commit ${typeClass} ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); + log.info(); } } }; @@ -543,6 +544,7 @@ const drawCommits = ( if (modifyGraph) { const typeClass = getCommitClassType(commit); const commitSymbolType = commit.customType ?? commit.type; + log.info('commitSymbolType', commitSymbolType); const branchIndex = branchPos.get(commit.branch)?.index ?? 0; drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commitSymbolType); drawCommitLabel(gLabels, commit, commitPosition, pos, gitGraphConfig); @@ -883,6 +885,9 @@ const setBranchPosition = function ( ): number { branchPos.set(name, { pos, index }); pos += 50 + (rotateCommitLabel ? 40 : 0) + (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); + log.info('bbox.width', bbox.width); + log.info('setBranchPosition', name, pos, index, bbox); + log.info('branchPos', branchPos); return pos; }; @@ -908,7 +913,9 @@ export const draw: DrawDefinition = function (txt, id, ver, diagObj) { 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); const bbox = labelElement.getBBox(); + pos = setBranchPosition(branch.name, pos, index, bbox, rotateCommitLabel); label.remove(); branchLabel.remove(); From aba306b68556b45a1200625e4dd7b8498894a254 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 12:32:25 -0400 Subject: [PATCH 37/49] fixed highlight color --- packages/mermaid/src/diagrams/git/gitGraphRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index ecf0f07670..2aef672fc0 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -193,7 +193,7 @@ const drawCommitBullet = ( .attr('height', 20) .attr( 'class', - `commit ${commit.id} commit-highlights${branchIndex % THEME_COLOR_LIMIT} ${typeClass}-outer` + `commit ${commit.id} commit-highlight${branchIndex % THEME_COLOR_LIMIT} ${typeClass}-outer` ); gBullets .append('rect') From d73a090875d45dabeec510ba6dd2b3c90a78b63b Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Sat, 10 Aug 2024 23:33:23 -0400 Subject: [PATCH 38/49] fixed BT with parallel commits and added more unit tests --- .../src/diagrams/git/gitGraphRenderer.ts | 224 ++++++++++++++++-- 1 file changed, 207 insertions(+), 17 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index 2aef672fc0..b187bee91e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -76,12 +76,11 @@ const drawText = (txt: string | string[]) => { return svgLabel; }; -const findClosestParent = (parents: string[], useBTLogic = false): string | undefined => { +const findClosestParent = (parents: string[]): string | undefined => { let closestParent: string | undefined; let comparisonFunc; let targetPosition: number; - - if (dir === 'BT' || useBTLogic) { + if (dir === 'BT') { comparisonFunc = (a: number, b: number) => a <= b; targetPosition = Infinity; } else { @@ -91,9 +90,7 @@ const findClosestParent = (parents: string[], useBTLogic = false): string | unde parents.forEach((parent) => { const parentPosition = - dir === 'TB' || dir == 'BT' || useBTLogic - ? commitPos.get(parent)?.y - : commitPos.get(parent)?.x; + dir === 'TB' || dir == 'BT' ? commitPos.get(parent)?.y : commitPos.get(parent)?.x; if (parentPosition !== undefined && comparisonFunc(parentPosition, targetPosition)) { closestParent = parent; @@ -104,6 +101,20 @@ const findClosestParent = (parents: string[], useBTLogic = false): string | unde return closestParent; }; +const findClosestParentBT = (parents: string[]) => { + let closestParent = ''; + let maxPosition = Infinity; + + parents.forEach((parent) => { + const parentPosition = commitPos.get(parent)!.y; + if (parentPosition <= maxPosition) { + closestParent = parent; + maxPosition = parentPosition; + } + }); + return closestParent || undefined; +}; + const setParallelBTPos = ( sortedKeys: string[], commits: Map, @@ -119,7 +130,7 @@ const setParallelBTPos = ( throw new Error(`Commit not found for key ${key}`); } - if (hasParents(commit)) { + if (commit.parents.length) { curPos = calculateCommitPosition(commit); maxPosition = Math.max(curPos, maxPosition); } else { @@ -132,9 +143,21 @@ const setParallelBTPos = ( roots.forEach((commit) => { setRootPosition(commit, curPos, defaultPos); }); -}; + sortedKeys.forEach((key) => { + const commit = commits.get(key); -const hasParents = (commit: Commit): boolean => commit.parents?.length > 0; + if (commit?.parents.length) { + const closestParent = findClosestParentBT(commit.parents)!; + curPos = commitPos.get(closestParent)!.y - COMMIT_STEP; + if (curPos <= maxPosition) { + maxPosition = curPos; + } + const x = branchPos.get(commit.branch)!.pos; + const y = curPos - LAYOUT_OFFSET; + commitPos.set(commit.id, { x: x, y: y }); + } + }); +}; const findClosestParentPos = (commit: Commit): number => { const closestParent = findClosestParent(commit.parents.filter((p) => p !== null)); @@ -154,8 +177,9 @@ const calculateCommitPosition = (commit: Commit): number => { return closestParentPos + COMMIT_STEP; }; -const setCommitPosition = (commit: Commit, curPos: number) => { +const setCommitPosition = (commit: Commit, curPos: number): CommitPosition => { const branch = branchPos.get(commit.branch); + if (!branch) { throw new Error(`Branch not found for commit ${commit.id}`); } @@ -163,6 +187,7 @@ const setCommitPosition = (commit: Commit, curPos: number) => { const x = branch.pos; const y = curPos + LAYOUT_OFFSET; commitPos.set(commit.id, { x, y }); + return { x, y }; }; const setRootPosition = (commit: Commit, curPos: number, defaultPos: number) => { @@ -266,7 +291,6 @@ const drawCommitBullet = ( `M ${commitPosition.x - 5},${commitPosition.y - 5}L${commitPosition.x + 5},${commitPosition.y + 5}M${commitPosition.x - 5},${commitPosition.y + 5}L${commitPosition.x + 5},${commitPosition.y - 5}` ) .attr('class', `commit ${typeClass} ${commit.id} commit${branchIndex % THEME_COLOR_LIMIT}`); - log.info(); } } }; @@ -522,12 +546,13 @@ const drawCommits = ( }; let sortedKeys = keys.sort(sortKeys); - if (dir === 'BT') { if (isParallelCommits) { setParallelBTPos(sortedKeys, commits, pos); + sortedKeys = sortedKeys.reverse(); + } else { + sortedKeys = sortedKeys.reverse(); } - sortedKeys = sortedKeys.reverse(); } sortedKeys.forEach((key) => { @@ -544,7 +569,6 @@ const drawCommits = ( if (modifyGraph) { const typeClass = getCommitClassType(commit); const commitSymbolType = commit.customType ?? commit.type; - log.info('commitSymbolType', commitSymbolType); const branchIndex = branchPos.get(commit.branch)?.index ?? 0; drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commitSymbolType); drawCommitLabel(gLabels, commit, commitPosition, pos, gitGraphConfig); @@ -885,9 +909,6 @@ const setBranchPosition = function ( ): number { branchPos.set(name, { pos, index }); pos += 50 + (rotateCommitLabel ? 40 : 0) + (dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0); - log.info('bbox.width', bbox.width); - log.info('setBranchPosition', name, pos, index, bbox); - log.info('branchPos', branchPos); return pos; }; @@ -1160,6 +1181,175 @@ if (import.meta.vitest) { }); }); }); + describe('building BT parallel commit diagram', () => { + const commits = new Map([ + [ + '1-abcdefg', + { + id: '1-abcdefg', + message: '', + seq: 0, + type: 0, + tags: [], + parents: [], + branch: 'main', + }, + ], + [ + '2-abcdefg', + { + id: '2-abcdefg', + message: '', + seq: 1, + type: 0, + tags: [], + parents: ['1-abcdefg'], + branch: 'main', + }, + ], + [ + '3-abcdefg', + { + id: '3-abcdefg', + message: '', + seq: 2, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'develop', + }, + ], + [ + '4-abcdefg', + { + id: '4-abcdefg', + message: '', + seq: 3, + type: 0, + tags: [], + parents: ['3-abcdefg'], + branch: 'develop', + }, + ], + [ + '5-abcdefg', + { + id: '5-abcdefg', + message: '', + seq: 4, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'feature', + }, + ], + [ + '6-abcdefg', + { + id: '6-abcdefg', + message: '', + seq: 5, + type: 0, + tags: [], + parents: ['5-abcdefg'], + branch: 'feature', + }, + ], + [ + '7-abcdefg', + { + id: '7-abcdefg', + message: '', + seq: 6, + type: 0, + tags: [], + parents: ['2-abcdefg'], + branch: 'main', + }, + ], + [ + '8-abcdefg', + { + id: '8-abcdefg', + message: '', + seq: 7, + type: 0, + tags: [], + parents: ['7-abcdefg'], + branch: 'main', + }, + ], + ]); + const expectedCommitPosition = new Map([ + ['1-abcdefg', { x: 0, y: 40 }], + ['2-abcdefg', { x: 0, y: 90 }], + ['3-abcdefg', { x: 107.49609375, y: 140 }], + ['4-abcdefg', { x: 107.49609375, y: 190 }], + ['5-abcdefg', { x: 225.70703125, y: 140 }], + ['6-abcdefg', { x: 225.70703125, y: 190 }], + ['7-abcdefg', { x: 0, y: 140 }], + ['8-abcdefg', { x: 0, y: 190 }], + ]); + + const expectedCommitPositionAfterParallel = new Map([ + ['1-abcdefg', { x: 0, y: 210 }], + ['2-abcdefg', { x: 0, y: 160 }], + ['3-abcdefg', { x: 107.49609375, y: 110 }], + ['4-abcdefg', { x: 107.49609375, y: 60 }], + ['5-abcdefg', { x: 225.70703125, y: 110 }], + ['6-abcdefg', { x: 225.70703125, y: 60 }], + ['7-abcdefg', { x: 0, y: 110 }], + ['8-abcdefg', { x: 0, y: 60 }], + ]); + + const expectedCommitCurrentPosition = new Map([ + ['1-abcdefg', 30], + ['2-abcdefg', 80], + ['3-abcdefg', 130], + ['4-abcdefg', 180], + ['5-abcdefg', 130], + ['6-abcdefg', 180], + ['7-abcdefg', 130], + ['8-abcdefg', 180], + ]); + const sortedKeys = [...expectedCommitPosition.keys()]; + it('should get the correct commit position and current position', () => { + dir = 'BT'; + let curPos = 30; + commitPos.clear(); + branchPos.clear(); + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('develop', { pos: 107.49609375, index: 1 }); + branchPos.set('feature', { pos: 225.70703125, index: 2 }); + //TODO: need to make sure you set the parallel commits to true + + commits.forEach((commit, key) => { + if (commit.parents.length > 0) { + curPos = calculateCommitPosition(commit); + } + const position = setCommitPosition(commit, curPos); + expect(position).toEqual(expectedCommitPosition.get(key)); + expect(curPos).toEqual(expectedCommitCurrentPosition.get(key)); + }); + }); + + it('should get the correct commit position after parallel commits', () => { + commitPos.clear(); + branchPos.clear(); + dir = 'BT'; + const curPos = 30; + commitPos.clear(); + branchPos.clear(); + branchPos.set('main', { pos: 0, index: 0 }); + branchPos.set('develop', { pos: 107.49609375, index: 1 }); + branchPos.set('feature', { pos: 225.70703125, index: 2 }); + setParallelBTPos(sortedKeys, commits, curPos); + sortedKeys.forEach((commit) => { + const position = commitPos.get(commit); + expect(position).toEqual(expectedCommitPositionAfterParallel.get(commit)); + }); + }); + }); it('add', () => { commitPos.set('parent1', { x: 1, y: 1 }); From 3539a35578f28395f0aadbb54cbe90b440c05c0f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:56:26 +0000 Subject: [PATCH 39/49] [autofix.ci] apply automated fixes --- docs/config/setup/interfaces/mermaid.Mermaid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/setup/interfaces/mermaid.Mermaid.md b/docs/config/setup/interfaces/mermaid.Mermaid.md index a340c7a970..86bdbe8fab 100644 --- a/docs/config/setup/interfaces/mermaid.Mermaid.md +++ b/docs/config/setup/interfaces/mermaid.Mermaid.md @@ -147,7 +147,7 @@ Internal helpers for mermaid | `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] | | `common.splitBreaks` | (`text`: `string`) => `string`\[] | | `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) | -| `insertCluster` | (`elem`: `any`, `node`: `any`) => `any` | +| `insertCluster` | (`elem`: `any`, `node`: `any`) => `Promise`<`any`> | | `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } | | `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> | | `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` | From 299e559aa518da4907dd624c76f866bed2428fda Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Wed, 14 Aug 2024 10:51:45 -0400 Subject: [PATCH 40/49] added config as global --- .../src/diagrams/git/gitGraphRenderer.ts | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index b187bee91e..1aba40cec6 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -12,6 +12,9 @@ import type { } from './gitGraphTypes.js'; import type { GitGraphDiagramConfig } from '../../config.type.js'; +const DEFAULT_CONFIG = getConfig(); +const DEFAULT_GITGRAPH_CONFIG: Required = DEFAULT_CONFIG.gitGraph; + let allCommitsDict = new Map(); const LAYOUT_OFFSET = 10; @@ -299,13 +302,12 @@ const drawCommitLabel = ( gLabels: d3.Selection, commit: Commit, commitPosition: CommitPositionOffset, - pos: number, - gitGraphConfig: GitGraphDiagramConfig + pos: number ) => { if ( commit.type !== commitType.CHERRY_PICK && ((commit.customId && commit.type === commitType.MERGE) || commit.type !== commitType.MERGE) && - gitGraphConfig.showCommitLabel + DEFAULT_GITGRAPH_CONFIG.showCommitLabel ) { const wrapper = gLabels.append('g'); const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); @@ -335,7 +337,7 @@ const drawCommitLabel = ( text.attr('x', commitPosition.posWithOffset - bbox.width / 2); } - if (gitGraphConfig.rotateCommitLabel) { + if (DEFAULT_GITGRAPH_CONFIG.rotateCommitLabel) { if (dir === 'TB' || dir === 'BT') { text.attr( 'transform', @@ -529,15 +531,14 @@ const drawCommits = ( commits: Map, modifyGraph: boolean ) => { - const gitGraphConfig = getConfig().gitGraph; - if (!gitGraphConfig) { + if (!DEFAULT_GITGRAPH_CONFIG) { throw new Error('GitGraph config not found'); } const gBullets = svg.append('g').attr('class', 'commit-bullets'); const gLabels = svg.append('g').attr('class', 'commit-labels'); let pos = dir === 'TB' || dir === 'BT' ? defaultPos : 0; const keys = [...commits.keys()]; - const isParallelCommits = gitGraphConfig?.parallelCommits ?? false; + const isParallelCommits = DEFAULT_GITGRAPH_CONFIG?.parallelCommits ?? false; const sortKeys = (a: string, b: string) => { const seqA = commits.get(a)?.seq; @@ -571,7 +572,7 @@ const drawCommits = ( const commitSymbolType = commit.customType ?? commit.type; const branchIndex = branchPos.get(commit.branch)?.index ?? 0; drawCommitBullet(gBullets, commit, commitPosition, typeClass, branchIndex, commitSymbolType); - drawCommitLabel(gLabels, commit, commitPosition, pos, gitGraphConfig); + drawCommitLabel(gLabels, commit, commitPosition, pos); drawCommitTags(gLabels, commit, commitPosition, pos); } if (dir === 'TB' || dir === 'BT') { @@ -830,7 +831,6 @@ const drawBranches = ( svg: d3.Selection, branches: { name: string }[] ) => { - const gitGraphConfig = getConfig().gitGraph; const g = svg.append('g'); branches.forEach((branch, index) => { const adjustIndexForTheme = index % THEME_COLOR_LIMIT; @@ -876,14 +876,14 @@ const drawBranches = ( .attr('class', 'branchLabelBkg label' + adjustIndexForTheme) .attr('rx', 4) .attr('ry', 4) - .attr('x', -bbox.width - 4 - (gitGraphConfig?.rotateCommitLabel === true ? 30 : 0)) + .attr('x', -bbox.width - 4 - (DEFAULT_GITGRAPH_CONFIG?.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)) + + (-bbox.width - 14 - (DEFAULT_GITGRAPH_CONFIG?.rotateCommitLabel === true ? 30 : 0)) + ', ' + (pos - bbox.height / 2 - 1) + ')' @@ -914,14 +914,12 @@ const setBranchPosition = function ( export const draw: DrawDefinition = function (txt, id, ver, diagObj) { clear(); - const conf = getConfig(); - const gitGraphConfig = conf.gitGraph; log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); - if (!gitGraphConfig) { + if (!DEFAULT_GITGRAPH_CONFIG) { throw new Error('GitGraph config not found'); } - const rotateCommitLabel = gitGraphConfig.rotateCommitLabel ?? false; + const rotateCommitLabel = DEFAULT_GITGRAPH_CONFIG.rotateCommitLabel ?? false; const db = diagObj.db as GitGraphDBRenderProvider; allCommitsDict = db.getCommits(); const branches = db.getBranchesAsObjArray(); @@ -944,7 +942,7 @@ export const draw: DrawDefinition = function (txt, id, ver, diagObj) { }); drawCommits(diagram, allCommitsDict, false); - if (gitGraphConfig.showBranches) { + if (DEFAULT_GITGRAPH_CONFIG.showBranches) { drawBranches(diagram, branches); } drawArrows(diagram, allCommitsDict); @@ -953,12 +951,17 @@ export const draw: DrawDefinition = function (txt, id, ver, diagObj) { utils.insertTitle( diagram, 'gitTitleText', - gitGraphConfig.titleTopMargin ?? 0, + DEFAULT_GITGRAPH_CONFIG.titleTopMargin ?? 0, db.getDiagramTitle() ); // Setup the view box and size of the svg element - setupGraphViewbox(undefined, diagram, gitGraphConfig.diagramPadding, gitGraphConfig.useMaxWidth); + setupGraphViewbox( + undefined, + diagram, + DEFAULT_GITGRAPH_CONFIG.diagramPadding, + DEFAULT_GITGRAPH_CONFIG.useMaxWidth + ); }; export default { @@ -1321,8 +1324,7 @@ if (import.meta.vitest) { branchPos.set('main', { pos: 0, index: 0 }); branchPos.set('develop', { pos: 107.49609375, index: 1 }); branchPos.set('feature', { pos: 225.70703125, index: 2 }); - //TODO: need to make sure you set the parallel commits to true - + DEFAULT_GITGRAPH_CONFIG.parallelCommits = true; commits.forEach((commit, key) => { if (commit.parents.length > 0) { curPos = calculateCommitPosition(commit); @@ -1350,7 +1352,7 @@ if (import.meta.vitest) { }); }); }); - + DEFAULT_GITGRAPH_CONFIG.parallelCommits = false; it('add', () => { commitPos.set('parent1', { x: 1, y: 1 }); commitPos.set('parent2', { x: 2, y: 2 }); From 53798beb9651ea6866520668ea88dd0b9ccd0fb0 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Thu, 15 Aug 2024 16:10:20 -0400 Subject: [PATCH 41/49] fixed gitgraphconfig problem --- packages/mermaid/src/diagrams/git/gitGraphRenderer.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index 1aba40cec6..bd242ce06e 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -10,10 +10,9 @@ import type { GitGraphDBRenderProvider, DiagramOrientation, } from './gitGraphTypes.js'; -import type { GitGraphDiagramConfig } from '../../config.type.js'; const DEFAULT_CONFIG = getConfig(); -const DEFAULT_GITGRAPH_CONFIG: Required = DEFAULT_CONFIG.gitGraph; +const DEFAULT_GITGRAPH_CONFIG = DEFAULT_CONFIG?.gitGraph; let allCommitsDict = new Map(); @@ -1324,7 +1323,7 @@ if (import.meta.vitest) { branchPos.set('main', { pos: 0, index: 0 }); branchPos.set('develop', { pos: 107.49609375, index: 1 }); branchPos.set('feature', { pos: 225.70703125, index: 2 }); - DEFAULT_GITGRAPH_CONFIG.parallelCommits = true; + DEFAULT_GITGRAPH_CONFIG!.parallelCommits = true; commits.forEach((commit, key) => { if (commit.parents.length > 0) { curPos = calculateCommitPosition(commit); @@ -1352,7 +1351,7 @@ if (import.meta.vitest) { }); }); }); - DEFAULT_GITGRAPH_CONFIG.parallelCommits = false; + DEFAULT_GITGRAPH_CONFIG!.parallelCommits = false; it('add', () => { commitPos.set('parent1', { x: 1, y: 1 }); commitPos.set('parent2', { x: 2, y: 2 }); From 66e53df04b04525e613deb908832eee81d0eaf74 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 00:30:01 -0400 Subject: [PATCH 42/49] added most suggested changes --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 28 ++++------- .../src/diagrams/git/gitGraphDiagram.ts | 4 +- .../src/diagrams/git/gitGraphParser.ts | 2 +- .../src/diagrams/git/gitGraphRenderer.ts | 46 ++++++------------- .../mermaid/src/diagrams/git/gitGraphTypes.ts | 33 +++++++------ 5 files changed, 45 insertions(+), 68 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index b47809aac4..c7ee21a142 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -10,7 +10,8 @@ import { setDiagramTitle, getDiagramTitle, } from '../common/commonDb.js'; -import type { DiagramOrientation, Commit, GitGraphDB, CommitType } from './gitGraphTypes.js'; +import type { DiagramOrientation, Commit, GitGraphDB } from './gitGraphTypes.js'; +import { commitType } from './gitGraphTypes.js'; import { ImperativeState } from '../../utils/imperativeState.js'; import DEFAULT_CONFIG from '../../defaultConfig.js'; @@ -132,11 +133,9 @@ export const merge = ( if (customId) { customId = common.sanitizeText(customId, config); } - const currentBranchCheck: string | null | undefined = state.records.branches.get( - state.records.currBranch - ); - const otherBranchCheck: string | null | undefined = state.records.branches.get(otherBranch); - const currentCommit: Commit | undefined = currentBranchCheck + 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 @@ -215,8 +214,8 @@ export const merge = ( const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this - const commit: Commit = { - id: customId ? customId : state.records.seq + '-' + getID(), + 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], @@ -224,8 +223,8 @@ export const merge = ( type: commitType.MERGE, customType: overrideType, customId: customId ? true : false, - tags: customTags ? customTags : [], - }; + tags: customTags ?? [], + } satisfies Commit; state.records.head = commit; state.records.commits.set(commit.id, commit); state.records.branches.set(state.records.currBranch, commit.id); @@ -379,7 +378,6 @@ function upsert(arr: any[], key: any, newVal: any) { } } -/** @param commitArr - array */ function prettyPrintCommitHistory(commitArr: Commit[]) { const commit = commitArr.reduce((out, commit) => { if (out.seq > commit.seq) { @@ -472,14 +470,6 @@ export const getHead = function () { return state.records.head; }; -export const commitType: CommitType = { - NORMAL: 0, - REVERSE: 1, - HIGHLIGHT: 2, - MERGE: 3, - CHERRY_PICK: 4, -}; - export const db: GitGraphDB = { commitType, getConfig, diff --git a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts index 01537e5511..d6e8a06134 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDiagram.ts @@ -6,8 +6,8 @@ import gitGraphStyles from './styles.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; export const diagram: DiagramDefinition = { - parser: parser, - db: db, + parser, + db, renderer: gitGraphRenderer, styles: gitGraphStyles, }; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 404f0c202b..6be06e84d2 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -4,7 +4,7 @@ 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 './gitGraphAst.js'; +import { commitType } from './gitGraphTypes.js'; import type { CheckoutAst, CherryPickingAst, diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts index bd242ce06e..39a64a623b 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.ts @@ -4,32 +4,8 @@ import { log } from '../../logger.js'; import utils from '../../utils.js'; import type { DrawDefinition } from '../../diagram-api/types.js'; import type d3 from 'd3'; -import type { - CommitType, - Commit, - GitGraphDBRenderProvider, - DiagramOrientation, -} from './gitGraphTypes.js'; - -const DEFAULT_CONFIG = getConfig(); -const DEFAULT_GITGRAPH_CONFIG = DEFAULT_CONFIG?.gitGraph; - -let allCommitsDict = new Map(); - -const LAYOUT_OFFSET = 10; -const COMMIT_STEP = 40; -const PX = 4; -const PY = 2; - -const commitType: CommitType = { - NORMAL: 0, - REVERSE: 1, - HIGHLIGHT: 2, - MERGE: 3, - CHERRY_PICK: 4, -}; - -const THEME_COLOR_LIMIT = 8; +import type { Commit, GitGraphDBRenderProvider, DiagramOrientation } from './gitGraphTypes.js'; +import { commitType } from './gitGraphTypes.js'; interface BranchPosition { pos: number; @@ -45,12 +21,22 @@ interface CommitPositionOffset extends CommitPosition { posWithOffset: number; } +const DEFAULT_CONFIG = getConfig(); +const DEFAULT_GITGRAPH_CONFIG = DEFAULT_CONFIG?.gitGraph; +const LAYOUT_OFFSET = 10; +const COMMIT_STEP = 40; +const PX = 4; +const PY = 2; + +const THEME_COLOR_LIMIT = 8; const branchPos = new Map(); const commitPos = new Map(); +const defaultPos = 30; + +let allCommitsDict = new Map(); let lanes: number[] = []; let maxPos = 0; let dir: DiagramOrientation = 'LR'; -const defaultPos = 30; const clear = () => { branchPos.clear(); @@ -306,7 +292,7 @@ const drawCommitLabel = ( if ( commit.type !== commitType.CHERRY_PICK && ((commit.customId && commit.type === commitType.MERGE) || commit.type !== commitType.MERGE) && - DEFAULT_GITGRAPH_CONFIG.showCommitLabel + DEFAULT_GITGRAPH_CONFIG?.showCommitLabel ) { const wrapper = gLabels.append('g'); const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg'); @@ -549,10 +535,8 @@ const drawCommits = ( if (dir === 'BT') { if (isParallelCommits) { setParallelBTPos(sortedKeys, commits, pos); - sortedKeys = sortedKeys.reverse(); - } else { - sortedKeys = sortedKeys.reverse(); } + sortedKeys = sortedKeys.reverse(); } sortedKeys.forEach((key) => { diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index b888cc297e..0b1b4f714c 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -1,13 +1,21 @@ import type { GitGraphDiagramConfig } from '../../config.type.js'; import type { DiagramDBBase } from '../../diagram-api/types.js'; -export interface CommitType { - NORMAL: number; - REVERSE: number; - HIGHLIGHT: number; - MERGE: number; - CHERRY_PICK: number; -} +export const commitType = { + NORMAL: 0, + REVERSE: 1, + HIGHLIGHT: 2, + MERGE: 3, + CHERRY_PICK: 4, +} as const; + +export const gitcommitType = { + NORMAL: 0, + REVERSE: 1, + HIGHLIGHT: 2, + MERGE: 3, + CHERRY_PICK: 4, +} as const; export interface Commit { id: string; @@ -25,11 +33,6 @@ export interface GitGraph { statements: Statement[]; } -export interface Position { - x: number; - y: number; -} - export type Statement = CommitAst | BranchAst | MergeAst | CheckoutAst | CherryPickingAst; export interface CommitAst { @@ -62,12 +65,12 @@ export interface CheckoutAst { export interface CherryPickingAst { $type: 'CherryPicking'; id: string; - tags?: string[]; parent: string; + tags?: string[]; } export interface GitGraphDB extends DiagramDBBase { - commitType: CommitType; + commitType: typeof commitType; setDirection: (dir: DiagramOrientation) => void; setOptions: (rawOptString: string) => void; getOptions: () => any; @@ -98,7 +101,7 @@ export interface GitGraphDB extends DiagramDBBase { } export interface GitGraphDBParseProvider extends Partial { - commitType: CommitType; + commitType: typeof commitType; setDirection: (dir: DiagramOrientation) => void; commit: (msg: string, id: string, type: number, tags?: string[]) => void; branch: (name: string, order?: number) => void; From b93691be0e93044778ca5277f215376c4acad7cd Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 01:00:13 -0400 Subject: [PATCH 43/49] fixed small error --- packages/mermaid/src/diagrams/git/gitGraphAst.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index c7ee21a142..0b09a66871 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -215,7 +215,7 @@ export const merge = ( const verifiedBranch: string = otherBranchCheck ? otherBranchCheck : ''; //figure out a cleaner way to do this const commit = { - id: customId ?? `${state.records.seq}-${getID()}`, + 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], From d9d9cc9ddc8584ba5ba89a978c35cd04c5f3cf32 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 06:06:19 -0400 Subject: [PATCH 44/49] added objects to be transfered from parser to db --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 1 + .../mermaid/src/diagrams/git/gitGraphTypes.ts | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 0b09a66871..1742836702 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -15,6 +15,7 @@ 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; diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 0b1b4f714c..90156cc58d 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -9,13 +9,31 @@ export const commitType = { CHERRY_PICK: 4, } as const; -export const gitcommitType = { - NORMAL: 0, - REVERSE: 1, - HIGHLIGHT: 2, - MERGE: 3, - CHERRY_PICK: 4, -} as const; +export interface CommitDB { + message: string; + id: string; + type: typeof commitType; + tags?: string[]; +} + +export interface BranchDB { + name: string; + order: number; +} + +export interface MergeDB { + branch: string; + id: string; + type?: typeof commitType; + tags?: string[]; +} + +export interface CherryPickDB { + id: string; + targetId: string; + parent: string; + tags?: string[]; +} export interface Commit { id: string; From 7a7b41557d6494e3bd33614c36561158e40f5352 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 06:37:49 -0400 Subject: [PATCH 45/49] implemented transfer objects from parser to db --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 43 ++++--- .../src/diagrams/git/gitGraphParser.ts | 107 ++++++++++++------ .../mermaid/src/diagrams/git/gitGraphTypes.ts | 42 ++----- 3 files changed, 109 insertions(+), 83 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 1742836702..99d5340cdd 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -10,7 +10,15 @@ import { setDiagramTitle, getDiagramTitle, } from '../common/commonDb.js'; -import type { DiagramOrientation, Commit, GitGraphDB } from './gitGraphTypes.js'; +import type { + DiagramOrientation, + Commit, + GitGraphDB, + CommitDB, + MergeDB, + BranchDB, + CherryPickDB, +} from './gitGraphTypes.js'; import { commitType } from './gitGraphTypes.js'; import { ImperativeState } from '../../utils/imperativeState.js'; @@ -86,7 +94,12 @@ export const getOptions = function () { return state.records.options; }; -export const commit = function (msg: string, id: string, type: number, tags?: string[]) { +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(); @@ -109,7 +122,9 @@ export const commit = function (msg: string, id: string, type: number, tags?: st log.debug('in pushCommit ' + newCommit.id); }; -export const branch = function (name: string, order?: number) { +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( @@ -123,12 +138,11 @@ export const branch = function (name: string, order?: number) { log.debug('in createBranch'); }; -export const merge = ( - otherBranch: string, - customId?: string, - overrideType?: number, - customTags?: string[] -): void => { +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) { @@ -233,12 +247,11 @@ export const merge = ( log.debug('in mergeBranch'); }; -export const cherryPick = function ( - sourceId: string, - targetId: string, - tags: string[] | undefined, - parentCommitId: string -) { +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); diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index 6be06e84d2..a6911d4427 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -12,6 +12,10 @@ import type { CommitAst, BranchAst, GitGraphDBParseProvider, + CommitDB, + BranchDB, + MergeDB, + CherryPickDB, } from './gitGraphTypes.js'; const populate = (ast: GitGraph, db: GitGraphDBParseProvider) => { @@ -28,11 +32,11 @@ const populate = (ast: GitGraph, db: GitGraphDBParseProvider) => { const parseStatement = (statement: any, db: GitGraphDBParseProvider) => { const parsers: Record void> = { - Commit: (stmt) => db.commit(...parseCommit(stmt)), - Branch: (stmt) => db.branch(...parseBranch(stmt)), - Merge: (stmt) => db.merge(...parseMerge(stmt)), + 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)), + CherryPicking: (stmt) => db.cherryPick(parseCherryPicking(stmt)), }; const parser = parsers[statement.$type]; @@ -43,29 +47,32 @@ const parseStatement = (statement: any, db: GitGraphDBParseProvider) => { } }; -const parseCommit = (commit: CommitAst): [string, string, number, string[] | undefined] => { - const id = commit.id; - const message = commit.message ?? ''; - const type = commit.type !== undefined ? commitType[commit.type] : commitType.NORMAL; - const tags = commit.tags ?? undefined; - - return [message, id, type, tags]; +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): [string, number] => { - const name = branch.name; - const order = branch.order ?? 0; - return [name, order]; +const parseBranch = (branch: BranchAst): BranchDB => { + const branchDB: BranchDB = { + name: branch.name, + order: branch.order ?? 0, + }; + return branchDB; }; -const parseMerge = ( - merge: MergeAst -): [string, string, number | undefined, string[] | undefined] => { - const branch = merge.branch; - const id = merge.id ?? ''; - const type = merge.type !== undefined ? commitType[merge.type] : undefined; - const tags = merge.tags ?? undefined; - return [branch, id, type, tags]; +const parseMerge = (merge: MergeAst): MergeDB => { + const mergeDB: MergeDB = { + branch: merge.branch, + id: merge.id ?? '', + type: merge.type !== undefined ? commitType[merge.type] : commitType.NORMAL, + tags: merge.tags ?? undefined, + }; + return mergeDB; }; const parseCheckout = (checkout: CheckoutAst): string => { @@ -73,13 +80,14 @@ const parseCheckout = (checkout: CheckoutAst): string => { return branch; }; -const parseCherryPicking = ( - cherryPicking: CherryPickingAst -): [string, string, string[] | undefined, string] => { - const id = cherryPicking.id; - const tags = cherryPicking.tags?.length === 0 ? undefined : cherryPicking.tags; - const parent = cherryPicking.parent; - return [id, '', tags, parent]; +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 = { @@ -113,7 +121,12 @@ if (import.meta.vitest) { type: 'NORMAL', }; parseStatement(commit, mockDB); - expect(mockDB.commit).toHaveBeenCalledWith('test', '1', 0, ['tag1', 'tag2']); + expect(mockDB.commit).toHaveBeenCalledWith({ + id: '1', + msg: 'test', + tags: ['tag1', 'tag2'], + type: 0, + }); }); it('should parse a branch statement', () => { const branch = { @@ -122,7 +135,7 @@ if (import.meta.vitest) { order: 1, }; parseStatement(branch, mockDB); - expect(mockDB.branch).toHaveBeenCalledWith('newBranch', 1); + expect(mockDB.branch).toHaveBeenCalledWith({ name: 'newBranch', order: 1 }); }); it('should parse a checkout statement', () => { const checkout = { @@ -141,7 +154,12 @@ if (import.meta.vitest) { type: 'NORMAL', }; parseStatement(merge, mockDB); - expect(mockDB.merge).toHaveBeenCalledWith('newBranch', '1', 0, ['tag1', 'tag2']); + expect(mockDB.merge).toHaveBeenCalledWith({ + branch: 'newBranch', + id: '1', + tags: ['tag1', 'tag2'], + type: 0, + }); }); it('should parse a cherry picking statement', () => { const cherryPick = { @@ -151,7 +169,12 @@ if (import.meta.vitest) { parent: '2', }; parseStatement(cherryPick, mockDB); - expect(mockDB.cherryPick).toHaveBeenCalledWith('1', '', ['tag1', 'tag2'], '2'); + expect(mockDB.cherryPick).toHaveBeenCalledWith({ + id: '1', + targetId: '', + parent: '2', + tags: ['tag1', 'tag2'], + }); }); it('should parse a langium generated gitGraph ast', () => { @@ -201,9 +224,19 @@ if (import.meta.vitest) { populate(gitGraphAst, mockDB); - expect(mockDB.commit).toHaveBeenCalledWith('test', '1', 0, ['tag1', 'tag2']); - expect(mockDB.branch).toHaveBeenCalledWith('newBranch', 1); - expect(mockDB.merge).toHaveBeenCalledWith('newBranch', '1', 0, ['tag1', 'tag2']); + 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'); }); }); diff --git a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts index 90156cc58d..32b951bcc4 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphTypes.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphTypes.ts @@ -10,9 +10,9 @@ export const commitType = { } as const; export interface CommitDB { - message: string; + msg: string; id: string; - type: typeof commitType; + type: number; tags?: string[]; } @@ -24,7 +24,7 @@ export interface BranchDB { export interface MergeDB { branch: string; id: string; - type?: typeof commitType; + type?: number; tags?: string[]; } @@ -92,20 +92,10 @@ export interface GitGraphDB extends DiagramDBBase { setDirection: (dir: DiagramOrientation) => void; setOptions: (rawOptString: string) => void; getOptions: () => any; - commit: (msg: string, id: string, type: number, tags?: string[]) => void; - branch: (name: string, order?: number) => void; - merge: ( - otherBranch: string, - customId?: string, - overrideType?: number, - customTags?: string[] - ) => void; - cherryPick: ( - sourceId: string, - targetId: string, - tags: string[] | undefined, - parentCommitId: string - ) => void; + commit: (commitDB: CommitDB) => void; + branch: (branchDB: BranchDB) => void; + merge: (mergeDB: MergeDB) => void; + cherryPick: (cherryPickDB: CherryPickDB) => void; checkout: (branch: string) => void; prettyPrint: () => void; clear: () => void; @@ -121,20 +111,10 @@ export interface GitGraphDB extends DiagramDBBase { export interface GitGraphDBParseProvider extends Partial { commitType: typeof commitType; setDirection: (dir: DiagramOrientation) => void; - commit: (msg: string, id: string, type: number, tags?: string[]) => void; - branch: (name: string, order?: number) => void; - merge: ( - otherBranch: string, - customId?: string, - overrideType?: number, - customTags?: string[] - ) => void; - cherryPick: ( - sourceId: string, - targetId: string, - tags: string[] | undefined, - parentCommitId: string - ) => void; + commit: (commitDB: CommitDB) => void; + branch: (branchDB: BranchDB) => void; + merge: (mergeDB: MergeDB) => void; + cherryPick: (cherryPickDB: CherryPickDB) => void; checkout: (branch: string) => void; } From 94ee076aad40b4009df672a8be9f17f03403f006 Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 06:46:33 -0400 Subject: [PATCH 46/49] fixed config for user configs --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 99d5340cdd..4b61c35630 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -1,5 +1,6 @@ import { log } from '../../logger.js'; -import { random } from '../../utils.js'; +import { cleanAndMerge, random } from '../../utils.js'; +import { getConfig as commonGetConfig } from '../../config.js'; import common from '../common/common.js'; import { setAccTitle, @@ -37,12 +38,16 @@ interface GitGraphState { } const DEFAULT_GITGRAPH_CONFIG: Required = DEFAULT_CONFIG.gitGraph; - -const mainBranchName = DEFAULT_GITGRAPH_CONFIG.mainBranchName; -const mainBranchOrder = DEFAULT_GITGRAPH_CONFIG.mainBranchOrder; -const config: Required = structuredClone(DEFAULT_GITGRAPH_CONFIG); - -const getConfig = (): Required => structuredClone(config); +const getConfig = (): Required => { + const config = cleanAndMerge({ + ...DEFAULT_GITGRAPH_CONFIG, + ...commonGetConfig().gitGraph, + }); + return config; +}; +const config = getConfig(); +const mainBranchName = config.mainBranchName; +const mainBranchOrder = config.mainBranchOrder; const state = new ImperativeState(() => ({ commits: new Map(), From 3ac242978ddc42e02c2ee569d6da72076a4028eb Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 07:04:05 -0400 Subject: [PATCH 47/49] fixed merge --- packages/mermaid/src/diagrams/git/gitGraphParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphParser.ts b/packages/mermaid/src/diagrams/git/gitGraphParser.ts index a6911d4427..c56bc6f449 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParser.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphParser.ts @@ -69,7 +69,7 @@ const parseMerge = (merge: MergeAst): MergeDB => { const mergeDB: MergeDB = { branch: merge.branch, id: merge.id ?? '', - type: merge.type !== undefined ? commitType[merge.type] : commitType.NORMAL, + type: merge.type !== undefined ? commitType[merge.type] : undefined, tags: merge.tags ?? undefined, }; return mergeDB; From 24ba5b73da6bba144bac5535ff90bc6260c3f05b Mon Sep 17 00:00:00 2001 From: Austin Fulbright Date: Tue, 20 Aug 2024 14:53:01 -0400 Subject: [PATCH 48/49] added gitgraph in imperative state --- .../mermaid/src/diagrams/git/gitGraphAst.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.ts b/packages/mermaid/src/diagrams/git/gitGraphAst.ts index 4b61c35630..44597e9d78 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.ts @@ -45,20 +45,23 @@ const getConfig = (): Required => { }); return config; }; -const config = getConfig(); -const mainBranchName = config.mainBranchName; -const mainBranchOrder = config.mainBranchOrder; - -const state = new ImperativeState(() => ({ - 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: {}, -})); + +const state = new ImperativeState(() => { + 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 }); @@ -121,7 +124,7 @@ export const commit = function (commitDB: CommitDB) { branch: state.records.currBranch, }; state.records.head = newCommit; - log.info('main branch', mainBranchName); + 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); From 5deaef456e74d796866431c26f69360e4e74dbff Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 23 Aug 2024 19:11:04 +0530 Subject: [PATCH 49/49] chore: add changeset --- .changeset/dirty-mails-watch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dirty-mails-watch.md diff --git a/.changeset/dirty-mails-watch.md b/.changeset/dirty-mails-watch.md new file mode 100644 index 0000000000..8fd5ec9410 --- /dev/null +++ b/.changeset/dirty-mails-watch.md @@ -0,0 +1,6 @@ +--- +'@mermaid-js/parser': minor +'mermaid': patch +--- + +chore: Migrate git graph to langium, use typescript for internals