diff --git a/cypress/integration/rendering/gitGraph.spec.js b/cypress/integration/rendering/gitGraph.spec.js index 52019c430..80981c31c 100644 --- a/cypress/integration/rendering/gitGraph.spec.js +++ b/cypress/integration/rendering/gitGraph.spec.js @@ -253,4 +253,32 @@ describe('Git Graph diagram', () => { {} ); }); + it('13: should render a simple gitgraph with three branches,custom merge commit id,tag,type', () => { + imgSnapshotTest( + `gitGraph + commit id: "1" + commit id: "2" + branch nice_feature + checkout nice_feature + commit id: "3" + checkout main + commit id: "4" + checkout nice_feature + branch very_nice_feature + checkout very_nice_feature + commit id: "5" + checkout main + commit id: "6" + checkout nice_feature + commit id: "7" + checkout main + merge nice_feature id: "customID" tag: "customTag" type: REVERSE + checkout very_nice_feature + commit id: "8" + checkout main + commit id: "9" + `, + {} + ); + }); }); diff --git a/docs/gitgraph.md b/docs/gitgraph.md index 53e802101..191bc513b 100644 --- a/docs/gitgraph.md +++ b/docs/gitgraph.md @@ -182,7 +182,40 @@ After this we made use of the `checkout` keyword to set the current branch as `m After this we merge the `develop` branch onto the current branch `main`, resulting in a merge commit. Since the current branch at this point is still `main`, the last two commits are registered against that. -Additionally, you may add a tag to the merge commit, or override the default id: `merge branch id:"1234" tag:"v1.0.0"` +You can also decorate your merge with similar attributes as you did for the commit using: +- `id`--> To override the default ID with custom ID +- `tag`--> To add a custom tag to your merge commit +- `type`--> To override the default shape of merge commit. Here you can use other commit type mentioned earlier. + +And you can choose to use none, some or all of these attributes together. +For example: `merge develop id: "my_custom_id" tag: "my_custom_tag" type: REVERSE` + +Let us see how this works with the help of the following diagram: + +```mermaid-example + gitGraph + commit id: "1" + commit id: "2" + branch nice_feature + checkout nice_feature + commit id: "3" + checkout main + commit id: "4" + checkout nice_feature + branch very_nice_feature + checkout very_nice_feature + commit id: "5" + checkout main + commit id: "6" + checkout nice_feature + commit id: "7" + checkout main + merge nice_feature id: "customID" tag: "customTag" type: REVERSE + checkout very_nice_feature + commit id: "8" + checkout main + commit id: "9" +``` ### Cherry Pick commit from another branch Similar to how 'git' allows you to cherry-pick a commit from **another branch** onto the **current** branch, Mermaid also supports this functionality. You can also cherry-pick a commit from another branch using the `cherry-pick` keyword. diff --git a/package.json b/package.json index a8e6221dc..824a4e13f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "9.1.5", + "version": "9.1.6", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "main": "dist/mermaid.min.js", "module": "dist/mermaid.esm.min.mjs", diff --git a/src/diagrams/git/gitGraphAst.js b/src/diagrams/git/gitGraphAst.js index 6cec8bdd8..47c3e9f30 100644 --- a/src/diagrams/git/gitGraphAst.js +++ b/src/diagrams/git/gitGraphAst.js @@ -148,16 +148,9 @@ export const branch = function (name, order) { } }; -/** - * Creates a merge commit. - * - * @param {string} otherBranch - Target branch to merge to. - * @param {string} [tag] - Git tag to use on this merge commit. - * @param {string} [id] - Git commit id. - */ -export const merge = function (otherBranch, tag, id) { +export const merge = function (otherBranch, custom_id, override_type, custom_tag) { otherBranch = common.sanitizeText(otherBranch, configApi.getConfig()); - id = common.sanitizeText(id, configApi.getConfig()); + custom_id = common.sanitizeText(custom_id, configApi.getConfig()); const currentCommit = commits[branches[curBranch]]; const otherCommit = commits[branches[otherBranch]]; @@ -216,6 +209,23 @@ export const merge = function (otherBranch, tag, id) { loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 }, expected: ['branch abc'], }; + throw error; + } else if (custom_id && typeof commits[custom_id] !== 'undefined') { + let error = new Error( + 'Incorrect usage of "merge". Commit with id:' + + custom_id + + ' already exists, use different custom Id' + ); + error.hash = { + text: 'merge ' + otherBranch + custom_id + override_type + custom_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_tag, + ], + }; + throw error; } // if (isReachableFrom(currentCommit, otherCommit)) { @@ -228,13 +238,15 @@ export const merge = function (otherBranch, tag, id) { // } else { // create merge commit const commit = { - id: id || seq + '-' + getId(), + id: custom_id ? custom_id : seq + '-' + getId(), message: 'merged branch ' + otherBranch + ' into ' + curBranch, seq: seq++, parents: [head == null ? null : head.id, branches[otherBranch]], branch: curBranch, type: commitType.MERGE, - tag: tag ? tag : '', + customType: override_type, + customId: custom_id ? true : false, + tag: custom_tag ? custom_tag : '', }; head = commit; commits[commit.id] = commit; diff --git a/src/diagrams/git/gitGraphParserV2.spec.js b/src/diagrams/git/gitGraphParserV2.spec.js index 9c8e47443..9ddc981af 100644 --- a/src/diagrams/git/gitGraphParserV2.spec.js +++ b/src/diagrams/git/gitGraphParserV2.spec.js @@ -496,7 +496,7 @@ describe('when parsing a gitGraph', function () { ]); }); - it('should handle merge ids', function () { + it('should handle merge with custom ids, tags and typr', function () { const str = `gitGraph: commit branch testBranch @@ -510,7 +510,7 @@ describe('when parsing a gitGraph', function () { commit checkout main %% Merge ID and Tag (reverse order) - merge testBranch2 id: "4-444" tag: "merge-tag2" + merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT branch testBranch3 checkout testBranch3 commit @@ -553,6 +553,8 @@ describe('when parsing a gitGraph', function () { expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]); expect(testBranch2Merge.tag).toBe('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]); @@ -687,6 +689,27 @@ describe('when parsing a gitGraph', function () { 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', function () { + 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 { + 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', function () { const str = `gitGraph commit diff --git a/src/diagrams/git/gitGraphRenderer.js b/src/diagrams/git/gitGraphRenderer.js index 7dce4d748..73588fa2d 100644 --- a/src/diagrams/git/gitGraphRenderer.js +++ b/src/diagrams/git/gitGraphRenderer.js @@ -91,7 +91,9 @@ const drawCommits = (svg, commits, modifyGraph) => { // Don't draw the commits now but calculate the positioning which is used by the branch lines etc. if (modifyGraph) { let typeClass; - switch (commit.type) { + let commitSymbolType = + typeof commit.customType !== 'undefined' ? commit.customType : commit.type; + switch (commitSymbolType) { case commitType.NORMAL: typeClass = 'commit-normal'; break; @@ -111,7 +113,7 @@ const drawCommits = (svg, commits, modifyGraph) => { typeClass = 'commit-normal'; } - if (commit.type === commitType.HIGHLIGHT) { + if (commitSymbolType === commitType.HIGHLIGHT) { const circle = gBullets.append('rect'); circle.attr('x', x - 10); circle.attr('y', y - 10); @@ -135,7 +137,7 @@ const drawCommits = (svg, commits, modifyGraph) => { branchPos[commit.branch].index % THEME_COLOR_LIMIT } ${typeClass}-inner` ); - } else if (commit.type === commitType.CHERRY_PICK) { + } else if (commitSymbolType === commitType.CHERRY_PICK) { gBullets .append('circle') .attr('cx', x) @@ -181,7 +183,7 @@ const drawCommits = (svg, commits, modifyGraph) => { 'class', `commit ${commit.id} commit${branchPos[commit.branch].index % THEME_COLOR_LIMIT}` ); - if (commit.type === commitType.MERGE) { + if (commitSymbolType === commitType.MERGE) { const circle2 = gBullets.append('circle'); circle2.attr('cx', x); circle2.attr('cy', y); @@ -193,7 +195,7 @@ const drawCommits = (svg, commits, modifyGraph) => { }` ); } - if (commit.type === commitType.REVERSE) { + 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}`) @@ -215,7 +217,12 @@ const drawCommits = (svg, commits, modifyGraph) => { const px = 4; const py = 2; // Draw the commit label - if (commit.type !== commitType.CHERRY_PICK && gitGraphConfig.showCommitLabel) { + 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'); diff --git a/src/diagrams/git/parser/gitGraph.jison b/src/diagrams/git/parser/gitGraph.jison index c7f1fbe27..937a7192a 100644 --- a/src/diagrams/git/parser/gitGraph.jison +++ b/src/diagrams/git/parser/gitGraph.jison @@ -121,11 +121,22 @@ cherryPickStatement ; mergeStatement - : MERGE ID {yy.merge($2)} - | MERGE ID COMMIT_TAG STR {yy.merge($2, $4)} - | MERGE ID COMMIT_ID STR {yy.merge($2, '', $4)} - | MERGE ID COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $4, $6)} - | MERGE ID COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $6, $4)} + : MERGE ID {yy.merge($2,'','','')} + | MERGE ID COMMIT_ID STR {yy.merge($2, $4,'','')} + | MERGE ID COMMIT_TYPE commitType {yy.merge($2,'', $4,'')} + | MERGE ID COMMIT_TAG STR {yy.merge($2, '','',$4)} + | MERGE ID COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $6,'', $4)} + | MERGE ID COMMIT_TAG STR COMMIT_TYPE commitType {yy.merge($2, '',$6, $4)} + | MERGE ID COMMIT_TYPE commitType COMMIT_TAG STR {yy.merge($2, '',$4, $6)} + | MERGE ID COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $4, $6, '')} + | MERGE ID COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $4, '', $6)} + | MERGE ID COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $6,$4, '')} + | MERGE ID COMMIT_ID STR COMMIT_TYPE commitType COMMIT_TAG STR {yy.merge($2, $4, $6, $8)} + | MERGE ID COMMIT_TYPE commitType COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $8, $4, $6)} + | MERGE ID COMMIT_ID STR COMMIT_TAG STR COMMIT_TYPE commitType {yy.merge($2, $4, $8, $6)} + | MERGE ID COMMIT_TYPE commitType COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $6, $4, $8)} + | MERGE ID COMMIT_TAG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $8, $6, $4)} + | MERGE ID COMMIT_TAG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $6, $8, $4)} ; commitStatement