PageRenderTime 109ms CodeModel.GetById 41ms app.highlight 63ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/thgrepo.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 566 lines | 542 code | 9 blank | 15 comment | 32 complexity | 991ef6197af9c844df510a68427a7950 MD5 | raw file
  1# thgrepo.py - TortoiseHg additions to key Mercurial classes
  2#
  3# Copyright 2010 George Marrows <george.marrows@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# See mercurial/extensions.py, comments to wrapfunction, for this approach
  9# to extending repositories and change contexts.
 10
 11import os
 12import sys
 13import shutil
 14import tempfile
 15
 16from PyQt4.QtCore import *
 17
 18from mercurial import hg, util, error, bundlerepo, extensions, filemerge, node
 19from mercurial import ui as uimod
 20from mercurial.util import propertycache
 21
 22from tortoisehg.util import hglib
 23from tortoisehg.util.patchctx import patchctx
 24
 25_repocache = {}
 26
 27if 'THGDEBUG' in os.environ:
 28    def dbgoutput(*args):
 29        sys.stdout.write(' '.join([str(a) for a in args])+'\n')
 30else:
 31    def dbgoutput(*args):
 32        pass
 33
 34def repository(_ui=None, path='', create=False, bundle=None):
 35    '''Returns a subclassed Mercurial repository to which new
 36    THG-specific methods have been added. The repository object
 37    is obtained using mercurial.hg.repository()'''
 38    if bundle:
 39        if _ui is None:
 40            _ui = uimod.ui()
 41        repo = bundlerepo.bundlerepository(_ui, path, bundle)
 42        repo._pyqtobj = ThgRepoWrapper(repo)
 43        repo.__class__ = _extendrepo(repo)
 44        return repo
 45    if create or path not in _repocache:
 46        if _ui is None:
 47            _ui = uimod.ui()
 48        repo = hg.repository(_ui, path, create)
 49        repo._pyqtobj = ThgRepoWrapper(repo)
 50        repo.__class__ = _extendrepo(repo)
 51        _repocache[path] = repo
 52        return repo
 53    if not os.path.exists(os.path.join(path, '.hg/')):
 54        del _repocache[path]
 55        # this error must be in local encoding
 56        raise error.RepoError('%s is not a valid repository' % path)
 57    return _repocache[path]
 58
 59class ThgRepoWrapper(QObject):
 60
 61    configChanged = pyqtSignal()
 62    repositoryChanged = pyqtSignal()
 63    repositoryDestroyed = pyqtSignal()
 64    workingBranchChanged = pyqtSignal()
 65
 66    def __init__(self, repo):
 67        QObject.__init__(self)
 68        self.repo = repo
 69        self.busycount = 0
 70        repo.configChanged = self.configChanged
 71        repo.repositoryChanged = self.repositoryChanged
 72        repo.workingBranchChanged = self.workingBranchChanged
 73        repo.repositoryDestroyed = self.repositoryDestroyed
 74        self.recordState()
 75        try:
 76            freq = repo.ui.config('tortoisehg', 'pollfreq', '500')
 77            freq = max(100, int(freq))
 78        except:
 79            freq = 500
 80        self._timerevent = self.startTimer(freq)
 81
 82    def timerEvent(self, event):
 83        if not os.path.exists(self.repo.path):
 84            dbgoutput('Repository destroyed', self.repo.root)
 85            self.repositoryDestroyed.emit()
 86            self.killTimer(self._timerevent)
 87            if self.repo.root in _repocache:
 88                del _repocache[self.repo.root]
 89        elif self.busycount == 0:
 90            self.pollStatus()
 91        else:
 92            dbgoutput('no poll, busy', self.busycount)
 93
 94    def pollStatus(self):
 95        if not os.path.exists(self.repo.path) or self.locked():
 96            return
 97        if self._checkdirstate():
 98            return
 99        self._checkrepotime()
