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