PageRenderTime 62ms CodeModel.GetById 15ms app.highlight 40ms RepoModel.GetById 1ms app.codeStats 1ms

/tortoisehg/util/hglib.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 647 lines | 592 code | 11 blank | 44 comment | 12 complexity | 766213ebfed80aa728abec1bc633d242 MD5 | raw file
  1# hglib.py - Mercurial API wrappers for TortoiseHg
  2#
  3# Copyright 2007 Steve Borho <steve@borho.org>
  4#
  5# This software may be used and distributed according to the terms of the
  6# GNU General Public License version 2, incorporated herein by reference.
  7
  8import os
  9import re
 10import sys
 11import shlex
 12import time
 13import inspect
 14
 15from mercurial import demandimport
 16demandimport.disable()
 17try:
 18    # hg >= 1.7
 19    from mercurial.cmdutil import updatedir
 20except ImportError:
 21    # hg <= 1.6
 22    from mercurial.patch import updatedir
 23demandimport.enable()
 24
 25from mercurial import ui, util, extensions, match, bundlerepo, url, cmdutil
 26from mercurial import dispatch, encoding, templatefilters, filemerge
 27
 28_encoding = encoding.encoding
 29_encodingmode = encoding.encodingmode
 30_fallbackencoding = encoding.fallbackencoding
 31
 32# extensions which can cause problem with TortoiseHg
 33_extensions_blacklist = ('color', 'pager', 'progress')
 34
 35from tortoisehg.util import paths
 36from tortoisehg.util.hgversion import hgversion
 37
 38def tounicode(s):
 39    """
 40    Convert the encoding of string from MBCS to Unicode.
 41
 42    Based on mercurial.util.tolocal().
 43    Return 'unicode' type string.
 44    """
 45    if s is None:
 46        return None
 47    if isinstance(s, unicode):
 48        return s
 49    for e in ('utf-8', _encoding):
 50        try:
 51            return s.decode(e, 'strict')
 52        except UnicodeDecodeError:
 53            pass
 54    return s.decode(_fallbackencoding, 'replace')
 55
 56def fromunicode(s, errors='strict'):
 57    """
 58    Convert the encoding of string from Unicode to MBCS.
 59
 60    Return 'str' type string.
 61
 62    If you don't want an exception for conversion failure,
 63    specify errors='replace'.
 64    """
 65    if s is None:
 66        return None
 67    s = unicode(s)  # s can be QtCore.QString
 68    for enc in (_encoding, _fallbackencoding):
 69        try:
 70            return s.encode(enc)
 71        except UnicodeEncodeError:
 72            pass
 73
 74    return s.encode(_encoding, errors)  # last ditch
 75
 76def toutf(s):
 77    """
 78    Convert the encoding of string from MBCS to UTF-8.
 79
 80    Return 'str' type string.
 81    """
 82    if s is None:
 83        return None
 84    return tounicode(s).encode('utf-8').replace('\0','')
 85
 86def fromutf(s):
 87    """
 88    Convert the encoding of string from UTF-8 to MBCS
 89
 90    Return 'str' type string.
 91    """
 92    if s is None:
 93        return None
 94    try:
 95        return s.decode('utf-8').encode(_encoding)
 96    except (UnicodeDecodeError, UnicodeEncodeError):
 97        pass
 98    try:
 99        return s.decode('utf-8').encode(_fallbackencoding)
