PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/.hgext/hg-prompt/prompt.py

https://bitbucket.org/flowblok/dotfiles
Python | 617 lines | 574 code | 34 blank | 9 comment | 14 complexity | ad8027a0103a8abfc0ea32f2af411672 MD5 | raw file
Possible License(s): WTFPL
  1. #!/usr/bin/env python
  2. from __future__ import with_statement
  3. '''get repository information for use in a shell prompt
  4. Take a string, parse any special variables inside, and output the result.
  5. Useful mostly for putting information about the current repository into
  6. a shell prompt.
  7. '''
  8. import re
  9. import os
  10. import subprocess
  11. from datetime import datetime, timedelta
  12. from os import path
  13. from mercurial import extensions, commands, cmdutil, help
  14. from mercurial.node import hex, short
  15. CACHE_PATH = ".hg/prompt/cache"
  16. CACHE_TIMEOUT = timedelta(minutes=15)
  17. FILTER_ARG = re.compile(r'\|.+\((.*)\)')
  18. def _cache_remote(repo, kind):
  19. cache = path.join(repo.root, CACHE_PATH, kind)
  20. c_tmp = cache + '.temp'
  21. # This is kind of a hack and I feel a little bit dirty for doing it.
  22. IGNORE = open('NUL:','w') if subprocess.mswindows else open('/dev/null','w')
  23. subprocess.call(['hg', kind, '--quiet'], stdout=file(c_tmp, 'w'), stderr=IGNORE)
  24. os.rename(c_tmp, cache)
  25. return
  26. def _with_groups(groups, out):
  27. out_groups = [groups[0]] + [groups[-1]]
  28. if any(out_groups) and not all(out_groups):
  29. print 'Error parsing prompt string. Mismatched braces?'
  30. out = out.replace('%', '%%')
  31. return ("%s" + out + "%s") % (out_groups[0][:-1] if out_groups[0] else '',
  32. out_groups[1][1:] if out_groups[1] else '')
  33. def _get_filter(name, g):
  34. '''Return the filter with the given name, or None if it was not used.'''
  35. matching_filters = filter(lambda s: s and s.startswith('|%s' % name), g)
  36. if not matching_filters:
  37. return None
  38. # Later filters will override earlier ones, for now.
  39. f = matching_filters[-1]
  40. return f
  41. def _get_filter_arg(f):
  42. if not f:
  43. return None
  44. args = FILTER_ARG.match(f).groups()
  45. if args:
  46. return args[0]
  47. else:
  48. return None
  49. def prompt(ui, repo, fs='', **opts):
  50. '''get repository information for use in a shell prompt
  51. Take a string and output it for use in a shell prompt. You can use
  52. keywords in curly braces::
  53. $ hg prompt "currently on {branch}"
  54. currently on default
  55. You can also use an extended form of any keyword::
  56. {optional text here{keyword}more optional text}
  57. This will expand the inner {keyword} and output it along with the extra
  58. text only if the {keyword} expands successfully. This is useful if you
  59. have a keyword that may not always apply to the current state and you
  60. have some text that you would like to see only if it is appropriate::
  61. $ hg prompt "currently at {bookmark}"
  62. currently at
  63. $ hg prompt "{currently at {bookmark}}"
  64. $ hg bookmark my-bookmark
  65. $ hg prompt "{currently at {bookmark}}"
  66. currently at my-bookmark
  67. See 'hg help prompt-keywords' for a list of available keywords.
  68. '''
  69. def _basename(m):
  70. return _with_groups(m.groups(), path.basename(repo.root)) if repo.root else ''
  71. def _bookmark(m):
  72. try:
  73. book = extensions.find('bookmarks').current(repo)
  74. except AttributeError:
  75. book = getattr(repo, '_bookmarkcurrent', None)
  76. except KeyError:
  77. book = getattr(repo, '_bookmarkcurrent', None)
  78. return _with_groups(m.groups(), book) if book else ''
  79. def _branch(m):
  80. g = m.groups()
  81. branch = repo.dirstate.branch()
  82. quiet = _get_filter('quiet', g)
  83. out = branch if (not quiet) or (branch != 'default') else ''
  84. return _with_groups(g, out) if out else ''
  85. def _closed(m):
  86. g = m.groups()
  87. quiet = _get_filter('quiet', g)
  88. p = repo[None].parents()[0]
  89. pn = p.node()
  90. branch = repo.dirstate.branch()
  91. closed = (p.extra().get('close')
  92. and pn in repo.branchheads(branch, closed=True))
  93. out = 'X' if (not quiet) and closed else ''
  94. return _with_groups(g, out) if out else ''
  95. def _count(m):
  96. g = m.groups()
  97. query = [g[1][1:]] if g[1] else ['all()']
  98. return _with_groups(g, str(len(cmdutil.revrange(repo, query))))
  99. def _node(m):
  100. g = m.groups()
  101. parents = repo[None].parents()
  102. p = 0 if '|merge' not in g else 1
  103. p = p if len(parents) > p else None
  104. format = short if '|short' in g else hex
  105. node = format(parents[p].node()) if p is not None else None
  106. return _with_groups(g, str(node)) if node else ''
  107. def _patch(m):
  108. g = m.groups()
  109. try:
  110. extensions.find('mq')
  111. except KeyError:
  112. return ''
  113. q = repo.mq
  114. if _get_filter('quiet', g) and not len(q.series):
  115. return ''
  116. if _get_filter('topindex', g):
  117. if len(q.applied):
  118. out = str(len(q.applied) - 1)
  119. else:
  120. out = ''
  121. elif _get_filter('applied', g):
  122. out = str(len(q.applied))
  123. elif _get_filter('unapplied', g):
  124. out = str(len(q.unapplied(repo)))
  125. elif _get_filter('count', g):
  126. out = str(len(q.series))
  127. else:
  128. out = q.applied[-1].name if q.applied else ''
  129. return _with_groups(g, out) if out else ''
  130. def _patches(m):
  131. g = m.groups()
  132. try:
  133. extensions.find('mq')
  134. except KeyError:
  135. return ''
  136. join_filter = _get_filter('join', g)
  137. join_filter_arg = _get_filter_arg(join_filter)
  138. sep = join_filter_arg if join_filter else ' -> '
  139. patches = repo.mq.series
  140. applied = [p.name for p in repo.mq.applied]
  141. unapplied = filter(lambda p: p not in applied, patches)
  142. if _get_filter('hide_applied', g):
  143. patches = filter(lambda p: p not in applied, patches)
  144. if _get_filter('hide_unapplied', g):
  145. patches = filter(lambda p: p not in unapplied, patches)
  146. if _get_filter('reverse', g):
  147. patches = reversed(patches)
  148. pre_applied_filter = _get_filter('pre_applied', g)
  149. pre_applied_filter_arg = _get_filter_arg(pre_applied_filter)
  150. post_applied_filter = _get_filter('post_applied', g)
  151. post_applied_filter_arg = _get_filter_arg(post_applied_filter)
  152. pre_unapplied_filter = _get_filter('pre_unapplied', g)
  153. pre_unapplied_filter_arg = _get_filter_arg(pre_unapplied_filter)
  154. post_unapplied_filter = _get_filter('post_unapplied', g)
  155. post_unapplied_filter_arg = _get_filter_arg(post_unapplied_filter)
  156. for n, patch in enumerate(patches):
  157. if patch in applied:
  158. if pre_applied_filter:
  159. patches[n] = pre_applied_filter_arg + patches[n]
  160. if post_applied_filter:
  161. patches[n] = patches[n] + post_applied_filter_arg
  162. elif patch in unapplied:
  163. if pre_unapplied_filter:
  164. patches[n] = pre_unapplied_filter_arg + patches[n]
  165. if post_unapplied_filter:
  166. patches[n] = patches[n] + post_unapplied_filter_arg
  167. return _with_groups(g, sep.join(patches)) if patches else ''
  168. def _queue(m):
  169. g = m.groups()
  170. try:
  171. extensions.find('mq')
  172. except KeyError:
  173. return ''
  174. q = repo.mq
  175. out = os.path.basename(q.path)
  176. if out == 'patches' and not os.path.isdir(q.path):
  177. out = ''
  178. elif out.startswith('patches-'):
  179. out = out[8:]
  180. return _with_groups(g, out) if out else ''
  181. def _remote(kind):
  182. def _r(m):
  183. g = m.groups()
  184. cache_dir = path.join(repo.root, CACHE_PATH)
  185. cache = path.join(cache_dir, kind)
  186. if not path.isdir(cache_dir):
  187. os.makedirs(cache_dir)
  188. cache_exists = path.isfile(cache)
  189. cache_time = (datetime.fromtimestamp(os.stat(cache).st_mtime)
  190. if cache_exists else None)
  191. if not cache_exists or cache_time < datetime.now() - CACHE_TIMEOUT:
  192. if not cache_exists:
  193. open(cache, 'w').close()
  194. subprocess.Popen(['hg', 'prompt', '--cache-%s' % kind])
  195. if cache_exists:
  196. with open(cache) as c:
  197. count = len(c.readlines())
  198. if g[1]:
  199. return _with_groups(g, str(count)) if count else ''
  200. else:
  201. return _with_groups(g, '') if count else ''
  202. else:
  203. return ''
  204. return _r
  205. def _rev(m):
  206. g = m.groups()
  207. parents = repo[None].parents()
  208. parent = 0 if '|merge' not in g else 1
  209. parent = parent if len(parents) > parent else None
  210. rev = parents[parent].rev() if parent is not None else -1
  211. return _with_groups(g, str(rev)) if rev >= 0 else ''
  212. def _root(m):
  213. return _with_groups(m.groups(), repo.root) if repo.root else ''
  214. def _status(m):
  215. g = m.groups()
  216. st = repo.status(unknown=True)[:5]
  217. modified = any(st[:4])
  218. unknown = len(st[-1]) > 0
  219. flag = ''
  220. if '|modified' not in g and '|unknown' not in g:
  221. flag = '!' if modified else '?' if unknown else ''
  222. else:
  223. if '|modified' in g:
  224. flag += '!' if modified else ''
  225. if '|unknown' in g:
  226. flag += '?' if unknown else ''
  227. return _with_groups(g, flag) if flag else ''
  228. def _tags(m):
  229. g = m.groups()
  230. sep = g[1][1:] if g[1] else ' '
  231. tags = repo[None].tags()
  232. return _with_groups(g, sep.join(tags)) if tags else ''
  233. def _task(m):
  234. try:
  235. task = extensions.find('tasks').current(repo)
  236. return _with_groups(m.groups(), task) if task else ''
  237. except KeyError:
  238. return ''
  239. def _tip(m):
  240. g = m.groups()
  241. format = short if '|short' in g else hex
  242. tip = repo[len(repo) - 1]
  243. rev = tip.rev()
  244. tip = format(tip.node()) if '|node' in g else tip.rev()
  245. return _with_groups(g, str(tip)) if rev >= 0 else ''
  246. def _update(m):
  247. if not repo.branchtags():
  248. # We are in an empty repository.
  249. return ''
  250. current_rev = repo[None].parents()[0]
  251. to = repo[repo.branchtags()[current_rev.branch()]]
  252. return _with_groups(m.groups(), '^') if current_rev != to else ''
  253. if opts.get("angle_brackets"):
  254. tag_start = r'\<([^><]*?\<)?'
  255. tag_end = r'(\>[^><]*?)?>'
  256. brackets = '<>'
  257. else:
  258. tag_start = r'\{([^{}]*?\{)?'
  259. tag_end = r'(\}[^{}]*?)?\}'
  260. brackets = '{}'
  261. patterns = {
  262. 'bookmark': _bookmark,
  263. 'branch(\|quiet)?': _branch,
  264. 'closed(\|quiet)?': _closed,
  265. 'count(\|[^%s]*?)?' % brackets[-1]: _count,
  266. 'node(?:'
  267. '(\|short)'
  268. '|(\|merge)'
  269. ')*': _node,
  270. 'patch(?:'
  271. '(\|topindex)'
  272. '|(\|applied)'
  273. '|(\|unapplied)'
  274. '|(\|count)'
  275. '|(\|quiet)'
  276. ')*': _patch,
  277. 'patches(?:' +
  278. '(\|join\([^%s]*?\))' % brackets[-1] +
  279. '|(\|reverse)' +
  280. '|(\|hide_applied)' +
  281. '|(\|hide_unapplied)' +
  282. '|(\|pre_applied\([^%s]*?\))' % brackets[-1] +
  283. '|(\|post_applied\([^%s]*?\))' % brackets[-1] +
  284. '|(\|pre_unapplied\([^%s]*?\))' % brackets[-1] +
  285. '|(\|post_unapplied\([^%s]*?\))' % brackets[-1] +
  286. ')*': _patches,
  287. 'queue': _queue,
  288. 'rev(\|merge)?': _rev,
  289. 'root': _root,
  290. 'root\|basename': _basename,
  291. 'status(?:'
  292. '(\|modified)'
  293. '|(\|unknown)'
  294. ')*': _status,
  295. 'tags(\|[^%s]*?)?' % brackets[-1]: _tags,
  296. 'task': _task,
  297. 'tip(?:'
  298. '(\|node)'
  299. '|(\|short)'
  300. ')*': _tip,
  301. 'update': _update,
  302. 'incoming(\|count)?': _remote('incoming'),
  303. 'outgoing(\|count)?': _remote('outgoing'),
  304. }
  305. if opts.get("cache_incoming"):
  306. _cache_remote(repo, 'incoming')
  307. if opts.get("cache_outgoing"):
  308. _cache_remote(repo, 'outgoing')
  309. for tag, repl in patterns.items():
  310. fs = re.sub(tag_start + tag + tag_end, repl, fs)
  311. ui.status(fs)
  312. def _pull_with_cache(orig, ui, repo, *args, **opts):
  313. """Wrap the pull command to delete the incoming cache as well."""
  314. res = orig(ui, repo, *args, **opts)
  315. cache = path.join(repo.root, CACHE_PATH, 'incoming')
  316. if path.isfile(cache):
  317. os.remove(cache)
  318. return res
  319. def _push_with_cache(orig, ui, repo, *args, **opts):
  320. """Wrap the push command to delete the outgoing cache as well."""
  321. res = orig(ui, repo, *args, **opts)
  322. cache = path.join(repo.root, CACHE_PATH, 'outgoing')
  323. if path.isfile(cache):
  324. os.remove(cache)
  325. return res
  326. def uisetup(ui):
  327. extensions.wrapcommand(commands.table, 'pull', _pull_with_cache)
  328. extensions.wrapcommand(commands.table, 'push', _push_with_cache)
  329. try:
  330. extensions.wrapcommand(extensions.find("fetch").cmdtable, 'fetch', _pull_with_cache)
  331. except KeyError:
  332. pass
  333. cmdtable = {
  334. "prompt":
  335. (prompt, [
  336. ('', 'angle-brackets', None, 'use angle brackets (<>) for keywords'),
  337. ('', 'cache-incoming', None, 'used internally by hg-prompt'),
  338. ('', 'cache-outgoing', None, 'used internally by hg-prompt'),
  339. ],
  340. 'hg prompt STRING')
  341. }
  342. help.helptable += (
  343. (['prompt-keywords', 'prompt-keywords'], ('Keywords supported by hg-prompt'),
  344. (r'''hg-prompt currently supports a number of keywords.
  345. Some keywords support filters. Filters can be chained when it makes
  346. sense to do so. When in doubt, try it!
  347. bookmark
  348. Display the current bookmark (requires the bookmarks extension).
  349. branch
  350. Display the current branch.
  351. |quiet
  352. Display the current branch only if it is not the default branch.
  353. closed
  354. Display `X` if working on a closed branch (i.e. committing now would reopen
  355. the branch).
  356. count
  357. Display the number of revisions in the given revset (the revset `all()`
  358. will be used if none is given).
  359. See `hg help revsets` for more information.
  360. |REVSET
  361. The revset to count.
  362. incoming
  363. Display nothing, but if the default path contains incoming changesets the
  364. extra text will be expanded.
  365. For example: `{incoming changes{incoming}}` will expand to
  366. `incoming changes` if there are changes, otherwise nothing.
  367. Checking for incoming changesets is an expensive operation, so `hg-prompt`
  368. will cache the results in `.hg/prompt/cache/` and refresh them every 15
  369. minutes.
  370. |count
  371. Display the number of incoming changesets (if greater than 0).
  372. node
  373. Display the (full) changeset hash of the current parent.
  374. |short
  375. Display the hash as the short, 12-character form.
  376. |merge
  377. Display the hash of the changeset you're merging with.
  378. outgoing
  379. Display nothing, but if the current repository contains outgoing
  380. changesets (to default) the extra text will be expanded.
  381. For example: `{outgoing changes{outgoing}}` will expand to
  382. `outgoing changes` if there are changes, otherwise nothing.
  383. Checking for outgoing changesets is an expensive operation, so `hg-prompt`
  384. will cache the results in `.hg/prompt/cache/` and refresh them every 15
  385. minutes.
  386. |count
  387. Display the number of outgoing changesets (if greater than 0).
  388. patch
  389. Display the topmost currently-applied patch (requires the mq
  390. extension).
  391. |count
  392. Display the number of patches in the queue.
  393. |topindex
  394. Display (zero-based) index of the topmost applied patch in the series
  395. list (as displayed by :hg:`qtop -v`), or the empty string if no patch
  396. is applied.
  397. |applied
  398. Display the number of currently applied patches in the queue.
  399. |unapplied
  400. Display the number of currently unapplied patches in the queue.
  401. |quiet
  402. Display a number only if there are any patches in the queue.
  403. patches
  404. Display a list of the current patches in the queue. It will look like
  405. this:
  406. :::console
  407. $ hg prompt '{patches}'
  408. bottom-patch -> middle-patch -> top-patch
  409. |reverse
  410. Display the patches in reverse order (i.e. topmost first).
  411. |hide_applied
  412. Do not display applied patches.
  413. |hide_unapplied
  414. Do not display unapplied patches.
  415. |join(SEP)
  416. Display SEP between each patch, instead of the default ` -> `.
  417. |pre_applied(STRING)
  418. Display STRING immediately before each applied patch. Useful for
  419. adding color codes.
  420. |post_applied(STRING)
  421. Display STRING immediately after each applied patch. Useful for
  422. resetting color codes.
  423. |pre_unapplied(STRING)
  424. Display STRING immediately before each unapplied patch. Useful for
  425. adding color codes.
  426. |post_unapplied(STRING)
  427. Display STRING immediately after each unapplied patch. Useful for
  428. resetting color codes.
  429. queue
  430. Display the name of the current MQ queue.
  431. rev
  432. Display the repository-local changeset number of the current parent.
  433. |merge
  434. Display the repository-local changeset number of the changeset you're
  435. merging with.
  436. root
  437. Display the full path to the root of the current repository, without a
  438. trailing slash.
  439. |basename
  440. Display the directory name of the root of the current repository. For
  441. example, if the repository is in `/home/u/myrepo` then this keyword
  442. would expand to `myrepo`.
  443. status
  444. Display `!` if the repository has any changed/added/removed files,
  445. otherwise `?` if it has any untracked (but not ignored) files, otherwise
  446. nothing.
  447. |modified
  448. Display `!` if the current repository contains files that have been
  449. modified, added, removed, or deleted, otherwise nothing.
  450. |unknown
  451. Display `?` if the current repository contains untracked files,
  452. otherwise nothing.
  453. tags
  454. Display the tags of the current parent, separated by a space.
  455. |SEP
  456. Display the tags of the current parent, separated by `SEP`.
  457. task
  458. Display the current task (requires the tasks extension).
  459. tip
  460. Display the repository-local changeset number of the current tip.
  461. |node
  462. Display the (full) changeset hash of the current tip.
  463. |short
  464. Display a short form of the changeset hash of the current tip (must be
  465. used with the **|node** filter)
  466. update
  467. Display `^` if the current parent is not the tip of the current branch,
  468. otherwise nothing. In effect, this lets you see if running `hg update`
  469. would do something.
  470. ''')),
  471. )