with apologies

Prompting bash with git and jj

· 2 min read · May 20, 2026 · #linux #tech

I recently decided to make a push on trying out jujutsu as an alternative to git.

Being easily distracted by side-quests I decided that I should sort out my bash prompt so that it displayed similar repository status information inside a jj repository as it did when inside a git repository. This inevitably meant reworking how I set my prompt, and finding a number of dead-ends online. So here’s my attempt at it, presented top-down like I’m some kind of Scheme programmer or something.1

First, the key setting: PROMPT_COMMAND, a sequence of commands run before PS1 is rendered:

PROMPT_COMMAND='__prompt_jjgit; _bash_history_sync'

Ignore _bash_history_sync for now,2 __prompt_jjgit is the function of interest—it attempts to determine whether it’s presently inside a git or jj repository or not, before setting PS1 to the appropriate status topped and tailed with codes to set HOST@USER:PATH into the window title and HOST@USER:CWD in the prompt, and the traditional $/#.

First, determine if inside a git/jj repository:

pwd_in_jjgit() {
  # echo "jj" or "git" if either is found in $PWD or its parent directories
  # using the shell is much faster than `git rev-parse --git-dir` or `jj root`
  local D="/$PWD"
  while test -n "$D"; do
    test -e "$D/.jj" && {
      echo jj
      return
    }
    test -e "$D/.git" && {
      echo git
      return
    }
    D="${D%/*}"
  done
}

Then setup the base prompt, USER@HOST:CWD with a little light highlighting via some ANSI escape codes:

__prompt_jjgit() {
  pwd_in_jjgit() {
    ...
  }

  PS1="\u@\h:\[$WHITE\]\W\[$COLOR_OFF\]"

…add the repository status if needed:

  case "$(pwd_in_jjgit)" in
    jj) PS1="$PS1$(__prompt_jj)" ;;
    git) PS1="$PS1$(__prompt_git)" ;;
  esac

…set the title if we’re in some sort of GUI window:

  if [[ $TERM != "dumb" ]]; then
    PS1="\[\e]0;\u@\h:\w\a\]$PS1"
  fi

…and finally, top and tail it with the usual $/#:

  PS1=": $PS1\$; "
}

Note the extra wrapping in : ... ; . The : and ; mean that whatever is in the prompt should be treated as parameters to be discarded to the no-op function :—this makes it easy to select & paste entire lines without worrying about having to avoid selecting the prompt.

Finally, provide the two functions that actually invoke git or jj as appropriate.

First, git

__prompt_git() {
  GIT_PS1_SHOWDIRTYSTATE=true
  GIT_PS1_SHOWSTASHSTATE=true
  GIT_PS1_SHOWUNTRACKEDFILES=true
  GIT_PS1_SHOWCOLORHINTS=true
  GIT_PS1_SHOWUPSTREAM="auto"
  GIT_PS1_COMPRESSSPARSESTATE=true

  printf "%s" "$(__git_ps1 "#%s")"
}

…as provided by some suitable package:

    if [ -r ~/.git-prompt.sh ]; then
      source ~/.git-prompt.sh
    elif $NIXOS; then
      gitdir=$(dirname $(readlink -f $(which git)))
      source ${gitdir}/../share/git/contrib/completion/git-prompt.sh
      source ${gitdir}/../share/git/contrib/completion/git-completion.bash
    else
      for d in /etc/bash_completion /etc/bash_completion.d; do
        if [ -d $d ]; then
          [ -r $d/git ] && source $d/git
          [ -r $d/git-prompt ] && source $d/git-prompt
        fi
      done
    fi

Second, jj by invoking it with a suitable template:

__prompt_jj() {
  # can't use environment variables from [colours.sh](https://github.com/mor1/rc-files/blob/main/scripts/colours.sh) in template due to surrounding by ''
  # \e[N;C] ANSI escape code, https://en.wikipedia.org/wiki/ANSI_escape_code
  # N= 0[reset] 1[bold] 2[faint]
  # C= 32[green] 35[purple] 37[underline white]
  printf "%s" "$(
    jj log --no-graph --ignore-working-copy --color=never --revisions @ \
      --template 'surround(
                    "(",
                    ")",
                    separate(
                        " ",
                        bookmarks.join(", "),
                        surround(
                          "\\[\\e[1;32m\\]",
                          "\\[\\e[0m\\]",
                          change_id.shortest(),
                        ),
                        surround(
                          "\\[\\e[2;35m\\]",
                          "\\[\\e[0m\\]",
                          commit_id.shortest(),
                        ),
                        surround(
                          "\\[\\e[4;37m\\]",
                          "\\[\\e[0m\\]",
                          truncate_end(15, description.first_line().trim(), "..."),
                        ),
                        if(conflict, label("conflict", "×")),
                        if(divergent, label("divergent", "??")),
                        if(hidden, label("hidden prefix", "(hidden)")),
                        if(immutable, label("node immutable", "◆")),
                        coalesce(
                            if(
                                empty,
                                coalesce(
                                    if(
                                        parents.len() > 1,
                                        label("empty", "(merged)"),
                                    ),
                                    label("empty", "(empty)"),
                                ),
                            ),
                            label("description placeholder", "*")
                        ),
                    )
                )
            ' 2> /dev/null
  )"
}

And that’s it. I’ll probably keep tweaking it but it seems to work OK for now at least.

  1. Genuinely, the first programming language I learned. It was fun, I enjoyed it.

  2. That attempts to make sure command history across multiple terminal windows on the same machine is synced each time a command is run; fancier solutions such as Atuin exist but I’ve never seen the need.