100    except (UnicodeDecodeError, UnicodeEncodeError):
101        pass
102    u = s.decode('utf-8', 'replace') # last ditch
103    return u.encode(_encoding, 'replace')
104
105_tabwidth = None
106def gettabwidth(ui):
107    global _tabwidth
108    if _tabwidth is not None:
109        return _tabwidth
110    tabwidth = ui.config('tortoisehg', 'tabwidth')
111    try:
112        tabwidth = int(tabwidth)
113        if tabwidth < 1 or tabwidth > 16:
114            tabwidth = 0
115    except (ValueError, TypeError):
116        tabwidth = 0
117    _tabwidth = tabwidth
118    return tabwidth
119
120_maxdiff = None
121def getmaxdiffsize(ui):
122    global _maxdiff
123    if _maxdiff is not None:
124        return _maxdiff
125    maxdiff = ui.config('tortoisehg', 'maxdiff')
126    try:
127        maxdiff = int(maxdiff)
128        if maxdiff < 1:
129            maxdiff = sys.maxint
130    except (ValueError, TypeError):
131        maxdiff = 1024 # 1MB by default
132    _maxdiff = maxdiff * 1024
133    return _maxdiff
134
135_deadbranch = None
136def getdeadbranch(ui):
137    '''return a list of dead branch names in UTF-8'''
138    global _deadbranch
139    if _deadbranch is None:
140        db = toutf(ui.config('tortoisehg', 'deadbranch', ''))
141        dblist = [b.strip() for b in db.split(',')]
142        _deadbranch = dblist
143    return _deadbranch
144
145def getlivebranch(repo):
146    '''return a list of live branch names in UTF-8'''
147    lives = []
148    deads = getdeadbranch(repo.ui)
149    cl = repo.changelog
150    for branch, heads in repo.branchmap().iteritems():
151        # branch encoded in UTF-8
152        if branch in deads:
153            # ignore branch names in tortoisehg.deadbranch
154            continue
155        bheads = [h for h in heads if ('close' not in cl.read(h)[5])]
156        if not bheads:
157            # ignore branches with all heads closed
158            continue
159        lives.append(branch.replace('\0', ''))
160    return lives
161
162def getlivebheads(repo):
163    '''return a list of revs of live branch heads'''
164    bheads = []
165    for b, ls in repo.branchmap().iteritems():
166        bheads += [repo[x] for x in ls]
167    heads = [x.rev() for x in bheads if not x.extra().get('close')]
168    heads.sort()
169    heads.reverse()
170    return heads
171
172_hidetags = None
173def gethidetags(ui):
174    global _hidetags
175    if _hidetags is None:
176        tags = toutf(ui.config('tortoisehg', 'hidetags', ''))
177        taglist = [t.strip() for t in tags.split()]
178        _hidetags = taglist
179    return _hidetags
180
181def getfilteredtags(repo):
182    filtered = []
183    hides = gethidetags(repo.ui)
184    for tag in list(repo.tags()):
185        if tag not in hides:
186            filtered.append(tag)
187    return filtered
188
189def getrawctxtags(changectx):
190    '''Returns the tags for changectx, converted to UTF-8 but
191    unfiltered for hidden tags'''
192    value = [toutf(tag) for tag in changectx.tags()]
193    if len(value) == 0:
194        return None
195    return value
196
197def getctxtags(changectx):
198    '''Returns all unhidden tags for changectx, converted to UTF-8'''
199    value = getrawctxtags(changectx)
200    if value:
201        htlist = gethidetags(changectx._repo.ui)
202        tags = [tag for tag in value if tag not in htlist]
203        if len(tags) == 0:
204            return None
205        return tags
206    return None
207
208def getmqpatchtags(repo):
209    '''Returns all tag names used by MQ patches, or []'''
210    if hasattr(repo, 'mq'):
211        repo.mq.parse_series()
212        return repo.mq.series[:]
213    else:
214        return []
215
216def getcurrentqqueue(repo):
217    """Return the name of the current patch queue."""
218    if not hasattr(repo, 'mq'):
219        return None
220    cur = os.path.basename(repo.mq.path)
221    if cur.startswith('patches-'):
222        cur = cur[8:]
223    return cur
224
225def diffexpand(line):
226    'Expand tabs in a line of diff/patch text'
227    if _tabwidth is None:
228        gettabwidth(ui.ui())
229    if not _tabwidth or len(line) < 2:
230        return line
231    return line[0] + line[1:].expandtabs(_tabwidth)
232
233_fontconfig = None
234def getfontconfig(_ui=None):
235    global _fontconfig
236    if _fontconfig is None:
237        if _ui is None:
238            _ui = ui.ui()
239        # defaults
240        _fontconfig = {'fontcomment': 'monospace 10',
241                       'fontdiff': 'monospace 10',
242                       'fontlist': 'sans 9',
243                       'fontlog': 'monospace 10'}
244        # overwrite defaults with configured values
245        for name, val in _ui.configitems('gtools'):
246            if val and name.startswith('font'):
247                _fontconfig[name] = val
248    return _fontconfig
249
250def invalidaterepo(repo):
251    repo.dirstate.invalidate()
252    if isinstance(repo, bundlerepo.bundlerepository):
253        # Work around a bug in hg-1.3.  repo.invalidate() breaks
254        # overlay bundlerepos
255        return
256    repo.invalidate()
257    # way _bookmarks / _bookmarkcurrent cached changed
258    # from 1.4 to 1.5...
259    for cachedAttr in ('_bookmarks', '_bookmarkcurrent'):
260        # Check if it's a property or normal value...
261        if is_descriptor(repo, cachedAttr):
262            # The very act of calling hasattr would
263            # re-cache the property, so just assume it's
264            # already cached, and catch the error if it wasn't.
265            try:
266                delattr(repo, cachedAttr)
267            except AttributeError:
268                pass
269        elif hasattr(repo, cachedAttr):
270            setattr(repo, cachedAttr, None)
271    if 'mq' in repo.__dict__: #do not create if it does not exist
272        repo.mq.invalidate()
273
274def enabledextensions():
275    """Return the {name: shortdesc} dict of enabled extensions
276
277    shortdesc is in local encoding.
278    """
279    return extensions.enabled()[0]
280
281def allextensions():
282    """Return the {name: shortdesc} dict of known extensions
283
284    shortdesc is in local encoding.
285    """
286    enabledexts = enabledextensions()
287    disabledexts = extensions.disabled()[0]
288    exts = (disabledexts or {}).copy()
289    exts.update(enabledexts)
290    return exts
291
292def validateextensions(enabledexts):
293    """Report extensions which should be disabled
294
295    Returns the dict {name: message} of extensions expected to be disabled.
296    message is 'utf-8'-encoded string.
297    """
298    from tortoisehg.util.i18n import _  # avoid cyclic dependency
299    exts = {}
300    if os.name != 'posix':
301        exts['inotify'] = _('inotify is not supported on this platform')
302    if 'win32text' in enabledexts:
303        exts['eol'] = _('eol is incompatible with win32text')
304    if 'eol' in enabledexts:
305        exts['win32text'] = _('win32text is incompatible with eol')
306    if 'perfarce' in enabledexts:
307        exts['hgsubversion'] = _('hgsubversion is incompatible with perfarce')
308    if 'hgsubversion' in enabledexts:
309        exts['perfarce'] = _('perfarce is incompatible with hgsubversion')
310    return exts
311
312def loadextension(ui, name):
313    # Between Mercurial revisions 1.2 and 1.3, extensions.load() stopped
314    # calling uisetup() after loading an extension.  This could do
315    # unexpected things if you use an hg version < 1.3
316    extensions.load(ui, name, None)
317    mod = extensions.find(name)
318    uisetup = getattr(mod, 'uisetup', None)
319    if uisetup:
320        uisetup(ui)
321
322def _loadextensionwithblacklist(orig, ui, name, path):
323    if name.startswith('hgext.') or name.startswith('hgext/'):
324        shortname = name[6:]
325    else:
326        shortname = name
327    if shortname in _extensions_blacklist and not path:  # only bundled ext
328        return
329
330    return orig(ui, name, path)
331
332def wrapextensionsloader():
333    """Wrap extensions.load(ui, name) for blacklist to take effect"""
334    extensions.wrapfunction(extensions, 'load',
335                            _loadextensionwithblacklist)
336
337def canonpaths(list):
338    'Get canonical paths (relative to root) for list of files'
339    # This is a horrible hack.  Please remove this when HG acquires a
340    # decent case-folding solution.
341    canonpats = []
342    cwd = os.getcwd()
343    root = paths.find_root(cwd)
344    for f in list:
345        try:
346            canonpats.append(util.canonpath(root, cwd, f))
347        except util.Abort:
348            # Attempt to resolve case folding conflicts.
349            fu = f.upper()
350            cwdu = cwd.upper()
351            if fu.startswith(cwdu):
352                canonpats.append(util.canonpath(root, cwd, f[len(cwd+os.sep):]))
353            else:
354                # May already be canonical
355                canonpats.append(f)
356    return canonpats
357
358def escapepath(path):
359    'Before passing a file path to hg API, it may need escaping'
360    p = path
361    if '[' in p or '{' in p or '*' in p or '?' in p:
362        return 'path:' + p
363    else:
364        return p
365
366def normpats(pats):
367    'Normalize file patterns'
368    normpats = []
369    for pat in pats:
370        kind, p = match._patsplit(pat, None)
371        if kind:
372            normpats.append(pat)
373        else:
374            if '[' in p or '{' in p or '*' in p or '?' in p:
375                normpats.append('glob:' + p)
376            else:
377                normpats.append('path:' + p)
378    return normpats
379
380
381def mergetools(ui, values=None):
382    'returns the configured merge tools and the internal ones'
383    if values == None:
384        values = []
385    seen = values[:]
386    for key, value in ui.configitems('merge-tools'):
387        t = key.split('.')[0]
388        if t not in seen:
389            seen.append(t)
390            # Ensure the tool is installed
391            if filemerge._findtool(ui, t):
392                values.append(t)
393    values.append('internal:merge')
394    values.append('internal:prompt')
395    values.append('internal:dump')
396    values.append('internal:local')
397    values.append('internal:other')
398    values.append('internal:fail')
399    return values
400
401
402_difftools = None
403def difftools(ui):
404    global _difftools
405    if _difftools:
406        return _difftools
407
408    def fixup_extdiff(diffopts):
409        if '$child' not in diffopts:
410            diffopts.append('$parent1')
411            diffopts.append('$child')
412        if '$parent2' in diffopts:
413            mergeopts = diffopts[:]
414            diffopts.remove('$parent2')
415        else:
416            mergeopts = []
417        return diffopts, mergeopts
418
419    tools = {}
420    for cmd, path in ui.configitems('extdiff'):
421        if cmd.startswith('cmd.'):
422            cmd = cmd[4:]
423            if not path:
424                path = cmd
425            diffopts = ui.config('extdiff', 'opts.' + cmd, '')
426            diffopts = shlex.split(diffopts)
427            diffopts, mergeopts = fixup_extdiff(diffopts)
428            tools[cmd] = [path, diffopts, mergeopts]
429        elif cmd.startswith('opts.'):
430            continue
431        else:
432            # command = path opts
433            if path:
434                diffopts = shlex.split(path)
435                path = diffopts.pop(0)
436            else:
437                path, diffopts = cmd, []
438            diffopts, mergeopts = fixup_extdiff(diffopts)
439            tools[cmd] = [path, diffopts, mergeopts]
440    mt = []
441    mergetools(ui, mt)
442    for t in mt:
443        if t.startswith('internal:'):
444            continue
445        dopts = ui.config('merge-tools', t + '.diffargs', '')
446        mopts = ui.config('merge-tools', t + '.diff3args', '')
447        dopts, mopts = shlex.split(dopts), shlex.split(mopts)
448        tools[t] = [filemerge._findtool(ui, t), dopts, mopts]
449    _difftools = tools
450    return tools
451
452
453_funcre = re.compile('\w')
454def getchunkfunction(data, linenum):
455    """Return the function containing the chunk at linenum.
456
457    Stolen from mercurial/mdiff.py.
458    """
459    # Walk backwards starting from the line before the chunk
460    # to find a line starting with an alphanumeric char.
461    for x in xrange(int(linenum) - 2, -1, -1):
462        t = data[x].rstrip()
463        if _funcre.match(t):
464            return ' ' + t[:40]
465    return None
466
467
468def hgcmd_toq(q, label, args):
469    '''
470    Run an hg command in a background thread, pipe all output to a Queue
471    object.  Assumes command is completely noninteractive.
472    '''
473    class Qui(ui.ui):
474        def __init__(self, src=None):
475            super(Qui, self).__init__(src)
476            self.setconfig('ui', 'interactive', 'off')
477
478        def write(self, *args, **opts):
479            if self._buffers:
480                self._buffers[-1].extend([str(a) for a in args])
481            else:
482                for a in args:
483                    if label:
484                        q.put((str(a), opts.get('label', '')))
485                    else:
486                        q.put(str(a))
487
488        def plain(self):
489            return True
490
491    u = Qui()
492    oldterm = os.environ.get('TERM')
493    os.environ['TERM'] = 'dumb'
494    ret = dispatch._dispatch(u, list(args))
495    if oldterm:
496        os.environ['TERM'] = oldterm
497    return ret
498
499def get_reponame(repo):
500    if repo.ui.config('tortoisehg', 'fullpath', False):
501        name = repo.root
502    elif repo.ui.config('web', 'name', False):
503        name = repo.ui.config('web', 'name')
504    else:
505        name = os.path.basename(repo.root)
506    return toutf(name)
507
508def displaytime(date):
509    return util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2')
510
511def utctime(date):
512    return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(date[0]))
513
514def age(date):
515    return templatefilters.age(date)
516
517def username(user):
518    author = templatefilters.person(user)
519    if not author:
520        author = util.shortuser(user)
521    return author
522
523def get_revision_desc(fctx, curpath=None):
524    """return the revision description as a string"""
525    author = tounicode(username(fctx.user()))
526    rev = fctx.linkrev()
527    # If the source path matches the current path, don't bother including it.
528    if curpath and curpath == fctx.path():
529        source = ''
530    else:
531        source = '(%s)' % fctx.path()
532    date = age(fctx.date())
533    l = tounicode(fctx.description()).replace('\0', '').splitlines()
534    summary = l and l[0] or ''
535    return '%s@%s%s:%s "%s"' % (author, rev, source, date, summary)
536
537def validate_synch_path(path, repo):
538    '''
539    Validate the path that must be used to sync operations (pull,
540    push, outgoing and incoming)
541    '''
542    return_path = path
543    for alias, path_aux in repo.ui.configitems('paths'):
544        if path == alias:
545            return_path = path_aux
546        elif path == url.hidepassword(path_aux):
547            return_path = path_aux
548    return return_path
549
550def get_repo_bookmarks(repo, values=False):
551    """
552    Will return the bookmarks for the given repo if the
553    bookmarks extension is loaded.
554
555    By default, returns a list of bookmark names; if
556    values is True, returns a dict mapping names to
557    nodes.
558
559    If the extension is not loaded, returns an empty
560    list/dict.
561    """
562    try:
563        bookmarks = extensions.find('bookmarks')
564    except KeyError:
565        return values and {} or []
566    if bookmarks:
567        # Bookmarks changed from 1.4 to 1.5...
568        if hasattr(bookmarks, 'parse'):
569            marks = bookmarks.parse(repo)
570        elif hasattr(repo, '_bookmarks'):
571            marks = repo._bookmarks
572        else:
573            marks = {}
574    else:
575        marks = {}
576
577    if values:
578        return marks
579    else:
580        return marks.keys()
581
582def get_repo_bookmarkcurrent(repo):
583    """
584    Will return the current bookmark for the given repo
585    if the bookmarks extension is loaded, and the
586    track.current option is on.
587
588    If the extension is not loaded, or track.current
589    is not set, returns None
590    """
591    try:
592        bookmarks = extensions.find('bookmarks')
593    except KeyError:
594        return None
595    if bookmarks and repo.ui.configbool('bookmarks', 'track.current'):
596        # Bookmarks changed from 1.4 to 1.5...
597        if hasattr(bookmarks, 'current'):
598            return bookmarks.current(repo)
599        elif hasattr(repo, '_bookmarkcurrent'):
600            return repo._bookmarkcurrent
601    return None
602
603def is_rev_current(repo, rev):
604    '''
605    Returns True if the revision indicated by 'rev' is the current
606    working directory parent.
607
608    If rev is '' or None, it is assumed to mean 'tip'.
609    '''
610    if rev in ('', None):
611        rev = 'tip'
612    rev = repo.lookup(rev)
613    parents = repo.parents()
614
615    if len(parents) > 1:
616        return False
617
618    return rev == parents[0].node()
619
620def is_descriptor(obj, attr):
621    """
622    Returns True if obj.attr is a descriptor - ie, accessing
623    the attribute will actually invoke the '__get__' method of
624    some object.
625
626    Returns False if obj.attr exists, but is not a descriptor,
627    and None if obj.attr was not found at all.
628    """
629    for cls in inspect.getmro(obj.__class__):
630        if attr in cls.__dict__:
631            return hasattr(cls.__dict__[attr], '__get__')
632    return None
633
634def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
635           opts=None):
636    '''
637    export changesets as hg patches.
638
639    Mercurial moved patch.export to cmdutil.export after version 1.5
640    (change e764f24a45ee in mercurial).
641    '''
642
643    try:
644        return cmdutil.export(repo, revs, template, fp, switch_parent, opts)
645    except AttributeError:
646        from mercurial import patch
647        return patch.export(repo, revs, template, fp, switch_parent, opts)