Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
54162e7
Use `git status --porcelain --branch` to improve performance
pedantic79 Sep 20, 2015
8e9faff
Fix issue parsing where there is no remote branch
pedantic79 Sep 20, 2015
7f63c3b
Fix a problem calculating clean in the python script
pedantic79 Sep 21, 2015
0231552
use bash substring rather than cut
pedantic79 Sep 21, 2015
00558b8
fix substring indexing
pedantic79 Sep 21, 2015
0dbac24
Set LC_ALL=C at git status to make sure in English
wkentaro Sep 23, 2015
7418da8
Set LC_ALL=C to force branch output to English
pedantic79 Sep 23, 2015
b9d7209
Merge branch 'wkentaro-set-lc-all-to-in-english' into porcelain
pedantic79 Sep 23, 2015
3a9239d
Fix issue where _NO_REMOTE_TRACKING_ doesn't show up when there is n…
pedantic79 Sep 23, 2015
e4c6b0a
Remove calls to grep/egrep
pedantic79 Sep 23, 2015
1e2ff37
Switch case matches from * to ?
pedantic79 Sep 23, 2015
ff7aa3a
Fix handling of stash
pedantic79 Sep 23, 2015
545cdce
Use printf instead of echo to display
pedantic79 Sep 23, 2015
01f5402
Fix behind parsing
pedantic79 Sep 24, 2015
606388d
Fix behind parsing
pedantic79 Sep 24, 2015
220d5c2
Fix parsing when both ahead and behind
pedantic79 Sep 24, 2015
0e6b70f
Merge branch 'porcelain-remove-grep' into porcelain
pedantic79 Sep 24, 2015
750425d
Changed all backticks to
ogr3 Sep 24, 2015
0e48e02
Remove unnecessary grep line
pedantic79 Sep 24, 2015
87c66ea
Missed one
ogr3 Sep 24, 2015
db6f219
Reorder AHEAD and BEHIND
pedantic79 Sep 24, 2015
b50cc5c
Merge remote-tracking branch 'ogr3/porcelain-cleanup' into porcelain
pedantic79 Sep 24, 2015
9955edb
Check for local branch needs to be moved so that check for behind/aft…
ogr3 Sep 25, 2015
589106d
Merge pull request #4 from ogr3/porcelain-no-remote
pedantic79 Sep 25, 2015
b989b7f
Able to parse tag names when on tag
ogr3 Sep 25, 2015
c5cac48
Initial commit on is always a local state - set no remote tracking
ogr3 Sep 25, 2015
5b82a33
Merge pull request #5 from ogr3/porcelain-tag
pedantic79 Sep 25, 2015
3621b5a
We set branch first, then remote
ogr3 Sep 25, 2015
a2906f4
Merge pull request #6 from ogr3/porcelain-tag
pedantic79 Sep 25, 2015
5fe390d
Fixed bug in stash parsing
ogr3 Sep 25, 2015
5cbd877
Merge pull request #7 from ogr3/porcelain-tag
pedantic79 Sep 25, 2015
c279c77
Resolved conflict
ogr3 Sep 27, 2015
89acb1c
Changed to match tabs and spaces
ogr3 Sep 27, 2015
5293850
Merge pull request #9 from ogr3/porcelain-sed
pedantic79 Sep 27, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 97 additions & 79 deletions gitstatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,93 +32,111 @@ def Print(*args, **kwd): # 2.4, 2.5, define our own Print function
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)
129 changes: 60 additions & 69 deletions gitstatus.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
#
# Alan K. Stebbens <aks@stebbens.org> [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
Expand All @@ -19,94 +15,89 @@ 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

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"

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`
else
stash_file="$( git rev-parse --git-dir )/logs/refs/stash"
if [[ -e "${stash_file}" ]]; then
wc_output=$( wc -l < "${stash_file}" )
num_stashed=${wc_output//[[:space:]]/}
else
num_stashed=0
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[-1]}
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

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