##// END OF EJS Templates
py3: make a pycompat.osdevnull, use it in extdiff...
Kyle Lippincott -
r44229:765a9c29 default
parent child Browse files
Show More
@@ -1,722 +1,722 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 cmd = procutil.quotecommand(cmd)
237 237 env = procutil.shellenviron(environ)
238 238 proc = subprocess.Popen(
239 239 procutil.tonativestr(cmd),
240 240 shell=True,
241 241 close_fds=procutil.closefds,
242 242 env=procutil.tonativeenv(env),
243 243 cwd=pycompat.rapply(procutil.tonativestr, cwd),
244 244 )
245 245 return proc
246 246
247 247
248 248 def _runperfilediff(
249 249 cmdline,
250 250 repo_root,
251 251 ui,
252 252 guitool,
253 253 do3way,
254 254 confirm,
255 255 commonfiles,
256 256 tmproot,
257 257 dir1a,
258 258 dir1b,
259 259 dir2root,
260 260 dir2,
261 261 rev1a,
262 262 rev1b,
263 263 rev2,
264 264 ):
265 265 # Note that we need to sort the list of files because it was
266 266 # built in an "unstable" way and it's annoying to get files in a
267 267 # random order, especially when "confirm" mode is enabled.
268 268 waitprocs = []
269 269 totalfiles = len(commonfiles)
270 270 for idx, commonfile in enumerate(sorted(commonfiles)):
271 271 path1a = os.path.join(tmproot, dir1a, commonfile)
272 272 label1a = commonfile + rev1a
273 273 if not os.path.isfile(path1a):
274 path1a = os.devnull
274 path1a = pycompat.osdevnull
275 275
276 276 path1b = b''
277 277 label1b = b''
278 278 if do3way:
279 279 path1b = os.path.join(tmproot, dir1b, commonfile)
280 280 label1b = commonfile + rev1b
281 281 if not os.path.isfile(path1b):
282 path1b = os.devnull
282 path1b = pycompat.osdevnull
283 283
284 284 path2 = os.path.join(dir2root, dir2, commonfile)
285 285 label2 = commonfile + rev2
286 286
287 287 if confirm:
288 288 # Prompt before showing this diff
289 289 difffiles = _(b'diff %s (%d of %d)') % (
290 290 commonfile,
291 291 idx + 1,
292 292 totalfiles,
293 293 )
294 294 responses = _(
295 295 b'[Yns?]'
296 296 b'$$ &Yes, show diff'
297 297 b'$$ &No, skip this diff'
298 298 b'$$ &Skip remaining diffs'
299 299 b'$$ &? (display help)'
300 300 )
301 301 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
302 302 if r == 3: # ?
303 303 while r == 3:
304 304 for c, t in ui.extractchoices(responses)[1]:
305 305 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
306 306 r = ui.promptchoice(b'%s %s' % (difffiles, responses))
307 307 if r == 0: # yes
308 308 pass
309 309 elif r == 1: # no
310 310 continue
311 311 elif r == 2: # skip
312 312 break
313 313
314 314 curcmdline = formatcmdline(
315 315 cmdline,
316 316 repo_root,
317 317 do3way=do3way,
318 318 parent1=path1a,
319 319 plabel1=label1a,
320 320 parent2=path1b,
321 321 plabel2=label1b,
322 322 child=path2,
323 323 clabel=label2,
324 324 )
325 325
326 326 if confirm or not guitool:
327 327 # Run the comparison program and wait for it to exit
328 328 # before we show the next file.
329 329 # This is because either we need to wait for confirmation
330 330 # from the user between each invocation, or because, as far
331 331 # as we know, the tool doesn't have a GUI, in which case
332 332 # we can't run multiple CLI programs at the same time.
333 333 ui.debug(
334 334 b'running %r in %s\n' % (pycompat.bytestr(curcmdline), tmproot)
335 335 )
336 336 ui.system(curcmdline, cwd=tmproot, blockedtag=b'extdiff')
337 337 else:
338 338 # Run the comparison program but don't wait, as we're
339 339 # going to rapid-fire each file diff and then wait on
340 340 # the whole group.
341 341 ui.debug(
342 342 b'running %r in %s (backgrounded)\n'
343 343 % (pycompat.bytestr(curcmdline), tmproot)
344 344 )
345 345 proc = _systembackground(curcmdline, cwd=tmproot)
346 346 waitprocs.append(proc)
347 347
348 348 if waitprocs:
349 349 with ui.timeblockedsection(b'extdiff'):
350 350 for proc in waitprocs:
351 351 proc.wait()
352 352
353 353
354 354 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
355 355 '''Do the actual diff:
356 356
357 357 - copy to a temp structure if diffing 2 internal revisions
358 358 - copy to a temp structure if diffing working revision with
359 359 another one and more than 1 file is changed
360 360 - just invoke the diff for a single file in the working dir
361 361 '''
362 362
363 363 revs = opts.get(b'rev')
364 364 change = opts.get(b'change')
365 365 do3way = b'$parent2' in cmdline
366 366
367 367 if revs and change:
368 368 msg = _(b'cannot specify --rev and --change at the same time')
369 369 raise error.Abort(msg)
370 370 elif change:
371 371 ctx2 = scmutil.revsingle(repo, change, None)
372 372 ctx1a, ctx1b = ctx2.p1(), ctx2.p2()
373 373 else:
374 374 ctx1a, ctx2 = scmutil.revpair(repo, revs)
375 375 if not revs:
376 376 ctx1b = repo[None].p2()
377 377 else:
378 378 ctx1b = repo[nullid]
379 379
380 380 perfile = opts.get(b'per_file')
381 381 confirm = opts.get(b'confirm')
382 382
383 383 node1a = ctx1a.node()
384 384 node1b = ctx1b.node()
385 385 node2 = ctx2.node()
386 386
387 387 # Disable 3-way merge if there is only one parent
388 388 if do3way:
389 389 if node1b == nullid:
390 390 do3way = False
391 391
392 392 subrepos = opts.get(b'subrepos')
393 393
394 394 matcher = scmutil.match(repo[node2], pats, opts)
395 395
396 396 if opts.get(b'patch'):
397 397 if subrepos:
398 398 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
399 399 if perfile:
400 400 raise error.Abort(_(b'--patch cannot be used with --per-file'))
401 401 if node2 is None:
402 402 raise error.Abort(_(b'--patch requires two revisions'))
403 403 else:
404 404 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
405 405 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
406 406 if do3way:
407 407 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
408 408 mod_b, add_b, rem_b = (
409 409 set(stb.modified),
410 410 set(stb.added),
411 411 set(stb.removed),
412 412 )
413 413 else:
414 414 mod_b, add_b, rem_b = set(), set(), set()
415 415 modadd = mod_a | add_a | mod_b | add_b
416 416 common = modadd | rem_a | rem_b
417 417 if not common:
418 418 return 0
419 419
420 420 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
421 421 try:
422 422 if not opts.get(b'patch'):
423 423 # Always make a copy of node1a (and node1b, if applicable)
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)[
426 426 0
427 427 ]
428 428 rev1a = b'@%d' % repo[node1a].rev()
429 429 if do3way:
430 430 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
431 431 dir1b = snapshot(
432 432 ui, repo, dir1b_files, node1b, tmproot, subrepos
433 433 )[0]
434 434 rev1b = b'@%d' % repo[node1b].rev()
435 435 else:
436 436 dir1b = None
437 437 rev1b = b''
438 438
439 439 fnsandstat = []
440 440
441 441 # If node2 in not the wc or there is >1 change, copy it
442 442 dir2root = b''
443 443 rev2 = b''
444 444 if node2:
445 445 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
446 446 rev2 = b'@%d' % repo[node2].rev()
447 447 elif len(common) > 1:
448 448 # we only actually need to get the files to copy back to
449 449 # the working dir in this case (because the other cases
450 450 # are: diffing 2 revisions or single file -- in which case
451 451 # the file is already directly passed to the diff tool).
452 452 dir2, fnsandstat = snapshot(
453 453 ui, repo, modadd, None, tmproot, subrepos
454 454 )
455 455 else:
456 456 # This lets the diff tool open the changed file directly
457 457 dir2 = b''
458 458 dir2root = repo.root
459 459
460 460 label1a = rev1a
461 461 label1b = rev1b
462 462 label2 = rev2
463 463
464 464 # If only one change, diff the files instead of the directories
465 465 # Handle bogus modifies correctly by checking if the files exist
466 466 if len(common) == 1:
467 467 common_file = util.localpath(common.pop())
468 468 dir1a = os.path.join(tmproot, dir1a, common_file)
469 469 label1a = common_file + rev1a
470 470 if not os.path.isfile(dir1a):
471 dir1a = os.devnull
471 dir1a = pycompat.osdevnull
472 472 if do3way:
473 473 dir1b = os.path.join(tmproot, dir1b, common_file)
474 474 label1b = common_file + rev1b
475 475 if not os.path.isfile(dir1b):
476 dir1b = os.devnull
476 dir1b = pycompat.osdevnull
477 477 dir2 = os.path.join(dir2root, dir2, common_file)
478 478 label2 = common_file + rev2
479 479 else:
480 480 template = b'hg-%h.patch'
481 481 with formatter.nullformatter(ui, b'extdiff', {}) as fm:
482 482 cmdutil.export(
483 483 repo,
484 484 [repo[node1a].rev(), repo[node2].rev()],
485 485 fm,
486 486 fntemplate=repo.vfs.reljoin(tmproot, template),
487 487 match=matcher,
488 488 )
489 489 label1a = cmdutil.makefilename(repo[node1a], template)
490 490 label2 = cmdutil.makefilename(repo[node2], template)
491 491 dir1a = repo.vfs.reljoin(tmproot, label1a)
492 492 dir2 = repo.vfs.reljoin(tmproot, label2)
493 493 dir1b = None
494 494 label1b = None
495 495 fnsandstat = []
496 496
497 497 if not perfile:
498 498 # Run the external tool on the 2 temp directories or the patches
499 499 cmdline = formatcmdline(
500 500 cmdline,
501 501 repo.root,
502 502 do3way=do3way,
503 503 parent1=dir1a,
504 504 plabel1=label1a,
505 505 parent2=dir1b,
506 506 plabel2=label1b,
507 507 child=dir2,
508 508 clabel=label2,
509 509 )
510 510 ui.debug(
511 511 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
512 512 )
513 513 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
514 514 else:
515 515 # Run the external tool once for each pair of files
516 516 _runperfilediff(
517 517 cmdline,
518 518 repo.root,
519 519 ui,
520 520 guitool=guitool,
521 521 do3way=do3way,
522 522 confirm=confirm,
523 523 commonfiles=common,
524 524 tmproot=tmproot,
525 525 dir1a=dir1a,
526 526 dir1b=dir1b,
527 527 dir2root=dir2root,
528 528 dir2=dir2,
529 529 rev1a=rev1a,
530 530 rev1b=rev1b,
531 531 rev2=rev2,
532 532 )
533 533
534 534 for copy_fn, working_fn, st in fnsandstat:
535 535 cpstat = os.lstat(copy_fn)
536 536 # Some tools copy the file and attributes, so mtime may not detect
537 537 # all changes. A size check will detect more cases, but not all.
538 538 # The only certain way to detect every case is to diff all files,
539 539 # which could be expensive.
540 540 # copyfile() carries over the permission, so the mode check could
541 541 # be in an 'elif' branch, but for the case where the file has
542 542 # changed without affecting mtime or size.
543 543 if (
544 544 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
545 545 or cpstat.st_size != st.st_size
546 546 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
547 547 ):
548 548 ui.debug(
549 549 b'file changed while diffing. '
550 550 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
551 551 )
552 552 util.copyfile(copy_fn, working_fn)
553 553
554 554 return 1
555 555 finally:
556 556 ui.note(_(b'cleaning up temp directory\n'))
557 557 shutil.rmtree(tmproot)
558 558
559 559
560 560 extdiffopts = (
561 561 [
562 562 (
563 563 b'o',
564 564 b'option',
565 565 [],
566 566 _(b'pass option to comparison program'),
567 567 _(b'OPT'),
568 568 ),
569 569 (b'r', b'rev', [], _(b'revision'), _(b'REV')),
570 570 (b'c', b'change', b'', _(b'change made by revision'), _(b'REV')),
571 571 (
572 572 b'',
573 573 b'per-file',
574 574 False,
575 575 _(b'compare each file instead of revision snapshots'),
576 576 ),
577 577 (
578 578 b'',
579 579 b'confirm',
580 580 False,
581 581 _(b'prompt user before each external program invocation'),
582 582 ),
583 583 (b'', b'patch', None, _(b'compare patches for two revisions')),
584 584 ]
585 585 + cmdutil.walkopts
586 586 + cmdutil.subrepoopts
587 587 )
588 588
589 589
590 590 @command(
591 591 b'extdiff',
592 592 [(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),]
593 593 + extdiffopts,
594 594 _(b'hg extdiff [OPT]... [FILE]...'),
595 595 helpcategory=command.CATEGORY_FILE_CONTENTS,
596 596 inferrepo=True,
597 597 )
598 598 def extdiff(ui, repo, *pats, **opts):
599 599 '''use external program to diff repository (or selected files)
600 600
601 601 Show differences between revisions for the specified files, using
602 602 an external program. The default program used is diff, with
603 603 default options "-Npru".
604 604
605 605 To select a different program, use the -p/--program option. The
606 606 program will be passed the names of two directories to compare,
607 607 unless the --per-file option is specified (see below). To pass
608 608 additional options to the program, use -o/--option. These will be
609 609 passed before the names of the directories or files to compare.
610 610
611 611 When two revision arguments are given, then changes are shown
612 612 between those revisions. If only one revision is specified then
613 613 that revision is compared to the working directory, and, when no
614 614 revisions are specified, the working directory files are compared
615 615 to its parent.
616 616
617 617 The --per-file option runs the external program repeatedly on each
618 618 file to diff, instead of once on two directories. By default,
619 619 this happens one by one, where the next file diff is open in the
620 620 external program only once the previous external program (for the
621 621 previous file diff) has exited. If the external program has a
622 622 graphical interface, it can open all the file diffs at once instead
623 623 of one by one. See :hg:`help -e extdiff` for information about how
624 624 to tell Mercurial that a given program has a graphical interface.
625 625
626 626 The --confirm option will prompt the user before each invocation of
627 627 the external program. It is ignored if --per-file isn't specified.
628 628 '''
629 629 opts = pycompat.byteskwargs(opts)
630 630 program = opts.get(b'program')
631 631 option = opts.get(b'option')
632 632 if not program:
633 633 program = b'diff'
634 634 option = option or [b'-Npru']
635 635 cmdline = b' '.join(map(procutil.shellquote, [program] + option))
636 636 return dodiff(ui, repo, cmdline, pats, opts)
637 637
638 638
639 639 class savedcmd(object):
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 the following program::
644 644
645 645 %(path)s
646 646
647 647 When two revision arguments are given, then changes are shown
648 648 between those revisions. If only one revision is specified then
649 649 that revision is compared to the working directory, and, when no
650 650 revisions are specified, the working directory files are compared
651 651 to its parent.
652 652 """
653 653
654 654 def __init__(self, path, cmdline, isgui):
655 655 # We can't pass non-ASCII through docstrings (and path is
656 656 # in an unknown encoding anyway), but avoid double separators on
657 657 # Windows
658 658 docpath = stringutil.escapestr(path).replace(b'\\\\', b'\\')
659 659 self.__doc__ %= {'path': pycompat.sysstr(stringutil.uirepr(docpath))}
660 660 self._cmdline = cmdline
661 661 self._isgui = isgui
662 662
663 663 def __call__(self, ui, repo, *pats, **opts):
664 664 opts = pycompat.byteskwargs(opts)
665 665 options = b' '.join(map(procutil.shellquote, opts[b'option']))
666 666 if options:
667 667 options = b' ' + options
668 668 return dodiff(
669 669 ui, repo, self._cmdline + options, pats, opts, guitool=self._isgui
670 670 )
671 671
672 672
673 673 def uisetup(ui):
674 674 for cmd, path in ui.configitems(b'extdiff'):
675 675 path = util.expandpath(path)
676 676 if cmd.startswith(b'cmd.'):
677 677 cmd = cmd[4:]
678 678 if not path:
679 679 path = procutil.findexe(cmd)
680 680 if path is None:
681 681 path = filemerge.findexternaltool(ui, cmd) or cmd
682 682 diffopts = ui.config(b'extdiff', b'opts.' + cmd)
683 683 cmdline = procutil.shellquote(path)
684 684 if diffopts:
685 685 cmdline += b' ' + diffopts
686 686 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
687 687 elif cmd.startswith(b'opts.') or cmd.startswith(b'gui.'):
688 688 continue
689 689 else:
690 690 if path:
691 691 # case "cmd = path opts"
692 692 cmdline = path
693 693 diffopts = len(pycompat.shlexsplit(cmdline)) > 1
694 694 else:
695 695 # case "cmd ="
696 696 path = procutil.findexe(cmd)
697 697 if path is None:
698 698 path = filemerge.findexternaltool(ui, cmd) or cmd
699 699 cmdline = procutil.shellquote(path)
700 700 diffopts = False
701 701 isgui = ui.configbool(b'extdiff', b'gui.' + cmd)
702 702 # look for diff arguments in [diff-tools] then [merge-tools]
703 703 if not diffopts:
704 704 key = cmd + b'.diffargs'
705 705 for section in (b'diff-tools', b'merge-tools'):
706 706 args = ui.config(section, key)
707 707 if args:
708 708 cmdline += b' ' + args
709 709 if isgui is None:
710 710 isgui = ui.configbool(section, cmd + b'.gui') or False
711 711 break
712 712 command(
713 713 cmd,
714 714 extdiffopts[:],
715 715 _(b'hg %s [OPTION]... [FILE]...') % cmd,
716 716 helpcategory=command.CATEGORY_FILE_CONTENTS,
717 717 inferrepo=True,
718 718 )(savedcmd(path, cmdline, isgui))
719 719
720 720
721 721 # tell hggettext to extract docstrings from these functions:
722 722 i18nfunctions = [savedcmd]
@@ -1,512 +1,514 b''
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import getopt
14 14 import inspect
15 15 import json
16 16 import os
17 17 import shlex
18 18 import sys
19 19 import tempfile
20 20
21 21 ispy3 = sys.version_info[0] >= 3
22 22 ispypy = '__pypy__' in sys.builtin_module_names
23 23 TYPE_CHECKING = False
24 24
25 25 if not globals(): # hide this from non-pytype users
26 26 import typing
27 27
28 28 TYPE_CHECKING = typing.TYPE_CHECKING
29 29
30 30 if not ispy3:
31 31 import cookielib
32 32 import cPickle as pickle
33 33 import httplib
34 34 import Queue as queue
35 35 import SocketServer as socketserver
36 36 import xmlrpclib
37 37
38 38 from .thirdparty.concurrent import futures
39 39
40 40 def future_set_exception_info(f, exc_info):
41 41 f.set_exception_info(*exc_info)
42 42
43 43
44 44 else:
45 45 import concurrent.futures as futures
46 46 import http.cookiejar as cookielib
47 47 import http.client as httplib
48 48 import pickle
49 49 import queue as queue
50 50 import socketserver
51 51 import xmlrpc.client as xmlrpclib
52 52
53 53 def future_set_exception_info(f, exc_info):
54 54 f.set_exception(exc_info[0])
55 55
56 56
57 57 def identity(a):
58 58 return a
59 59
60 60
61 61 def _rapply(f, xs):
62 62 if xs is None:
63 63 # assume None means non-value of optional data
64 64 return xs
65 65 if isinstance(xs, (list, set, tuple)):
66 66 return type(xs)(_rapply(f, x) for x in xs)
67 67 if isinstance(xs, dict):
68 68 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
69 69 return f(xs)
70 70
71 71
72 72 def rapply(f, xs):
73 73 """Apply function recursively to every item preserving the data structure
74 74
75 75 >>> def f(x):
76 76 ... return 'f(%s)' % x
77 77 >>> rapply(f, None) is None
78 78 True
79 79 >>> rapply(f, 'a')
80 80 'f(a)'
81 81 >>> rapply(f, {'a'}) == {'f(a)'}
82 82 True
83 83 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
84 84 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
85 85
86 86 >>> xs = [object()]
87 87 >>> rapply(identity, xs) is xs
88 88 True
89 89 """
90 90 if f is identity:
91 91 # fast path mainly for py2
92 92 return xs
93 93 return _rapply(f, xs)
94 94
95 95
96 96 if ispy3:
97 97 import builtins
98 98 import codecs
99 99 import functools
100 100 import io
101 101 import struct
102 102
103 103 if os.name == r'nt' and sys.version_info >= (3, 6):
104 104 # MBCS (or ANSI) filesystem encoding must be used as before.
105 105 # Otherwise non-ASCII filenames in existing repositories would be
106 106 # corrupted.
107 107 # This must be set once prior to any fsencode/fsdecode calls.
108 108 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
109 109
110 110 fsencode = os.fsencode
111 111 fsdecode = os.fsdecode
112 112 oscurdir = os.curdir.encode('ascii')
113 113 oslinesep = os.linesep.encode('ascii')
114 114 osname = os.name.encode('ascii')
115 115 ospathsep = os.pathsep.encode('ascii')
116 116 ospardir = os.pardir.encode('ascii')
117 117 ossep = os.sep.encode('ascii')
118 118 osaltsep = os.altsep
119 119 if osaltsep:
120 120 osaltsep = osaltsep.encode('ascii')
121 osdevnull = os.devnull.encode('ascii')
121 122
122 123 sysplatform = sys.platform.encode('ascii')
123 124 sysexecutable = sys.executable
124 125 if sysexecutable:
125 126 sysexecutable = os.fsencode(sysexecutable)
126 127 bytesio = io.BytesIO
127 128 # TODO deprecate stringio name, as it is a lie on Python 3.
128 129 stringio = bytesio
129 130
130 131 def maplist(*args):
131 132 return list(map(*args))
132 133
133 134 def rangelist(*args):
134 135 return list(range(*args))
135 136
136 137 def ziplist(*args):
137 138 return list(zip(*args))
138 139
139 140 rawinput = input
140 141 getargspec = inspect.getfullargspec
141 142
142 143 long = int
143 144
144 145 # TODO: .buffer might not exist if std streams were replaced; we'll need
145 146 # a silly wrapper to make a bytes stream backed by a unicode one.
146 147 stdin = sys.stdin.buffer
147 148 stdout = sys.stdout.buffer
148 149 stderr = sys.stderr.buffer
149 150
150 151 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
151 152 # we can use os.fsencode() to get back bytes argv.
152 153 #
153 154 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
154 155 #
155 156 # On Windows, the native argv is unicode and is converted to MBCS bytes
156 157 # since we do enable the legacy filesystem encoding.
157 158 if getattr(sys, 'argv', None) is not None:
158 159 sysargv = list(map(os.fsencode, sys.argv))
159 160
160 161 bytechr = struct.Struct('>B').pack
161 162 byterepr = b'%r'.__mod__
162 163
163 164 class bytestr(bytes):
164 165 """A bytes which mostly acts as a Python 2 str
165 166
166 167 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
167 168 ('', 'foo', 'ascii', '1')
168 169 >>> s = bytestr(b'foo')
169 170 >>> assert s is bytestr(s)
170 171
171 172 __bytes__() should be called if provided:
172 173
173 174 >>> class bytesable(object):
174 175 ... def __bytes__(self):
175 176 ... return b'bytes'
176 177 >>> bytestr(bytesable())
177 178 'bytes'
178 179
179 180 There's no implicit conversion from non-ascii str as its encoding is
180 181 unknown:
181 182
182 183 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
183 184 Traceback (most recent call last):
184 185 ...
185 186 UnicodeEncodeError: ...
186 187
187 188 Comparison between bytestr and bytes should work:
188 189
189 190 >>> assert bytestr(b'foo') == b'foo'
190 191 >>> assert b'foo' == bytestr(b'foo')
191 192 >>> assert b'f' in bytestr(b'foo')
192 193 >>> assert bytestr(b'f') in b'foo'
193 194
194 195 Sliced elements should be bytes, not integer:
195 196
196 197 >>> s[1], s[:2]
197 198 (b'o', b'fo')
198 199 >>> list(s), list(reversed(s))
199 200 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
200 201
201 202 As bytestr type isn't propagated across operations, you need to cast
202 203 bytes to bytestr explicitly:
203 204
204 205 >>> s = bytestr(b'foo').upper()
205 206 >>> t = bytestr(s)
206 207 >>> s[0], t[0]
207 208 (70, b'F')
208 209
209 210 Be careful to not pass a bytestr object to a function which expects
210 211 bytearray-like behavior.
211 212
212 213 >>> t = bytes(t) # cast to bytes
213 214 >>> assert type(t) is bytes
214 215 """
215 216
216 217 def __new__(cls, s=b''):
217 218 if isinstance(s, bytestr):
218 219 return s
219 220 if not isinstance(
220 221 s, (bytes, bytearray)
221 222 ) and not hasattr( # hasattr-py3-only
222 223 s, u'__bytes__'
223 224 ):
224 225 s = str(s).encode('ascii')
225 226 return bytes.__new__(cls, s)
226 227
227 228 def __getitem__(self, key):
228 229 s = bytes.__getitem__(self, key)
229 230 if not isinstance(s, bytes):
230 231 s = bytechr(s)
231 232 return s
232 233
233 234 def __iter__(self):
234 235 return iterbytestr(bytes.__iter__(self))
235 236
236 237 def __repr__(self):
237 238 return bytes.__repr__(self)[1:] # drop b''
238 239
239 240 def iterbytestr(s):
240 241 """Iterate bytes as if it were a str object of Python 2"""
241 242 return map(bytechr, s)
242 243
243 244 def maybebytestr(s):
244 245 """Promote bytes to bytestr"""
245 246 if isinstance(s, bytes):
246 247 return bytestr(s)
247 248 return s
248 249
249 250 def sysbytes(s):
250 251 """Convert an internal str (e.g. keyword, __doc__) back to bytes
251 252
252 253 This never raises UnicodeEncodeError, but only ASCII characters
253 254 can be round-trip by sysstr(sysbytes(s)).
254 255 """
255 256 return s.encode('utf-8')
256 257
257 258 def sysstr(s):
258 259 """Return a keyword str to be passed to Python functions such as
259 260 getattr() and str.encode()
260 261
261 262 This never raises UnicodeDecodeError. Non-ascii characters are
262 263 considered invalid and mapped to arbitrary but unique code points
263 264 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
264 265 """
265 266 if isinstance(s, builtins.str):
266 267 return s
267 268 return s.decode('latin-1')
268 269
269 270 def strurl(url):
270 271 """Converts a bytes url back to str"""
271 272 if isinstance(url, bytes):
272 273 return url.decode('ascii')
273 274 return url
274 275
275 276 def bytesurl(url):
276 277 """Converts a str url to bytes by encoding in ascii"""
277 278 if isinstance(url, str):
278 279 return url.encode('ascii')
279 280 return url
280 281
281 282 def raisewithtb(exc, tb):
282 283 """Raise exception with the given traceback"""
283 284 raise exc.with_traceback(tb)
284 285
285 286 def getdoc(obj):
286 287 """Get docstring as bytes; may be None so gettext() won't confuse it
287 288 with _('')"""
288 289 doc = getattr(obj, '__doc__', None)
289 290 if doc is None:
290 291 return doc
291 292 return sysbytes(doc)
292 293
293 294 def _wrapattrfunc(f):
294 295 @functools.wraps(f)
295 296 def w(object, name, *args):
296 297 return f(object, sysstr(name), *args)
297 298
298 299 return w
299 300
300 301 # these wrappers are automagically imported by hgloader
301 302 delattr = _wrapattrfunc(builtins.delattr)
302 303 getattr = _wrapattrfunc(builtins.getattr)
303 304 hasattr = _wrapattrfunc(builtins.hasattr)
304 305 setattr = _wrapattrfunc(builtins.setattr)
305 306 xrange = builtins.range
306 307 unicode = str
307 308
308 309 def open(name, mode=b'r', buffering=-1, encoding=None):
309 310 return builtins.open(name, sysstr(mode), buffering, encoding)
310 311
311 312 safehasattr = _wrapattrfunc(builtins.hasattr)
312 313
313 314 def _getoptbwrapper(orig, args, shortlist, namelist):
314 315 """
315 316 Takes bytes arguments, converts them to unicode, pass them to
316 317 getopt.getopt(), convert the returned values back to bytes and then
317 318 return them for Python 3 compatibility as getopt.getopt() don't accepts
318 319 bytes on Python 3.
319 320 """
320 321 args = [a.decode('latin-1') for a in args]
321 322 shortlist = shortlist.decode('latin-1')
322 323 namelist = [a.decode('latin-1') for a in namelist]
323 324 opts, args = orig(args, shortlist, namelist)
324 325 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
325 326 args = [a.encode('latin-1') for a in args]
326 327 return opts, args
327 328
328 329 def strkwargs(dic):
329 330 """
330 331 Converts the keys of a python dictonary to str i.e. unicodes so that
331 332 they can be passed as keyword arguments as dictonaries with bytes keys
332 333 can't be passed as keyword arguments to functions on Python 3.
333 334 """
334 335 dic = dict((k.decode('latin-1'), v) for k, v in dic.items())
335 336 return dic
336 337
337 338 def byteskwargs(dic):
338 339 """
339 340 Converts keys of python dictonaries to bytes as they were converted to
340 341 str to pass that dictonary as a keyword argument on Python 3.
341 342 """
342 343 dic = dict((k.encode('latin-1'), v) for k, v in dic.items())
343 344 return dic
344 345
345 346 # TODO: handle shlex.shlex().
346 347 def shlexsplit(s, comments=False, posix=True):
347 348 """
348 349 Takes bytes argument, convert it to str i.e. unicodes, pass that into
349 350 shlex.split(), convert the returned value to bytes and return that for
350 351 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
351 352 """
352 353 ret = shlex.split(s.decode('latin-1'), comments, posix)
353 354 return [a.encode('latin-1') for a in ret]
354 355
355 356 iteritems = lambda x: x.items()
356 357 itervalues = lambda x: x.values()
357 358
358 359 # Python 3.5's json.load and json.loads require str. We polyfill its
359 360 # code for detecting encoding from bytes.
360 361 if sys.version_info[0:2] < (3, 6):
361 362
362 363 def _detect_encoding(b):
363 364 bstartswith = b.startswith
364 365 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
365 366 return 'utf-32'
366 367 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
367 368 return 'utf-16'
368 369 if bstartswith(codecs.BOM_UTF8):
369 370 return 'utf-8-sig'
370 371
371 372 if len(b) >= 4:
372 373 if not b[0]:
373 374 # 00 00 -- -- - utf-32-be
374 375 # 00 XX -- -- - utf-16-be
375 376 return 'utf-16-be' if b[1] else 'utf-32-be'
376 377 if not b[1]:
377 378 # XX 00 00 00 - utf-32-le
378 379 # XX 00 00 XX - utf-16-le
379 380 # XX 00 XX -- - utf-16-le
380 381 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
381 382 elif len(b) == 2:
382 383 if not b[0]:
383 384 # 00 XX - utf-16-be
384 385 return 'utf-16-be'
385 386 if not b[1]:
386 387 # XX 00 - utf-16-le
387 388 return 'utf-16-le'
388 389 # default
389 390 return 'utf-8'
390 391
391 392 def json_loads(s, *args, **kwargs):
392 393 if isinstance(s, (bytes, bytearray)):
393 394 s = s.decode(_detect_encoding(s), 'surrogatepass')
394 395
395 396 return json.loads(s, *args, **kwargs)
396 397
397 398 else:
398 399 json_loads = json.loads
399 400
400 401 else:
401 402 import cStringIO
402 403
403 404 xrange = xrange
404 405 unicode = unicode
405 406 bytechr = chr
406 407 byterepr = repr
407 408 bytestr = str
408 409 iterbytestr = iter
409 410 maybebytestr = identity
410 411 sysbytes = identity
411 412 sysstr = identity
412 413 strurl = identity
413 414 bytesurl = identity
414 415 open = open
415 416 delattr = delattr
416 417 getattr = getattr
417 418 hasattr = hasattr
418 419 setattr = setattr
419 420
420 421 # this can't be parsed on Python 3
421 422 exec(b'def raisewithtb(exc, tb):\n raise exc, None, tb\n')
422 423
423 424 def fsencode(filename):
424 425 """
425 426 Partial backport from os.py in Python 3, which only accepts bytes.
426 427 In Python 2, our paths should only ever be bytes, a unicode path
427 428 indicates a bug.
428 429 """
429 430 if isinstance(filename, str):
430 431 return filename
431 432 else:
432 433 raise TypeError("expect str, not %s" % type(filename).__name__)
433 434
434 435 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
435 436 # better not to touch Python 2 part as it's already working fine.
436 437 fsdecode = identity
437 438
438 439 def getdoc(obj):
439 440 return getattr(obj, '__doc__', None)
440 441
441 442 _notset = object()
442 443
443 444 def safehasattr(thing, attr):
444 445 return getattr(thing, attr, _notset) is not _notset
445 446
446 447 def _getoptbwrapper(orig, args, shortlist, namelist):
447 448 return orig(args, shortlist, namelist)
448 449
449 450 strkwargs = identity
450 451 byteskwargs = identity
451 452
452 453 oscurdir = os.curdir
453 454 oslinesep = os.linesep
454 455 osname = os.name
455 456 ospathsep = os.pathsep
456 457 ospardir = os.pardir
457 458 ossep = os.sep
458 459 osaltsep = os.altsep
460 osdevnull = os.devnull
459 461 long = long
460 462 stdin = sys.stdin
461 463 stdout = sys.stdout
462 464 stderr = sys.stderr
463 465 if getattr(sys, 'argv', None) is not None:
464 466 sysargv = sys.argv
465 467 sysplatform = sys.platform
466 468 sysexecutable = sys.executable
467 469 shlexsplit = shlex.split
468 470 bytesio = cStringIO.StringIO
469 471 stringio = bytesio
470 472 maplist = map
471 473 rangelist = range
472 474 ziplist = zip
473 475 rawinput = raw_input
474 476 getargspec = inspect.getargspec
475 477 iteritems = lambda x: x.iteritems()
476 478 itervalues = lambda x: x.itervalues()
477 479 json_loads = json.loads
478 480
479 481 isjython = sysplatform.startswith(b'java')
480 482
481 483 isdarwin = sysplatform.startswith(b'darwin')
482 484 islinux = sysplatform.startswith(b'linux')
483 485 isposix = osname == b'posix'
484 486 iswindows = osname == b'nt'
485 487
486 488
487 489 def getoptb(args, shortlist, namelist):
488 490 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
489 491
490 492
491 493 def gnugetoptb(args, shortlist, namelist):
492 494 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
493 495
494 496
495 497 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
496 498 return tempfile.mkdtemp(suffix, prefix, dir)
497 499
498 500
499 501 # text=True is not supported; use util.from/tonativeeol() instead
500 502 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
501 503 return tempfile.mkstemp(suffix, prefix, dir)
502 504
503 505
504 506 # mode must include 'b'ytes as encoding= is not supported
505 507 def namedtempfile(
506 508 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
507 509 ):
508 510 mode = sysstr(mode)
509 511 assert 'b' in mode
510 512 return tempfile.NamedTemporaryFile(
511 513 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
512 514 )
@@ -1,517 +1,541 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "extdiff=" >> $HGRCPATH
3 3
4 4 $ hg init a
5 5 $ cd a
6 6 $ echo a > a
7 7 $ echo b > b
8 8 $ hg add
9 9 adding a
10 10 adding b
11 11
12 12 Should diff cloned directories:
13 13
14 14 $ hg extdiff -o -r $opt
15 15 Only in a: a
16 16 Only in a: b
17 17 [1]
18 18
19 19 $ cat <<EOF >> $HGRCPATH
20 20 > [extdiff]
21 21 > cmd.falabala = echo
22 22 > opts.falabala = diffing
23 23 > cmd.edspace = echo
24 24 > opts.edspace = "name <user@example.com>"
25 25 > alabalaf =
26 26 > [merge-tools]
27 27 > alabalaf.executable = echo
28 28 > alabalaf.diffargs = diffing
29 29 > EOF
30 30
31 31 $ hg falabala
32 32 diffing a.000000000000 a
33 33 [1]
34 34
35 35 $ hg help falabala
36 36 hg falabala [OPTION]... [FILE]...
37 37
38 38 use external program to diff repository (or selected files)
39 39
40 40 Show differences between revisions for the specified files, using the
41 41 following program:
42 42
43 43 'echo'
44 44
45 45 When two revision arguments are given, then changes are shown between
46 46 those revisions. If only one revision is specified then that revision is
47 47 compared to the working directory, and, when no revisions are specified,
48 48 the working directory files are compared to its parent.
49 49
50 50 options ([+] can be repeated):
51 51
52 52 -o --option OPT [+] pass option to comparison program
53 53 -r --rev REV [+] revision
54 54 -c --change REV change made by revision
55 55 --per-file compare each file instead of revision snapshots
56 56 --confirm prompt user before each external program invocation
57 57 --patch compare patches for two revisions
58 58 -I --include PATTERN [+] include names matching the given patterns
59 59 -X --exclude PATTERN [+] exclude names matching the given patterns
60 60 -S --subrepos recurse into subrepositories
61 61
62 62 (some details hidden, use --verbose to show complete help)
63 63
64 64 $ hg ci -d '0 0' -mtest1
65 65
66 66 $ echo b >> a
67 67 $ hg ci -d '1 0' -mtest2
68 68
69 69 Should diff cloned files directly:
70 70
71 71 $ hg falabala -r 0:1
72 72 diffing "*\\extdiff.*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
73 73 diffing */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
74 74 [1]
75 75
76 76 Specifying an empty revision should abort.
77 77
78 78 $ hg extdiff -p diff --patch --rev 'ancestor()' --rev 1
79 79 abort: empty revision on one side of range
80 80 [255]
81 81
82 82 Test diff during merge:
83 83
84 84 $ hg update -C 0
85 85 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 86 $ echo c >> c
87 87 $ hg add c
88 88 $ hg ci -m "new branch" -d '1 0'
89 89 created new head
90 90 $ hg merge 1
91 91 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92 (branch merge, don't forget to commit)
93 93
94 94 Should diff cloned file against wc file:
95 95
96 96 $ hg falabala
97 97 diffing "*\\extdiff.*\\a.2a13a4d2da36\\a" "*\\a\\a" (glob) (windows !)
98 98 diffing */extdiff.*/a.2a13a4d2da36/a */a/a (glob) (no-windows !)
99 99 [1]
100 100
101 101
102 102 Test --change option:
103 103
104 104 $ hg ci -d '2 0' -mtest3
105 105
106 106 $ hg falabala -c 1
107 107 diffing "*\\extdiff.*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
108 108 diffing */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
109 109 [1]
110 110
111 111 Check diff are made from the first parent:
112 112
113 113 $ hg falabala -c 3 || echo "diff-like tools yield a non-zero exit code"
114 114 diffing "*\\extdiff.*\\a.2a13a4d2da36\\a" "a.46c0e4daeb72\\a" (glob) (windows !)
115 115 diffing */extdiff.*/a.2a13a4d2da36/a a.46c0e4daeb72/a (glob) (no-windows !)
116 116 diff-like tools yield a non-zero exit code
117 117
118 118 issue3153: ensure using extdiff with removed subrepos doesn't crash:
119 119
120 120 $ hg init suba
121 121 $ cd suba
122 122 $ echo suba > suba
123 123 $ hg add
124 124 adding suba
125 125 $ hg ci -m "adding suba file"
126 126 $ cd ..
127 127 $ echo suba=suba > .hgsub
128 128 $ hg add
129 129 adding .hgsub
130 130 $ hg ci -Sm "adding subrepo"
131 131 $ echo > .hgsub
132 132 $ hg ci -m "removing subrepo"
133 133 $ hg falabala -r 4 -r 5 -S
134 134 diffing a.398e36faf9c6 a.5ab95fb166c4
135 135 [1]
136 136
137 137 Test --per-file option:
138 138
139 139 $ hg up -q -C 3
140 140 $ echo a2 > a
141 141 $ echo b2 > b
142 142 $ hg ci -d '3 0' -mtestmode1
143 143 created new head
144 144 $ hg falabala -c 6 --per-file
145 145 diffing "*\\extdiff.*\\a.46c0e4daeb72\\a" "a.81906f2b98ac\\a" (glob) (windows !)
146 146 diffing */extdiff.*/a.46c0e4daeb72/a a.81906f2b98ac/a (glob) (no-windows !)
147 147 diffing "*\\extdiff.*\\a.46c0e4daeb72\\b" "a.81906f2b98ac\\b" (glob) (windows !)
148 148 diffing */extdiff.*/a.46c0e4daeb72/b a.81906f2b98ac/b (glob) (no-windows !)
149 149 [1]
150 150
151 151 Test --per-file option for gui tool:
152 152
153 153 $ hg --config extdiff.gui.alabalaf=True alabalaf -c 6 --per-file --debug
154 154 diffing */extdiff.*/a.46c0e4daeb72/* a.81906f2b98ac/* (glob)
155 155 diffing */extdiff.*/a.46c0e4daeb72/* a.81906f2b98ac/* (glob)
156 156 making snapshot of 2 files from rev 46c0e4daeb72
157 157 a
158 158 b
159 159 making snapshot of 2 files from rev 81906f2b98ac
160 160 a
161 161 b
162 162 running '* diffing * *' in * (backgrounded) (glob)
163 163 running '* diffing * *' in * (backgrounded) (glob)
164 164 cleaning up temp directory
165 165 [1]
166 166
167 167 Test --per-file option for gui tool again:
168 168
169 169 $ hg --config merge-tools.alabalaf.gui=True alabalaf -c 6 --per-file --debug
170 170 diffing */extdiff.*/a.46c0e4daeb72/* a.81906f2b98ac/* (glob)
171 171 diffing */extdiff.*/a.46c0e4daeb72/* a.81906f2b98ac/* (glob)
172 172 making snapshot of 2 files from rev 46c0e4daeb72
173 173 a
174 174 b
175 175 making snapshot of 2 files from rev 81906f2b98ac
176 176 a
177 177 b
178 178 running '* diffing * *' in * (backgrounded) (glob)
179 179 running '* diffing * *' in * (backgrounded) (glob)
180 180 cleaning up temp directory
181 181 [1]
182 182
183 183 Test --per-file and --confirm options:
184 184
185 185 $ hg --config ui.interactive=True falabala -c 6 --per-file --confirm <<EOF
186 186 > n
187 187 > y
188 188 > EOF
189 189 diff a (1 of 2) [Yns?] n
190 190 diff b (2 of 2) [Yns?] y
191 191 diffing "*\\extdiff.*\\a.46c0e4daeb72\\b" "a.81906f2b98ac\\b" (glob) (windows !)
192 192 diffing */extdiff.*/a.46c0e4daeb72/b a.81906f2b98ac/b (glob) (no-windows !)
193 193 [1]
194 194
195 195 Test --per-file and --confirm options with skipping:
196 196
197 197 $ hg --config ui.interactive=True falabala -c 6 --per-file --confirm <<EOF
198 198 > s
199 199 > EOF
200 200 diff a (1 of 2) [Yns?] s
201 201 [1]
202 202
203 203 issue4463: usage of command line configuration without additional quoting
204 204
205 205 $ cat <<EOF >> $HGRCPATH
206 206 > [extdiff]
207 207 > cmd.4463a = echo
208 208 > opts.4463a = a-naked 'single quoted' "double quoted"
209 209 > 4463b = echo b-naked 'single quoted' "double quoted"
210 210 > echo =
211 211 > EOF
212 212 $ hg update -q -C 0
213 213 $ echo a >> a
214 214
215 215 $ hg --debug 4463a | grep '^running'
216 216 running 'echo a-naked \'single quoted\' "double quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
217 217 running 'echo a-naked \'single quoted\' "double quoted" */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
218 218 $ hg --debug 4463b | grep '^running'
219 219 running 'echo b-naked \'single quoted\' "double quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
220 220 running 'echo b-naked \'single quoted\' "double quoted" */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
221 221 $ hg --debug echo | grep '^running'
222 222 running '*echo* "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
223 223 running '*echo */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
224 224
225 225 (getting options from other than extdiff section)
226 226
227 227 $ cat <<EOF >> $HGRCPATH
228 228 > [extdiff]
229 229 > # using diff-tools diffargs
230 230 > 4463b2 = echo
231 231 > # using merge-tools diffargs
232 232 > 4463b3 = echo
233 233 > # no diffargs
234 234 > 4463b4 = echo
235 235 > [diff-tools]
236 236 > 4463b2.diffargs = b2-naked 'single quoted' "double quoted"
237 237 > [merge-tools]
238 238 > 4463b3.diffargs = b3-naked 'single quoted' "double quoted"
239 239 > EOF
240 240
241 241 $ hg --debug 4463b2 | grep '^running'
242 242 running 'echo b2-naked \'single quoted\' "double quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
243 243 running 'echo b2-naked \'single quoted\' "double quoted" */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
244 244 $ hg --debug 4463b3 | grep '^running'
245 245 running 'echo b3-naked \'single quoted\' "double quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
246 246 running 'echo b3-naked \'single quoted\' "double quoted" */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
247 247 $ hg --debug 4463b4 | grep '^running'
248 248 running 'echo "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
249 249 running 'echo */a $TESTTMP/a/a' in */extdiff.* (glob) (no-windows !)
250 250 $ hg --debug 4463b4 --option b4-naked --option 'being quoted' | grep '^running'
251 251 running 'echo b4-naked "being quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
252 252 running "echo b4-naked 'being quoted' */a $TESTTMP/a/a" in */extdiff.* (glob) (no-windows !)
253 253 $ hg --debug extdiff -p echo --option echo-naked --option 'being quoted' | grep '^running'
254 254 running 'echo echo-naked "being quoted" "*\\a" "*\\a"' in */extdiff.* (glob) (windows !)
255 255 running "echo echo-naked 'being quoted' */a $TESTTMP/a/a" in */extdiff.* (glob) (no-windows !)
256 256
257 257 $ touch 'sp ace'
258 258 $ hg add 'sp ace'
259 259 $ hg ci -m 'sp ace'
260 260 created new head
261 261 $ echo > 'sp ace'
262 262
263 263 Test pre-72a89cf86fcd backward compatibility with half-baked manual quoting
264 264
265 265 $ cat <<EOF >> $HGRCPATH
266 266 > [extdiff]
267 267 > odd =
268 268 > [merge-tools]
269 269 > odd.diffargs = --foo='\$clabel' '\$clabel' "--bar=\$clabel" "\$clabel"
270 270 > odd.executable = echo
271 271 > EOF
272 272
273 273 $ hg --debug odd | grep '^running'
274 274 running '"*\\echo.exe" --foo="sp ace" "sp ace" --bar="sp ace" "sp ace"' in * (glob) (windows !)
275 275 running "*/echo --foo='sp ace' 'sp ace' --bar='sp ace' 'sp ace'" in * (glob) (no-windows !)
276 276
277 277 Empty argument must be quoted
278 278
279 279 $ cat <<EOF >> $HGRCPATH
280 280 > [extdiff]
281 281 > kdiff3 = echo
282 282 > [merge-tools]
283 283 > kdiff3.diffargs=--L1 \$plabel1 --L2 \$clabel \$parent \$child
284 284 > EOF
285 285
286 286 $ hg --debug kdiff3 -r0 | grep '^running'
287 287 running 'echo --L1 "@0" --L2 "" a.8a5febb7f867 a' in * (glob) (windows !)
288 288 running "echo --L1 '@0' --L2 '' a.8a5febb7f867 a" in * (glob) (no-windows !)
289 289
290 290
291 291 Test extdiff of multiple files in tmp dir:
292 292
293 293 $ hg update -C 0 > /dev/null
294 294 $ echo changed > a
295 295 $ echo changed > b
296 296 #if execbit
297 297 $ chmod +x b
298 298 #endif
299 299
300 300 Diff in working directory, before:
301 301
302 302 $ hg diff --git
303 303 diff --git a/a b/a
304 304 --- a/a
305 305 +++ b/a
306 306 @@ -1,1 +1,1 @@
307 307 -a
308 308 +changed
309 309 diff --git a/b b/b
310 310 old mode 100644 (execbit !)
311 311 new mode 100755 (execbit !)
312 312 --- a/b
313 313 +++ b/b
314 314 @@ -1,1 +1,1 @@
315 315 -b
316 316 +changed
317 317
318 318
319 319 Edit with extdiff -p:
320 320
321 321 Prepare custom diff/edit tool:
322 322
323 323 $ cat > 'diff tool.py' << EOT
324 324 > #!$PYTHON
325 325 > import time
326 326 > time.sleep(1) # avoid unchanged-timestamp problems
327 327 > open('a/a', 'ab').write(b'edited\n')
328 328 > open('a/b', 'ab').write(b'edited\n')
329 329 > EOT
330 330
331 331 #if execbit
332 332 $ chmod +x 'diff tool.py'
333 333 #endif
334 334
335 335 will change to /tmp/extdiff.TMP and populate directories a.TMP and a
336 336 and start tool
337 337
338 338 #if windows
339 339 $ cat > 'diff tool.bat' << EOF
340 340 > @"$PYTHON" "`pwd`/diff tool.py"
341 341 > EOF
342 342 $ hg extdiff -p "`pwd`/diff tool.bat"
343 343 [1]
344 344 #else
345 345 $ hg extdiff -p "`pwd`/diff tool.py"
346 346 [1]
347 347 #endif
348 348
349 349 Diff in working directory, after:
350 350
351 351 $ hg diff --git
352 352 diff --git a/a b/a
353 353 --- a/a
354 354 +++ b/a
355 355 @@ -1,1 +1,2 @@
356 356 -a
357 357 +changed
358 358 +edited
359 359 diff --git a/b b/b
360 360 old mode 100644 (execbit !)
361 361 new mode 100755 (execbit !)
362 362 --- a/b
363 363 +++ b/b
364 364 @@ -1,1 +1,2 @@
365 365 -b
366 366 +changed
367 367 +edited
368 368
369 369 Test extdiff with --option:
370 370
371 371 $ hg extdiff -p echo -o this -c 1
372 372 this "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
373 373 this */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
374 374 [1]
375 375
376 376 $ hg falabala -o this -c 1
377 377 diffing this "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
378 378 diffing this */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
379 379 [1]
380 380
381 381 Test extdiff's handling of options with spaces in them:
382 382
383 383 $ hg edspace -c 1
384 384 "name <user@example.com>" "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
385 385 name <user@example.com> */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
386 386 [1]
387 387
388 388 $ hg extdiff -p echo -o "name <user@example.com>" -c 1
389 389 "name <user@example.com>" "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
390 390 name <user@example.com> */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
391 391 [1]
392 392
393 393 Test with revsets:
394 394
395 395 $ hg extdif -p echo -c "rev(1)"
396 396 "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
397 397 */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
398 398 [1]
399 399
400 400 $ hg extdif -p echo -r "0::1"
401 401 "*\\a.8a5febb7f867\\a" "a.34eed99112ab\\a" (glob) (windows !)
402 402 */extdiff.*/a.8a5febb7f867/a a.34eed99112ab/a (glob) (no-windows !)
403 403 [1]
404 404
405 405 Fallback to merge-tools.tool.executable|regkey
406 406 $ mkdir dir
407 407 $ cat > 'dir/tool.sh' << 'EOF'
408 408 > #!/bin/sh
409 409 > # Mimic a tool that syncs all attrs, including mtime
410 410 > cp $1/a $2/a
411 411 > touch -r $1/a $2/a
412 412 > chmod +x $2/a
413 413 > echo "** custom diff **"
414 414 > EOF
415 415 #if execbit
416 416 $ chmod +x dir/tool.sh
417 417 #endif
418 418
419 419 Windows can't run *.sh directly, so create a shim executable that can be.
420 420 Without something executable, the next hg command will try to run `tl` instead
421 421 of $tool (and fail).
422 422 #if windows
423 423 $ cat > dir/tool.bat <<EOF
424 424 > @sh -c "`pwd`/dir/tool.sh %1 %2"
425 425 > EOF
426 426 $ tool=`pwd`/dir/tool.bat
427 427 #else
428 428 $ tool=`pwd`/dir/tool.sh
429 429 #endif
430 430
431 431 $ cat a
432 432 changed
433 433 edited
434 434 $ hg --debug tl --config extdiff.tl= --config merge-tools.tl.executable=$tool
435 435 making snapshot of 2 files from rev * (glob)
436 436 a
437 437 b
438 438 making snapshot of 2 files from working directory
439 439 a
440 440 b
441 441 running '$TESTTMP/a/dir/tool.bat a.* a' in */extdiff.* (glob) (windows !)
442 442 running '$TESTTMP/a/dir/tool.sh a.* a' in */extdiff.* (glob) (no-windows !)
443 443 ** custom diff **
444 444 file changed while diffing. Overwriting: $TESTTMP/a/a (src: */extdiff.*/a/a) (glob)
445 445 cleaning up temp directory
446 446 [1]
447 447 $ cat a
448 448 a
449 449
450 450 #if execbit
451 451 $ [ -x a ]
452 452
453 453 $ cat > 'dir/tool.sh' << 'EOF'
454 454 > #!/bin/sh
455 455 > chmod -x $2/a
456 456 > echo "** custom diff **"
457 457 > EOF
458 458
459 459 $ hg --debug tl --config extdiff.tl= --config merge-tools.tl.executable=$tool
460 460 making snapshot of 2 files from rev * (glob)
461 461 a
462 462 b
463 463 making snapshot of 2 files from working directory
464 464 a
465 465 b
466 466 running '$TESTTMP/a/dir/tool.sh a.* a' in */extdiff.* (glob)
467 467 ** custom diff **
468 468 file changed while diffing. Overwriting: $TESTTMP/a/a (src: */extdiff.*/a/a) (glob)
469 469 cleaning up temp directory
470 470 [1]
471 471
472 472 $ [ -x a ]
473 473 [1]
474 474 #endif
475 475
476 476 $ cd ..
477 477
478 478 #if symlink
479 479
480 480 Test symlinks handling (issue1909)
481 481
482 482 $ hg init testsymlinks
483 483 $ cd testsymlinks
484 484 $ echo a > a
485 485 $ hg ci -Am adda
486 486 adding a
487 487 $ echo a >> a
488 488 $ ln -s missing linka
489 489 $ hg add linka
490 490 $ hg falabala -r 0 --traceback
491 491 diffing testsymlinks.07f494440405 testsymlinks
492 492 [1]
493 493 $ cd ..
494 494
495 495 #endif
496 496
497 497 Test handling of non-ASCII paths in generated docstrings (issue5301)
498 498
499 499 >>> with open("u", "wb") as f:
500 500 ... n = f.write(b"\xa5\xa5")
501 501 $ U=`cat u`
502 502
503 503 $ HGPLAIN=1 hg --config hgext.extdiff= --config extdiff.cmd.td=hi help -k xyzzy
504 504 abort: no matches
505 505 (try 'hg help' for a list of topics)
506 506 [255]
507 507
508 508 $ HGPLAIN=1 hg --config hgext.extdiff= --config extdiff.cmd.td=hi help td > /dev/null
509 509
510 510 $ LC_MESSAGES=ja_JP.UTF-8 hg --config hgext.extdiff= --config extdiff.cmd.td=$U help -k xyzzy
511 511 abort: no matches
512 512 (try 'hg help' for a list of topics)
513 513 [255]
514 514
515 515 $ LC_MESSAGES=ja_JP.UTF-8 hg --config hgext.extdiff= --config extdiff.cmd.td=$U help td \
516 516 > | grep "^ '"
517 517 '\xa5\xa5'
518
519 $ cd $TESTTMP
520
521 Test that diffing a single file works, even if that file is new
522
523 $ hg init testsinglefile
524 $ cd testsinglefile
525 $ echo a > a
526 $ hg add a
527 $ hg falabala
528 diffing * */a (glob)
529 [1]
530 $ hg ci -qm a
531 $ hg falabala -c .
532 diffing * */a (glob)
533 [1]
534 $ echo a >> a
535 $ hg falabala
536 diffing */a */a (glob)
537 [1]
538 $ hg ci -qm 2a
539 $ hg falabala -c .
540 diffing */a */a (glob)
541 [1]
General Comments 0
You need to be logged in to leave comments. Login now