eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/extdiff.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # extdiff.py - external diff program support for mercurial
       
     2 #
       
     3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 '''command to allow external programs to compare revisions
       
     9 
       
    10 The extdiff Mercurial extension allows you to use external programs
       
    11 to compare revisions, or revision with working directory. The external
       
    12 diff programs are called with a configurable set of options and two
       
    13 non-option arguments: paths to directories containing snapshots of
       
    14 files to compare.
       
    15 
       
    16 The extdiff extension also allows to configure new diff commands, so
       
    17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
       
    18 
       
    19   [extdiff]
       
    20   # add new command that runs GNU diff(1) in 'context diff' mode
       
    21   cdiff = gdiff -Nprc5
       
    22   ## or the old way:
       
    23   #cmd.cdiff = gdiff
       
    24   #opts.cdiff = -Nprc5
       
    25 
       
    26   # add new command called vdiff, runs kdiff3
       
    27   vdiff = kdiff3
       
    28 
       
    29   # add new command called meld, runs meld (no need to name twice)
       
    30   meld =
       
    31 
       
    32   # add new command called vimdiff, runs gvimdiff with DirDiff plugin
       
    33   # (see http://www.vim.org/scripts/script.php?script_id=102) Non
       
    34   # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
       
    35   # your .vimrc
       
    36   vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
       
    37 
       
    38 Tool arguments can include variables that are expanded at runtime::
       
    39 
       
    40   $parent1, $plabel1 - filename, descriptive label of first parent
       
    41   $child,   $clabel  - filename, descriptive label of child revision
       
    42   $parent2, $plabel2 - filename, descriptive label of second parent
       
    43   $parent is an alias for $parent1.
       
    44 
       
    45 The extdiff extension will look in your [diff-tools] and [merge-tools]
       
    46 sections for diff tool arguments, when none are specified in [extdiff].
       
    47 
       
    48 ::
       
    49 
       
    50   [extdiff]
       
    51   kdiff3 =
       
    52 
       
    53   [diff-tools]
       
    54   kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
       
    55 
       
    56 You can use -I/-X and list of file or directory names like normal
       
    57 :hg:`diff` command. The extdiff extension makes snapshots of only
       
    58 needed files, so running the external diff program will actually be
       
    59 pretty fast (at least faster than having to compare the entire tree).
       
    60 '''
       
    61 
       
    62 from mercurial.i18n import _
       
    63 from mercurial.node import short, nullid
       
    64 from mercurial import cmdutil, util, commands, encoding
       
    65 import os, shlex, shutil, tempfile, re
       
    66 
       
    67 def snapshot(ui, repo, files, node, tmproot):
       
    68     '''snapshot files as of some revision
       
    69     if not using snapshot, -I/-X does not work and recursive diff
       
    70     in tools like kdiff3 and meld displays too many files.'''
       
    71     dirname = os.path.basename(repo.root)
       
    72     if dirname == "":
       
    73         dirname = "root"
       
    74     if node is not None:
       
    75         dirname = '%s.%s' % (dirname, short(node))
       
    76     base = os.path.join(tmproot, dirname)
       
    77     os.mkdir(base)
       
    78     if node is not None:
       
    79         ui.note(_('making snapshot of %d files from rev %s\n') %
       
    80                 (len(files), short(node)))
       
    81     else:
       
    82         ui.note(_('making snapshot of %d files from working directory\n') %
       
    83             (len(files)))
       
    84     wopener = util.opener(base)
       
    85     fns_and_mtime = []
       
    86     ctx = repo[node]
       
    87     for fn in files:
       
    88         wfn = util.pconvert(fn)
       
    89         if not wfn in ctx:
       
    90             # File doesn't exist; could be a bogus modify
       
    91             continue
       
    92         ui.note('  %s\n' % wfn)
       
    93         dest = os.path.join(base, wfn)
       
    94         fctx = ctx[wfn]
       
    95         data = repo.wwritedata(wfn, fctx.data())
       
    96         if 'l' in fctx.flags():
       
    97             wopener.symlink(data, wfn)
       
    98         else:
       
    99             wopener(wfn, 'w').write(data)
       
   100             if 'x' in fctx.flags():
       
   101                 util.set_flags(dest, False, True)
       
   102         if node is None:
       
   103             fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
       
   104     return dirname, fns_and_mtime
       
   105 
       
   106 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
       
   107     '''Do the actuall diff:
       
   108 
       
   109     - copy to a temp structure if diffing 2 internal revisions
       
   110     - copy to a temp structure if diffing working revision with
       
   111       another one and more than 1 file is changed
       
   112     - just invoke the diff for a single file in the working dir
       
   113     '''
       
   114 
       
   115     revs = opts.get('rev')
       
   116     change = opts.get('change')
       
   117     args = ' '.join(diffopts)
       
   118     do3way = '$parent2' in args
       
   119 
       
   120     if revs and change:
       
   121         msg = _('cannot specify --rev and --change at the same time')
       
   122         raise util.Abort(msg)
       
   123     elif change:
       
   124         node2 = repo.lookup(change)
       
   125         node1a, node1b = repo.changelog.parents(node2)
       
   126     else:
       
   127         node1a, node2 = cmdutil.revpair(repo, revs)
       
   128         if not revs:
       
   129             node1b = repo.dirstate.parents()[1]
       
   130         else:
       
   131             node1b = nullid
       
   132 
       
   133     # Disable 3-way merge if there is only one parent
       
   134     if do3way:
       
   135         if node1b == nullid:
       
   136             do3way = False
       
   137 
       
   138     matcher = cmdutil.match(repo, pats, opts)
       
   139     mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3])
       
   140     if do3way:
       
   141         mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3])
       
   142     else:
       
   143         mod_b, add_b, rem_b = set(), set(), set()
       
   144     modadd = mod_a | add_a | mod_b | add_b
       
   145     common = modadd | rem_a | rem_b
       
   146     if not common:
       
   147         return 0
       
   148 
       
   149     tmproot = tempfile.mkdtemp(prefix='extdiff.')
       
   150     try:
       
   151         # Always make a copy of node1a (and node1b, if applicable)
       
   152         dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
       
   153         dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0]
       
   154         rev1a = '@%d' % repo[node1a].rev()
       
   155         if do3way:
       
   156             dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
       
   157             dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0]
       
   158             rev1b = '@%d' % repo[node1b].rev()
       
   159         else:
       
   160             dir1b = None
       
   161             rev1b = ''
       
   162 
       
   163         fns_and_mtime = []
       
   164 
       
   165         # If node2 in not the wc or there is >1 change, copy it
       
   166         dir2root = ''
       
   167         rev2 = ''
       
   168         if node2:
       
   169             dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0]
       
   170             rev2 = '@%d' % repo[node2].rev()
       
   171         elif len(common) > 1:
       
   172             #we only actually need to get the files to copy back to
       
   173             #the working dir in this case (because the other cases
       
   174             #are: diffing 2 revisions or single file -- in which case
       
   175             #the file is already directly passed to the diff tool).
       
   176             dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot)
       
   177         else:
       
   178             # This lets the diff tool open the changed file directly
       
   179             dir2 = ''
       
   180             dir2root = repo.root
       
   181 
       
   182         label1a = rev1a
       
   183         label1b = rev1b
       
   184         label2 = rev2
       
   185 
       
   186         # If only one change, diff the files instead of the directories
       
   187         # Handle bogus modifies correctly by checking if the files exist
       
   188         if len(common) == 1:
       
   189             common_file = util.localpath(common.pop())
       
   190             dir1a = os.path.join(dir1a, common_file)
       
   191             label1a = common_file + rev1a
       
   192             if not os.path.isfile(os.path.join(tmproot, dir1a)):
       
   193                 dir1a = os.devnull
       
   194             if do3way:
       
   195                 dir1b = os.path.join(dir1b, common_file)
       
   196                 label1b = common_file + rev1b
       
   197                 if not os.path.isfile(os.path.join(tmproot, dir1b)):
       
   198                     dir1b = os.devnull
       
   199             dir2 = os.path.join(dir2root, dir2, common_file)
       
   200             label2 = common_file + rev2
       
   201 
       
   202         # Function to quote file/dir names in the argument string.
       
   203         # When not operating in 3-way mode, an empty string is
       
   204         # returned for parent2
       
   205         replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
       
   206                        plabel1=label1a, plabel2=label1b,
       
   207                        clabel=label2, child=dir2)
       
   208         def quote(match):
       
   209             key = match.group()[1:]
       
   210             if not do3way and key == 'parent2':
       
   211                 return ''
       
   212             return util.shellquote(replace[key])
       
   213 
       
   214         # Match parent2 first, so 'parent1?' will match both parent1 and parent
       
   215         regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel)'
       
   216         if not do3way and not re.search(regex, args):
       
   217             args += ' $parent1 $child'
       
   218         args = re.sub(regex, quote, args)
       
   219         cmdline = util.shellquote(diffcmd) + ' ' + args
       
   220 
       
   221         ui.debug('running %r in %s\n' % (cmdline, tmproot))
       
   222         util.system(cmdline, cwd=tmproot)
       
   223 
       
   224         for copy_fn, working_fn, mtime in fns_and_mtime:
       
   225             if os.path.getmtime(copy_fn) != mtime:
       
   226                 ui.debug('file changed while diffing. '
       
   227                          'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
       
   228                 util.copyfile(copy_fn, working_fn)
       
   229 
       
   230         return 1
       
   231     finally:
       
   232         ui.note(_('cleaning up temp directory\n'))
       
   233         shutil.rmtree(tmproot)
       
   234 
       
   235 def extdiff(ui, repo, *pats, **opts):
       
   236     '''use external program to diff repository (or selected files)
       
   237 
       
   238     Show differences between revisions for the specified files, using
       
   239     an external program. The default program used is diff, with
       
   240     default options "-Npru".
       
   241 
       
   242     To select a different program, use the -p/--program option. The
       
   243     program will be passed the names of two directories to compare. To
       
   244     pass additional options to the program, use -o/--option. These
       
   245     will be passed before the names of the directories to compare.
       
   246 
       
   247     When two revision arguments are given, then changes are shown
       
   248     between those revisions. If only one revision is specified then
       
   249     that revision is compared to the working directory, and, when no
       
   250     revisions are specified, the working directory files are compared
       
   251     to its parent.'''
       
   252     program = opts.get('program')
       
   253     option = opts.get('option')
       
   254     if not program:
       
   255         program = 'diff'
       
   256         option = option or ['-Npru']
       
   257     return dodiff(ui, repo, program, option, pats, opts)
       
   258 
       
   259 cmdtable = {
       
   260     "extdiff":
       
   261     (extdiff,
       
   262      [('p', 'program', '',
       
   263        _('comparison program to run'), _('CMD')),
       
   264       ('o', 'option', [],
       
   265        _('pass option to comparison program'), _('OPT')),
       
   266       ('r', 'rev', [],
       
   267        _('revision'), _('REV')),
       
   268       ('c', 'change', '',
       
   269        _('change made by revision'), _('REV')),
       
   270      ] + commands.walkopts,
       
   271      _('hg extdiff [OPT]... [FILE]...')),
       
   272     }
       
   273 
       
   274 def uisetup(ui):
       
   275     for cmd, path in ui.configitems('extdiff'):
       
   276         if cmd.startswith('cmd.'):
       
   277             cmd = cmd[4:]
       
   278             if not path:
       
   279                 path = cmd
       
   280             diffopts = ui.config('extdiff', 'opts.' + cmd, '')
       
   281             diffopts = diffopts and [diffopts] or []
       
   282         elif cmd.startswith('opts.'):
       
   283             continue
       
   284         else:
       
   285             # command = path opts
       
   286             if path:
       
   287                 diffopts = shlex.split(path)
       
   288                 path = diffopts.pop(0)
       
   289             else:
       
   290                 path, diffopts = cmd, []
       
   291         # look for diff arguments in [diff-tools] then [merge-tools]
       
   292         if diffopts == []:
       
   293             args = ui.config('diff-tools', cmd+'.diffargs') or \
       
   294                    ui.config('merge-tools', cmd+'.diffargs')
       
   295             if args:
       
   296                 diffopts = shlex.split(args)
       
   297         def save(cmd, path, diffopts):
       
   298             '''use closure to save diff command to use'''
       
   299             def mydiff(ui, repo, *pats, **opts):
       
   300                 return dodiff(ui, repo, path, diffopts + opts['option'],
       
   301                               pats, opts)
       
   302             doc = _('''\
       
   303 use %(path)s to diff repository (or selected files)
       
   304 
       
   305     Show differences between revisions for the specified files, using
       
   306     the %(path)s program.
       
   307 
       
   308     When two revision arguments are given, then changes are shown
       
   309     between those revisions. If only one revision is specified then
       
   310     that revision is compared to the working directory, and, when no
       
   311     revisions are specified, the working directory files are compared
       
   312     to its parent.\
       
   313 ''') % dict(path=util.uirepr(path))
       
   314 
       
   315             # We must translate the docstring right away since it is
       
   316             # used as a format string. The string will unfortunately
       
   317             # be translated again in commands.helpcmd and this will
       
   318             # fail when the docstring contains non-ASCII characters.
       
   319             # Decoding the string to a Unicode string here (using the
       
   320             # right encoding) prevents that.
       
   321             mydiff.__doc__ = doc.decode(encoding.encoding)
       
   322             return mydiff
       
   323         cmdtable[cmd] = (save(cmd, path, diffopts),
       
   324                          cmdtable['extdiff'][1][1:],
       
   325                          _('hg %s [OPTION]... [FILE]...') % cmd)