Mercurial > hg
comparison hgext/extdiff.py @ 9512:e7bde4680eec
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.
author | Sune Foldager <cryo@cyanite.org> |
---|---|
date | Thu, 17 Sep 2009 21:12:32 +0200 |
parents | 4c041f1ee1b4 |
children | d932dc655881 |
comparison
equal
deleted
inserted
replaced
9511:33033af09308 | 9512:e7bde4680eec |
---|---|
40 files, so running the external diff program will actually be pretty | 40 files, so running the external diff program will actually be pretty |
41 fast (at least faster than having to compare the entire tree). | 41 fast (at least faster than having to compare the entire tree). |
42 ''' | 42 ''' |
43 | 43 |
44 from mercurial.i18n import _ | 44 from mercurial.i18n import _ |
45 from mercurial.node import short | 45 from mercurial.node import short, nullid |
46 from mercurial import cmdutil, util, commands | 46 from mercurial import cmdutil, util, commands |
47 import os, shlex, shutil, tempfile | 47 import os, shlex, shutil, tempfile, re |
48 | 48 |
49 def snapshot(ui, repo, files, node, tmproot): | 49 def snapshot(ui, repo, files, node, tmproot): |
50 '''snapshot files as of some revision | 50 '''snapshot files as of some revision |
51 if not using snapshot, -I/-X does not work and recursive diff | 51 if not using snapshot, -I/-X does not work and recursive diff |
52 in tools like kdiff3 and meld displays too many files.''' | 52 in tools like kdiff3 and meld displays too many files.''' |
67 fns_and_mtime = [] | 67 fns_and_mtime = [] |
68 ctx = repo[node] | 68 ctx = repo[node] |
69 for fn in files: | 69 for fn in files: |
70 wfn = util.pconvert(fn) | 70 wfn = util.pconvert(fn) |
71 if not wfn in ctx: | 71 if not wfn in ctx: |
72 # skipping new file after a merge ? | 72 # File doesn't exist; could be a bogus modify |
73 continue | 73 continue |
74 ui.note(' %s\n' % wfn) | 74 ui.note(' %s\n' % wfn) |
75 dest = os.path.join(base, wfn) | 75 dest = os.path.join(base, wfn) |
76 fctx = ctx[wfn] | 76 fctx = ctx[wfn] |
77 data = repo.wwritedata(wfn, fctx.data()) | 77 data = repo.wwritedata(wfn, fctx.data()) |
94 - just invoke the diff for a single file in the working dir | 94 - just invoke the diff for a single file in the working dir |
95 ''' | 95 ''' |
96 | 96 |
97 revs = opts.get('rev') | 97 revs = opts.get('rev') |
98 change = opts.get('change') | 98 change = opts.get('change') |
99 args = ' '.join(diffopts) | |
100 do3way = '$parent2' in args | |
99 | 101 |
100 if revs and change: | 102 if revs and change: |
101 msg = _('cannot specify --rev and --change at the same time') | 103 msg = _('cannot specify --rev and --change at the same time') |
102 raise util.Abort(msg) | 104 raise util.Abort(msg) |
103 elif change: | 105 elif change: |
104 node2 = repo.lookup(change) | 106 node2 = repo.lookup(change) |
105 node1 = repo[node2].parents()[0].node() | 107 node1a, node1b = repo.changelog.parents(node2) |
106 else: | 108 else: |
107 node1, node2 = cmdutil.revpair(repo, revs) | 109 node1a, node2 = cmdutil.revpair(repo, revs) |
110 if not revs: | |
111 node1b = repo.dirstate.parents()[1] | |
112 else: | |
113 node1b = nullid | |
114 | |
115 # Disable 3-way merge if there is only one parent | |
116 if do3way: | |
117 if node1b == nullid: | |
118 do3way = False | |
108 | 119 |
109 matcher = cmdutil.match(repo, pats, opts) | 120 matcher = cmdutil.match(repo, pats, opts) |
110 modified, added, removed = repo.status(node1, node2, matcher)[:3] | 121 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3]) |
111 if not (modified or added or removed): | 122 if do3way: |
112 return 0 | 123 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3]) |
124 else: | |
125 mod_b, add_b, rem_b = set(), set(), set() | |
126 modadd = mod_a | add_a | mod_b | add_b | |
127 common = modadd | rem_a | rem_b | |
128 if not common: | |
129 return 0 | |
113 | 130 |
114 tmproot = tempfile.mkdtemp(prefix='extdiff.') | 131 tmproot = tempfile.mkdtemp(prefix='extdiff.') |
115 dir2root = '' | |
116 try: | 132 try: |
117 # Always make a copy of node1 | 133 # Always make a copy of node1a (and node1b, if applicable) |
118 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0] | 134 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a) |
119 changes = len(modified) + len(removed) + len(added) | 135 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0] |
136 if do3way: | |
137 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b) | |
138 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0] | |
139 else: | |
140 dir1b = None | |
141 | |
142 fns_and_mtime = [] | |
120 | 143 |
121 # If node2 in not the wc or there is >1 change, copy it | 144 # If node2 in not the wc or there is >1 change, copy it |
122 if node2 or changes > 1: | 145 dir2root = '' |
123 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot) | 146 if node2: |
147 dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0] | |
148 elif len(common) > 1: | |
149 #we only actually need to get the files to copy back to the working | |
150 #dir in this case (because the other cases are: diffing 2 revisions | |
151 #or single file -- in which case the file is already directly passed | |
152 #to the diff tool). | |
153 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot) | |
124 else: | 154 else: |
125 # This lets the diff tool open the changed file directly | 155 # This lets the diff tool open the changed file directly |
126 dir2 = '' | 156 dir2 = '' |
127 dir2root = repo.root | 157 dir2root = repo.root |
128 fns_and_mtime = [] | |
129 | 158 |
130 # If only one change, diff the files instead of the directories | 159 # If only one change, diff the files instead of the directories |
131 if changes == 1 : | 160 # Handle bogus modifies correctly by checking if the files exist |
132 if len(modified): | 161 if len(common) == 1: |
133 dir1 = os.path.join(dir1, util.localpath(modified[0])) | 162 common_file = util.localpath(common.pop()) |
134 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0])) | 163 dir1a = os.path.join(dir1a, common_file) |
135 elif len(removed) : | 164 if not os.path.isfile(os.path.join(tmproot, dir1a)): |
136 dir1 = os.path.join(dir1, util.localpath(removed[0])) | 165 dir1a = os.devnull |
137 dir2 = os.devnull | 166 if do3way: |
138 else: | 167 dir1b = os.path.join(dir1b, common_file) |
139 dir1 = os.devnull | 168 if not os.path.isfile(os.path.join(tmproot, dir1b)): |
140 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0])) | 169 dir1b = os.devnull |
141 | 170 dir2 = os.path.join(dir2root, dir2, common_file) |
142 cmdline = ('%s %s %s %s' % | 171 |
143 (util.shellquote(diffcmd), ' '.join(diffopts), | 172 # Function to quote file/dir names in the argument string |
144 util.shellquote(dir1), util.shellquote(dir2))) | 173 # When not operating in 3-way mode, an empty string is returned for parent2 |
174 replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, child=dir2) | |
175 def quote(match): | |
176 key = match.group()[1:] | |
177 if not do3way and key == 'parent2': | |
178 return '' | |
179 return util.shellquote(replace[key]) | |
180 | |
181 # Match parent2 first, so 'parent1?' will match both parent1 and parent | |
182 regex = '\$(parent2|parent1?|child)' | |
183 if not do3way and not re.search(regex, args): | |
184 args += ' $parent1 $child' | |
185 args = re.sub(regex, quote, args) | |
186 cmdline = util.shellquote(diffcmd) + ' ' + args | |
187 | |
145 ui.debug('running %r in %s\n' % (cmdline, tmproot)) | 188 ui.debug('running %r in %s\n' % (cmdline, tmproot)) |
146 util.system(cmdline, cwd=tmproot) | 189 util.system(cmdline, cwd=tmproot) |
147 | 190 |
148 for copy_fn, working_fn, mtime in fns_and_mtime: | 191 for copy_fn, working_fn, mtime in fns_and_mtime: |
149 if os.path.getmtime(copy_fn) != mtime: | 192 if os.path.getmtime(copy_fn) != mtime: |