Hacking the git shell prompt
Git comes with
a very complicated shell function,, called
__git_ps1 ,
for interpolating Git information into your shell prompt. A typical
use would be:
PS1='>>> $(__git_ps1) :) '
PS1 is the variable that contains the shell's main prompt. Before
printing the prompt, the shell does variable and command interpolation
on this string. This means that if PS1 contains something like $(command
args...) , the shell replaces that string with the output from running
command args… . Here, it runs __git_ps1 and inserts the output into the
prompt. In the simplest case, __git_ps1 emits the name of the
currently-checked-out branch, so that the shell will actually print
this prompt:
>>> the-branch :)
But __git_ps1 has many other features besides. If you are in the middle
of a rebase or cherry-pick operation, it will emit something
like
the-branch|REBASE-i 1/5
or
the-branch|CHERRY-PICKING
instead. If HEAD is detached, it can still display the head
location in several formats. There are options to have the emitted string
indicate when the working tree is dirty and other things. My own
PS1 looks like this:
PS1='[$(_path) $(__git_ps1 "(%s)" )]> '
The _path command is something I
wrote to emit the path of the current working directory, abbreviated
in a contextually dependent way. It makes my prompt look like this:
[lib/app (the-branch)]>
Here lib/app is the path relative to the root of the repository.
The %s thing is an additional formatting instruction to __git_ps1 .
After it computes the description string, __git_ps1 inserts it into
"(%s)" in place of the %s , and emits the result of that
replacement. If you don't give __git_ps1 an argument, it uses "(%s) "
as a default, which has an extra space compared with what I have.
Lately I have been experimenting with appending .mjd.yyyymmdd to my
public branch names, to help me remember to delete my old dead
branches from the shared repository. This makes the branch names
annoyingly long:
gh1067-sort-dates-chronologically.mjd.20210103
gh1067-sort-dates-no-test.mjd.20210112
gh1088-cache-analysis-list.mjd.20210105
and these annoyingly long names appear in the output of __git_ps1 that is
inserted into my shell prompts.
One way to deal with this is to have the local branch names be
abbreviated and configure their upstream names to the long versions.
And that does work: I now have a little program called new-branch
that creates a new branch with the local short name, pushes it to the
long remote name, and sets the upstream. But I also wanted a generic
mechanism for abbreviating or transforming the branch name in the
prompt.
The supplied __git_ps1 function didn't seem to have an option
for that, or a callback for modifying the branch name before inserting
it into the prompt. I could have copied the function, modified the
parts I wanted, and used the modified version in place of the supplied
version, but it is 243 lines long, so I preferred not to do that.
But __git_ps1 does have one hook. Under the right circumstances,
it will attempt to colorize the prompt by inserting terminal
escape codes. To do this it invokes __git_ps_colorize_gitstring to
insert the escape codes into the various prompt components before it
assembles them. I can work with that!
The goal is now:
- Figure out how to tell
__git_ps1 to call __git_ps_colorize_gitstring
- Figure out how
__git_ps1 and __git_ps_colorize_gitstring communicate prompt components
- Write my own
__git_ps_colorize_gitstring to do something else
How to tell __git_ps1 to call __git_ps_colorize_gitstring
You have to do two things to get __git_ps1 to call the hook:
Set GIT_PS1_SHOWCOLORHINTS to some nonempty string. I set it to
true , which is a little deceptive, because false would have
worked as well.
Invoke __git_ps1 with two or more arguments.
Unfortunately, invoking the __git_ps1 with two or more arguments changes
its behavior in another way. It still computes a string, but it no
longer prints the string. Instead, it computes the string and
assigns it to PS1 . This means that
PS1="$(__git_ps arg arg….)"
won't work properly: the next time the shell wants to prompt, it will
evaluate PS1 , which will call __git_ps arg arg… , which will set
PS1 to some string like (the-branch) . Then the next time the
shell wants to print the prompt, it will evaluate PS1 , which will be
just some dead string like (the-branch) , with nothing in it to call
__git_ps1 again.
So we need to use a different shell feature. Instead of setting PS1
directly, we set PROMPT_COMMAND . This command is run before the
prompt is printed. Although this doesn't have anything to do directly
with the prompt, the command can change the prompt. If we set
PROMPT_COMMAND to invoke __git_ps1 , and if __git_ps1 modifies PS1 , the
prompt will change.
Formerly I had had this:
PS1='[$(_path) $(__git_ps1 "(%s)")]> '
but instead I needed to use:
GIT_PS1_SHOWCOLORHINTS=true
PROMPT_COMMAND='__git_ps1 "[$(_path) " " ] "' "(%s)"
Here __git_ps1 is getting three arguments:
"[$(_path) "
" ] "
"(%s)"
__git_ps1 computes its description of the Git state and inserts it into
the third argument in place of the %s . Then it takes the result of
this replacement, appends the first argument on the front and the
second on the back, and sets the prompt to the result. The shell will
still invoke _path in the course of evaluating the first string,
before passing it to __git_ps1 as an argument. Whew.
How __git_ps1 communicates prompt components to __git_ps_colorize_gitstring
The end result of all this rigamarole is that __git_ps1 is now being
called before every prompt, as before, but now it will also invoke
__git_ps_colorize_gitstring along the way. What does that actually get us?
The internals of __git_ps_colorize_gitstring aren't documented because I don't think this
is a planned use case, and __git_ps_colorize_gitstring isn't an advertised part of the
interface. __git_ps1 does something to construct the prompt, possibly
colorizing it in the process, but how it does the colorizing is
forbidden knowledge. From looking at the code I can see that the
colorizing is done by __git_ps_colorize_gitstring , and I needed to know what was going in
inside.
The (current) interface is that __git_ps1 puts the various components of
the prompts into a family of single-letter variables, which __git_ps_colorize_gitstring
modifies. Here's what these variables do, as best as I have been able
to ascertain:
b contains a description of the current HEAD, either the
current branch name or some other description
c indicates if you are in a bare repository
i indicates if changes have been recorded to the index
p contains information about whether the current head
is behind or ahead of its upstream branch
r describes the rebase / merge / cherry-pick state
s indicates if there is something in the stash
u indicates whether there are untracked files
w indicates whether the working tree is dirty
z is the separator between the branch name and the other indicators
Oddly, the one thing I wanted to change is the only one that __git_ps_colorize_gitstring
doesn't modify: the b variable that contains the name or
description of the current branch. Fortunately, it does exist and
there's nothing stopping me from writing a replacement __git_ps_colorize_gitstring that
does modify it.
Write a replacement for __git_ps_colorize_gitstring to do something else
So in the end all I needed was:
GIT_PS1_SHOWCOLORHINTS=true
PROMPT_COMMAND='__git_ps1 "[$(_path) " " ] "' "(%s)"
__git_ps1_colorize_gitstring () {
b=${b%%.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]}
b=${b%%.mjd}
}
The ${b%%PAT} thing produces the value of the variable b , except
that if the value ends with something matching the pattern PAT , that
part is removed. So the first assignment trims a trailing
.20210206 from the branch name, if there is one, and the second
trims off a trailing .mjd . If I wanted to trim off the leading gh
also I could use b=${b##gh} .
There's probably some way to use this in addition to the standard
__git_ps_colorize_gitstring , rather than in place of it. But I don't know how.
In conclusion
This was way harder to figure out than it should have been.
[Other articles in category /prog]
permanent link
|