From acffcc1616b896463d226385f9d7f6e756cc88ec Mon Sep 17 00:00:00 2001 From: Lucie Lenglet <6071537+Magador@users.noreply.github.com> Date: Fri, 15 Dec 2023 19:32:47 +0100 Subject: [PATCH] feat: command to simulate Merge/Pull Request - Adds `git merge(M|P)R [--delete-after-merge]` command to simulate a Merge/Pull Request being merged into the target branch - The optional flag `--delete-after-merge` will remove the merged branch on the origin tree - The local remote branch remains as it is Git default behavior, even after `git fetch` (it would require `git fetch --prune` support to automatically prune remote tracking branches) - You can push a branch with a deleted remote tracking branch on origin, the ref will just update. Resolves #1057 --- __tests__/remote.spec.js | 78 +++++++++++++++++++++++++++++++++++++++- src/js/git/commands.js | 41 +++++++++++++++++++++ src/js/git/index.js | 15 +++++--- 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/__tests__/remote.spec.js b/__tests__/remote.spec.js index 47f4b3e39..194d5a3a6 100644 --- a/__tests__/remote.spec.js +++ b/__tests__/remote.spec.js @@ -1,5 +1,8 @@ var base = require('./base'); +var intl = require('../src/js/intl'); +var Q = require('q'); var expectTreeAsync = base.expectTreeAsync; +var runCommand = base.runCommand; describe('Git Remotes', function() { it('clones', function() { @@ -242,6 +245,20 @@ describe('Git Remotes', function() { ); }); + it('will push to a new remote branch if tracking was previously set up but remote branch was merged on origin', function() { + return expectTreeAsync( + `git clone; + git switch -c feat; + git commit; + git push; + git fakeTeamwork; + git mergeMR feat main --delete-after-merge; + git commit; + git push;`, + '{"branches":{"main":{"remoteTrackingBranchID":"o/main","target":"C1","id":"main"},"o/main":{"remoteTrackingBranchID":null,"target":"C1","id":"o/main"},"feat":{"remoteTrackingBranchID":"o/feat","target":"C5","id":"feat"},"o/feat":{"remoteTrackingBranchID":null,"target":"C5","id":"o/feat"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C5":{"parents":["C2"],"id":"C5"}},"tags":{},"HEAD":{"id":"HEAD","target":"feat"},"originTree":{"branches":{"main":{"remoteTrackingBranchID":null,"target":"C4","id":"main"},"feat":{"remoteTrackingBranchID":null,"target":"C5","id":"feat"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"},"C4":{"parents":["C3","C2"],"id":"C4"},"C5":{"parents":["C2"],"id":"C5"}},"tags":{},"HEAD":{"target":"main","id":"HEAD"}}}' + ); + }); + it('will not fetch if ref does not exist on remote', function() { return expectTreeAsync( 'git clone; git fakeTeamwork; git fetch foo:main', @@ -440,5 +457,64 @@ describe('Git Remotes', function() { ); }); - + describe('mergeMR/mergePR on remote', function() { + it('requires a remote', function() { + return runCommand('git mergeMR', function(commandMsg) { + expect(commandMsg).toBe(intl.str('git-error-origin-required')); + }); + }); + + it('requires exactly 2 parameters', function() { + return Q.all([ + runCommand('git clone; git mergeMR', function(commandMsg) { + expect(commandMsg).toBe( + intl.str('git-error-args-few', { + lower: '2', + what: 'with git mergeMR', + }) + ); + }), + runCommand('git clone; git mergeMR feat', function(commandMsg) { + expect(commandMsg).toBe( + intl.str('git-error-args-few', { + lower: '2', + what: 'with git mergeMR', + }) + ); + }), + runCommand('git clone; git mergeMR a b main', function(commandMsg) { + expect(commandMsg).toBe( + intl.str('git-error-args-many', { + upper: '2', + what: 'with git mergeMR', + }) + ); + }), + ]); + }); + + it('merges one remote branch into another', function() { + return expectTreeAsync( + `git clone; + git switch -c feat; + git commit; + git push; + git fakeTeamwork; + git mergeMR feat main`, + '{"branches":{"main":{"target":"C1","id":"main","remoteTrackingBranchID":"o/main"},"o/main":{"target":"C1","id":"o/main","remoteTrackingBranchID":null},"feat":{"target":"C2","id":"feat","remoteTrackingBranchID":"o/feat"},"o/feat":{"target":"C2","id":"o/feat","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"tags":{},"HEAD":{"target":"feat","id":"HEAD"},"originTree":{"branches":{"main":{"target":"C4","id":"main","remoteTrackingBranchID":null},"feat":{"target":"C2","id":"feat","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"},"C4":{"parents":["C3","C2"],"id":"C4"}},"tags":{},"HEAD":{"target":"main","id":"HEAD"}}}' + ); + }); + + it('deletes the merged remote branch after merging', function() { + return expectTreeAsync( + `git clone; + git switch -c feat; + git commit; + git push; + git fakeTeamwork; + git mergeMR feat main --delete-after-merge`, + '{"branches":{"main":{"target":"C1","id":"main","remoteTrackingBranchID":"o/main"},"o/main":{"target":"C1","id":"o/main","remoteTrackingBranchID":null},"feat":{"target":"C2","id":"feat","remoteTrackingBranchID":"o/feat"},"o/feat":{"target":"C2","id":"o/feat","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"tags":{},"HEAD":{"target":"feat","id":"HEAD"},"originTree":{"branches":{"main":{"target":"C4","id":"main","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"},"C4":{"parents":["C3","C2"],"id":"C4"}},"tags":{},"HEAD":{"target":"main","id":"HEAD"}}}' + ); + }); + }); }); diff --git a/src/js/git/commands.js b/src/js/git/commands.js index e857b5d03..2e5c8dad4 100644 --- a/src/js/git/commands.js +++ b/src/js/git/commands.js @@ -589,6 +589,47 @@ var commandConfig = { } }, + mergeMR: { + regex: /^git +merge[MP]R($|\s)/, + options: ['--delete-after-merge'], + execute: function(engine, command) { + var generalArgs = command.getGeneralArgs(); + var commandOptions = command.getOptionsMap(); + if (!engine.hasOrigin()) { + throw new GitError({ + msg: intl.str('git-error-origin-required'), + }); + } + + command.validateArgBounds(generalArgs, 2, 2); + + var fromBranch = validateOriginBranchName(engine, generalArgs[0]); + var intoBranch = validateOriginBranchName(engine, generalArgs[1]); + + var origin = engine.origin; + + origin.checkout(intoBranch); + var mergeCommit = origin.merge(fromBranch, { noFF: true }); + + origin.animationFactory.genCommitBirthAnimation( + origin.animationQueue, + mergeCommit, + origin.gitVisuals + ); + + if (!!commandOptions['--delete-after-merge']) { + origin.validateAndDeleteBranch(fromBranch); + } + + origin.checkout('main'); + + origin.animationFactory.playRefreshAnimationAndFinish( + origin.gitVisuals, + origin.animationQueue + ); + } + }, + revlist: { dontCountForGolf: true, displayName: 'rev-list', diff --git a/src/js/git/index.js b/src/js/git/index.js index 5b96b114d..b8fbafea5 100644 --- a/src/js/git/index.js +++ b/src/js/git/index.js @@ -453,10 +453,17 @@ GitEngine.prototype.findCommonAncestorWithRemote = function(originTarget) { }; GitEngine.prototype.makeBranchOnOriginAndTrack = function(branchName, target) { - var remoteBranch = this.makeBranch( - ORIGIN_PREFIX + branchName, - this.getCommitFromRef(target) - ); + var remoteBranch = this.refs[ORIGIN_PREFIX + branchName]; + + // If the remote branch exists but the branch on origin was deleted, updates its target location + if (remoteBranch) { + this.setTargetLocation(remoteBranch, target); + } else { + remoteBranch = this.makeBranch( + ORIGIN_PREFIX + branchName, + this.getCommitFromRef(target) + ); + } if (this.refs[branchName]) { // not all remote branches have tracking ones this.setLocalToTrackRemote(this.refs[branchName], remoteBranch);