/tortoisehg/hgqt/thgrepo.py

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