diff --git a/hgext/mergecommit.py b/hgext/mergecommit.py new file mode 100644 --- /dev/null +++ b/hgext/mergecommit.py @@ -0,0 +1,108 @@ +from mercurial import ( + context, + destutil, + error, + extensions, + hg, + merge as mergemod, + phases, + registrar, + scmutil, +) +from mercurial.i18n import _ + +cmdtable = {} +command = registrar.command(cmdtable) + +configtable = {} +configitem = registrar.configitem(configtable) + +configitem('merge', 'inmemory', True) + +@command('mergecommit', + [('', 'dest', '', _('merge destination')), + ('', 'message', '', _('description of the merge commit')), + ('', 'date', '', _('date of the merge commit')), + ('r', 'rev', '', _('revision to merge')), + ('', 'tool', '', _('specify merge tool')), + ], ()) +def mergecommit(ui, repo, node=None, **opts): + """Merge the rev passed into dest (or working directory parent) and + creates a commit with specified options if no conflicts occur. + + If conlicts occur, returns 1 and print the list of unresolved files on + stderr. Also, the conflicted state is not applied to the working directory. + To do that, you should run `hg merge` + + This does not update to destination node to merge the rev, it uses in-memory + merging and also creates in-memory commit. This also does not update to the + new commit formed.""" + + if opts.get('rev') and node: + raise error.Abort(_("please specify just one revision")) + if not node: + node = opts.get('rev') + + if node: + node = scmutil.revsingle(repo, node).node() + else: + node = repo[destutil.destmerge(repo)].node() + + destnode = None + if opts.get('dest'): + destnode = scmutil.revsingle(repo, opts.get('dest')).node() + + overrides = {('ui', 'forcemerge'): opts.get('tool', '')} + with ui.configoverride(overrides, 'mergecommit'): + return merge(repo, node, destnode, date=opts['date'], + message=opts['message']) + +def merge(repo, node, destnode, date, message): + """merges the node into destnode or parent of working directory and creates + a commit if no conflicts occur. + + If conflicts are there, it returns 1 and prints the list of unresolved files + on stderr""" + + # create a memwctx to merge + wctx = context.overlayworkingctx(repo) + # setting destnode as p1 if passed + if destnode: + currentp1 = repo[destnode] + else: + currentp1 = repo['.'] + wctx.setbase(currentp1) + + stats = None + try: + # actual merging + stats = mergemod.update(repo, node, True, None, wc=wctx) + except error.InMemoryMergeConflictsError: + pass + + if stats is None or stats.unresolvedcount > 0: + # there were conflicts + ms = mergemod.mergestate.read(repo) + unresolved = list(ms.unresolved()) + repo.ui.warn("list of unresolved files: %s\n" % ', '.join(unresolved)) + mergemod.mergestate.clean(repo) + return 1 + + branch = currentp1.branch() + desc = message + if not desc: + desc = "in-memory merge commit" + if not date: + date = None + p1 = currentp1.node() + p2 = node + + # creating a memctx and then commiting it + memctx = wctx.tomemctx(desc, parents=(p1, p2), branch=branch, date=date) + overrides = {('phases', 'new-commit'): phases.secret} + with repo.ui.configoverride(overrides, 'memorymerge'): + newctx = repo.commitctx(memctx) + wctx.clean() + mergemod.mergestate.clean(repo) + repo.ui.status("new commit formed is %s\n" % repo[newctx].hex()[:12]) + return 0 diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py --- a/mercurial/filemerge.py +++ b/mercurial/filemerge.py @@ -959,7 +959,7 @@ if r: if onfailure: - if wctx.isinmemory(): + if wctx.isinmemory() and not ui.configbool('merge', 'inmemory'): raise error.InMemoryMergeConflictsError('in-memory merge ' 'does not support ' 'merge conflicts') diff --git a/tests/test-mergecommit.t b/tests/test-mergecommit.t new file mode 100644 --- /dev/null +++ b/tests/test-mergecommit.t @@ -0,0 +1,235 @@ +#testcases mergecommit normal-merge + +Testing the mergecommit extension. The test demonstrates how this extension can +be used by hosting providers to handle merging of PRs. It has two testcases, +'mergecommit' and 'normal-merge' so that the normal-merge workflow can be +compared with the workflow implemented by mergecommit extension. + +The test is using named branches to denote user PRs. + + + $ cat << EOF >> $HGRCPATH + > [alias] + > glog = log -GT "{rev}:{node|short} {desc}\n({branch})" + > EOF + +#if mergecommit + $ cat << EOF >> $HGRCPATH + > [extensions] + > mergecommit = + > EOF +#endif + +Initialize a server + $ hg init server + $ cd server + $ for ch in a b c d; do echo foo > $ch; hg ci -Aqm "added "$ch; done; + $ hg glog + @ 3:a44c3a524808 added d + | (default) + o 2:8be98ac1a569 added c + | (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + +Initialize a client, make some changes and create a PR + + $ cd .. + $ hg clone server client1 + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client1 + $ hg up 1 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg glog + o 3:a44c3a524808 added d + | (default) + o 2:8be98ac1a569 added c + | (default) + @ 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + $ hg branch pr1 + marked working directory as branch pr1 + (branches are permanent and global, did you want a bookmark?) + $ echo bar > bar + $ hg ci -Aqm "added bar" + $ hg push -r . --new-branch + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + +Lets get back to server and handle the merge + + $ cd ../server + $ hg glog + o 4:8933c09873b1 added bar + | (pr1) + | @ 3:a44c3a524808 added d + | | (default) + | o 2:8be98ac1a569 added c + |/ (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + +Merging default into user branch + +#if mergecommit + + $ hg mergecommit -r default --dest pr1 --message "merge commit for pr1" + new commit formed is d7aed1d69d65 + +#else + + $ hg up pr1 + 1 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg merge default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg ci -m "merge commit for pr1" + $ hg up default + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + +#endif + + $ hg glog + o 5:d7aed1d69d65 merge commit for pr1 + |\ (pr1) + | o 4:8933c09873b1 added bar + | | (pr1) + @ | 3:a44c3a524808 added d + | | (default) + o | 2:8be98ac1a569 added c + |/ (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + +Merging user branch into default (accepting the PR) + +#if mergecommit + + $ hg mergecommit -r 4 --dest default --message "merged pr1 into default" + new commit formed is 083ccc50a6c2 + +#else + + $ hg merge 4 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg ci -m "merged pr1 into default" + $ hg up 3 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + +#endif + + $ hg glog + o 6:083ccc50a6c2 merged pr1 into default + |\ (default) + +---o 5:d7aed1d69d65 merge commit for pr1 + | |/ (pr1) + | o 4:8933c09873b1 added bar + | | (pr1) + @ | 3:a44c3a524808 added d + | | (default) + o | 2:8be98ac1a569 added c + |/ (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + + +Creating a conflicting PR on a new client side + + $ cd .. + $ hg clone server client2 -r 3 + adding changesets + adding manifests + adding file changes + added 4 changesets with 4 changes to 4 files + new changesets f7ad41964313:a44c3a524808 + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client2 + $ hg glog + @ 3:a44c3a524808 added d + | (default) + o 2:8be98ac1a569 added c + | (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default) + + $ hg branch pr2 + marked working directory as branch pr2 + (branches are permanent and global, did you want a bookmark?) + $ echo foo > bar + $ hg ci -Aqm "added foo to bar" + $ hg push -r . --new-branch + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + +Go back on server and try to create a review by merging default into the branch +which will lead to conflicts + + $ cd ../server + +#if mergecommit + + $ hg mergecommit -r default --dest pr2 + merging bar + warning: conflicts while merging bar! (edit, then use 'hg resolve --mark') + list of unresolved files: bar + [1] + +#else + + $ hg update pr2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge default + merging bar + warning: conflicts while merging bar! (edit, then use 'hg resolve --mark') + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + [1] + $ hg resolve -l + U bar + $ hg merge --abort + aborting the merge, updating back to 9e82a702b48b + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg update 3 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved +#endif + + $ hg glog + o 7:9e82a702b48b added foo to bar + | (pr2) + | o 6:083ccc50a6c2 merged pr1 into default + |/| (default) + +---o 5:d7aed1d69d65 merge commit for pr1 + | |/ (pr1) + | o 4:8933c09873b1 added bar + | | (pr1) + @ | 3:a44c3a524808 added d + | | (default) + o | 2:8be98ac1a569 added c + |/ (default) + o 1:80e6d2c47cfe added b + | (default) + o 0:f7ad41964313 added a + (default)