# HG changeset patch # User Sune Foldager # Date 1253214752 -7200 # Node ID e7bde4680eeccc95d3ad88363056d001fa2e039f # Parent 33033af093088d7c5a234a008dde4dd675d33dd4 extdiff: add 3-way diff for merge changesets This adds 3-way diff for merge changesets (using -c) and for diffing the working directory context against two parents. To enable it, use the new magic value '$parent2' in the argument line. In order to work, your differ must support that the second parent argument is left out; this will happen in 2-way mode. Default arguments are as before, without enabling 3-way mode, ensuring backwards compatibility. This also fixes a problem when diffing a merge changeset with a single file change. Extdiff would sometimes do the wrong thing in that situation. diff -r 33033af09308 -r e7bde4680eec hgext/extdiff.py --- a/hgext/extdiff.py Wed Sep 23 21:29:47 2009 -0500 +++ b/hgext/extdiff.py Thu Sep 17 21:12:32 2009 +0200 @@ -42,9 +42,9 @@ ''' from mercurial.i18n import _ -from mercurial.node import short +from mercurial.node import short, nullid from mercurial import cmdutil, util, commands -import os, shlex, shutil, tempfile +import os, shlex, shutil, tempfile, re def snapshot(ui, repo, files, node, tmproot): '''snapshot files as of some revision @@ -69,7 +69,7 @@ for fn in files: wfn = util.pconvert(fn) if not wfn in ctx: - # skipping new file after a merge ? + # File doesn't exist; could be a bogus modify continue ui.note(' %s\n' % wfn) dest = os.path.join(base, wfn) @@ -96,52 +96,95 @@ revs = opts.get('rev') change = opts.get('change') + args = ' '.join(diffopts) + do3way = '$parent2' in args if revs and change: msg = _('cannot specify --rev and --change at the same time') raise util.Abort(msg) elif change: node2 = repo.lookup(change) - node1 = repo[node2].parents()[0].node() + node1a, node1b = repo.changelog.parents(node2) else: - node1, node2 = cmdutil.revpair(repo, revs) + node1a, node2 = cmdutil.revpair(repo, revs) + if not revs: + node1b = repo.dirstate.parents()[1] + else: + node1b = nullid + + # Disable 3-way merge if there is only one parent + if do3way: + if node1b == nullid: + do3way = False matcher = cmdutil.match(repo, pats, opts) - modified, added, removed = repo.status(node1, node2, matcher)[:3] - if not (modified or added or removed): - return 0 + mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3]) + if do3way: + mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3]) + else: + mod_b, add_b, rem_b = set(), set(), set() + modadd = mod_a | add_a | mod_b | add_b + common = modadd | rem_a | rem_b + if not common: + return 0 tmproot = tempfile.mkdtemp(prefix='extdiff.') - dir2root = '' try: - # Always make a copy of node1 - dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0] - changes = len(modified) + len(removed) + len(added) + # Always make a copy of node1a (and node1b, if applicable) + dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a) + dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0] + if do3way: + dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b) + dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0] + else: + dir1b = None + + fns_and_mtime = [] # If node2 in not the wc or there is >1 change, copy it - if node2 or changes > 1: - dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot) + dir2root = '' + if node2: + dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0] + elif len(common) > 1: + #we only actually need to get the files to copy back to the working + #dir in this case (because the other cases are: diffing 2 revisions + #or single file -- in which case the file is already directly passed + #to the diff tool). + dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot) else: # This lets the diff tool open the changed file directly dir2 = '' dir2root = repo.root - fns_and_mtime = [] # If only one change, diff the files instead of the directories - if changes == 1 : - if len(modified): - dir1 = os.path.join(dir1, util.localpath(modified[0])) - dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0])) - elif len(removed) : - dir1 = os.path.join(dir1, util.localpath(removed[0])) - dir2 = os.devnull - else: - dir1 = os.devnull - dir2 = os.path.join(dir2root, dir2, util.localpath(added[0])) + # Handle bogus modifies correctly by checking if the files exist + if len(common) == 1: + common_file = util.localpath(common.pop()) + dir1a = os.path.join(dir1a, common_file) + if not os.path.isfile(os.path.join(tmproot, dir1a)): + dir1a = os.devnull + if do3way: + dir1b = os.path.join(dir1b, common_file) + if not os.path.isfile(os.path.join(tmproot, dir1b)): + dir1b = os.devnull + dir2 = os.path.join(dir2root, dir2, common_file) - cmdline = ('%s %s %s %s' % - (util.shellquote(diffcmd), ' '.join(diffopts), - util.shellquote(dir1), util.shellquote(dir2))) + # Function to quote file/dir names in the argument string + # When not operating in 3-way mode, an empty string is returned for parent2 + replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, child=dir2) + def quote(match): + key = match.group()[1:] + if not do3way and key == 'parent2': + return '' + return util.shellquote(replace[key]) + + # Match parent2 first, so 'parent1?' will match both parent1 and parent + regex = '\$(parent2|parent1?|child)' + if not do3way and not re.search(regex, args): + args += ' $parent1 $child' + args = re.sub(regex, quote, args) + cmdline = util.shellquote(diffcmd) + ' ' + args + ui.debug('running %r in %s\n' % (cmdline, tmproot)) util.system(cmdline, cwd=tmproot)