100        self._checkuimtime()
101
102    def locked(self):
103        if os.path.lexists(self.repo.join('wlock')):
104            return True
105        if os.path.lexists(self.repo.sjoin('lock')):
106            return True
107        return False
108
109    def recordState(self):
110        try:
111            self._parentnodes = self._getrawparents()
112            self._repomtime = self._getrepomtime()
113            self._dirstatemtime = os.path.getmtime(self.repo.join('dirstate'))
114            self._branchmtime = os.path.getmtime(self.repo.join('branch'))
115            self._rawbranch = self.repo.opener('branch').read()
116        except EnvironmentError, ValueError:
117            self._dirstatemtime = None
118            self._branchmtime = None
119            self._rawbranch = None
120
121    def _getrawparents(self):
122        try:
123            return self.repo.opener('dirstate').read(40)
124        except EnvironmentError:
125            return None
126
127    def _getrepomtime(self):
128        'Return the last modification time for the repo'
129        watchedfiles = [self.repo.sjoin('00changelog.i')]
130        if hasattr(self.repo, 'mq'):
131            watchedfiles.append(self.repo.mq.join('series'))
132            watchedfiles.append(self.repo.mq.join('guards'))
133            watchedfiles.append(self.repo.join('patches.queue'))
134        try:
135            mtime = [os.path.getmtime(wf) for wf in watchedfiles \
136                     if os.path.isfile(wf)]
137            if mtime:
138                return max(mtime)
139        except EnvironmentError:
140            return None
141
142    def _checkrepotime(self):
143        'Check for new changelog entries, or MQ status changes'
144        if self._repomtime < self._getrepomtime():
145            dbgoutput('detected repository change')
146            if self.locked():
147                dbgoutput('lock still held - ignoring for now')
148                return
149            self.recordState()
150            self.repo.thginvalidate()
151            self.repositoryChanged.emit()
152
153    def _checkdirstate(self):
154        'Check for new dirstate mtime, then working parent changes'
155        try:
156            mtime = os.path.getmtime(self.repo.join('dirstate'))
157        except EnvironmentError:
158            return False
159        if mtime <= self._dirstatemtime:
160            return False
161        self._dirstatemtime = mtime
162        nodes = self._getrawparents()
163        if nodes != self._parentnodes:
164            dbgoutput('dirstate change found')
165            if self.locked():
166                dbgoutput('lock still held - ignoring for now')
167                return True
168            self.recordState()
169            self.repo.thginvalidate()
170            self.repositoryChanged.emit()
171            return True
172        try:
173            mtime = os.path.getmtime(self.repo.join('branch'))
174        except EnvironmentError:
175            return False
176        if mtime <= self._branchmtime:
177            return False
178        self._branchmtime = mtime
179        try:
180            newbranch = self.repo.opener('branch').read()
181        except EnvironmentError:
182            return False
183        if newbranch != self._rawbranch:
184            dbgoutput('branch time change')
185            if self.locked():
186                dbgoutput('lock still held - ignoring for now')
187                return True
188            self._rawbranch = newbranch
189            self.repo.thginvalidate()
190            self.workingBranchChanged.emit()
191            return True
192        return False
193
194    def _checkuimtime(self):
195        'Check for modified config files, or a new .hg/hgrc file'
196        try:
197            oldmtime, files = self.repo.uifiles()
198            mtime = [os.path.getmtime(f) for f in files if os.path.isfile(f)]
199            if max(mtime) > oldmtime:
200                dbgoutput('config change detected')
201                self.repo.invalidateui()
202                self.configChanged.emit()
203        except (EnvironmentError, ValueError):
204            pass
205
206_uiprops = '''_uifiles _uimtime _shell postpull tabwidth maxdiff
207              deadbranches _exts _thghiddentags displayname summarylen
208              shortname mergetools bookmarks bookmarkcurrent'''.split()
209_thgrepoprops = '''_thgmqpatchnames thgmqunappliedpatches
210                   _branchheads'''.split()
211
212def _extendrepo(repo):
213    class thgrepository(repo.__class__):
214
215        def changectx(self, changeid):
216            '''Extends Mercurial's standard changectx() method to
217            a) return a thgchangectx with additional methods
218            b) return a patchctx if changeid is the name of an MQ
219            unapplied patch
220            c) return a patchctx if changeid is an absolute patch path
221            '''
222
223            # Mercurial's standard changectx() (rather, lookup())
224            # implies that tags and branch names live in the same namespace.
225            # This code throws patch names in the same namespace, but as
226            # applied patches have a tag that matches their patch name this
227            # seems safe.
228            if changeid in self.thgmqunappliedpatches:
229                q = self.mq # must have mq to pass the previous if
230                return genPatchContext(self, q.join(changeid), rev=changeid)
231            elif type(changeid) is str and os.path.isabs(changeid) and \
232                    os.path.isfile(changeid):
233                return genPatchContext(repo, changeid)
234
235            changectx = super(thgrepository, self).changectx(changeid)
236            changectx.__class__ = _extendchangectx(changectx)
237            return changectx
238
239        @propertycache
240        def _thghiddentags(self):
241            ht = self.ui.config('tortoisehg', 'hidetags', '')
242            return [t.strip() for t in ht.split()]
243
244        @propertycache
245        def thgmqunappliedpatches(self):
246            '''Returns a list of (patch name, patch path) of all self's
247            unapplied MQ patches, in patch series order, first unapplied
248            patch first.'''
249            if not hasattr(self, 'mq'): return []
250
251            q = self.mq
252            applied = set([p.name for p in q.applied])
253
254            return [pname for pname in q.series if not pname in applied]
255
256        @propertycache
257        def _thgmqpatchnames(self):
258            '''Returns all tag names used by MQ patches. Returns []
259            if MQ not in use.'''
260            if not hasattr(self, 'mq'): return []
261
262            self.mq.parse_series()
263            return self.mq.series[:]
264
265        @propertycache
266        def _shell(self):
267            s = self.ui.config('tortoisehg', 'shell')
268            if s:
269                return s
270            if sys.platform == 'darwin':
271                return None # Terminal.App does not support open-to-folder
272            elif os.name == 'nt':
273                return 'cmd.exe'
274            else:
275                return 'xterm'
276
277        @propertycache
278        def _uifiles(self):
279            cfg = self.ui._ucfg
280            files = set()
281            for line in cfg._source.values():
282                f = line.rsplit(':', 1)[0]
283                files.add(f)
284            files.add(self.join('hgrc'))
285            return files
286
287        @propertycache
288        def _uimtime(self):
289            mtimes = [0] # zero will be taken if no config files
290            for f in self._uifiles:
291                try:
292                    if os.path.exists(f):
293                        mtimes.append(os.path.getmtime(f))
294                except EnvironmentError:
295                    pass
296            return max(mtimes)
297
298        @propertycache
299        def _exts(self):
300            lclexts = []
301            allexts = [n for n,m in extensions.extensions()]
302            for name, path in self.ui.configitems('extensions'):
303                if name.startswith('hgext.'):
304                    name = name[6:]
305                if name in allexts:
306                    lclexts.append(name)
307            return lclexts
308
309        @propertycache
310        def postpull(self):
311            pp = self.ui.config('tortoisehg', 'postpull')
312            if pp in ('rebase', 'update', 'fetch'):
313                return pp
314            return 'none'
315
316        @propertycache
317        def tabwidth(self):
318            tw = self.ui.config('tortoisehg', 'tabwidth')
319            try:
320                tw = int(tw)
321                tw = min(tw, 16)
322                return max(tw, 2)
323            except (ValueError, TypeError):
324                return 8
325
326        @propertycache
327        def maxdiff(self):
328            maxdiff = self.ui.config('tortoisehg', 'maxdiff')
329            try:
330                maxdiff = int(maxdiff)
331                if maxdiff < 1:
332                    return sys.maxint
333            except (ValueError, TypeError):
334                maxdiff = 1024 # 1MB by default
335            return maxdiff * 1024
336
337        @propertycache
338        def summarylen(self):
339            slen = self.ui.config('tortoisehg', 'summarylen')
340            try:
341                slen = int(slen)
342                if slen < 10:
343                    return 80
344            except (ValueError, TypeError):
345                slen = 80
346            return slen
347
348        @propertycache
349        def deadbranches(self):
350            db = self.ui.config('tortoisehg', 'deadbranch', '')
351            return [b.strip() for b in db.split(',')]
352
353        @propertycache
354        def displayname(self):
355            'Display name is for window titles and similar'
356            if self.ui.config('tortoisehg', 'fullpath', False):
357                name = self.root
358            elif self.ui.config('web', 'name', False):
359                name = self.ui.config('web', 'name')
360            else:
361                name = os.path.basename(self.root)
362            return hglib.tounicode(name)
363
364        @propertycache
365        def shortname(self):
366            'Short name is for tables, tabs, and sentences'
367            if self.ui.config('web', 'name', False):
368                name = self.ui.config('web', 'name')
369            else:
370                name = os.path.basename(self.root)
371            return hglib.tounicode(name)
372
373        @propertycache
374        def mergetools(self):
375            seen, installed = [], []
376            for key, value in self.ui.configitems('merge-tools'):
377                t = key.split('.')[0]
378                if t not in seen:
379                    seen.append(t)
380                    if filemerge._findtool(self.ui, t):
381                        installed.append(t)
382            return installed
383
384        @propertycache
385        def namedbranches(self):
386            allbranches = self.branchtags()
387            openbrnodes = []
388            for br in allbranches.iterkeys():
389                openbrnodes.extend(self.branchheads(br, closed=False))
390            dead = self.deadbranches
391            return sorted(br for br, n in allbranches.iteritems()
392                          if n in openbrnodes and br not in dead)
393
394        @propertycache
395        def bookmarks(self):
396            if 'bookmarks' in self._exts and hasattr(self, '_bookmarks'):
397                return self._bookmarks
398            else:
399                return {}
400
401        @propertycache
402        def bookmarkcurrent(self):
403            if 'bookmarks' in self._exts and hasattr(self, '_bookmarkcurrent'):
404                return self._bookmarkcurrent
405            else:
406                return None
407
408        @propertycache
409        def _branchheads(self):
410            return [self.changectx(x) for x in self.branchmap()]
411
412        def shell(self):
413            'Returns terminal shell configured for this repo'
414            return self._shell
415
416        def uifiles(self):
417            'Returns latest mtime and complete list of config files'
418            return self._uimtime, self._uifiles
419
420        def extensions(self):
421            'Returns list of extensions enabled in this repository'
422            return self._exts
423
424        def thgmqtag(self, tag):
425            'Returns true if `tag` marks an applied MQ patch'
426            return tag in self._thgmqpatchnames
427
428        def getcurrentqqueue(self):
429            'Returns the name of the current MQ queue'
430            if 'mq' not in self._exts:
431                return None
432            cur = os.path.basename(self.mq.path)
433            if cur.startswith('patches-'):
434                cur = cur[8:]
435            return cur
436
437        def thgshelves(self):
438            self.shelfdir = sdir = self.join('shelves')
439            if os.path.isdir(sdir):
440                return os.listdir(sdir)
441            return []
442
443        def thginvalidate(self):
444            'Should be called when mtime of repo store/dirstate are changed'
445            self.dirstate.invalidate()
446            if not isinstance(repo, bundlerepo.bundlerepository):
447                self.invalidate()
448            # mq.queue.invalidate does not handle queue changes, so force
449            # the queue object to be rebuilt
450            if 'mq' in self.__dict__:
451                delattr(self, 'mq')
452            for a in _thgrepoprops + _uiprops:
453                if a in self.__dict__:
454                    delattr(self, a)
455
456        def invalidateui(self):
457            'Should be called when mtime of ui files are changed'
458            self.ui = uimod.ui()
459            self.ui.readconfig(self.join('hgrc'))
460            for a in _uiprops:
461                if a in self.__dict__:
462                    delattr(self, a)
463
464        def incrementBusyCount(self):
465            'A GUI widget is starting a transaction'
466            self._pyqtobj.busycount += 1
467
468        def decrementBusyCount(self):
469            'A GUI widget has finished a transaction'
470            self._pyqtobj.busycount -= 1
471            if self._pyqtobj.busycount == 0:
472                self._pyqtobj.pollStatus()
473            else:
474                # A lot of logic will depend on invalidation happening
475                # within the context of this call.  Signals will not be
476                # emitted till later, but we at least invalidate cached
477                # data in the repository
478                self.thginvalidate()
479
480        def thgbackup(self, path):
481            'Make a backup of the given file in the repository "trashcan"'
482            trashcan = self.join('Trashcan')
483            if not os.path.isdir(trashcan):
484                os.mkdir(trashcan)
485            if not os.path.exists(path):
486                return
487            name = os.path.basename(path)
488            root, ext = os.path.splitext(name)
489            dest = tempfile.mktemp(ext, root+'_', trashcan)
490            shutil.copyfile(path, dest)
491
492    return thgrepository
493
494
495def _extendchangectx(changectx):
496    class thgchangectx(changectx.__class__):
497        def thgtags(self):
498            '''Returns all unhidden tags for self'''
499            htlist = self._repo._thghiddentags
500            return [tag for tag in self.tags() if tag not in htlist]
501
502        def thgwdparent(self):
503            '''True if self is a parent of the working directory'''
504            return self.rev() in [ctx.rev() for ctx in self._repo.parents()]
505
506        def _thgmqpatchtags(self):
507            '''Returns the set of self's tags which are MQ patch names'''
508            mytags = set(self.tags())
509            patchtags = self._repo._thgmqpatchnames
510            result = mytags.intersection(patchtags)
511            assert len(result) <= 1, "thgmqpatchname: rev has more than one tag in series"
512            return result
513
514        def thgmqappliedpatch(self):
515            '''True if self is an MQ applied patch'''
516            return self.rev() is not None and bool(self._thgmqpatchtags())
517
518        def thgmqunappliedpatch(self):
519            return False
520
521        def thgmqpatchname(self):
522            '''Return self's MQ patch name. AssertionError if self not an MQ patch'''
523            patchtags = self._thgmqpatchtags()
524            assert len(patchtags) == 1, "thgmqpatchname: called on non-mq patch"
525            return list(patchtags)[0]
526
527        def thgbranchhead(self):
528            '''True if self is a branch head'''
529            return self in self._repo._branchheads
530
531        def changesToParent(self, whichparent):
532            parent = self.parents()[whichparent]
533            return self._repo.status(parent.node(), self.node())[:3]
534
535        def longsummary(self):
536            summary = hglib.tounicode(self.description())
537            if self._repo.ui.configbool('tortoisehg', 'longsummary'):
538                limit = 80
539                lines = summary.splitlines()
540                if lines:
541                    summary = lines.pop(0)
542                    while len(summary) < limit and lines:
543                        summary += u'  ' + lines.pop(0)
544                    summary = summary[0:limit]
545                else:
546                    summary = ''
547            else:
548                lines = summary.splitlines()
549                summary = lines and lines[0] or ''
550            return summary
551
552    return thgchangectx
553
554
555
556_pctxcache = {}
557def genPatchContext(repo, patchpath, rev=None):
558    global _pctxcache
559    if os.path.exists(patchpath) and patchpath in _pctxcache:
560        cachedctx = _pctxcache[patchpath]
561        if cachedctx._mtime == os.path.getmtime(patchpath):
562            return cachedctx
563    # create a new context object
564    ctx = patchctx(patchpath, repo, rev=rev)
565    _pctxcache[patchpath] = ctx
566    return ctx