##// END OF EJS Templates
extdiff: avoid double backslashes in the displayed tool path on Windows...
Matt Harbison -
r40842:151aec64 default
parent child Browse files
Show More
@@ -1,435 +1,436 b''
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 If there is more than one file being compared and the "child" revision
17 17 is the working directory, any modifications made in the external diff
18 18 program will be copied back to the working directory from the temporary
19 19 directory.
20 20
21 21 The extdiff extension also allows you to configure new diff commands, so
22 22 you do not need to type :hg:`extdiff -p kdiff3` always. ::
23 23
24 24 [extdiff]
25 25 # add new command that runs GNU diff(1) in 'context diff' mode
26 26 cdiff = gdiff -Nprc5
27 27 ## or the old way:
28 28 #cmd.cdiff = gdiff
29 29 #opts.cdiff = -Nprc5
30 30
31 31 # add new command called meld, runs meld (no need to name twice). If
32 32 # the meld executable is not available, the meld tool in [merge-tools]
33 33 # will be used, if available
34 34 meld =
35 35
36 36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37 37 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
38 38 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
39 39 # your .vimrc
40 40 vimdiff = gvim -f "+next" \\
41 41 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
42 42
43 43 Tool arguments can include variables that are expanded at runtime::
44 44
45 45 $parent1, $plabel1 - filename, descriptive label of first parent
46 46 $child, $clabel - filename, descriptive label of child revision
47 47 $parent2, $plabel2 - filename, descriptive label of second parent
48 48 $root - repository root
49 49 $parent is an alias for $parent1.
50 50
51 51 The extdiff extension will look in your [diff-tools] and [merge-tools]
52 52 sections for diff tool arguments, when none are specified in [extdiff].
53 53
54 54 ::
55 55
56 56 [extdiff]
57 57 kdiff3 =
58 58
59 59 [diff-tools]
60 60 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
61 61
62 62 You can use -I/-X and list of file or directory names like normal
63 63 :hg:`diff` command. The extdiff extension makes snapshots of only
64 64 needed files, so running the external diff program will actually be
65 65 pretty fast (at least faster than having to compare the entire tree).
66 66 '''
67 67
68 68 from __future__ import absolute_import
69 69
70 70 import os
71 71 import re
72 72 import shutil
73 73 import stat
74 74
75 75 from mercurial.i18n import _
76 76 from mercurial.node import (
77 77 nullid,
78 78 short,
79 79 )
80 80 from mercurial import (
81 81 archival,
82 82 cmdutil,
83 83 error,
84 84 filemerge,
85 85 formatter,
86 86 pycompat,
87 87 registrar,
88 88 scmutil,
89 89 util,
90 90 )
91 91 from mercurial.utils import (
92 92 procutil,
93 93 stringutil,
94 94 )
95 95
96 96 cmdtable = {}
97 97 command = registrar.command(cmdtable)
98 98
99 99 configtable = {}
100 100 configitem = registrar.configitem(configtable)
101 101
102 102 configitem('extdiff', br'opts\..*',
103 103 default='',
104 104 generic=True,
105 105 )
106 106
107 107 configitem('diff-tools', br'.*\.diffargs$',
108 108 default=None,
109 109 generic=True,
110 110 )
111 111
112 112 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
113 113 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
114 114 # be specifying the version(s) of Mercurial they are tested with, or
115 115 # leave the attribute unspecified.
116 116 testedwith = 'ships-with-hg-core'
117 117
118 118 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
119 119 '''snapshot files as of some revision
120 120 if not using snapshot, -I/-X does not work and recursive diff
121 121 in tools like kdiff3 and meld displays too many files.'''
122 122 dirname = os.path.basename(repo.root)
123 123 if dirname == "":
124 124 dirname = "root"
125 125 if node is not None:
126 126 dirname = '%s.%s' % (dirname, short(node))
127 127 base = os.path.join(tmproot, dirname)
128 128 os.mkdir(base)
129 129 fnsandstat = []
130 130
131 131 if node is not None:
132 132 ui.note(_('making snapshot of %d files from rev %s\n') %
133 133 (len(files), short(node)))
134 134 else:
135 135 ui.note(_('making snapshot of %d files from working directory\n') %
136 136 (len(files)))
137 137
138 138 if files:
139 139 repo.ui.setconfig("ui", "archivemeta", False)
140 140
141 141 archival.archive(repo, base, node, 'files',
142 142 match=scmutil.matchfiles(repo, files),
143 143 subrepos=listsubrepos)
144 144
145 145 for fn in sorted(files):
146 146 wfn = util.pconvert(fn)
147 147 ui.note(' %s\n' % wfn)
148 148
149 149 if node is None:
150 150 dest = os.path.join(base, wfn)
151 151
152 152 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
153 153 return dirname, fnsandstat
154 154
155 155 def dodiff(ui, repo, cmdline, pats, opts):
156 156 '''Do the actual diff:
157 157
158 158 - copy to a temp structure if diffing 2 internal revisions
159 159 - copy to a temp structure if diffing working revision with
160 160 another one and more than 1 file is changed
161 161 - just invoke the diff for a single file in the working dir
162 162 '''
163 163
164 164 revs = opts.get('rev')
165 165 change = opts.get('change')
166 166 do3way = '$parent2' in cmdline
167 167
168 168 if revs and change:
169 169 msg = _('cannot specify --rev and --change at the same time')
170 170 raise error.Abort(msg)
171 171 elif change:
172 172 ctx2 = scmutil.revsingle(repo, change, None)
173 173 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
174 174 else:
175 175 ctx1a, ctx2 = scmutil.revpair(repo, revs)
176 176 if not revs:
177 177 ctx1b = repo[None].p2()
178 178 else:
179 179 ctx1b = repo[nullid]
180 180
181 181 node1a = ctx1a.node()
182 182 node1b = ctx1b.node()
183 183 node2 = ctx2.node()
184 184
185 185 # Disable 3-way merge if there is only one parent
186 186 if do3way:
187 187 if node1b == nullid:
188 188 do3way = False
189 189
190 190 subrepos=opts.get('subrepos')
191 191
192 192 matcher = scmutil.match(repo[node2], pats, opts)
193 193
194 194 if opts.get('patch'):
195 195 if subrepos:
196 196 raise error.Abort(_('--patch cannot be used with --subrepos'))
197 197 if node2 is None:
198 198 raise error.Abort(_('--patch requires two revisions'))
199 199 else:
200 200 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher,
201 201 listsubrepos=subrepos)[:3])
202 202 if do3way:
203 203 mod_b, add_b, rem_b = map(set,
204 204 repo.status(node1b, node2, matcher,
205 205 listsubrepos=subrepos)[:3])
206 206 else:
207 207 mod_b, add_b, rem_b = set(), set(), set()
208 208 modadd = mod_a | add_a | mod_b | add_b
209 209 common = modadd | rem_a | rem_b
210 210 if not common:
211 211 return 0
212 212
213 213 tmproot = pycompat.mkdtemp(prefix='extdiff.')
214 214 try:
215 215 if not opts.get('patch'):
216 216 # Always make a copy of node1a (and node1b, if applicable)
217 217 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
218 218 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot,
219 219 subrepos)[0]
220 220 rev1a = '@%d' % repo[node1a].rev()
221 221 if do3way:
222 222 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
223 223 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot,
224 224 subrepos)[0]
225 225 rev1b = '@%d' % repo[node1b].rev()
226 226 else:
227 227 dir1b = None
228 228 rev1b = ''
229 229
230 230 fnsandstat = []
231 231
232 232 # If node2 in not the wc or there is >1 change, copy it
233 233 dir2root = ''
234 234 rev2 = ''
235 235 if node2:
236 236 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
237 237 rev2 = '@%d' % repo[node2].rev()
238 238 elif len(common) > 1:
239 239 #we only actually need to get the files to copy back to
240 240 #the working dir in this case (because the other cases
241 241 #are: diffing 2 revisions or single file -- in which case
242 242 #the file is already directly passed to the diff tool).
243 243 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot,
244 244 subrepos)
245 245 else:
246 246 # This lets the diff tool open the changed file directly
247 247 dir2 = ''
248 248 dir2root = repo.root
249 249
250 250 label1a = rev1a
251 251 label1b = rev1b
252 252 label2 = rev2
253 253
254 254 # If only one change, diff the files instead of the directories
255 255 # Handle bogus modifies correctly by checking if the files exist
256 256 if len(common) == 1:
257 257 common_file = util.localpath(common.pop())
258 258 dir1a = os.path.join(tmproot, dir1a, common_file)
259 259 label1a = common_file + rev1a
260 260 if not os.path.isfile(dir1a):
261 261 dir1a = os.devnull
262 262 if do3way:
263 263 dir1b = os.path.join(tmproot, dir1b, common_file)
264 264 label1b = common_file + rev1b
265 265 if not os.path.isfile(dir1b):
266 266 dir1b = os.devnull
267 267 dir2 = os.path.join(dir2root, dir2, common_file)
268 268 label2 = common_file + rev2
269 269 else:
270 270 template = 'hg-%h.patch'
271 271 with formatter.nullformatter(ui, 'extdiff', {}) as fm:
272 272 cmdutil.export(repo, [repo[node1a].rev(), repo[node2].rev()],
273 273 fm,
274 274 fntemplate=repo.vfs.reljoin(tmproot, template),
275 275 match=matcher)
276 276 label1a = cmdutil.makefilename(repo[node1a], template)
277 277 label2 = cmdutil.makefilename(repo[node2], template)
278 278 dir1a = repo.vfs.reljoin(tmproot, label1a)
279 279 dir2 = repo.vfs.reljoin(tmproot, label2)
280 280 dir1b = None
281 281 label1b = None
282 282 fnsandstat = []
283 283
284 284 # Function to quote file/dir names in the argument string.
285 285 # When not operating in 3-way mode, an empty string is
286 286 # returned for parent2
287 287 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
288 288 'plabel1': label1a, 'plabel2': label1b,
289 289 'clabel': label2, 'child': dir2,
290 290 'root': repo.root}
291 291 def quote(match):
292 292 pre = match.group(2)
293 293 key = match.group(3)
294 294 if not do3way and key == 'parent2':
295 295 return pre
296 296 return pre + procutil.shellquote(replace[key])
297 297
298 298 # Match parent2 first, so 'parent1?' will match both parent1 and parent
299 299 regex = (br'''(['"]?)([^\s'"$]*)'''
300 300 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1')
301 301 if not do3way and not re.search(regex, cmdline):
302 302 cmdline += ' $parent1 $child'
303 303 cmdline = re.sub(regex, quote, cmdline)
304 304
305 305 ui.debug('running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
306 306 ui.system(cmdline, cwd=tmproot, blockedtag='extdiff')
307 307
308 308 for copy_fn, working_fn, st in fnsandstat:
309 309 cpstat = os.lstat(copy_fn)
310 310 # Some tools copy the file and attributes, so mtime may not detect
311 311 # all changes. A size check will detect more cases, but not all.
312 312 # The only certain way to detect every case is to diff all files,
313 313 # which could be expensive.
314 314 # copyfile() carries over the permission, so the mode check could
315 315 # be in an 'elif' branch, but for the case where the file has
316 316 # changed without affecting mtime or size.
317 317 if (cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
318 318 or cpstat.st_size != st.st_size
319 319 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)):
320 320 ui.debug('file changed while diffing. '
321 321 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
322 322 util.copyfile(copy_fn, working_fn)
323 323
324 324 return 1
325 325 finally:
326 326 ui.note(_('cleaning up temp directory\n'))
327 327 shutil.rmtree(tmproot)
328 328
329 329 extdiffopts = [
330 330 ('o', 'option', [],
331 331 _('pass option to comparison program'), _('OPT')),
332 332 ('r', 'rev', [], _('revision'), _('REV')),
333 333 ('c', 'change', '', _('change made by revision'), _('REV')),
334 334 ('', 'patch', None, _('compare patches for two revisions'))
335 335 ] + cmdutil.walkopts + cmdutil.subrepoopts
336 336
337 337 @command('extdiff',
338 338 [('p', 'program', '', _('comparison program to run'), _('CMD')),
339 339 ] + extdiffopts,
340 340 _('hg extdiff [OPT]... [FILE]...'),
341 341 helpcategory=command.CATEGORY_FILE_CONTENTS,
342 342 inferrepo=True)
343 343 def extdiff(ui, repo, *pats, **opts):
344 344 '''use external program to diff repository (or selected files)
345 345
346 346 Show differences between revisions for the specified files, using
347 347 an external program. The default program used is diff, with
348 348 default options "-Npru".
349 349
350 350 To select a different program, use the -p/--program option. The
351 351 program will be passed the names of two directories to compare. To
352 352 pass additional options to the program, use -o/--option. These
353 353 will be passed before the names of the directories to compare.
354 354
355 355 When two revision arguments are given, then changes are shown
356 356 between those revisions. If only one revision is specified then
357 357 that revision is compared to the working directory, and, when no
358 358 revisions are specified, the working directory files are compared
359 359 to its parent.'''
360 360 opts = pycompat.byteskwargs(opts)
361 361 program = opts.get('program')
362 362 option = opts.get('option')
363 363 if not program:
364 364 program = 'diff'
365 365 option = option or ['-Npru']
366 366 cmdline = ' '.join(map(procutil.shellquote, [program] + option))
367 367 return dodiff(ui, repo, cmdline, pats, opts)
368 368
369 369 class savedcmd(object):
370 370 """use external program to diff repository (or selected files)
371 371
372 372 Show differences between revisions for the specified files, using
373 373 the following program::
374 374
375 375 %(path)s
376 376
377 377 When two revision arguments are given, then changes are shown
378 378 between those revisions. If only one revision is specified then
379 379 that revision is compared to the working directory, and, when no
380 380 revisions are specified, the working directory files are compared
381 381 to its parent.
382 382 """
383 383
384 384 def __init__(self, path, cmdline):
385 385 # We can't pass non-ASCII through docstrings (and path is
386 # in an unknown encoding anyway)
387 docpath = stringutil.escapestr(path)
386 # in an unknown encoding anyway), but avoid double separators on
387 # Windows
388 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
388 389 self.__doc__ %= {r'path': pycompat.sysstr(stringutil.uirepr(docpath))}
389 390 self._cmdline = cmdline
390 391
391 392 def __call__(self, ui, repo, *pats, **opts):
392 393 opts = pycompat.byteskwargs(opts)
393 394 options = ' '.join(map(procutil.shellquote, opts['option']))
394 395 if options:
395 396 options = ' ' + options
396 397 return dodiff(ui, repo, self._cmdline + options, pats, opts)
397 398
398 399 def uisetup(ui):
399 400 for cmd, path in ui.configitems('extdiff'):
400 401 path = util.expandpath(path)
401 402 if cmd.startswith('cmd.'):
402 403 cmd = cmd[4:]
403 404 if not path:
404 405 path = procutil.findexe(cmd)
405 406 if path is None:
406 407 path = filemerge.findexternaltool(ui, cmd) or cmd
407 408 diffopts = ui.config('extdiff', 'opts.' + cmd)
408 409 cmdline = procutil.shellquote(path)
409 410 if diffopts:
410 411 cmdline += ' ' + diffopts
411 412 elif cmd.startswith('opts.'):
412 413 continue
413 414 else:
414 415 if path:
415 416 # case "cmd = path opts"
416 417 cmdline = path
417 418 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
418 419 else:
419 420 # case "cmd ="
420 421 path = procutil.findexe(cmd)
421 422 if path is None:
422 423 path = filemerge.findexternaltool(ui, cmd) or cmd
423 424 cmdline = procutil.shellquote(path)
424 425 diffopts = False
425 426 # look for diff arguments in [diff-tools] then [merge-tools]
426 427 if not diffopts:
427 428 args = ui.config('diff-tools', cmd+'.diffargs') or \
428 429 ui.config('merge-tools', cmd+'.diffargs')
429 430 if args:
430 431 cmdline += ' ' + args
431 432 command(cmd, extdiffopts[:], _('hg %s [OPTION]... [FILE]...') % cmd,
432 433 inferrepo=True)(savedcmd(path, cmdline))
433 434
434 435 # tell hggettext to extract docstrings from these functions:
435 436 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now