#!/usr/bin/perl -w eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}' if 0; # not running under some shell eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}' if 0; # not running under some shell use strict; # Examples: # Legacy style (mcanz): # export PSGIT='[\t]\[\e[36m\]%{(%b)\[\e[0;1m\][%c%u%f%t]%}\[\e[0m\]\u\$ ' # export PROMPT_COMMAND=$PROMPT_COMMAND';export PS1=$(gitprompt.pl statuscount=1)' # Trailing symbols (ewastl): # export PSGIT='...%{[%b\[\e[0m\]%c%u%f%t\[\e[30;1m\]]%}\[\e[0m\]...' # export PROMPT_COMMAND=$PROMPT_COMMAND';export PS1=$(gitprompt.pl c=\+ u=\~ f=\* statuscount=1)' # Change branchname color (inspired by amirabella): # export PSGIT='%{[\[%f%c%u%t\]%b\[\e[0m\]]%}\[\e[0m\]\u\$ ' # export PROMPT_COMMAND=$PROMPT_COMMAND';export PS1=$(gitprompt.pl c=%e[32m u=%e[31m f=%e[35m t=%e[30\;1m)' # Colored counts instead of flags (cmaher): # export PSGIT='%{\[\e[0;36m\](\[\e[1;36m\]%b\[\e[0;36m\])[%c%u%f%t\[\e[0;36m\]]%}\[\e[0m\]$ ' # export PROMPT_COMMAND=$PROMPT_COMMAND';export PS1=$(gitprompt.pl statuscount=1 u=%[%e[31m%] c=%[%e[32m%] f=%[%e[1\;30m%])' # # # Format codes: # These can be placed in PSGIT or the option definitions. In PSGIT, bash escapes # should be preferred when available. # # %b - current branch name # %i - current commit id # %c - to-be-committed flag # %u - touched-files flag # %f - untracked-files flag # %A - merge commits ahead flag # %B - merge commits behind flag # %F - can-fast-forward flag # %t - terrible tragedy flag # %g - is-git-repo flag # %e - ascii escape # %[ - literal '\[' to mark the start of nonprinting characters for bash # %] - literal '\]' to mark the end of nonprinting characters for bash # %% - literal '%' # %{ - begin conditionally printed block, only shown if a nonliteral expands within # %} - end conditionally printed block # # # Options: # These are specified as arguments to the call to gitprompt.pl in the form # name=value, such as $(gitprompt.pl c=\+ u=\~ f=\* statuscount=1). # # c - string to use for %c; defaults to 'c' # u - string to use for %u; defaults to 'u' # f - string to use for %f; defaults to 'f' # A - string to use for %A; defaults to 'A' # B - string to use for %B; defaults to 'B' # F - string to use for %F; defaults to 'F' # t - string to use for %t after a timeout; defaults to '?' # l - string to use for %t when the repo is locked; defaults to '?~' # n - string to use for %t when no data could be collected, such as # if run from within a .git directory; defaults to '??' # g - string to use for %g; defaults to the empty string (see %{) # statuscount - boolean; whether to suffix %c/%u with counts ("c4u8") # # # Notes: # - If your .bashrc doesn't already define a $PROMPT_COMMAND (this is common # in /etc/bashrc, which is often sourced by default), use this # PROMPT_COMMAND line instead: # export PROMPT_COMMAND='export PS1=$(gitprompt.pl ...)' # - A good rule of thumb is to use real bash escapes (backslash flavor) inside # the definition for PSGIT (where escaping is normal) and gitprompt.pl escapes # (percent flavor) inside the arguments to gitprompt.pl (where escaping is # troublesome). # - To prevent your prompt from getting garbled, wrap all nonprinting sequences # (like color codes) in \[...\] or %[...%]. This tells Bash not to count # those characters when determining the length of your prompt and prevents it # from becoming confused. # - For... (assuming %c is whatever flags you care about) # - brackets no matter what, use... # [%c] # - brackets only in a git repo, regardless of status, use... # %{[%c%g]%} # - brackets only when a flag is set, use... # %{[%c]%} use IO::Handle; use IPC::Open3; use Time::HiRes qw(time); ### prechecks ### my $ps0 = $ENV{PSGIT}; unless ($ps0) { print "!define PSGIT!> "; exit 1; } ### global definitions ### my %formatliteral = ( e => "\e", '%' => '%', '[' => "\\[", ']' => "\\]", ); my %formatvalue = %{gitdata()}; my $output = ""; my @ps0 = split(/\%\{(.*?)\%\}/, $ps0); my $conditional = 0; foreach my $part (@ps0) { if ($conditional) { my $keep = 0; $part =~ s/\%(.)/exists $formatliteral{$1} ? $formatliteral{$1} : exists $formatvalue{$1} ? (($keep=1),$formatvalue{$1}) : ''/ge; $output .= $part if $keep; } else { $part =~ s/\%(.)/exists $formatliteral{$1} ? $formatliteral{$1} : exists $formatvalue{$1} ? $formatvalue{$1} : ''/ge; $output .= $part; } $conditional = !$conditional; } $output = "\\[\e[0;30;41m\\]! $formatvalue{error} !\\[\e[0m\\]$output" if exists $formatvalue{error}; print $output; sub gitdata { ### prechecks ### chomp(my $headref = `git symbolic-ref HEAD 2>&1`); return {} if $headref =~ /fatal: Not a git repository|fatal: Unable to read current working directory/i; ### definitions ### my %opt = ( c => 'c', u => 'u', f => 'f', A => 'A', B => 'B', F => 'F', t => '?', l => '?~', n => '??', g => '', statuscount => 0, ); ### read options ### if (@ARGV) { foreach (@ARGV) { return {error=>"invalid parameter $_"} unless /^(\w+)\=(.*?)$/; my ($key,$val) = ($1,$2); $val =~ s/\%(.)/exists $formatliteral{$1} ? $formatliteral{$1} : ''/ge; $opt{$key} = $val; } } ### collect branch data ### chomp(my $commitid = `git rev-parse --short HEAD 2>&1`); my $branch = $commitid; #fallback value if ($headref =~ /fatal: ref HEAD is not a symbolic ref/i) { # find gitdir chomp(my $gitdir = `git rev-parse --git-dir`); # parse HEAD log open(HEADLOG, "$gitdir/logs/HEAD"); my $lastrelevant = ''; while () { $lastrelevant = $_ if /^\s*\w+\s+$branch\w+\s/; } # if the log mentions switching to the commit id, use whatever it calls it $branch = $1 if $lastrelevant =~ /\scheckout\:\s+moving\s+from\s+\S+\s+to\s+(\S+)\s*$/ || $lastrelevant =~ /\smerge\s+(\S+)\:\s+Fast\-forward\s*$/; } elsif ($headref =~ /^refs\/heads\/(.+?)\s*$/) { # normal branch $branch = $1; } else { # unexpected input $headref =~ s/[^\x20-\x7e]//g; return {error=>$headref}; } ### collect status data ### my ($statusexitcode, $statusout, @status); $SIG{CHLD} = sub { wait(); $statusexitcode = $?>>8; }; my $statuspid = open3(undef,$statusout,undef,"git status"); $statusout->blocking(0); my ($running, $waiting, $start, $valid) = (1, 1, time, 0); while ($running && $waiting) { while (<$statusout>) { push @status, $_; } $running = kill 0 => $statuspid; select undef, undef, undef, .001; #yield, actually $waiting = time < $start + 1; } ### parse status data ### my %statuscount; my %sectionmap = ( 'Changes to be committed' => 'c', 'Changed but not updated' => 'u', 'Changes not staged for commit' => 'u', 'Untracked files' => 'f', 'Unmerged paths' => 'u', ); $statuscount{$_} = 0 foreach values %sectionmap; my $can_fast_forward = ''; if (!$running) { # if it terminated, parse output my ($section); foreach (@status) { if (/^(?:\# )?(\S.+?)\:\s*$/ && exists $sectionmap{$1}) { $section = $sectionmap{$1}; } elsif ($section && /^\#?\t\S/) { $statuscount{$section}++; $valid = 1; } elsif (/^nothing (added )?to commit\b/) { $valid = 1; } elsif (/\bis (ahead|behind) .+ by (\d+) commits?(\,? and can be fast\-forwarded)?/) { $statuscount{($1 eq 'ahead') ? 'A' : 'B'} = $2; $can_fast_forward = 1 if $3; } elsif (/^\# and have (\d+) and (\d+) different commit/) { $statuscount{A} = $1; $statuscount{B} = $2; } } } my $timeout = ''; if ($running) { # it was running when we stopped caring $timeout = $opt{t}; kill 2 => $statuspid; } elsif (!$valid) { #determine cause of failure if ($status[0] =~ /\.git\/index\.lock/) { $timeout = $opt{l}; } elsif ($status[0] =~ /must be run in a work tree/) { $timeout = $opt{n}; } else { print "\\[\e[41m\\]!! gitprompt.pl: \\`git status\' returned xxxwith exit code $statusexitcode and message:\n$status[0]\\[\e[0m\\]"; $timeout = "\\[\e[41m\\]!$statusexitcode!\\[\e[0m\\]"; } } ### produce output ### my %formatvalue = ( b => $branch, i => $commitid, t => $timeout, g => $opt{g}, ); $formatvalue{F} = $opt{F} if $can_fast_forward; foreach my $flag (keys %statuscount) { $formatvalue{$flag} = $statuscount{$flag} ? ($opt{$flag}.($opt{statuscount} ? $statuscount{$flag} : '')) : ''; } return \%formatvalue; }