comparison prompt.py @ 64:39b07c5f8785

Add Git and Mercurial support to command prompt
author IBBoard <dev@ibboard.co.uk>
date Tue, 25 Apr 2017 11:42:15 +0100
parents
children 38843a80f378
comparison
equal deleted inserted replaced
63:327c85286b54 64:39b07c5f8785
1 #!/usr/bin/env python
2
3 '''get repository information for use in a shell prompt
4
5 Take a string, parse any special variables inside, and output the result.
6
7 Useful mostly for putting information about the current repository into
8 a shell prompt.
9
10
11
12 Taken from https://bitbucket.org/sjl/hg-prompt/src @ 5334581
13 '''
14
15 from __future__ import with_statement
16
17 import re
18 import os
19 import subprocess
20 from datetime import datetime, timedelta
21 from contextlib import closing
22 from os import path
23 from mercurial import extensions, commands, cmdutil, help
24 from mercurial.i18n import _
25 from mercurial.node import hex, short
26
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
29
30 # `revrange' has been moved into module `scmutil' since v1.9.
31 try :
32 from mercurial import scmutil
33 revrange = scmutil.revrange
34 except :
35 revrange = cmdutil.revrange
36
37 CACHE_PATH = ".hg/prompt/cache"
38 CACHE_TIMEOUT = timedelta(minutes=15)
39
40 FILTER_ARG = re.compile(r'\|.+\((.*)\)')
41
42
43 def _cache_remote(repo, kind):
44 cache = path.join(repo.root, CACHE_PATH, kind)
45 c_tmp = cache + '.temp'
46
47 popenargs = ['hg', kind, '--quiet']
48 remote_path = repo.ui.config('prompt', 'remote')
49 if remote_path is not None:
50 popenargs.append(remote_path)
51
52 null_path = 'NUL:' if subprocess.mswindows else '/dev/null'
53 with open(null_path, 'w') as null_fp:
54 with open(c_tmp, 'w') as stdout_fp:
55 exit_code = subprocess.call(popenargs, stdout=stdout_fp, stderr=null_fp)
56
57 if exit_code not in (0, 1): # (changesets_found, changesets_not_found)
58 msg = "hg-prompt error: "
59 if remote_path: # Failure likely due to bad remote. Is 255 a valid check?
60 msg += "Can't access remote '%s'" % remote_path
61 else:
62 msg += "Error attempting 'hg %s'" % kind
63 print msg
64
65 os.rename(c_tmp, cache)
66 return
67
68
69 def _with_groups(groups, out):
70 out_groups = [groups[0]] + [groups[-1]]
71
72 if any(out_groups) and not all(out_groups):
73 print 'Error parsing prompt string. Mismatched braces?'
74
75 out = out.replace('%', '%%')
76 return ("%s" + out + "%s") % (out_groups[0][:-1] if out_groups[0] else '',
77 out_groups[1][1:] if out_groups[1] else '')
78
79 def _get_filter(name, g):
80 '''Return the filter with the given name, or None if it was not used.'''
81 matching_filters = filter(lambda s: s and s.startswith('|%s' % name), g)
82 if not matching_filters:
83 return None
84
85 # Later filters will override earlier ones, for now.
86 f = matching_filters[-1]
87
88 return f
89
90 def _get_filter_arg(f):
91 if not f:
92 return None
93
94 args = FILTER_ARG.match(f).groups()
95 if args:
96 return args[0]
97 else:
98 return None
99
100 @command('prompt',
101 [('', 'angle-brackets', None, 'use angle brackets (<>) for keywords'),
102 ('', 'cache-incoming', None, 'used internally by hg-prompt'),
103 ('', 'cache-outgoing', None, 'used internally by hg-prompt')],
104 'hg prompt STRING')
105 def prompt(ui, repo, fs='', **opts):
106 '''get repository information for use in a shell prompt
107
108 Take a string and output it for use in a shell prompt. You can use
109 keywords in curly braces::
110
111 $ hg prompt "currently on {branch}"
112 currently on default
113
114 You can also use an extended form of any keyword::
115
116 {optional text here{keyword}more optional text}
117
118 This will expand the inner {keyword} and output it along with the extra
119 text only if the {keyword} expands successfully. This is useful if you
120 have a keyword that may not always apply to the current state and you
121 have some text that you would like to see only if it is appropriate::
122
123 $ hg prompt "currently at {bookmark}"
124 currently at
125 $ hg prompt "{currently at {bookmark}}"
126 $ hg bookmark my-bookmark
127 $ hg prompt "{currently at {bookmark}}"
128 currently at my-bookmark
129
130 See 'hg help prompt-keywords' for a list of available keywords.
131
132 The format string may also be defined in an hgrc file::
133
134 [prompt]
135 template = {currently at {bookmark}}
136
137 This is used when no format string is passed on the command line.
138 '''
139
140 def _basename(m):
141 return _with_groups(m.groups(), path.basename(repo.root)) if repo.root else ''
142
143 def _bookmark(m):
144 try:
145 book = extensions.find('bookmarks').current(repo)
146 except AttributeError:
147 book = getattr(repo, '_bookmarkcurrent', None)
148 except KeyError:
149 book = getattr(repo, '_bookmarkcurrent', None)
150 if book is None:
151 book = getattr(repo, '_activebookmark', None)
152 if book:
153 cur = repo['.'].node()
154 if repo._bookmarks[book] == cur:
155 return _with_groups(m.groups(), book)
156 else:
157 return ''
158
159 def _branch(m):
160 g = m.groups()
161
162 branch = repo.dirstate.branch()
163 quiet = _get_filter('quiet', g)
164
165 out = branch if (not quiet) or (branch != 'default') else ''
166
167 return _with_groups(g, out) if out else ''
168
169 def _closed(m):
170 g = m.groups()
171
172 quiet = _get_filter('quiet', g)
173
174 p = repo[None].parents()[0]
175 pn = p.node()
176 branch = repo.dirstate.branch()
177 closed = (p.extra().get('close')
178 and pn in repo.branchheads(branch, closed=True))
179 out = 'X' if (not quiet) and closed else ''
180
181 return _with_groups(g, out) if out else ''
182
183 def _count(m):
184 g = m.groups()
185 query = [g[1][1:]] if g[1] else ['all()']
186 return _with_groups(g, str(len(revrange(repo, query))))
187
188 def _node(m):
189 g = m.groups()
190
191 parents = repo[None].parents()
192 p = 0 if '|merge' not in g else 1
193 p = p if len(parents) > p else None
194
195 format = short if '|short' in g else hex
196
197 node = format(parents[p].node()) if p is not None else None
198 return _with_groups(g, str(node)) if node else ''
199
200 def _patch(m):
201 g = m.groups()
202
203 try:
204 extensions.find('mq')
205 except KeyError:
206 return ''
207
208 q = repo.mq
209
210 if _get_filter('quiet', g) and not len(q.series):
211 return ''
212
213 if _get_filter('topindex', g):
214 if len(q.applied):
215 out = str(len(q.applied) - 1)
216 else:
217 out = ''
218 elif _get_filter('applied', g):
219 out = str(len(q.applied))
220 elif _get_filter('unapplied', g):
221 out = str(len(q.unapplied(repo)))
222 elif _get_filter('count', g):
223 out = str(len(q.series))
224 else:
225 out = q.applied[-1].name if q.applied else ''
226
227 return _with_groups(g, out) if out else ''
228
229 def _patches(m):
230 g = m.groups()
231
232 try:
233 extensions.find('mq')
234 except KeyError:
235 return ''
236
237 join_filter = _get_filter('join', g)
238 join_filter_arg = _get_filter_arg(join_filter)
239 sep = join_filter_arg if join_filter else ' -> '
240
241 patches = repo.mq.series
242 applied = [p.name for p in repo.mq.applied]
243 unapplied = filter(lambda p: p not in applied, patches)
244
245 if _get_filter('hide_applied', g):
246 patches = filter(lambda p: p not in applied, patches)
247 if _get_filter('hide_unapplied', g):
248 patches = filter(lambda p: p not in unapplied, patches)
249
250 if _get_filter('reverse', g):
251 patches = reversed(patches)
252
253 pre_applied_filter = _get_filter('pre_applied', g)
254 pre_applied_filter_arg = _get_filter_arg(pre_applied_filter)
255 post_applied_filter = _get_filter('post_applied', g)
256 post_applied_filter_arg = _get_filter_arg(post_applied_filter)
257
258 pre_unapplied_filter = _get_filter('pre_unapplied', g)
259 pre_unapplied_filter_arg = _get_filter_arg(pre_unapplied_filter)
260 post_unapplied_filter = _get_filter('post_unapplied', g)
261 post_unapplied_filter_arg = _get_filter_arg(post_unapplied_filter)
262
263 for n, patch in enumerate(patches):
264 if patch in applied:
265 if pre_applied_filter:
266 patches[n] = pre_applied_filter_arg + patches[n]
267 if post_applied_filter:
268 patches[n] = patches[n] + post_applied_filter_arg
269 elif patch in unapplied:
270 if pre_unapplied_filter:
271 patches[n] = pre_unapplied_filter_arg + patches[n]
272 if post_unapplied_filter:
273 patches[n] = patches[n] + post_unapplied_filter_arg
274
275 return _with_groups(g, sep.join(patches)) if patches else ''
276
277 def _queue(m):
278 g = m.groups()
279
280 try:
281 extensions.find('mq')
282 except KeyError:
283 return ''
284
285 q = repo.mq
286
287 out = os.path.basename(q.path)
288 if out == 'patches' and not os.path.isdir(q.path):
289 out = ''
290 elif out.startswith('patches-'):
291 out = out[8:]
292
293 return _with_groups(g, out) if out else ''
294
295 def _remote(kind):
296 def _r(m):
297 g = m.groups()
298
299 cache_dir = path.join(repo.root, CACHE_PATH)
300 cache = path.join(cache_dir, kind)
301 if not path.isdir(cache_dir):
302 os.makedirs(cache_dir)
303
304 cache_exists = path.isfile(cache)
305
306 cache_time = (datetime.fromtimestamp(os.stat(cache).st_mtime)
307 if cache_exists else None)
308 if not cache_exists or cache_time < datetime.now() - CACHE_TIMEOUT:
309 if not cache_exists:
310 open(cache, 'w').close()
311 subprocess.Popen(['hg', 'prompt', '--cache-%s' % kind])
312
313 if cache_exists:
314 with open(cache) as c:
315 count = len(c.readlines())
316 if g[1] and count > 0:
317 return _with_groups(g, str(count))
318 elif g[2]:
319 return _with_groups(g, '0') if not count else ''
320 else:
321 return _with_groups(g, '')
322 else:
323 return ''
324 return _r
325
326 def _rev(m):
327 g = m.groups()
328
329 parents = repo[None].parents()
330 parent = 0 if '|merge' not in g else 1
331 parent = parent if len(parents) > parent else None
332
333 rev = parents[parent].rev() if parent is not None else -1
334 return _with_groups(g, str(rev)) if rev >= 0 else ''
335
336 def _root(m):
337 return _with_groups(m.groups(), repo.root) if repo.root else ''
338
339 def _status(m):
340 g = m.groups()
341
342 st = repo.status(unknown=True)[:5]
343 modified = any(st[:4])
344 unknown = len(st[-1]) > 0
345
346 flag = ''
347 if '|modified' not in g and '|unknown' not in g:
348 flag = '!' if modified else '?' if unknown else ''
349 else:
350 if '|modified' in g:
351 flag += '!' if modified else ''
352 if '|unknown' in g:
353 flag += '?' if unknown else ''
354
355 return _with_groups(g, flag) if flag else ''
356
357 def _tags(m):
358 g = m.groups()
359
360 sep = g[2][1:] if g[2] else ' '
361 tags = repo[None].tags()
362
363 quiet = _get_filter('quiet', g)
364 if quiet:
365 tags = filter(lambda tag: tag != 'tip', tags)
366
367 return _with_groups(g, sep.join(tags)) if tags else ''
368
369 def _task(m):
370 try:
371 task = extensions.find('tasks').current(repo)
372 return _with_groups(m.groups(), task) if task else ''
373 except KeyError:
374 return ''
375
376 def _tip(m):
377 g = m.groups()
378
379 format = short if '|short' in g else hex
380
381 tip = repo[len(repo) - 1]
382 rev = tip.rev()
383 tip = format(tip.node()) if '|node' in g else tip.rev()
384
385 return _with_groups(g, str(tip)) if rev >= 0 else ''
386
387 def _update(m):
388 current_rev = repo[None].parents()[0]
389
390 # Get the tip of the branch for the current branch
391 try:
392 heads = repo.branchmap()[current_rev.branch()]
393 tip = heads[-1]
394 except (KeyError, IndexError):
395 # We are in an empty repository.
396
397 return ''
398
399 for head in reversed(heads):
400 if not repo[head].closesbranch():
401 tip = head
402 break
403
404 return _with_groups(m.groups(), '^') if current_rev.children() else ''
405
406 if opts.get("angle_brackets"):
407 tag_start = r'\<([^><]*?\<)?'
408 tag_end = r'(\>[^><]*?)?>'
409 brackets = '<>'
410 else:
411 tag_start = r'\{([^{}]*?\{)?'
412 tag_end = r'(\}[^{}]*?)?\}'
413 brackets = '{}'
414
415 patterns = {
416 'bookmark': _bookmark,
417 'branch(\|quiet)?': _branch,
418 'closed(\|quiet)?': _closed,
419 'count(\|[^%s]*?)?' % brackets[-1]: _count,
420 'node(?:'
421 '(\|short)'
422 '|(\|merge)'
423 ')*': _node,
424 'patch(?:'
425 '(\|topindex)'
426 '|(\|applied)'
427 '|(\|unapplied)'
428 '|(\|count)'
429 '|(\|quiet)'
430 ')*': _patch,
431 'patches(?:' +
432 '(\|join\([^%s]*?\))' % brackets[-1] +
433 '|(\|reverse)' +
434 '|(\|hide_applied)' +
435 '|(\|hide_unapplied)' +
436 '|(\|pre_applied\([^%s]*?\))' % brackets[-1] +
437 '|(\|post_applied\([^%s]*?\))' % brackets[-1] +
438 '|(\|pre_unapplied\([^%s]*?\))' % brackets[-1] +
439 '|(\|post_unapplied\([^%s]*?\))' % brackets[-1] +
440 ')*': _patches,
441 'queue': _queue,
442 'rev(\|merge)?': _rev,
443 'root': _root,
444 'root\|basename': _basename,
445 'status(?:'
446 '(\|modified)'
447 '|(\|unknown)'
448 ')*': _status,
449 'tags(?:' +
450 '(\|quiet)' +
451 '|(\|[^%s]*?)' % brackets[-1] +
452 ')*': _tags,
453 'task': _task,
454 'tip(?:'
455 '(\|node)'
456 '|(\|short)'
457 ')*': _tip,
458 'update': _update,
459
460 'incoming(?:'
461 '(\|count)'
462 '|(\|zero)'
463 ')*': _remote('incoming'),
464 'outgoing(?:'
465 '(\|count)'
466 '|(\|zero)'
467 ')*': _remote('outgoing')
468 }
469
470 if opts.get("cache_incoming"):
471 _cache_remote(repo, 'incoming')
472
473 if opts.get("cache_outgoing"):
474 _cache_remote(repo, 'outgoing')
475
476 if not fs:
477 fs = repo.ui.config("prompt", "template", "")
478
479 for tag, repl in patterns.items():
480 fs = re.sub(tag_start + tag + tag_end, repl, fs)
481 ui.status(fs)
482
483 def _pull_with_cache(orig, ui, repo, *args, **opts):
484 """Wrap the pull command to delete the incoming cache as well."""
485 res = orig(ui, repo, *args, **opts)
486 cache = path.join(repo.root, CACHE_PATH, 'incoming')
487 if path.isfile(cache):
488 os.remove(cache)
489 return res
490
491 def _push_with_cache(orig, ui, repo, *args, **opts):
492 """Wrap the push command to delete the outgoing cache as well."""
493 res = orig(ui, repo, *args, **opts)
494 cache = path.join(repo.root, CACHE_PATH, 'outgoing')
495 if path.isfile(cache):
496 os.remove(cache)
497 return res
498
499 def uisetup(ui):
500 extensions.wrapcommand(commands.table, 'pull', _pull_with_cache)
501 extensions.wrapcommand(commands.table, 'push', _push_with_cache)
502 try:
503 extensions.wrapcommand(extensions.find("fetch").cmdtable, 'fetch', _pull_with_cache)
504 except KeyError:
505 pass
506
507 help.helptable += (
508 (['prompt-keywords'], _('Keywords supported by hg-prompt'),
509 lambda _: r'''hg-prompt currently supports a number of keywords.
510
511 Some keywords support filters. Filters can be chained when it makes
512 sense to do so. When in doubt, try it!
513
514 bookmark
515 Display the current bookmark (requires the bookmarks extension).
516
517 branch
518 Display the current branch.
519
520 |quiet
521 Display the current branch only if it is not the default branch.
522
523 closed
524 Display `X` if working on a closed branch (i.e. committing now would reopen
525 the branch).
526
527 count
528 Display the number of revisions in the given revset (the revset `all()`
529 will be used if none is given).
530
531 See `hg help revsets` for more information.
532
533 |REVSET
534 The revset to count.
535
536 incoming
537 Display nothing, but if the default path contains incoming changesets the
538 extra text will be expanded.
539
540 For example: `{incoming changes{incoming}}` will expand to
541 `incoming changes` if there are changes, otherwise nothing.
542
543 Checking for incoming changesets is an expensive operation, so `hg-prompt`
544 will cache the results in `.hg/prompt/cache/` and refresh them every 15
545 minutes.
546
547 |count
548 Display the number of incoming changesets (if greater than 0).
549 |zero
550 Display 0 if there are no incoming changesets.
551
552 node
553 Display the (full) changeset hash of the current parent.
554
555 |short
556 Display the hash as the short, 12-character form.
557
558 |merge
559 Display the hash of the changeset you're merging with.
560
561 outgoing
562 Display nothing, but if the current repository contains outgoing
563 changesets (to default) the extra text will be expanded.
564
565 For example: `{outgoing changes{outgoing}}` will expand to
566 `outgoing changes` if there are changes, otherwise nothing.
567
568 Checking for outgoing changesets is an expensive operation, so `hg-prompt`
569 will cache the results in `.hg/prompt/cache/` and refresh them every 15
570 minutes.
571
572 |count
573 Display the number of outgoing changesets (if greater than 0).
574 |zero
575 Display 0 if there are no incoming changesets.
576
577 patch
578 Display the topmost currently-applied patch (requires the mq
579 extension).
580
581 |count
582 Display the number of patches in the queue.
583
584 |topindex
585 Display (zero-based) index of the topmost applied patch in the series
586 list (as displayed by :hg:`qtop -v`), or the empty string if no patch
587 is applied.
588
589 |applied
590 Display the number of currently applied patches in the queue.
591
592 |unapplied
593 Display the number of currently unapplied patches in the queue.
594
595 |quiet
596 Display a number only if there are any patches in the queue.
597
598 patches
599 Display a list of the current patches in the queue. It will look like
600 this:
601
602 :::console
603 $ hg prompt '{patches}'
604 bottom-patch -> middle-patch -> top-patch
605
606 |reverse
607 Display the patches in reverse order (i.e. topmost first).
608
609 |hide_applied
610 Do not display applied patches.
611
612 |hide_unapplied
613 Do not display unapplied patches.
614
615 |join(SEP)
616 Display SEP between each patch, instead of the default ` -> `.
617
618 |pre_applied(STRING)
619 Display STRING immediately before each applied patch. Useful for
620 adding color codes.
621
622 |post_applied(STRING)
623 Display STRING immediately after each applied patch. Useful for
624 resetting color codes.
625
626 |pre_unapplied(STRING)
627 Display STRING immediately before each unapplied patch. Useful for
628 adding color codes.
629
630 |post_unapplied(STRING)
631 Display STRING immediately after each unapplied patch. Useful for
632 resetting color codes.
633
634 queue
635 Display the name of the current MQ queue.
636
637 rev
638 Display the repository-local changeset number of the current parent.
639
640 |merge
641 Display the repository-local changeset number of the changeset you're
642 merging with.
643
644 root
645 Display the full path to the root of the current repository, without a
646 trailing slash.
647
648 |basename
649 Display the directory name of the root of the current repository. For
650 example, if the repository is in `/home/u/myrepo` then this keyword
651 would expand to `myrepo`.
652
653 status
654 Display `!` if the repository has any changed/added/removed files,
655 otherwise `?` if it has any untracked (but not ignored) files, otherwise
656 nothing.
657
658 |modified
659 Display `!` if the current repository contains files that have been
660 modified, added, removed, or deleted, otherwise nothing.
661
662 |unknown
663 Display `?` if the current repository contains untracked files,
664 otherwise nothing.
665
666 tags
667 Display the tags of the current parent, separated by a space.
668
669 |quiet
670 Display the tags of the current parent, excluding the tag "tip".
671
672 |SEP
673 Display the tags of the current parent, separated by `SEP`.
674
675 task
676 Display the current task (requires the tasks extension).
677
678 tip
679 Display the repository-local changeset number of the current tip.
680
681 |node
682 Display the (full) changeset hash of the current tip.
683
684 |short
685 Display a short form of the changeset hash of the current tip (must be
686 used with the **|node** filter)
687
688 update
689 Display `^` if the current parent is not the tip of the current branch,
690 otherwise nothing. In effect, this lets you see if running `hg update`
691 would do something.
692 '''),
693 )