/tortoisehg/util/hglib.py

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