##// END OF EJS Templates
extdiff: refactor logic which does diff of patches...
Pulkit Goyal -
r45686:48c38018 default
parent child Browse files
Show More
@@ -1,719 +1,736 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 If a program has a graphical interface, it might be interesting to tell
63 63 Mercurial about it. It will prevent the program from being mistakenly
64 64 used in a terminal-only environment (such as an SSH terminal session),
65 65 and will make :hg:`extdiff --per-file` open multiple file diffs at once
66 66 instead of one by one (if you still want to open file diffs one by one,
67 67 you can use the --confirm option).
68 68
69 69 Declaring that a tool has a graphical interface can be done with the
70 70 ``gui`` flag next to where ``diffargs`` are specified:
71 71
72 72 ::
73 73
74 74 [diff-tools]
75 75 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
76 76 kdiff3.gui = true
77 77
78 78 You can use -I/-X and list of file or directory names like normal
79 79 :hg:`diff` command. The extdiff extension makes snapshots of only
80 80 needed files, so running the external diff program will actually be
81 81 pretty fast (at least faster than having to compare the entire tree).
82 82 '''
83 83
84 84 from __future__ import absolute_import
85 85
86 86 import os
87 87 import re
88 88 import shutil
89 89 import stat
90 90 import subprocess
91 91
92 92 from mercurial.i18n import _
93 93 from mercurial.node import (
94 94 nullid,
95 95 short,
96 96 )
97 97 from mercurial import (
98 98 archival,
99 99 cmdutil,
100 100 encoding,
101 101 error,
102 102 filemerge,
103 103 formatter,
104 104 pycompat,
105 105 registrar,
106 106 scmutil,
107 107 util,
108 108 )
109 109 from mercurial.utils import (
110 110 procutil,
111 111 stringutil,
112 112 )
113 113
114 114 cmdtable = {}
115 115 command = registrar.command(cmdtable)
116 116
117 117 configtable = {}
118 118 configitem = registrar.configitem(configtable)
119 119
120 120 configitem(
121 121 b'extdiff', br'opts\..*', default=b'', generic=True,
122 122 )
123 123
124 124 configitem(
125 125 b'extdiff', br'gui\..*', generic=True,
126 126 )
127 127
128 128 configitem(
129 129 b'diff-tools', br'.*\.diffargs$', default=None, generic=True,
130 130 )
131 131
132 132 configitem(
133 133 b'diff-tools', br'.*\.gui$', generic=True,
134 134 )
135 135
136 136 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
137 137 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
138 138 # be specifying the version(s) of Mercurial they are tested with, or
139 139 # leave the attribute unspecified.
140 140 testedwith = b'ships-with-hg-core'
141 141
142 142
143 143 def snapshot(ui, repo, files, node, tmproot, listsubrepos):
144 144 '''snapshot files as of some revision
145 145 if not using snapshot, -I/-X does not work and recursive diff
146 146 in tools like kdiff3 and meld displays too many files.'''
147 147 dirname = os.path.basename(repo.root)
148 148 if dirname == b"":
149 149 dirname = b"root"
150 150 if node is not None:
151 151 dirname = b'%s.%s' % (dirname, short(node))
152 152 base = os.path.join(tmproot, dirname)
153 153 os.mkdir(base)
154 154 fnsandstat = []
155 155
156 156 if node is not None:
157 157 ui.note(
158 158 _(b'making snapshot of %d files from rev %s\n')
159 159 % (len(files), short(node))
160 160 )
161 161 else:
162 162 ui.note(
163 163 _(b'making snapshot of %d files from working directory\n')
164 164 % (len(files))
165 165 )
166 166
167 167 if files:
168 168 repo.ui.setconfig(b"ui", b"archivemeta", False)
169 169
170 170 archival.archive(
171 171 repo,
172 172 base,
173 173 node,
174 174 b'files',
175 175 match=scmutil.matchfiles(repo, files),
176 176 subrepos=listsubrepos,
177 177 )
178 178
179 179 for fn in sorted(files):
180 180 wfn = util.pconvert(fn)
181 181 ui.note(b' %s\n' % wfn)
182 182
183 183 if node is None:
184 184 dest = os.path.join(base, wfn)
185 185
186 186 fnsandstat.append((dest, repo.wjoin(fn), os.lstat(dest)))
187 187 return dirname, fnsandstat
188 188
189 189
190 190 def formatcmdline(
191 191 cmdline,
192 192 repo_root,
193 193 do3way,
194 194 parent1,
195 195 plabel1,
196 196 parent2,
197 197 plabel2,
198 198 child,
199 199 clabel,
200 200 ):
201 201 # Function to quote file/dir names in the argument string.
202 202 # When not operating in 3-way mode, an empty string is
203 203 # returned for parent2
204 204 replace = {
205 205 b'parent': parent1,
206 206 b'parent1': parent1,
207 207 b'parent2': parent2,
208 208 b'plabel1': plabel1,
209 209 b'plabel2': plabel2,
210 210 b'child': child,
211 211 b'clabel': clabel,
212 212 b'root': repo_root,
213 213 }
214 214
215 215 def quote(match):
216 216 pre = match.group(2)
217 217 key = match.group(3)
218 218 if not do3way and key == b'parent2':
219 219 return pre
220 220 return pre + procutil.shellquote(replace[key])
221 221
222 222 # Match parent2 first, so 'parent1?' will match both parent1 and parent
223 223 regex = (
224 224 br'''(['"]?)([^\s'"$]*)'''
225 225 br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1'
226 226 )
227 227 if not do3way and not re.search(regex, cmdline):
228 228 cmdline += b' $parent1 $child'
229 229 return re.sub(regex, quote, cmdline)
230 230
231 231
232 232 def _systembackground(cmd, environ=None, cwd=None):
233 233 ''' like 'procutil.system', but returns the Popen object directly
234 234 so we don't have to wait on it.
235 235 '''
236 236 env = procutil.shellenviron(environ)
237 237 proc = subprocess.Popen(
238 238 procutil.tonativestr(cmd),
239 239 shell=True,
240 240 close_fds=procutil.closefds,
241 241 env=procutil.tonativeenv(env),
242 242 cwd=pycompat.rapply(procutil.tonativestr, cwd),
243 243 )
244 244 return proc
245 245
246 246
247 247 def _runperfilediff(
248 248 cmdline,
249 249 repo_root,
250 250 ui,
251 251 guitool,
252 252 do3way,
253 253 confirm,
254 254 commonfiles,
255 255 tmproot,
256 256 dir1a,
257 257 dir1b,
258 258 dir2root,
259 259 dir2,
260 260 rev1a,
261 261 rev1b,
262 262 rev2,
263 263 ):
264 264 # Note that we need to sort the list of files because it was
265 265 # built in an "unstable" way and it's annoying to get files in a
266 266 # random order, especially when "confirm" mode is enabled.
267 267 waitprocs = []
268 268 totalfiles = len(commonfiles)
269 269 for idx, commonfile in enumerate(sorted(commonfiles)):
270 270 path1a = os.path.join(tmproot, dir1a, commonfile)
271 271 label1a = commonfile + rev1a
272 272 if not os.path.isfile(path1a):
273 273 path1a = pycompat.osdevnull
274 274
275 275 path1b = b''
276 276 label1b = b''
277 277 if do3way:
278 278 path1b = os.path.join(tmproot, dir1b, commonfile)
279 279 label1b = commonfile + rev1b
280 280 if not os.path.isfile(path1b):
281 281 path1b = pycompat.osdevnull
282 282
283 283 path2 = os.path.join(dir2root, dir2, commonfile)
284 284 label2 = commonfile + rev2
285 285
286 286 if confirm:
287 287 # Prompt before showing this diff
288 288 difffiles = _(b'diff %s (%d of %d)') % (
289 289 commonfile,
290 290 idx + 1,
291 291 totalfiles,
292 292 )
293 293 responses = _(
294 294 b'[Yns?]'
295 295 b'$$ &Yes, show diff'
296 296 b'$$ &No, skip this diff'
297 297 b'$$ &Skip remaining diffs'
298 298 b'$$ &? (display help)'
299 299 )
300 300 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
301 301 if r == 3: # ?
302 302 while r == 3:
303 303 for c, t in ui.extractchoices(responses)[1]:
304 304 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
305 305 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
306 306 if r == 0: # yes
307 307 pass
308 308 elif r == 1: # no
309 309 continue
310 310 elif r == 2: # skip
311 311 break
312 312
313 313 curcmdline = formatcmdline(
314 314 cmdline,
315 315 repo_root,
316 316 do3way=do3way,
317 317 parent1=path1a,
318 318 plabel1=label1a,
319 319 parent2=path1b,
320 320 plabel2=label1b,
321 321 child=path2,
322 322 clabel=label2,
323 323 )
324 324
325 325 if confirm or not guitool:
326 326 # Run the comparison program and wait for it to exit
327 327 # before we show the next file.
328 328 # This is because either we need to wait for confirmation
329 329 # from the user between each invocation, or because, as far
330 330 # as we know, the tool doesn't have a GUI, in which case
331 331 # we can't run multiple CLI programs at the same time.
332 332 ui.debug(
333 333 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
334 334 )
335 335 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
336 336 else:
337 337 # Run the comparison program but don't wait, as we're
338 338 # going to rapid-fire each file diff and then wait on
339 339 # the whole group.
340 340 ui.debug(
341 341 b'running %r in %s (backgrounded)\n'
342 342 % (pycompat.bytestr(curcmdline), tmproot)
343 343 )
344 344 proc = _systembackground(curcmdline, cwd=tmproot)
345 345 waitprocs.append(proc)
346 346
347 347 if waitprocs:
348 348 with ui.timeblockedsection(b'extdiff'):
349 349 for proc in waitprocs:
350 350 proc.wait()
351 351
352 352
353 def diffpatch(ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way):
354 template = b'hg-%h.patch'
355 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
356 cmdutil.export(
357 repo,
358 [repo[node1a].rev(), repo[node2].rev()],
359 fm,
360 fntemplate=repo.vfs.reljoin(tmproot, template),
361 match=matcher,
362 )
363 label1a = cmdutil.makefilename(repo[node1a], template)
364 label2 = cmdutil.makefilename(repo[node2], template)
365 dir1a = repo.vfs.reljoin(tmproot, label1a)
366 dir2 = repo.vfs.reljoin(tmproot, label2)
367 dir1b = None
368 label1b = None
369 cmdline = formatcmdline(
370 cmdline,
371 repo.root,
372 do3way=do3way,
373 parent1=dir1a,
374 plabel1=label1a,
375 parent2=dir1b,
376 plabel2=label1b,
377 child=dir2,
378 clabel=label2,
379 )
380 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
381 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
382 return 1
383
384
353 385 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
354 386 '''Do the actual diff:
355 387
356 388 - copy to a temp structure if diffing 2 internal revisions
357 389 - copy to a temp structure if diffing working revision with
358 390 another one and more than 1 file is changed
359 391 - just invoke the diff for a single file in the working dir
360 392 '''
361 393
362 394 cmdutil.check_at_most_one_arg(opts, b'rev', b'change')
363 395 revs = opts.get(b'rev')
364 396 change = opts.get(b'change')
365 397 do3way = b'$parent2' in cmdline
366 398
367 399 if change:
368 400 ctx2 = scmutil.revsingle(repo, change, None)
369 401 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
370 402 else:
371 403 ctx1a, ctx2 = scmutil.revpair(repo, revs)
372 404 if not revs:
373 405 ctx1b = repo[None].p2()
374 406 else:
375 407 ctx1b = repo[nullid]
376 408
377 409 perfile = opts.get(b'per_file')
378 410 confirm = opts.get(b'confirm')
379 411
380 412 node1a = ctx1a.node()
381 413 node1b = ctx1b.node()
382 414 node2 = ctx2.node()
383 415
384 416 # Disable 3-way merge if there is only one parent
385 417 if do3way:
386 418 if node1b == nullid:
387 419 do3way = False
388 420
389 421 subrepos = opts.get(b'subrepos')
390 422
391 423 matcher = scmutil.match(repo[node2], pats, opts)
392 424
393 425 if opts.get(b'patch'):
394 426 if subrepos:
395 427 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
396 428 if perfile:
397 429 raise error.Abort(_(b'--patch cannot be used with --per-file'))
398 430 if node2 is None:
399 431 raise error.Abort(_(b'--patch requires two revisions'))
400 432 else:
401 433 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
402 434 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
403 435 if do3way:
404 436 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
405 437 mod_b, add_b, rem_b = (
406 438 set(stb.modified),
407 439 set(stb.added),
408 440 set(stb.removed),
409 441 )
410 442 else:
411 443 mod_b, add_b, rem_b = set(), set(), set()
412 444 modadd = mod_a | add_a | mod_b | add_b
413 445 common = modadd | rem_a | rem_b
414 446 if not common:
415 447 return 0
416 448
417 449 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
418 450 try:
419 if not opts.get(b'patch'):
420 # Always make a copy of node1a (and node1b, if applicable)
421 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
422 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[
451 if opts.get(b'patch'):
452 return diffpatch(
453 ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way
454 )
455
456 # Always make a copy of node1a (and node1b, if applicable)
457 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
458 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
459 rev1a = b'@%d' % repo[node1a].rev()
460 if do3way:
461 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
462 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[
423 463 0
424 464 ]
425 rev1a = b'@%d' % repo[node1a].rev()
426 if do3way:
427 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
428 dir1b = snapshot(
429 ui, repo, dir1b_files, node1b, tmproot, subrepos
430 )[0]
431 rev1b = b'@%d' % repo[node1b].rev()
432 else:
433 dir1b = None
434 rev1b = b''
435
436 fnsandstat = []
465 rev1b = b'@%d' % repo[node1b].rev()
466 else:
467 dir1b = None
468 rev1b = b''
437 469
438 # If node2 in not the wc or there is >1 change, copy it
439 dir2root = b''
440 rev2 = b''
441 if node2:
442 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
443 rev2 = b'@%d' % repo[node2].rev()
444 elif len(common) > 1:
445 # we only actually need to get the files to copy back to
446 # the working dir in this case (because the other cases
447 # are: diffing 2 revisions or single file -- in which case
448 # the file is already directly passed to the diff tool).
449 dir2, fnsandstat = snapshot(
450 ui, repo, modadd, None, tmproot, subrepos
451 )
452 else:
453 # This lets the diff tool open the changed file directly
454 dir2 = b''
455 dir2root = repo.root
470 fnsandstat = []
456 471
457 label1a = rev1a
458 label1b = rev1b
459 label2 = rev2
472 # If node2 in not the wc or there is >1 change, copy it
473 dir2root = b''
474 rev2 = b''
475 if node2:
476 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
477 rev2 = b'@%d' % repo[node2].rev()
478 elif len(common) > 1:
479 # we only actually need to get the files to copy back to
480 # the working dir in this case (because the other cases
481 # are: diffing 2 revisions or single file -- in which case
482 # the file is already directly passed to the diff tool).
483 dir2, fnsandstat = snapshot(
484 ui, repo, modadd, None, tmproot, subrepos
485 )
486 else:
487 # This lets the diff tool open the changed file directly
488 dir2 = b''
489 dir2root = repo.root
460 490
461 # If only one change, diff the files instead of the directories
462 # Handle bogus modifies correctly by checking if the files exist
463 if len(common) == 1:
464 common_file = util.localpath(common.pop())
465 dir1a = os.path.join(tmproot, dir1a, common_file)
466 label1a = common_file + rev1a
467 if not os.path.isfile(dir1a):
468 dir1a = pycompat.osdevnull
469 if do3way:
470 dir1b = os.path.join(tmproot, dir1b, common_file)
471 label1b = common_file + rev1b
472 if not os.path.isfile(dir1b):
473 dir1b = pycompat.osdevnull
474 dir2 = os.path.join(dir2root, dir2, common_file)
475 label2 = common_file + rev2
476 else:
477 template = b'hg-%h.patch'
478 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
479 cmdutil.export(
480 repo,
481 [repo[node1a].rev(), repo[node2].rev()],
482 fm,
483 fntemplate=repo.vfs.reljoin(tmproot, template),
484 match=matcher,
485 )
486 label1a = cmdutil.makefilename(repo[node1a], template)
487 label2 = cmdutil.makefilename(repo[node2], template)
488 dir1a = repo.vfs.reljoin(tmproot, label1a)
489 dir2 = repo.vfs.reljoin(tmproot, label2)
490 dir1b = None
491 label1b = None
492 fnsandstat = []
491 label1a = rev1a
492 label1b = rev1b
493 label2 = rev2
494
495 # If only one change, diff the files instead of the directories
496 # Handle bogus modifies correctly by checking if the files exist
497 if len(common) == 1:
498 common_file = util.localpath(common.pop())
499 dir1a = os.path.join(tmproot, dir1a, common_file)
500 label1a = common_file + rev1a
501 if not os.path.isfile(dir1a):
502 dir1a = pycompat.osdevnull
503 if do3way:
504 dir1b = os.path.join(tmproot, dir1b, common_file)
505 label1b = common_file + rev1b
506 if not os.path.isfile(dir1b):
507 dir1b = pycompat.osdevnull
508 dir2 = os.path.join(dir2root, dir2, common_file)
509 label2 = common_file + rev2
493 510
494 511 if not perfile:
495 512 # Run the external tool on the 2 temp directories or the patches
496 513 cmdline = formatcmdline(
497 514 cmdline,
498 515 repo.root,
499 516 do3way=do3way,
500 517 parent1=dir1a,
501 518 plabel1=label1a,
502 519 parent2=dir1b,
503 520 plabel2=label1b,
504 521 child=dir2,
505 522 clabel=label2,
506 523 )
507 524 ui.debug(
508 525 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
509 526 )
510 527 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
511 528 else:
512 529 # Run the external tool once for each pair of files
513 530 _runperfilediff(
514 531 cmdline,
515 532 repo.root,
516 533 ui,
517 534 guitool=guitool,
518 535 do3way=do3way,
519 536 confirm=confirm,
520 537 commonfiles=common,
521 538 tmproot=tmproot,
522 539 dir1a=dir1a,
523 540 dir1b=dir1b,
524 541 dir2root=dir2root,
525 542 dir2=dir2,
526 543 rev1a=rev1a,
527 544 rev1b=rev1b,
528 545 rev2=rev2,
529 546 )
530 547
531 548 for copy_fn, working_fn, st in fnsandstat:
532 549 cpstat = os.lstat(copy_fn)
533 550 # Some tools copy the file and attributes, so mtime may not detect
534 551 # all changes. A size check will detect more cases, but not all.
535 552 # The only certain way to detect every case is to diff all files,
536 553 # which could be expensive.
537 554 # copyfile() carries over the permission, so the mode check could
538 555 # be in an 'elif' branch, but for the case where the file has
539 556 # changed without affecting mtime or size.
540 557 if (
541 558 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
542 559 or cpstat.st_size != st.st_size
543 560 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
544 561 ):
545 562 ui.debug(
546 563 b'file changed while diffing. '
547 564 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
548 565 )
549 566 util.copyfile(copy_fn, working_fn)
550 567
551 568 return 1
552 569 finally:
553 570 ui.note(_(b'cleaning up temp directory\n'))
554 571 shutil.rmtree(tmproot)
555 572
556 573
557 574 extdiffopts = (
558 575 [
559 576 (
560 577 b'o',
561 578 b'option',
562 579 [],
563 580 _(b'pass option to comparison program'),
564 581 _(b'OPT'),
565 582 ),
566 583 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
567 584 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
568 585 (
569 586 b'',
570 587 b'per-file',
571 588 False,
572 589 _(b'compare each file instead of revision snapshots'),
573 590 ),
574 591 (
575 592 b'',
576 593 b'confirm',
577 594 False,
578 595 _(b'prompt user before each external program invocation'),
579 596 ),
580 597 (b'', b'patch', None, _(b'compare patches for two revisions')),
581 598 ]
582 599 + cmdutil.walkopts
583 600 + cmdutil.subrepoopts
584 601 )
585 602
586 603
587 604 @command(
588 605 b'extdiff',
589 606 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
590 607 + extdiffopts,
591 608 _(b'hg extdiff [OPT]... [FILE]...'),
592 609 helpcategory=command.CATEGORY_FILE_CONTENTS,
593 610 inferrepo=True,
594 611 )
595 612 def extdiff(ui, repo, *pats, **opts):
596 613 '''use external program to diff repository (or selected files)
597 614
598 615 Show differences between revisions for the specified files, using
599 616 an external program. The default program used is diff, with
600 617 default options "-Npru".
601 618
602 619 To select a different program, use the -p/--program option. The
603 620 program will be passed the names of two directories to compare,
604 621 unless the --per-file option is specified (see below). To pass
605 622 additional options to the program, use -o/--option. These will be
606 623 passed before the names of the directories or files to compare.
607 624
608 625 When two revision arguments are given, then changes are shown
609 626 between those revisions. If only one revision is specified then
610 627 that revision is compared to the working directory, and, when no
611 628 revisions are specified, the working directory files are compared
612 629 to its parent.
613 630
614 631 The --per-file option runs the external program repeatedly on each
615 632 file to diff, instead of once on two directories. By default,
616 633 this happens one by one, where the next file diff is open in the
617 634 external program only once the previous external program (for the
618 635 previous file diff) has exited. If the external program has a
619 636 graphical interface, it can open all the file diffs at once instead
620 637 of one by one. See :hg:`help -e extdiff` for information about how
621 638 to tell Mercurial that a given program has a graphical interface.
622 639
623 640 The --confirm option will prompt the user before each invocation of
624 641 the external program. It is ignored if --per-file isn't specified.
625 642 '''
626 643 opts = pycompat.byteskwargs(opts)
627 644 program = opts.get(b'program')
628 645 option = opts.get(b'option')
629 646 if not program:
630 647 program = b'diff'
631 648 option = option or [b'-Npru']
632 649 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
633 650 return dodiff(ui, repo, cmdline, pats, opts)
634 651
635 652
636 653 class savedcmd(object):
637 654 """use external program to diff repository (or selected files)
638 655
639 656 Show differences between revisions for the specified files, using
640 657 the following program::
641 658
642 659 %(path)s
643 660
644 661 When two revision arguments are given, then changes are shown
645 662 between those revisions. If only one revision is specified then
646 663 that revision is compared to the working directory, and, when no
647 664 revisions are specified, the working directory files are compared
648 665 to its parent.
649 666 """
650 667
651 668 def __init__(self, path, cmdline, isgui):
652 669 # We can't pass non-ASCII through docstrings (and path is
653 670 # in an unknown encoding anyway), but avoid double separators on
654 671 # Windows
655 672 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
656 673 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
657 674 self._cmdline = cmdline
658 675 self._isgui = isgui
659 676
660 677 def __call__(self, ui, repo, *pats, **opts):
661 678 opts = pycompat.byteskwargs(opts)
662 679 options = b' '.join(map(procutil.shellquote, opts[b'option']))
663 680 if options:
664 681 options = b' ' + options
665 682 return dodiff(
666 683 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
667 684 )
668 685
669 686
670 687 def uisetup(ui):
671 688 for cmd, path in ui.configitems(b'extdiff'):
672 689 path = util.expandpath(path)
673 690 if cmd.startswith(b'cmd.'):
674 691 cmd = cmd[4:]
675 692 if not path:
676 693 path = procutil.findexe(cmd)
677 694 if path is None:
678 695 path = filemerge.findexternaltool(ui, cmd) or cmd
679 696 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
680 697 cmdline = procutil.shellquote(path)
681 698 if diffopts:
682 699 cmdline += b' ' + diffopts
683 700 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
684 701 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
685 702 continue
686 703 else:
687 704 if path:
688 705 # case "cmd = path opts"
689 706 cmdline = path
690 707 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
691 708 else:
692 709 # case "cmd ="
693 710 path = procutil.findexe(cmd)
694 711 if path is None:
695 712 path = filemerge.findexternaltool(ui, cmd) or cmd
696 713 cmdline = procutil.shellquote(path)
697 714 diffopts = False
698 715 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
699 716 # look for diff arguments in [diff-tools] then [merge-tools]
700 717 if not diffopts:
701 718 key = cmd + b'.diffargs'
702 719 for section in (b'diff-tools', b'merge-tools'):
703 720 args = ui.config(section, key)
704 721 if args:
705 722 cmdline += b' ' + args
706 723 if isgui is None:
707 724 isgui = ui.configbool(section, cmd + b'.gui') or False
708 725 break
709 726 command(
710 727 cmd,
711 728 extdiffopts[:],
712 729 _(b'hg %s [OPTION]... [FILE]...') % cmd,
713 730 helpcategory=command.CATEGORY_FILE_CONTENTS,
714 731 inferrepo=True,
715 732 )(savedcmd(path, cmdline, isgui))
716 733
717 734
718 735 # tell hggettext to extract docstrings from these functions:
719 736 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now