##// END OF EJS Templates
extdiff: allow modifications in subrepos to be copied back...
Matt Harbison -
r25876:415709a4 stable
parent child Browse files
Show More
@@ -1,353 +1,349
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to allow external programs to compare revisions
9 9
10 10 The extdiff Mercurial extension allows you to use external programs
11 11 to compare revisions, or revision with working directory. The external
12 12 diff programs are called with a configurable set of options and two
13 13 non-option arguments: paths to directories containing snapshots of
14 14 files to compare.
15 15
16 16 The extdiff extension also allows you to configure new diff commands, so
17 17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
18 18
19 19 [extdiff]
20 20 # add new command that runs GNU diff(1) in 'context diff' mode
21 21 cdiff = gdiff -Nprc5
22 22 ## or the old way:
23 23 #cmd.cdiff = gdiff
24 24 #opts.cdiff = -Nprc5
25 25
26 26 # add new command called meld, runs meld (no need to name twice). If
27 27 # the meld executable is not available, the meld tool in [merge-tools]
28 28 # will be used, if available
29 29 meld =
30 30
31 31 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
32 32 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
33 33 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
34 34 # your .vimrc
35 35 vimdiff = gvim -f "+next" \\
36 36 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
37 37
38 38 Tool arguments can include variables that are expanded at runtime::
39 39
40 40 $parent1, $plabel1 - filename, descriptive label of first parent
41 41 $child, $clabel - filename, descriptive label of child revision
42 42 $parent2, $plabel2 - filename, descriptive label of second parent
43 43 $root - repository root
44 44 $parent is an alias for $parent1.
45 45
46 46 The extdiff extension will look in your [diff-tools] and [merge-tools]
47 47 sections for diff tool arguments, when none are specified in [extdiff].
48 48
49 49 ::
50 50
51 51 [extdiff]
52 52 kdiff3 =
53 53
54 54 [diff-tools]
55 55 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
56 56
57 57 You can use -I/-X and list of file or directory names like normal
58 58 :hg:`diff` command. The extdiff extension makes snapshots of only
59 59 needed files, so running the external diff program will actually be
60 60 pretty fast (at least faster than having to compare the entire tree).
61 61 '''
62 62
63 63 from mercurial.i18n import _
64 64 from mercurial.node import short, nullid
65 65 from mercurial import cmdutil, scmutil, util, commands, encoding, filemerge
66 66 from mercurial import archival
67 67 import os, shlex, shutil, tempfile, re
68 68
69 69 cmdtable = {}
70 70 command = cmdutil.command(cmdtable)
71 71 # Note for extension authors: ONLY specify testedwith = 'internal' for
72 72 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
73 73 # be specifying the version(s) of Mercurial they are tested with, or
74 74 # leave the attribute unspecified.
75 75 testedwith = 'internal'
76 76
77 77 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
78 78 '''snapshot files as of some revision
79 79 if not using snapshot, -I/-X does not work and recursive diff
80 80 in tools like kdiff3 and meld displays too many files.'''
81 81 dirname = os.path.basename(repo.root)
82 82 if dirname == "":
83 83 dirname = "root"
84 84 if node is not None:
85 85 dirname = '%s.%s' % (dirname, short(node))
86 86 base = os.path.join(tmproot, dirname)
87 87 os.mkdir(base)
88 88 fns_and_mtime = []
89 89
90 90 if node is not None:
91 91 ui.note(_('making snapshot of %d files from rev %s\n') %
92 92 (len(files), short(node)))
93 93 else:
94 94 ui.note(_('making snapshot of %d files from working directory\n') %
95 95 (len(files)))
96 96
97 97 if files:
98 98 repo.ui.setconfig("ui", "archivemeta", False)
99 99
100 100 archival.archive(repo, base, node, 'files',
101 101 matchfn=scmutil.matchfiles(repo, files),
102 102 subrepos=listsubrepos)
103 103
104 ctx = repo[node]
105 104 for fn in sorted(files):
106 105 wfn = util.pconvert(fn)
107 if wfn not in ctx:
108 # File doesn't exist; could be a bogus modify
109 continue
110 106 ui.note(' %s\n' % wfn)
111 107
112 108 if node is None:
113 109 dest = os.path.join(base, wfn)
114 110
115 111 fns_and_mtime.append((dest, repo.wjoin(fn),
116 112 os.lstat(dest).st_mtime))
117 113 return dirname, fns_and_mtime
118 114
119 115 def dodiff(ui, repo, cmdline, pats, opts):
120 116 '''Do the actual diff:
121 117
122 118 - copy to a temp structure if diffing 2 internal revisions
123 119 - copy to a temp structure if diffing working revision with
124 120 another one and more than 1 file is changed
125 121 - just invoke the diff for a single file in the working dir
126 122 '''
127 123
128 124 revs = opts.get('rev')
129 125 change = opts.get('change')
130 126 do3way = '$parent2' in cmdline
131 127
132 128 if revs and change:
133 129 msg = _('cannot specify --rev and --change at the same time')
134 130 raise util.Abort(msg)
135 131 elif change:
136 132 node2 = scmutil.revsingle(repo, change, None).node()
137 133 node1a, node1b = repo.changelog.parents(node2)
138 134 else:
139 135 node1a, node2 = scmutil.revpair(repo, revs)
140 136 if not revs:
141 137 node1b = repo.dirstate.p2()
142 138 else:
143 139 node1b = nullid
144 140
145 141 # Disable 3-way merge if there is only one parent
146 142 if do3way:
147 143 if node1b == nullid:
148 144 do3way = False
149 145
150 146 subrepos=opts.get('subrepos')
151 147
152 148 matcher = scmutil.match(repo[node2], pats, opts)
153 149 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
154 150 listsubrepos=subrepos)[:3])
155 151 if do3way:
156 152 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher,
157 153 listsubrepos=subrepos)[:3])
158 154 else:
159 155 mod_b, add_b, rem_b = set(), set(), set()
160 156 modadd = mod_a | add_a | mod_b | add_b
161 157 common = modadd | rem_a | rem_b
162 158 if not common:
163 159 return 0
164 160
165 161 tmproot = tempfile.mkdtemp(prefix='extdiff.')
166 162 try:
167 163 # Always make a copy of node1a (and node1b, if applicable)
168 164 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
169 165 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
170 166 rev1a = '@%d' % repo[node1a].rev()
171 167 if do3way:
172 168 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
173 169 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
174 170 subrepos)[0]
175 171 rev1b = '@%d' % repo[node1b].rev()
176 172 else:
177 173 dir1b = None
178 174 rev1b = ''
179 175
180 176 fns_and_mtime = []
181 177
182 178 # If node2 in not the wc or there is >1 change, copy it
183 179 dir2root = ''
184 180 rev2 = ''
185 181 if node2:
186 182 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
187 183 rev2 = '@%d' % repo[node2].rev()
188 184 elif len(common) > 1:
189 185 #we only actually need to get the files to copy back to
190 186 #the working dir in this case (because the other cases
191 187 #are: diffing 2 revisions or single file -- in which case
192 188 #the file is already directly passed to the diff tool).
193 189 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot,
194 190 subrepos)
195 191 else:
196 192 # This lets the diff tool open the changed file directly
197 193 dir2 = ''
198 194 dir2root = repo.root
199 195
200 196 label1a = rev1a
201 197 label1b = rev1b
202 198 label2 = rev2
203 199
204 200 # If only one change, diff the files instead of the directories
205 201 # Handle bogus modifies correctly by checking if the files exist
206 202 if len(common) == 1:
207 203 common_file = util.localpath(common.pop())
208 204 dir1a = os.path.join(tmproot, dir1a, common_file)
209 205 label1a = common_file + rev1a
210 206 if not os.path.isfile(dir1a):
211 207 dir1a = os.devnull
212 208 if do3way:
213 209 dir1b = os.path.join(tmproot, dir1b, common_file)
214 210 label1b = common_file + rev1b
215 211 if not os.path.isfile(dir1b):
216 212 dir1b = os.devnull
217 213 dir2 = os.path.join(dir2root, dir2, common_file)
218 214 label2 = common_file + rev2
219 215
220 216 # Function to quote file/dir names in the argument string.
221 217 # When not operating in 3-way mode, an empty string is
222 218 # returned for parent2
223 219 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
224 220 'plabel1': label1a, 'plabel2': label1b,
225 221 'clabel': label2, 'child': dir2,
226 222 'root': repo.root}
227 223 def quote(match):
228 224 pre = match.group(2)
229 225 key = match.group(3)
230 226 if not do3way and key == 'parent2':
231 227 return pre
232 228 return pre + util.shellquote(replace[key])
233 229
234 230 # Match parent2 first, so 'parent1?' will match both parent1 and parent
235 231 regex = (r'''(['"]?)([^\s'"$]*)'''
236 232 r'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
237 233 if not do3way and not re.search(regex, cmdline):
238 234 cmdline += ' $parent1 $child'
239 235 cmdline = re.sub(regex, quote, cmdline)
240 236
241 237 ui.debug('running %r in %s\n' % (cmdline, tmproot))
242 238 ui.system(cmdline, cwd=tmproot)
243 239
244 240 for copy_fn, working_fn, mtime in fns_and_mtime:
245 241 if os.lstat(copy_fn).st_mtime != mtime:
246 242 ui.debug('file changed while diffing. '
247 243 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
248 244 util.copyfile(copy_fn, working_fn)
249 245
250 246 return 1
251 247 finally:
252 248 ui.note(_('cleaning up temp directory\n'))
253 249 shutil.rmtree(tmproot)
254 250
255 251 @command('extdiff',
256 252 [('p', 'program', '',
257 253 _('comparison program to run'), _('CMD')),
258 254 ('o', 'option', [],
259 255 _('pass option to comparison program'), _('OPT')),
260 256 ('r', 'rev', [], _('revision'), _('REV')),
261 257 ('c', 'change', '', _('change made by revision'), _('REV')),
262 258 ] + commands.walkopts + commands.subrepoopts,
263 259 _('hg extdiff [OPT]... [FILE]...'),
264 260 inferrepo=True)
265 261 def extdiff(ui, repo, *pats, **opts):
266 262 '''use external program to diff repository (or selected files)
267 263
268 264 Show differences between revisions for the specified files, using
269 265 an external program. The default program used is diff, with
270 266 default options "-Npru".
271 267
272 268 To select a different program, use the -p/--program option. The
273 269 program will be passed the names of two directories to compare. To
274 270 pass additional options to the program, use -o/--option. These
275 271 will be passed before the names of the directories to compare.
276 272
277 273 When two revision arguments are given, then changes are shown
278 274 between those revisions. If only one revision is specified then
279 275 that revision is compared to the working directory, and, when no
280 276 revisions are specified, the working directory files are compared
281 277 to its parent.'''
282 278 program = opts.get('program')
283 279 option = opts.get('option')
284 280 if not program:
285 281 program = 'diff'
286 282 option = option or ['-Npru']
287 283 cmdline = ' '.join(map(util.shellquote, [program] + option))
288 284 return dodiff(ui, repo, cmdline, pats, opts)
289 285
290 286 def uisetup(ui):
291 287 for cmd, path in ui.configitems('extdiff'):
292 288 path = util.expandpath(path)
293 289 if cmd.startswith('cmd.'):
294 290 cmd = cmd[4:]
295 291 if not path:
296 292 path = util.findexe(cmd)
297 293 if path is None:
298 294 path = filemerge.findexternaltool(ui, cmd) or cmd
299 295 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
300 296 cmdline = util.shellquote(path)
301 297 if diffopts:
302 298 cmdline += ' ' + diffopts
303 299 elif cmd.startswith('opts.'):
304 300 continue
305 301 else:
306 302 if path:
307 303 # case "cmd = path opts"
308 304 cmdline = path
309 305 diffopts = len(shlex.split(cmdline)) > 1
310 306 else:
311 307 # case "cmd ="
312 308 path = util.findexe(cmd)
313 309 if path is None:
314 310 path = filemerge.findexternaltool(ui, cmd) or cmd
315 311 cmdline = util.shellquote(path)
316 312 diffopts = False
317 313 # look for diff arguments in [diff-tools] then [merge-tools]
318 314 if not diffopts:
319 315 args = ui.config('diff-tools', cmd+'.diffargs') or \
320 316 ui.config('merge-tools', cmd+'.diffargs')
321 317 if args:
322 318 cmdline += ' ' + args
323 319 def save(cmdline):
324 320 '''use closure to save diff command to use'''
325 321 def mydiff(ui, repo, *pats, **opts):
326 322 options = ' '.join(map(util.shellquote, opts['option']))
327 323 if options:
328 324 options = ' ' + options
329 325 return dodiff(ui, repo, cmdline + options, pats, opts)
330 326 doc = _('''\
331 327 use %(path)s to diff repository (or selected files)
332 328
333 329 Show differences between revisions for the specified files, using
334 330 the %(path)s program.
335 331
336 332 When two revision arguments are given, then changes are shown
337 333 between those revisions. If only one revision is specified then
338 334 that revision is compared to the working directory, and, when no
339 335 revisions are specified, the working directory files are compared
340 336 to its parent.\
341 337 ''') % {'path': util.uirepr(path)}
342 338
343 339 # We must translate the docstring right away since it is
344 340 # used as a format string. The string will unfortunately
345 341 # be translated again in commands.helpcmd and this will
346 342 # fail when the docstring contains non-ASCII characters.
347 343 # Decoding the string to a Unicode string here (using the
348 344 # right encoding) prevents that.
349 345 mydiff.__doc__ = doc.decode(encoding.encoding)
350 346 return mydiff
351 347 cmdtable[cmd] = (save(cmdline),
352 348 cmdtable['extdiff'][1][1:],
353 349 _('hg %s [OPTION]... [FILE]...') % cmd)
General Comments 0
You need to be logged in to leave comments. Login now