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