diff --git a/cypress/integration/rendering/gitGraph.spec.js b/cypress/integration/rendering/gitGraph.spec.js index 2aab34c343..d3e4dd9dde 100644 --- a/cypress/integration/rendering/gitGraph.spec.js +++ b/cypress/integration/rendering/gitGraph.spec.js @@ -811,4 +811,19 @@ gitGraph TB: {} ); }); + it('40: should render a simple gitgraph with cherry pick merge commit', () => { + imgSnapshotTest( + `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"` + ); + }); }); diff --git a/docs/syntax/gitgraph.md b/docs/syntax/gitgraph.md index 8d39ddbcbd..c6a397a982 100644 --- a/docs/syntax/gitgraph.md +++ b/docs/syntax/gitgraph.md @@ -366,41 +366,49 @@ A few important rules to note here are: 1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above. 2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch. 3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw. +4. When cherry-picking a merge commit, providing a parent commit ID is mandatory. If the parent attribute is omitted or an invalid parent commit ID is provided, an error will be thrown. +5. The specified parent commit must be an immediate parent of the merge commit being cherry-picked. Let see an example: ```mermaid-example gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - commit id:"ONE" - checkout develop - commit id:"B" - checkout main - commit id:"TWO" - cherry-pick id:"A" - commit id:"THREE" - checkout develop - commit id:"C" + commit id: "ZERO" + branch develop + branch release + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + merge develop id:"MERGE" + commit id:"TWO" + checkout release + cherry-pick id:"MERGE" parent:"B" + commit id:"THREE" + checkout develop + commit id:"C" ``` ```mermaid gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - commit id:"ONE" - checkout develop - commit id:"B" - checkout main - commit id:"TWO" - cherry-pick id:"A" - commit id:"THREE" - checkout develop - commit id:"C" + commit id: "ZERO" + branch develop + branch release + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + merge develop id:"MERGE" + commit id:"TWO" + checkout release + cherry-pick id:"MERGE" parent:"B" + commit id:"THREE" + checkout develop + commit id:"C" ``` ## Gitgraph specific configuration options diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.js b/packages/mermaid/src/diagrams/git/gitGraphAst.js index f8cfba6f51..bd90c87173 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.js +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.js @@ -255,11 +255,12 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag log.debug('in mergeBranch'); }; -export const cherryPick = function (sourceId, targetId, tag) { +export const cherryPick = function (sourceId, targetId, tag, parentCommitId) { 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[sourceId] === undefined) { let error = new Error( @@ -274,20 +275,21 @@ export const cherryPick = function (sourceId, targetId, tag) { }; throw error; } - let sourceCommit = commits[sourceId]; let sourceCommitBranch = sourceCommit.branch; - if (sourceCommit.type === commitType.MERGE) { + if ( + parentCommitId && + !(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId)) + ) { let error = new Error( - 'Incorrect usage of "cherryPick". Source commit should not be a merge commit' + 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.' + ); + throw error; + } + if (sourceCommit.type === commitType.MERGE && !parentCommitId) { + let error = new Error( + 'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.' ); - 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; } if (!targetId || commits[targetId] === undefined) { @@ -327,7 +329,11 @@ export const cherryPick = function (sourceId, targetId, tag) { parents: [head == null ? null : head.id, sourceCommit.id], branch: curBranch, type: commitType.CHERRY_PICK, - tag: tag ?? 'cherry-pick:' + sourceCommit.id, + tag: + tag ?? + `cherry-pick:${sourceCommit.id}${ + sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : '' + }`, }; head = commit; commits[commit.id] = commit; diff --git a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js index 540ab773bf..09a9cdb259 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js +++ b/packages/mermaid/src/diagrams/git/gitGraphParserV2.spec.js @@ -673,6 +673,145 @@ describe('when parsing a gitGraph', function () { expect(commits[cherryPickCommitID].branch).toBe('main'); }); + it('should support cherry-picking of merge commits', function () { + 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" + `; + + parser.parse(str); + const commits = parser.yy.getCommits(); + const cherryPickCommitID = Object.keys(commits)[4]; + expect(commits[cherryPickCommitID].tag).toBe('cherry-pick:M|parent:B'); + expect(commits[cherryPickCommitID].branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with tag', function () { + 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" + `; + + parser.parse(str); + const commits = parser.yy.getCommits(); + const cherryPickCommitID = Object.keys(commits)[4]; + expect(commits[cherryPickCommitID].tag).toBe('v1.0'); + expect(commits[cherryPickCommitID].branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with additional commit', function () { + 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" + `; + + parser.parse(str); + const commits = parser.yy.getCommits(); + const cherryPickCommitID = Object.keys(commits)[5]; + expect(commits[cherryPickCommitID].tag).toBe('v2.1:ZERO'); + expect(commits[cherryPickCommitID].branch).toBe('release'); + }); + + it('should support cherry-picking of merge commits with empty tag', function () { + 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" + `; + + parser.parse(str); + const commits = parser.yy.getCommits(); + const cherryPickCommitID = Object.keys(commits)[5]; + const cherryPickCommitID2 = Object.keys(commits)[7]; + expect(commits[cherryPickCommitID].tag).toBe(''); + expect(commits[cherryPickCommitID2].tag).toBe(''); + expect(commits[cherryPickCommitID].branch).toBe('release'); + }); + + it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', function () { + 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" + ` + ) + .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 + 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', function () { const str = `gitGraph commit diff --git a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison index 2297db17cd..40fab8b590 100644 --- a/packages/mermaid/src/diagrams/git/parser/gitGraph.jison +++ b/packages/mermaid/src/diagrams/git/parser/gitGraph.jison @@ -39,6 +39,7 @@ branch(?=\s|$) return 'BRANCH'; "order:" return 'ORDER'; merge(?=\s|$) return 'MERGE'; cherry\-pick(?=\s|$) return 'CHERRY_PICK'; +"parent:" return 'PARENT_COMMIT' // "reset" return 'RESET'; checkout(?=\s|$) return 'CHECKOUT'; "LR" return 'DIR'; @@ -109,10 +110,17 @@ branchStatement 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 COMMIT_TAG STR {yy.cherryPick($3, '', $5)} - | CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '')} + | CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR COMMIT_TAG STR {yy.cherryPick($3, '', $7,$5)} + | CHERRY_PICK COMMIT_ID STR COMMIT_TAG STR PARENT_COMMIT STR {yy.cherryPick($3, '', $5,$7)} | CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR {yy.cherryPick($5, '', $3)} - | CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR {yy.cherryPick($3, '', '')} + | CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR {yy.cherryPick($5, '', '')} + | CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '')} + | CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '',$5)} + | CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR PARENT_COMMIT STR {yy.cherryPick($3, '', '',$7)} + | CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($5, '', $3,$7)} + | CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR PARENT_COMMIT STR{yy.cherryPick($5, '', '',$7)} ; mergeStatement diff --git a/packages/mermaid/src/docs/syntax/gitgraph.md b/packages/mermaid/src/docs/syntax/gitgraph.md index 5fa09cb225..3f7e42d436 100644 --- a/packages/mermaid/src/docs/syntax/gitgraph.md +++ b/packages/mermaid/src/docs/syntax/gitgraph.md @@ -244,24 +244,29 @@ A few important rules to note here are: 1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above. 2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch. 3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw. +4. When cherry-picking a merge commit, providing a parent commit ID is mandatory. If the parent attribute is omitted or an invalid parent commit ID is provided, an error will be thrown. +5. The specified parent commit must be an immediate parent of the merge commit being cherry-picked. Let see an example: ```mermaid-example gitGraph - commit id: "ZERO" - branch develop - commit id:"A" - checkout main - commit id:"ONE" - checkout develop - commit id:"B" - checkout main - commit id:"TWO" - cherry-pick id:"A" - commit id:"THREE" - checkout develop - commit id:"C" + commit id: "ZERO" + branch develop + branch release + commit id:"A" + checkout main + commit id:"ONE" + checkout develop + commit id:"B" + checkout main + merge develop id:"MERGE" + commit id:"TWO" + checkout release + cherry-pick id:"MERGE" parent:"B" + commit id:"THREE" + checkout develop + commit id:"C" ``` ## Gitgraph specific configuration options