##// END OF EJS Templates
extdiff: pass full paths of `dir1a` and `dir1b` to `_runperfilediff()`...
Pulkit Goyal -
r45957:e7c57354 default
parent child Browse files
Show More
@@ -1,763 +1,763 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 path1a = os.path.join(tmproot, dir1a, commonfile)
270 path1a = os.path.join(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 path1b = os.path.join(tmproot, dir1b, commonfile)
278 path1b = os.path.join(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 353 def diffpatch(ui, repo, node1, node2, tmproot, matcher, cmdline):
354 354 template = b'hg-%h.patch'
355 355 # write patches to temporary files
356 356 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
357 357 cmdutil.export(
358 358 repo,
359 359 [repo[node1].rev(), repo[node2].rev()],
360 360 fm,
361 361 fntemplate=repo.vfs.reljoin(tmproot, template),
362 362 match=matcher,
363 363 )
364 364 label1 = cmdutil.makefilename(repo[node1], template)
365 365 label2 = cmdutil.makefilename(repo[node2], template)
366 366 file1 = repo.vfs.reljoin(tmproot, label1)
367 367 file2 = repo.vfs.reljoin(tmproot, label2)
368 368 cmdline = formatcmdline(
369 369 cmdline,
370 370 repo.root,
371 371 # no 3way while comparing patches
372 372 do3way=False,
373 373 parent1=file1,
374 374 plabel1=label1,
375 375 # while comparing patches, there is no second parent
376 376 parent2=None,
377 377 plabel2=None,
378 378 child=file2,
379 379 clabel=label2,
380 380 )
381 381 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
382 382 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
383 383 return 1
384 384
385 385
386 386 def diffrevs(
387 387 ui,
388 388 repo,
389 389 node1a,
390 390 node1b,
391 391 node2,
392 392 matcher,
393 393 tmproot,
394 394 cmdline,
395 395 do3way,
396 396 guitool,
397 397 opts,
398 398 ):
399 399
400 400 subrepos = opts.get(b'subrepos')
401 401
402 402 # calculate list of files changed between both revs
403 403 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
404 404 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
405 405 if do3way:
406 406 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
407 407 mod_b, add_b, rem_b = (
408 408 set(stb.modified),
409 409 set(stb.added),
410 410 set(stb.removed),
411 411 )
412 412 else:
413 413 mod_b, add_b, rem_b = set(), set(), set()
414 414 modadd = mod_a | add_a | mod_b | add_b
415 415 common = modadd | rem_a | rem_b
416 416 if not common:
417 417 return 0
418 418
419 419 # Always make a copy of node1a (and node1b, if applicable)
420 420 # dir1a should contain files which are:
421 421 # * modified or removed from node1a to node2
422 422 # * modified or added from node1b to node2
423 423 # (except file added from node1a to node2 as they were not present in
424 424 # node1a)
425 425 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
426 426 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
427 427 rev1a = b'@%d' % repo[node1a].rev()
428 428 if do3way:
429 429 # file calculation criteria same as dir1a
430 430 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
431 431 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[0]
432 432 rev1b = b'@%d' % repo[node1b].rev()
433 433 else:
434 434 dir1b = None
435 435 rev1b = b''
436 436
437 437 fnsandstat = []
438 438
439 439 # If node2 in not the wc or there is >1 change, copy it
440 440 dir2root = b''
441 441 rev2 = b''
442 442 if node2:
443 443 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
444 444 rev2 = b'@%d' % repo[node2].rev()
445 445 elif len(common) > 1:
446 446 # we only actually need to get the files to copy back to
447 447 # the working dir in this case (because the other cases
448 448 # are: diffing 2 revisions or single file -- in which case
449 449 # the file is already directly passed to the diff tool).
450 450 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot, subrepos)
451 451 else:
452 452 # This lets the diff tool open the changed file directly
453 453 dir2 = b''
454 454 dir2root = repo.root
455 455
456 456 label1a = rev1a
457 457 label1b = rev1b
458 458 label2 = rev2
459 459
460 460 # If only one change, diff the files instead of the directories
461 461 # Handle bogus modifies correctly by checking if the files exist
462 462 if len(common) == 1:
463 463 common_file = util.localpath(common.pop())
464 464 dir1a = os.path.join(tmproot, dir1a, common_file)
465 465 label1a = common_file + rev1a
466 466 if not os.path.isfile(dir1a):
467 467 dir1a = pycompat.osdevnull
468 468 if do3way:
469 469 dir1b = os.path.join(tmproot, dir1b, common_file)
470 470 label1b = common_file + rev1b
471 471 if not os.path.isfile(dir1b):
472 472 dir1b = pycompat.osdevnull
473 473 dir2 = os.path.join(dir2root, dir2, common_file)
474 474 label2 = common_file + rev2
475 475
476 476 if not opts.get(b'per_file'):
477 477 # Run the external tool on the 2 temp directories or the patches
478 478 cmdline = formatcmdline(
479 479 cmdline,
480 480 repo.root,
481 481 do3way=do3way,
482 482 parent1=dir1a,
483 483 plabel1=label1a,
484 484 parent2=dir1b,
485 485 plabel2=label1b,
486 486 child=dir2,
487 487 clabel=label2,
488 488 )
489 489 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
490 490 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
491 491 else:
492 492 # Run the external tool once for each pair of files
493 493 _runperfilediff(
494 494 cmdline,
495 495 repo.root,
496 496 ui,
497 497 guitool=guitool,
498 498 do3way=do3way,
499 499 confirm=opts.get(b'confirm'),
500 500 commonfiles=common,
501 501 tmproot=tmproot,
502 dir1a=dir1a,
503 dir1b=dir1b,
502 dir1a=os.path.join(tmproot, dir1a),
503 dir1b=os.path.join(tmproot, dir1b) if do3way else None,
504 504 dir2root=dir2root,
505 505 dir2=dir2,
506 506 rev1a=rev1a,
507 507 rev1b=rev1b,
508 508 rev2=rev2,
509 509 )
510 510
511 511 for copy_fn, working_fn, st in fnsandstat:
512 512 cpstat = os.lstat(copy_fn)
513 513 # Some tools copy the file and attributes, so mtime may not detect
514 514 # all changes. A size check will detect more cases, but not all.
515 515 # The only certain way to detect every case is to diff all files,
516 516 # which could be expensive.
517 517 # copyfile() carries over the permission, so the mode check could
518 518 # be in an 'elif' branch, but for the case where the file has
519 519 # changed without affecting mtime or size.
520 520 if (
521 521 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
522 522 or cpstat.st_size != st.st_size
523 523 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
524 524 ):
525 525 ui.debug(
526 526 b'file changed while diffing. '
527 527 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
528 528 )
529 529 util.copyfile(copy_fn, working_fn)
530 530
531 531 return 1
532 532
533 533
534 534 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
535 535 '''Do the actual diff:
536 536
537 537 - copy to a temp structure if diffing 2 internal revisions
538 538 - copy to a temp structure if diffing working revision with
539 539 another one and more than 1 file is changed
540 540 - just invoke the diff for a single file in the working dir
541 541 '''
542 542
543 543 cmdutil.check_at_most_one_arg(opts, b'rev', b'change')
544 544 revs = opts.get(b'rev')
545 545 change = opts.get(b'change')
546 546 do3way = b'$parent2' in cmdline
547 547
548 548 if change:
549 549 ctx2 = scmutil.revsingle(repo, change, None)
550 550 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
551 551 else:
552 552 ctx1a, ctx2 = scmutil.revpair(repo, revs)
553 553 if not revs:
554 554 ctx1b = repo[None].p2()
555 555 else:
556 556 ctx1b = repo[nullid]
557 557
558 558 node1a = ctx1a.node()
559 559 node1b = ctx1b.node()
560 560 node2 = ctx2.node()
561 561
562 562 # Disable 3-way merge if there is only one parent
563 563 if do3way:
564 564 if node1b == nullid:
565 565 do3way = False
566 566
567 567 matcher = scmutil.match(repo[node2], pats, opts)
568 568
569 569 if opts.get(b'patch'):
570 570 if opts.get(b'subrepos'):
571 571 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
572 572 if opts.get(b'per_file'):
573 573 raise error.Abort(_(b'--patch cannot be used with --per-file'))
574 574 if node2 is None:
575 575 raise error.Abort(_(b'--patch requires two revisions'))
576 576
577 577 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
578 578 try:
579 579 if opts.get(b'patch'):
580 580 return diffpatch(ui, repo, node1a, node2, tmproot, matcher, cmdline)
581 581
582 582 return diffrevs(
583 583 ui,
584 584 repo,
585 585 node1a,
586 586 node1b,
587 587 node2,
588 588 matcher,
589 589 tmproot,
590 590 cmdline,
591 591 do3way,
592 592 guitool,
593 593 opts,
594 594 )
595 595
596 596 finally:
597 597 ui.note(_(b'cleaning up temp directory\n'))
598 598 shutil.rmtree(tmproot)
599 599
600 600
601 601 extdiffopts = (
602 602 [
603 603 (
604 604 b'o',
605 605 b'option',
606 606 [],
607 607 _(b'pass option to comparison program'),
608 608 _(b'OPT'),
609 609 ),
610 610 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
611 611 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
612 612 (
613 613 b'',
614 614 b'per-file',
615 615 False,
616 616 _(b'compare each file instead of revision snapshots'),
617 617 ),
618 618 (
619 619 b'',
620 620 b'confirm',
621 621 False,
622 622 _(b'prompt user before each external program invocation'),
623 623 ),
624 624 (b'', b'patch', None, _(b'compare patches for two revisions')),
625 625 ]
626 626 + cmdutil.walkopts
627 627 + cmdutil.subrepoopts
628 628 )
629 629
630 630
631 631 @command(
632 632 b'extdiff',
633 633 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
634 634 + extdiffopts,
635 635 _(b'hg extdiff [OPT]... [FILE]...'),
636 636 helpcategory=command.CATEGORY_FILE_CONTENTS,
637 637 inferrepo=True,
638 638 )
639 639 def extdiff(ui, repo, *pats, **opts):
640 640 '''use external program to diff repository (or selected files)
641 641
642 642 Show differences between revisions for the specified files, using
643 643 an external program. The default program used is diff, with
644 644 default options "-Npru".
645 645
646 646 To select a different program, use the -p/--program option. The
647 647 program will be passed the names of two directories to compare,
648 648 unless the --per-file option is specified (see below). To pass
649 649 additional options to the program, use -o/--option. These will be
650 650 passed before the names of the directories or files to compare.
651 651
652 652 When two revision arguments are given, then changes are shown
653 653 between those revisions. If only one revision is specified then
654 654 that revision is compared to the working directory, and, when no
655 655 revisions are specified, the working directory files are compared
656 656 to its parent.
657 657
658 658 The --per-file option runs the external program repeatedly on each
659 659 file to diff, instead of once on two directories. By default,
660 660 this happens one by one, where the next file diff is open in the
661 661 external program only once the previous external program (for the
662 662 previous file diff) has exited. If the external program has a
663 663 graphical interface, it can open all the file diffs at once instead
664 664 of one by one. See :hg:`help -e extdiff` for information about how
665 665 to tell Mercurial that a given program has a graphical interface.
666 666
667 667 The --confirm option will prompt the user before each invocation of
668 668 the external program. It is ignored if --per-file isn't specified.
669 669 '''
670 670 opts = pycompat.byteskwargs(opts)
671 671 program = opts.get(b'program')
672 672 option = opts.get(b'option')
673 673 if not program:
674 674 program = b'diff'
675 675 option = option or [b'-Npru']
676 676 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
677 677 return dodiff(ui, repo, cmdline, pats, opts)
678 678
679 679
680 680 class savedcmd(object):
681 681 """use external program to diff repository (or selected files)
682 682
683 683 Show differences between revisions for the specified files, using
684 684 the following program::
685 685
686 686 %(path)s
687 687
688 688 When two revision arguments are given, then changes are shown
689 689 between those revisions. If only one revision is specified then
690 690 that revision is compared to the working directory, and, when no
691 691 revisions are specified, the working directory files are compared
692 692 to its parent.
693 693 """
694 694
695 695 def __init__(self, path, cmdline, isgui):
696 696 # We can't pass non-ASCII through docstrings (and path is
697 697 # in an unknown encoding anyway), but avoid double separators on
698 698 # Windows
699 699 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
700 700 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
701 701 self._cmdline = cmdline
702 702 self._isgui = isgui
703 703
704 704 def __call__(self, ui, repo, *pats, **opts):
705 705 opts = pycompat.byteskwargs(opts)
706 706 options = b' '.join(map(procutil.shellquote, opts[b'option']))
707 707 if options:
708 708 options = b' ' + options
709 709 return dodiff(
710 710 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
711 711 )
712 712
713 713
714 714 def uisetup(ui):
715 715 for cmd, path in ui.configitems(b'extdiff'):
716 716 path = util.expandpath(path)
717 717 if cmd.startswith(b'cmd.'):
718 718 cmd = cmd[4:]
719 719 if not path:
720 720 path = procutil.findexe(cmd)
721 721 if path is None:
722 722 path = filemerge.findexternaltool(ui, cmd) or cmd
723 723 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
724 724 cmdline = procutil.shellquote(path)
725 725 if diffopts:
726 726 cmdline += b' ' + diffopts
727 727 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
728 728 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
729 729 continue
730 730 else:
731 731 if path:
732 732 # case "cmd = path opts"
733 733 cmdline = path
734 734 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
735 735 else:
736 736 # case "cmd ="
737 737 path = procutil.findexe(cmd)
738 738 if path is None:
739 739 path = filemerge.findexternaltool(ui, cmd) or cmd
740 740 cmdline = procutil.shellquote(path)
741 741 diffopts = False
742 742 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
743 743 # look for diff arguments in [diff-tools] then [merge-tools]
744 744 if not diffopts:
745 745 key = cmd + b'.diffargs'
746 746 for section in (b'diff-tools', b'merge-tools'):
747 747 args = ui.config(section, key)
748 748 if args:
749 749 cmdline += b' ' + args
750 750 if isgui is None:
751 751 isgui = ui.configbool(section, cmd + b'.gui') or False
752 752 break
753 753 command(
754 754 cmd,
755 755 extdiffopts[:],
756 756 _(b'hg %s [OPTION]... [FILE]...') % cmd,
757 757 helpcategory=command.CATEGORY_FILE_CONTENTS,
758 758 inferrepo=True,
759 759 )(savedcmd(path, cmdline, isgui))
760 760
761 761
762 762 # tell hggettext to extract docstrings from these functions:
763 763 i18nfunctions = [savedcmd]
General Comments 0
You need to be logged in to leave comments. Login now