diff --git a/README.md b/README.md index 75663eb..09a24f9 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The symbols are as follows: - Local Status Symbols - ``✔``: repository clean - ``●n``: there are ``n`` staged files - - ``✖n``: there are ``n`` unmerged files + - ``✖n``: there are ``n`` files with merge conflict(s) - ``✚n``: there are ``n`` changed but *unstaged* files - ``…n``: there are ``n`` untracked files - ``⚑n``: there are ``n`` stash entries diff --git a/git-prompt-help.sh b/git-prompt-help.sh index 40e98cd..d15db0e 100755 --- a/git-prompt-help.sh +++ b/git-prompt-help.sh @@ -22,7 +22,7 @@ LOCALSTATUS is one of the following: ${GIT_PROMPT_CLEAN}${ResetColor} - repository clean ${GIT_PROMPT_STAGED}N${ResetColor} - N staged files - ${GIT_PROMPT_CONFLICTS}N${ResetColor} - N conflicted files + ${GIT_PROMPT_CONFLICTS}N${ResetColor} - N files with merge conflicts ${GIT_PROMPT_CHANGED}N${ResetColor} - N changed but *unstaged* files ${GIT_PROMPT_UNTRACKED}N${ResetColor} - N untracked files ${GIT_PROMPT_STASHED}N${ResetColor} - N stash entries diff --git a/gitstatus.py b/gitstatus.py index f350409..1e1f385 100755 --- a/gitstatus.py +++ b/gitstatus.py @@ -32,93 +32,111 @@ except SyntaxError: w(str(a)) w(kwd.get("end", "\n")) - # change those symbols to whatever you prefer symbols = {'ahead of': '↑·', 'behind': '↓·', 'prehash':':'} -from subprocess import Popen, PIPE - import sys -gitsym = Popen(['git', 'symbolic-ref', 'HEAD'], stdout=PIPE, stderr=PIPE) -branch, error = gitsym.communicate() - -error_string = error.decode('utf-8') - -if 'fatal: Not a git repository' in error_string: - sys.exit(0) - -branch = branch.decode('utf-8').strip()[11:] - -res, err = Popen(['git','diff','--name-status'], stdout=PIPE, stderr=PIPE).communicate() -err_string = err.decode('utf-8') - -if 'fatal' in err_string: - sys.exit(0) - -changed_files = [namestat[0] for namestat in res.splitlines()] -staged_files = [namestat[0] for namestat in Popen(['git','diff', '--staged','--name-status'], stdout=PIPE).communicate()[0].splitlines()] -nb_changed = len(changed_files) - changed_files.count('U') -nb_U = staged_files.count('U') -nb_staged = len(staged_files) - nb_U -staged = str(nb_staged) -conflicts = str(nb_U) -changed = str(nb_changed) -status_lines = Popen(['git','status','-s','-uall'],stdout=PIPE).communicate()[0].splitlines() -untracked_lines = [a for a in map(lambda s: s.decode('utf-8'), status_lines) if a.startswith("??")] -nb_untracked = len(untracked_lines) -untracked = str(nb_untracked) -stashes = Popen(['git','stash','list'],stdout=PIPE).communicate()[0].splitlines() -nb_stashed = len(stashes) -stashed = str(nb_stashed) - -if not nb_changed and not nb_staged and not nb_U and not nb_untracked and not nb_stashed: - clean = '1' -else: - clean = '0' - +import re +import shlex +from subprocess import Popen, PIPE, check_output + + +def get_tagname_or_hash(): + """return tagname if exists else hash""" + cmd = 'git log -1 --format="%h%d"' + output = check_output(shlex.split(cmd)).decode('utf-8').strip() + hash_, tagname = None, None + # get hash + m = re.search('\(.*\)$', output) + if m: + hash_ = output[:m.start()-1] + # get tagname + m = re.search('tag: .*[,\)]', output) + if m: + tagname = 'tags/' + output[m.start()+len('tag: '): m.end()-1] + + if tagname: + return tagname + elif hash_: + return hash_ + return None + +def get_stash(): + cmd = Popen(['git', 'rev-parse', '--git-dir'], stdout=PIPE, stderr=PIPE) + so, se = cmd.communicate() + stashFile = '%s%s' % (so.decode('utf-8').rstrip(),'/logs/refs/stash') + + try: + with open(stashFile) as f: + return sum(1 for _ in f) + except IOError: + return 0 + +# `git status --porcelain --branch` can collect all information +# branch, remote_branch, untracked, staged, changed, conflicts, ahead, behind +po = Popen(['git', 'status', '--porcelain', '--branch'], env={'LC_ALL': 'C'}, + stdout=PIPE, stderr=PIPE) +stdout, sterr = po.communicate() +if po.returncode != 0: + sys.exit(0) # Not a git repository + +# collect git status information +untracked, staged, changed, conflicts = [], [], [], [] +ahead, behind = 0, 0 remote = '' - -tag, tag_error = Popen(['git', 'describe', '--exact-match'], stdout=PIPE, stderr=PIPE).communicate() - -if not branch: # not on any branch - if tag: # if we are on a tag, print the tag's name - branch = tag - else: - branch = symbols['prehash']+ Popen(['git','rev-parse','--short','HEAD'], stdout=PIPE).communicate()[0].decode('utf-8')[:-1] +status = [(line[0], line[1], line[2:]) for line in stdout.decode('utf-8').splitlines()] +for st in status: + if st[0] == '#' and st[1] == '#': + if re.search('Initial commit on', st[2]): + branch = st[2].split(' ')[-1] + elif re.search('no branch', st[2]): # detached status + branch = get_tagname_or_hash() + elif len(st[2].strip().split('...')) == 1: + branch = st[2].strip() + else: + # current and remote branch info + branch, rest = st[2].strip().split('...') + if len(rest.split(' ')) == 1: + # remote_branch = rest.split(' ')[0] + pass + else: + # ahead or behind + divergence = ' '.join(rest.split(' ')[1:]) + divergence = divergence.lstrip('[').rstrip(']') + for div in divergence.split(', '): + if 'ahead' in div: + ahead = int(div[len('ahead '):].strip()) + remote += '%s%s' % (symbols['ahead of'], ahead) + elif 'behind' in div: + behind = int(div[len('behind '):].strip()) + remote += '%s%s' % (symbols['behind'], behind) + elif st[0] == '?' and st[1] == '?': + untracked.append(st) + else: + if st[1] == 'M': + changed.append(st) + if st[0] == 'U': + conflicts.append(st) + elif st[0] != ' ': + staged.append(st) + +stashed=get_stash() +if not changed and not staged and not conflicts and not untracked and not stashed: + clean = 1 else: - remote_name = Popen(['git','config','branch.%s.remote' % branch], stdout=PIPE).communicate()[0].strip() - if remote_name: - merge_name = Popen(['git','config','branch.%s.merge' % branch], stdout=PIPE).communicate()[0].strip() - else: - remote_name = "origin" - merge_name = "refs/heads/%s" % branch - - if remote_name == '.': # local - remote_ref = merge_name - else: - remote_ref = 'refs/remotes/%s/%s' % (remote_name, merge_name[11:]) - revgit = Popen(['git', 'rev-list', '--left-right', '%s...HEAD' % remote_ref],stdout=PIPE, stderr=PIPE) - revlist = revgit.communicate()[0] - if revgit.poll(): # fallback to local - revlist = Popen(['git', 'rev-list', '--left-right', '%s...HEAD' % merge_name],stdout=PIPE, stderr=PIPE).communicate()[0] - behead = revlist.splitlines() - ahead = len([x for x in behead if x[0]=='>']) - behind = len(behead) - ahead - if behind: - remote += '%s%s' % (symbols['behind'], behind) - if ahead: - remote += '%s%s' % (symbols['ahead of'], ahead) + clean = 0 if remote == "": - remote = '.' + remote = '.' out = '\n'.join([ - str(branch), - str(remote), - staged, - conflicts, - changed, - untracked, - stashed, - clean]) + branch, + remote.decode('utf-8'), + str(len(staged)), + str(len(conflicts)), + str(len(changed)), + str(len(untracked)), + str(stashed), + str(clean) +]) Print(out) diff --git a/gitstatus.sh b/gitstatus.sh index c9b72c2..5416fec 100755 --- a/gitstatus.sh +++ b/gitstatus.sh @@ -5,10 +5,6 @@ # # Alan K. Stebbens [http://github.com/aks] -# helper functions -count_lines() { echo "$1" | egrep -c "^$2" ; } -all_lines() { echo "$1" | grep -v "^$" | wc -l ; } - if [ -z "${__GIT_PROMPT_DIR}" ]; then SOURCE="${BASH_SOURCE[0]}" while [ -h "${SOURCE}" ]; do @@ -19,81 +15,72 @@ if [ -z "${__GIT_PROMPT_DIR}" ]; then __GIT_PROMPT_DIR="$( cd -P "$( dirname "${SOURCE}" )" && pwd )" fi -gitsym=`git symbolic-ref HEAD` - -# if "fatal: Not a git repo .., then exit -case "$gitsym" in fatal*) exit 0 ;; esac - -# the current branch is the tail end of the symbolic reference -branch="${gitsym##refs/heads/}" # get the basename after "refs/heads/" - -gitstatus=`git diff --name-status 2>&1` - -# if the diff is fatal, exit now -case "$gitstatus" in fatal*) exit 0 ;; esac - - -staged_files=`git diff --staged --name-status` - -num_changed=$(( `all_lines "$gitstatus"` - `count_lines "$gitstatus" U` )) -num_conflicts=`count_lines "$staged_files" U` -num_staged=$(( `all_lines "$staged_files"` - num_conflicts )) -num_untracked=`git ls-files --others --exclude-standard $(git rev-parse --show-cdup) | wc -l` -if [[ "$__GIT_PROMPT_IGNORE_STASH" = "1" ]]; then - num_stashed=0 -else - num_stashed=`git stash list | wc -l` +gitstatus=$( LC_ALL=C git status --porcelain --branch ) + +# if the status is fatal, exit now +[[ "$?" -ne 0 ]] && exit 0 + +num_staged=0 +num_changed=0 +num_conflicts=0 +num_untracked=0 +while IFS='' read -r line || [[ -n "$line" ]]; do + status=${line:0:2} + case "$status" in + \#\#) branch_line="$line" ;; + ?M) ((num_changed++)) ;; + U?) ((num_conflicts++)) ;; + \?\?) ((num_untracked++)) ;; + *) ((num_staged++)) ;; + esac +done <<< "$gitstatus" + +num_stashed=0 +if [[ "$__GIT_PROMPT_IGNORE_STASH" != "1" ]]; then + stash_file="$( git rev-parse --git-dir )/logs/refs/stash" + if [[ -e "${stash_file}" ]]; then + while IFS='' read -r wcline || [[ -n "$wcline" ]]; do + ((num_stashed++)) + done < ${stash_file} + fi fi clean=0 -if (( num_changed == 0 && num_staged == 0 && num_U == 0 && num_untracked == 0 && num_stashed == 0 )) ; then +if (( num_changed == 0 && num_staged == 0 && num_untracked == 0 && num_stashed == 0 )) ; then clean=1 fi +IFS="." read -ra line <<< "${branch_line/\#\# }" +branch="${line[0]}" remote= -if [[ -z "$branch" ]]; then - tag=`git describe --exact-match` +if [[ "$branch" == *"Initial commit on"* ]]; then + IFS=" " read -ra branch_line <<< "$branch" + branch="${branch_line[3]}" + remote="_NO_REMOTE_TRACKING_" +elif [[ "$branch" == *"no branch"* ]]; then + tag=$( git describe --exact-match ) if [[ -n "$tag" ]]; then branch="$tag" else - branch="_PREHASH_`git rev-parse --short HEAD`" + branch="_PREHASH_$( git rev-parse --short HEAD )" fi else - remote_name=`git config branch.${branch}.remote` - - if [[ -n "$remote_name" ]]; then - merge_name=`git config branch.${branch}.merge` + if [[ "${#line[@]}" -eq 1 ]]; then + remote="_NO_REMOTE_TRACKING_" else - remote_name='origin' - merge_name="refs/heads/${branch}" - fi - - if [[ "$remote_name" == '.' ]]; then - remote_ref="$merge_name" - else - remote_ref="refs/remotes/$remote_name/${merge_name##refs/heads/}" - fi - - # detect if the local branch have a remote tracking branch - cmd_output=$(git rev-parse --abbrev-ref ${branch}@{upstream} 2>&1 >/dev/null) - - if [ `count_lines "$cmd_output" "fatal: no upstream"` == 1 ] ; then - has_remote_tracking=0 - else - has_remote_tracking=1 - fi - - # get the revision list, and count the leading "<" and ">" - revgit=`git rev-list --left-right ${remote_ref}...HEAD` - num_revs=`all_lines "$revgit"` - num_ahead=`count_lines "$revgit" "^>"` - num_behind=$(( num_revs - num_ahead )) - if (( num_behind > 0 )) ; then - remote="${remote}_BEHIND_${num_behind}" - fi - if (( num_ahead > 0 )) ; then - remote="${remote}_AHEAD_${num_ahead}" + IFS="[,]" read -ra remote_line <<< "${line[3]}" + for rline in "${remote_line[@]}"; do + if [[ "$rline" == *ahead* ]]; then + num_ahead=${rline:6} + ahead="_AHEAD_${num_ahead}" + fi + if [[ "$rline" == *behind* ]]; then + num_behind=${rline:7} + behind="_BEHIND_${num_behind# }" + fi + done + remote="${behind}${ahead}" fi fi @@ -101,12 +88,14 @@ if [[ -z "$remote" ]] ; then remote='.' fi -if [[ "$has_remote_tracking" == "0" ]] ; then - remote='_NO_REMOTE_TRACKING_' -fi - -for w in "$branch" "$remote" $num_staged $num_conflicts $num_changed $num_untracked $num_stashed $clean ; do - echo "$w" -done +printf "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n" \ + "$branch" \ + "$remote" \ + $num_staged \ + $num_conflicts \ + $num_changed \ + $num_untracked \ + $num_stashed \ + $clean exit diff --git a/themes/TruncatedPwd_WindowTitle.bgptheme b/themes/TruncatedPwd_WindowTitle.bgptheme index 734600c..3c2c4cc 100644 --- a/themes/TruncatedPwd_WindowTitle.bgptheme +++ b/themes/TruncatedPwd_WindowTitle.bgptheme @@ -1,13 +1,13 @@ ############################################################################## # Changes the prompt to a Debian-style one that truncates pwd to a max length -# depending on the terminal column width. Also uses the prompt_callback -# function of bash-git-prompt to set the window title to almost the same +# depending on the terminal column width. Also uses the prompt_callback +# function of bash-git-prompt to set the window title to almost the same # Debian-style. # # The prompt will use a Debian-style on the form # # [user@host: ] [bash-git-prompt-info] -# HH:MM $ +# HH:MM $ # # The window title will have the form # user@host: @@ -44,7 +44,7 @@ override_git_prompt_colors() { } Time12a="\$(date +%H:%M)" - + GIT_PROMPT_START_USER="${Yellow}" GIT_PROMPT_START_ROOT="${GIT_PROMPT_START_USER}" GIT_PROMPT_END_USER=" _LAST_COMMAND_INDICATOR_\n${White}${Time12a}${ResetColor} $ " diff --git a/themes/TruncatedPwd_WindowTitle_NoExitState.bgptheme b/themes/TruncatedPwd_WindowTitle_NoExitState.bgptheme new file mode 100644 index 0000000..fdd0f50 --- /dev/null +++ b/themes/TruncatedPwd_WindowTitle_NoExitState.bgptheme @@ -0,0 +1,54 @@ +############################################################################## +# Changes the prompt to a Debian-style one that truncates pwd to a max length +# depending on the terminal column width. Also uses the prompt_callback +# function of bash-git-prompt to set the window title to almost the same +# Debian-style. +# +# The prompt will use a Debian-style on the form +# +# [user@host: ] [bash-git-prompt-info] +# HH:MM $ +# +# The window title will have the form +# user@host: +# +# Example usage: +# if [ -f ~/.bash-git-prompt/gitprompt.sh ]; then +# GIT_PROMPT_THEME=TruncatedPwd_WindowTitle_NoExitState +# source ~/.bash-git-prompt/gitprompt.sh +# fi +# +# oGre [https://github.com/ogr3] +############################################################################## +override_git_prompt_colors() { + GIT_PROMPT_THEME_NAME="TruncatedPwd_WindowTitle_NoExitState" + + #Sets the window title to the given argument string + function gp_set_title { + echo -ne "\033]0;"$@"\007" + } + + #Helper function that truncates $PWD depending on window width + function gp_truncate_pwd { + local newPWD="${PWD/#$HOME/~}" + local pwdmaxlen=$((${COLUMNS:-80}/3)) + [ ${#newPWD} -gt $pwdmaxlen ] && newPWD="...${newPWD:3-$pwdmaxlen}" + echo -n "$newPWD" + } + + #Overrides the prompt_callback function used by bash-git-prompt + function prompt_callback { + local PS1="\u@\h: $(gp_truncate_pwd)" + gp_set_title $PS1 + echo -n "[${PS1}]${ResetColor}" + } + + Time12a="\$(date +%H:%M)" + + GIT_PROMPT_START_USER="${Yellow}" + GIT_PROMPT_START_ROOT="${GIT_PROMPT_START_USER}" + GIT_PROMPT_END_USER="\n${White}${Time12a}${ResetColor} $ " + GIT_PROMPT_END_ROOT="\n${White}${Time12a}${ResetColor} # " +} + +reload_git_prompt_colors "TruncatedPwd_WindowTitle_NoExitState"