comparison hgext/extdiff.py @ 45127:da2e69a278df

extdiff: refactor logic to diff revs of versions of files Now that code for both cases, diffing patches or files is in separate function, it will be better to refactor them more and understand. Differential Revision: https://phab.mercurial-scm.org/D8687
author Pulkit Goyal <7895pulkit@gmail.com>
date Tue, 07 Jul 2020 13:13:18 +0530
parents 48c38018bd77
children d23881b17388
comparison
equal deleted inserted replaced
45126:48c38018bd77 45127:da2e69a278df
380 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)) 380 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
381 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff') 381 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
382 return 1 382 return 1
383 383
384 384
385 def diffrevs(
386 ui,
387 repo,
388 node1a,
389 node1b,
390 node2,
391 matcher,
392 tmproot,
393 cmdline,
394 do3way,
395 guitool,
396 opts,
397 ):
398
399 subrepos = opts.get(b'subrepos')
400 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
401 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
402 if do3way:
403 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
404 mod_b, add_b, rem_b = (
405 set(stb.modified),
406 set(stb.added),
407 set(stb.removed),
408 )
409 else:
410 mod_b, add_b, rem_b = set(), set(), set()
411 modadd = mod_a | add_a | mod_b | add_b
412 common = modadd | rem_a | rem_b
413 if not common:
414 return 0
415 # Always make a copy of node1a (and node1b, if applicable)
416 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
417 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0]
418 rev1a = b'@%d' % repo[node1a].rev()
419 if do3way:
420 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
421 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[0]
422 rev1b = b'@%d' % repo[node1b].rev()
423 else:
424 dir1b = None
425 rev1b = b''
426
427 fnsandstat = []
428
429 # If node2 in not the wc or there is >1 change, copy it
430 dir2root = b''
431 rev2 = b''
432 if node2:
433 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
434 rev2 = b'@%d' % repo[node2].rev()
435 elif len(common) > 1:
436 # we only actually need to get the files to copy back to
437 # the working dir in this case (because the other cases
438 # are: diffing 2 revisions or single file -- in which case
439 # the file is already directly passed to the diff tool).
440 dir2, fnsandstat = snapshot(ui, repo, modadd, None, tmproot, subrepos)
441 else:
442 # This lets the diff tool open the changed file directly
443 dir2 = b''
444 dir2root = repo.root
445
446 label1a = rev1a
447 label1b = rev1b
448 label2 = rev2
449
450 # If only one change, diff the files instead of the directories
451 # Handle bogus modifies correctly by checking if the files exist
452 if len(common) == 1:
453 common_file = util.localpath(common.pop())
454 dir1a = os.path.join(tmproot, dir1a, common_file)
455 label1a = common_file + rev1a
456 if not os.path.isfile(dir1a):
457 dir1a = pycompat.osdevnull
458 if do3way:
459 dir1b = os.path.join(tmproot, dir1b, common_file)
460 label1b = common_file + rev1b
461 if not os.path.isfile(dir1b):
462 dir1b = pycompat.osdevnull
463 dir2 = os.path.join(dir2root, dir2, common_file)
464 label2 = common_file + rev2
465
466 if not opts.get(b'per_file'):
467 # Run the external tool on the 2 temp directories or the patches
468 cmdline = formatcmdline(
469 cmdline,
470 repo.root,
471 do3way=do3way,
472 parent1=dir1a,
473 plabel1=label1a,
474 parent2=dir1b,
475 plabel2=label1b,
476 child=dir2,
477 clabel=label2,
478 )
479 ui.debug(b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot))
480 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
481 else:
482 # Run the external tool once for each pair of files
483 _runperfilediff(
484 cmdline,
485 repo.root,
486 ui,
487 guitool=guitool,
488 do3way=do3way,
489 confirm=opts.get(b'confirm'),
490 commonfiles=common,
491 tmproot=tmproot,
492 dir1a=dir1a,
493 dir1b=dir1b,
494 dir2root=dir2root,
495 dir2=dir2,
496 rev1a=rev1a,
497 rev1b=rev1b,
498 rev2=rev2,
499 )
500
501 for copy_fn, working_fn, st in fnsandstat:
502 cpstat = os.lstat(copy_fn)
503 # Some tools copy the file and attributes, so mtime may not detect
504 # all changes. A size check will detect more cases, but not all.
505 # The only certain way to detect every case is to diff all files,
506 # which could be expensive.
507 # copyfile() carries over the permission, so the mode check could
508 # be in an 'elif' branch, but for the case where the file has
509 # changed without affecting mtime or size.
510 if (
511 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
512 or cpstat.st_size != st.st_size
513 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
514 ):
515 ui.debug(
516 b'file changed while diffing. '
517 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
518 )
519 util.copyfile(copy_fn, working_fn)
520
521 return 1
522
523
385 def dodiff(ui, repo, cmdline, pats, opts, guitool=False): 524 def dodiff(ui, repo, cmdline, pats, opts, guitool=False):
386 '''Do the actual diff: 525 '''Do the actual diff:
387 526
388 - copy to a temp structure if diffing 2 internal revisions 527 - copy to a temp structure if diffing 2 internal revisions
389 - copy to a temp structure if diffing working revision with 528 - copy to a temp structure if diffing working revision with
404 if not revs: 543 if not revs:
405 ctx1b = repo[None].p2() 544 ctx1b = repo[None].p2()
406 else: 545 else:
407 ctx1b = repo[nullid] 546 ctx1b = repo[nullid]
408 547
409 perfile = opts.get(b'per_file')
410 confirm = opts.get(b'confirm')
411
412 node1a = ctx1a.node() 548 node1a = ctx1a.node()
413 node1b = ctx1b.node() 549 node1b = ctx1b.node()
414 node2 = ctx2.node() 550 node2 = ctx2.node()
415 551
416 # Disable 3-way merge if there is only one parent 552 # Disable 3-way merge if there is only one parent
417 if do3way: 553 if do3way:
418 if node1b == nullid: 554 if node1b == nullid:
419 do3way = False 555 do3way = False
420 556
421 subrepos = opts.get(b'subrepos')
422
423 matcher = scmutil.match(repo[node2], pats, opts) 557 matcher = scmutil.match(repo[node2], pats, opts)
424 558
425 if opts.get(b'patch'): 559 if opts.get(b'patch'):
426 if subrepos: 560 if opts.get(b'subrepos'):
427 raise error.Abort(_(b'--patch cannot be used with --subrepos')) 561 raise error.Abort(_(b'--patch cannot be used with --subrepos'))
428 if perfile: 562 if opts.get(b'per_file'):
429 raise error.Abort(_(b'--patch cannot be used with --per-file')) 563 raise error.Abort(_(b'--patch cannot be used with --per-file'))
430 if node2 is None: 564 if node2 is None:
431 raise error.Abort(_(b'--patch requires two revisions')) 565 raise error.Abort(_(b'--patch requires two revisions'))
432 else:
433 st = repo.status(node1a, node2, matcher, listsubrepos=subrepos)
434 mod_a, add_a, rem_a = set(st.modified), set(st.added), set(st.removed)
435 if do3way:
436 stb = repo.status(node1b, node2, matcher, listsubrepos=subrepos)
437 mod_b, add_b, rem_b = (
438 set(stb.modified),
439 set(stb.added),
440 set(stb.removed),
441 )
442 else:
443 mod_b, add_b, rem_b = set(), set(), set()
444 modadd = mod_a | add_a | mod_b | add_b
445 common = modadd | rem_a | rem_b
446 if not common:
447 return 0
448 566
449 tmproot = pycompat.mkdtemp(prefix=b'extdiff.') 567 tmproot = pycompat.mkdtemp(prefix=b'extdiff.')
450 try: 568 try:
451 if opts.get(b'patch'): 569 if opts.get(b'patch'):
452 return diffpatch( 570 return diffpatch(
453 ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way 571 ui, repo, node1a, node2, tmproot, matcher, cmdline, do3way
454 ) 572 )
455 573
456 # Always make a copy of node1a (and node1b, if applicable) 574 return diffrevs(
457 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a) 575 ui,
458 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot, subrepos)[0] 576 repo,
459 rev1a = b'@%d' % repo[node1a].rev() 577 node1a,
460 if do3way: 578 node1b,
461 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b) 579 node2,
462 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot, subrepos)[ 580 matcher,
463 0 581 tmproot,
464 ] 582 cmdline,
465 rev1b = b'@%d' % repo[node1b].rev() 583 do3way,
466 else: 584 guitool,
467 dir1b = None 585 opts,
468 rev1b = b'' 586 )
469 587
470 fnsandstat = []
471
472 # If node2 in not the wc or there is >1 change, copy it
473 dir2root = b''
474 rev2 = b''
475 if node2:
476 dir2 = snapshot(ui, repo, modadd, node2, tmproot, subrepos)[0]
477 rev2 = b'@%d' % repo[node2].rev()
478 elif len(common) > 1:
479 # we only actually need to get the files to copy back to
480 # the working dir in this case (because the other cases
481 # are: diffing 2 revisions or single file -- in which case
482 # the file is already directly passed to the diff tool).
483 dir2, fnsandstat = snapshot(
484 ui, repo, modadd, None, tmproot, subrepos
485 )
486 else:
487 # This lets the diff tool open the changed file directly
488 dir2 = b''
489 dir2root = repo.root
490
491 label1a = rev1a
492 label1b = rev1b
493 label2 = rev2
494
495 # If only one change, diff the files instead of the directories
496 # Handle bogus modifies correctly by checking if the files exist
497 if len(common) == 1:
498 common_file = util.localpath(common.pop())
499 dir1a = os.path.join(tmproot, dir1a, common_file)
500 label1a = common_file + rev1a
501 if not os.path.isfile(dir1a):
502 dir1a = pycompat.osdevnull
503 if do3way:
504 dir1b = os.path.join(tmproot, dir1b, common_file)
505 label1b = common_file + rev1b
506 if not os.path.isfile(dir1b):
507 dir1b = pycompat.osdevnull
508 dir2 = os.path.join(dir2root, dir2, common_file)
509 label2 = common_file + rev2
510
511 if not perfile:
512 # Run the external tool on the 2 temp directories or the patches
513 cmdline = formatcmdline(
514 cmdline,
515 repo.root,
516 do3way=do3way,
517 parent1=dir1a,
518 plabel1=label1a,
519 parent2=dir1b,
520 plabel2=label1b,
521 child=dir2,
522 clabel=label2,
523 )
524 ui.debug(
525 b'running %r in %s\n' % (pycompat.bytestr(cmdline), tmproot)
526 )
527 ui.system(cmdline, cwd=tmproot, blockedtag=b'extdiff')
528 else:
529 # Run the external tool once for each pair of files
530 _runperfilediff(
531 cmdline,
532 repo.root,
533 ui,
534 guitool=guitool,
535 do3way=do3way,
536 confirm=confirm,
537 commonfiles=common,
538 tmproot=tmproot,
539 dir1a=dir1a,
540 dir1b=dir1b,
541 dir2root=dir2root,
542 dir2=dir2,
543 rev1a=rev1a,
544 rev1b=rev1b,
545 rev2=rev2,
546 )
547
548 for copy_fn, working_fn, st in fnsandstat:
549 cpstat = os.lstat(copy_fn)
550 # Some tools copy the file and attributes, so mtime may not detect
551 # all changes. A size check will detect more cases, but not all.
552 # The only certain way to detect every case is to diff all files,
553 # which could be expensive.
554 # copyfile() carries over the permission, so the mode check could
555 # be in an 'elif' branch, but for the case where the file has
556 # changed without affecting mtime or size.
557 if (
558 cpstat[stat.ST_MTIME] != st[stat.ST_MTIME]
559 or cpstat.st_size != st.st_size
560 or (cpstat.st_mode & 0o100) != (st.st_mode & 0o100)
561 ):
562 ui.debug(
563 b'file changed while diffing. '
564 b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)
565 )
566 util.copyfile(copy_fn, working_fn)
567
568 return 1
569 finally: 588 finally:
570 ui.note(_(b'cleaning up temp directory\n')) 589 ui.note(_(b'cleaning up temp directory\n'))
571 shutil.rmtree(tmproot) 590 shutil.rmtree(tmproot)
572 591
573 592