hgext/extdiff.py
changeset 45127 da2e69a278df
parent 45126 48c38018bd77
child 45128 d23881b17388
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