1. 24
    hg prompt design vcs

This is a text post rather than a direct link because I want to talk about hg prompt’s optional prefix/postfix syntax. Links at the bottom.

So, hg prompt is a Mercurial extension by Steve Losh that lets you format all Mercurial-related info in a single command like this, so you can use the output in your prompt:

$ hg prompt "{on {bookmark}}{ at {rev}}{ ({tags})}{{status|modified|unknown}}"
on main at 1471 (tip)!?

The part I want to discuss here is the syntax for the placeholder/format string/string interpolation, such as

{on {bookmark}}

That format string means:

  • if there is an active bookmark (Git users: think ‘active branch [pointer]’), then:
    • first print “on “,
    • then print the bookmark’s name
    • then print nothing.
  • If there is no active bookmark, print nothing.

The innovation is here is the first-class syntax for “use this prefix/suffix, but omit them if the placeholder turns out to be empty”:


In every other formatting DSL I know of, you’d have to separately make the prefix and the suffix conditional, something like this (on multiple lines for clarity):

{if(placeholder, prefix, "")}
{if(placeholder, suffix, "")}

So! If you’re ever in a position to create a formatting DSL where some placeholders might be empty, this syntax is a wonderful way for users to specify parentheses or spaces that must disappear if the placeholder is empty.


(The design tag on this story is because that is the de facto UI/UX tag. Furthermore, I am of the opinion we need a UI/UX tag.)

  1. 1

    Doesn’t groovy formatting work the same way? I remember something along those lines when using Filebot title formatting.

    1. 1

      Apparently?!! It seems Groovy placeholders are formed from an expression inside braces; and the expression can look like {title} but also like {"$title"} or {"this is the $title"}; and if the value title is undefined the entire placeholder {...} evaluates to the empty string. Which in the last example means the prefix “this is the “ also gets suppressed.

      I’m not sure the Groovy devs themselves are aware that you can use this to get prefix-only-if-value-is-defined behaviour … the docs don’t mention it! But Okam from 2012 figured it out, and wrote it in the only post they ever made on the Filebot forum. I can’t say we’re lucky to have Google, but we’re lucky to have search engines.

    2. 1

      Installation page seems to have a dead link to http://bitbucket.org/sjl/hg-prompt/

      1. 1

        Yep, that’s https://hg.stevelosh.com/hg-prompt/ nowadays, since Bitbucket dropped Mercurial support.

      2. 1

        Installation page seems to have a dead link to http://bitbucket.org/sjl/hg-prompt/

        Does it play nice with evolve?

        It’s just dying messily with…

        hg prompt 'test'
        Traceback (most recent call last):
          File "/usr/lib/python3/dist-packages/mercurial/extensions.py", line 251, in _runuisetup
          File "/usr/lib/python3/dist-packages/hgext3rd/evolve/exthelper.py", line 149, in finaluisetup
          File "/usr/lib/python3/dist-packages/hgext3rd/evolve/__init__.py", line 1278, in _setuphelp
        TypeError: '<' not supported between instances of 'str' and 'bytes'
        *** failed to set up extension evolve: '<' not supported between instances of 'str' and 'bytes'
        ** Unknown exception encountered with possibly-broken third-party extension prompt
        ** which supports versions unknown of Mercurial.
        ** Please disable prompt and try your action again.
        ** If that fixes the bug please report it to the extension author.
        ** Python 3.6.9 (default, Oct  8 2020, 12:12:24) [GCC 8.4.0]
        ** Mercurial Distributed SCM (version 5.5.2)
        ** Extensions loaded: graphlog, strip, mq, extdiff, rebase, convert, purge, eol, churn, hgk, record, prompt
        Traceback (most recent call last):
          File "/usr/bin/hg", line 43, in <module>
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 113, in run
            status = dispatch(req)
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 303, in dispatch
            ret = _runcatch(req) or 0
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 479, in _runcatch
            return _callcatch(ui, _runcatchfunc)
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 488, in _callcatch
            return scmutil.callcatch(ui, func)
          File "/usr/lib/python3/dist-packages/mercurial/scmutil.py", line 152, in callcatch
            return func()
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 469, in _runcatchfunc
            return _dispatch(req)
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 1233, in _dispatch
            lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 917, in runcommand
            ret = _runcommand(ui, options, cmd, d)
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 1244, in _runcommand
            return cmdfunc()
          File "/usr/lib/python3/dist-packages/mercurial/dispatch.py", line 1230, in <lambda>
            d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
          File "/usr/lib/python3/dist-packages/mercurial/util.py", line 1867, in check
            return func(*args, **kwargs)
          File "/usr/lib/python3/dist-packages/mercurial/util.py", line 1867, in check
            return func(*args, **kwargs)
          File "/usr/lib/python3/dist-packages/hgext/mq.py", line 4226, in mqcommand
            return orig(ui, repo, *args, **kwargs)
          File "/usr/lib/python3/dist-packages/mercurial/util.py", line 1867, in check
            return func(*args, **kwargs)
          File "/home/johnc/builds/hg-prompt/prompt.py", line 410, in prompt
            fs = re.sub(tag_start + tag + tag_end, repl, fs)
          File "/usr/lib/python3.6/re.py", line 191, in sub
            return _compile(pattern, flags).sub(repl, string, count)
        TypeError: cannot use a string pattern on a bytes-like object
        1. 2

          If you run Mercurial with Python 2, you won’t get this error. The error arises because (a) ‘…’ meant/means a bytes array in Python 2, (b) ‘…’ means an array of unicode code points in Python 3, and (c) hg-prompt dates from Mercurial’s Python 2 days. If the subject interests you, I can recommend this blog post that reflects on Mercurial’s 2-to-3 transition.

          The Python 3 fix would involve turning nearly every '...' string into a b'...' string (easy-ish). If you want to fix it for yourself,, that’s all. If you want to submit the patch upstream, you’d probably need to set up a testing harness to make sure it works in both 2 and 3 (not so easy). I see a two recent commits ([1], [2]) on this subject, so the interest is probably there.

          Hg-prompt plays nicely with evolve in the sense that it doesn’t crash, and treats hidden revs the same as non-hidden. It does not yet have keywords for topic-related info. I’ve got a draft patch for that, but I haven’t submitted it upstream yet. It defines keywords {topic} (the active topic), {revtopic|idx} (the changeset’s topic|stack number s0, s1, … even if another topic is active) and {stackidx} (stack id s0, s1, …, for the active topic’s stack).

        2. 1

          Whoa, I just realised I can user-define this (albeit with less elegant syntax) in the Mercurial formatting language by adding this to my .hgrc:

          # Example usage: hg log -r . -T 'text{maybe("bookmark", " >>", bookmarks, "<<")} more text'
          maybe(mylabel, prefix, main, suffix) = '\
              {ifeq(main, "", "", label(mylabel, prefix))}\
              {label(mylabel, main)}\
              {ifeq(main, "", "", label(mylabel, suffix))}'