# Copyright (C) 2015 Facebook, Inc # Maintained by Ryan McElroy # # Inspiration and derivation from git-completion.bash by Shawn O. Pearce. # # Distributed under the GNU General Public License, version 2.0. # # ======================================================================== # # Quickly determines the and emits some useful information about the state # of your current mercurial or git repository. Useful for PS1 prompts. # # Design goals: # * Useful for both git and mercurial # * Portable to both zsh and bash # * Portable to both Mac (BSD-based utils) and Linux (GNU-based utils) # * As fast as possible given the above constraints (few command invocations) # * Avoids invoking git or mercurial, which may be slow on large repositories # # To use from zsh: # # NOTE! the single quotes are important; if you use double quotes # then the prompt won't change when you chdir or checkout different # branches! # # setopt PROMPT_SUBST # source /path/to/scm-prompt # export PS1='$(_scm_prompt)$USER@%m:%~%% ' # # To use from bash: # # source /path/to/scm-prompt # export PS1="\$(_scm_prompt)\u@\h:\W\$ " # # NOTE! You *EITHER* need to single-quote the whole thing *OR* back-slash # the $(...) (as above), but not both. Which one you use depends on if # you need the rest of your PS1 to interpolate variables. # # You may additionally pass a format-string to the scm_info command. This # allows you to control the format of the prompt string without interfering # with the prompt outside of a mercurial or git repository. For example: # # $(_scm_prompt "%s") # # The default format string is " (%s)" (note the space) # # Options: you may want to set these environment variables # * HOME_IS_NOT_A_REPO : Stop walking up the filesystem looking for a git or # hg repo at (but not including) /home instead of /. This helps when /home # is a remote or autofs mount point. # * WANT_OLD_SCM_PROMPT : Use '%s' as the formatting for the prompt instead # of ' (%s)' # # Notes to developers: # # * Aliases can screw up the default commands. To prevent this issue, use # the 'builtin' prefix for built-in shell commands (eg, 'cd' and 'echo') # and use the 'command' prefix for external commands that you do not want # to invoke aliases for (eg, 'grep', 'cut'). # # ========================================================================= # _find_most_relevant() { # We don't want to output all remote bookmarks because there can be many # of them. This function finds the most relevant remote bookmark using this # algorithm: # 1. If 'master' or '@' bookmark is available then output it # 2. Sort remote bookmarks and output the first in reverse sorted order ( # it's a heuristic that tries to find the newest bookmark. It will work well # with bookmarks like 'release20160926' and 'release20161010'). relevantbook="$(command grep -m1 -E -o "^[^/]+/(master|@)$" <<< "$1")" if [[ -n $relevantbook ]]; then builtin echo $relevantbook return 0 fi builtin echo "$(command sort -r <<< "$1" | command head -n 1)" } _hg_prompt() { local hg br extra hg="$1" if [[ -f "$hg/bisect.state" ]]; then extra="|BISECT" elif [[ -f "$hg/histedit-state" ]]; then extra="|HISTEDIT" elif [[ -f "$hg/graftstate" ]]; then extra="|GRAFT" elif [[ -f "$hg/unshelverebasestate" ]]; then extra="|UNSHELVE" elif [[ -f "$hg/rebasestate" ]]; then extra="|REBASE" elif [[ -d "$hg/merge" ]]; then extra="|MERGE" elif [[ -L "$hg/store/lock" ]]; then extra="|STORE-LOCKED" elif [[ -L "$hg/wlock" ]]; then extra="|WDIR-LOCKED" fi local dirstate="$( \ ( [[ -f "$hg/dirstate" ]] && \ command hexdump -vn 20 -e '1/1 "%02x"' "$hg/dirstate") || \ ( [[ -f "$hg/../.eden/client/SNAPSHOT" ]] && \ command hexdump -s 8 -vn 20 -e '1/1 "%02x"' \ "$hg/../.eden/client/SNAPSHOT") || \ builtin echo "empty")" local active="$hg/bookmarks.current" if [[ -f "$active" ]]; then br="$(command cat "$active")" # check to see if active bookmark needs update (eg, moved after pull) local marks="$hg/bookmarks" if [[ -f "$hg/sharedpath" && -f "$hg/shared" ]] && command grep -q '^bookmarks$' "$hg/shared"; then marks="$(command cat $hg/sharedpath)/bookmarks" fi if [[ -z "$extra" ]] && [[ -f "$marks" ]]; then local markstate="$(command grep " $br$" "$marks" | \ command cut -f 1 -d ' ')" if [[ $markstate != "$dirstate" ]]; then extra="|UPDATE_NEEDED" fi fi else br="$(builtin echo "$dirstate" | command cut -c 1-7)" fi local remote="$hg/remotenames" if [[ -f "$remote" ]]; then local allremotemarks="$(command grep "^$dirstate bookmarks" "$remote" | \ command cut -f 3 -d ' ')" if [[ -n "$allremotemarks" ]]; then local remotemark="$(_find_most_relevant "$allremotemarks")" if [[ -n "$remotemark" ]]; then br="$br|$remotemark" if [[ "$remotemark" != "$allremotemarks" ]]; then # if there is more than one, let the user know with an elipsis br="${br}..." fi fi fi fi local branch if [[ -f "$hg/branch" ]]; then branch="$(command cat "$hg/branch")" if [[ "$branch" != "default" ]]; then br="$br|$branch" fi fi br="$br$extra" builtin printf "%s" "$br" } _git_prompt() { local git br git="$1" if [[ -f "$git/HEAD" ]]; then read br < "$git/HEAD" case $br in ref:\ refs/heads/*) br=${br#ref: refs/heads/} ;; *) br="$(builtin echo "$br" | command cut -c 1-7)" ;; esac if [[ -f "$git/rebase-merge/interactive" ]]; then b="$(command cat "$git/rebase-merge/head-name")" b="${b#refs/heads/}" br="$br|REBASE-i|$b" elif [[ -d "$git/rebase-merge" ]]; then b="$(command cat "$git/rebase-merge/head-name")" b="${b#refs/heads/}" br="$br|REBASE-m|$b" else if [[ -d "$git/rebase-apply" ]]; then if [[ -f "$git/rebase-apply/rebasing" ]]; then b="$(command cat "$git/rebase-apply/head-name")" b="${b#refs/heads/}" br="$br|REBASE|$b" elif [[ -f "$git/rebase-apply/applying" ]]; then br="$br|AM" else br="$br|AM/REBASE" fi elif [[ -f "$git/CHERRY_PICK_HEAD" ]]; then br="$br|CHERRY-PICKING" elif [[ -f "$git/REVERT_HEAD" ]]; then br="$br|REVERTING" elif [[ -f "$git/MERGE_HEAD" ]]; then br="$br|MERGE" elif [[ -f "$git/BISECT_LOG" ]]; then br="$br|BISECT" fi fi fi builtin printf "%s" "$br" } _scm_prompt() { local dir fmt br # Default to be compatable with __git_ps1. In particular: # - provide a space for the user so that they don't have to have # random extra spaces in their prompt when not in a repo # - provide parens so it's differentiated from other crap in their prompt fmt="${1:- (%s)}" # find out if we're in a git or hg repo by looking for the control dir dir="$PWD" while : ; do [[ -n "$HOME_IS_NOT_A_REPO" ]] && [[ "$dir" = "/home" ]] && break if [[ -d "$dir/.git" ]]; then br="$(_git_prompt "$dir/.git")" break elif [[ -d "$dir/.hg" ]]; then br="$(_hg_prompt "$dir/.hg")" break fi [[ "$dir" = "/" ]] && break # portable "realpath" equivalent dir="$(builtin cd -P "$dir/.." && builtin echo "$PWD")" done if [[ -n "$br" ]]; then builtin printf "$fmt" "$br" fi } # # Backwards-compatibility layer for odl scm-prompt script # # Older versions of this file at Facebook used a longer function name. # These versions also included support for an environmental directive # called $WANT_OLD_SCM_PROMPT. Support this to remain compatible. # _dotfiles_scm_info() { local fmt fmt=$1 if [[ -z "$fmt" ]]; then if [[ -n "$WANT_OLD_SCM_PROMPT" ]]; then fmt="%s" else fmt=' (%s)' fi fi _scm_prompt "$fmt" }