Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 573 lines (481 sloc) 12.9 KB
#!/usr/bin/env bash
# Use Homebrew GNU commands on OS X if available
if [ "`uname -s`" = "Darwin" ]; then
shopt -s expand_aliases
for command in readlink sed; do
if which g$command &>/dev/null; then
alias $command="g$command"
fi
done
fi
workdir=`echo "$PWD" | sed -r "s|^$HOME|~|"`
# default options
AG="ag --nocolor --smart-case --follow"
GIT="git -c color.ui=always"
GIT_FORMAT="%C(yellow)[%h] %C(black bold)%ar:%Creset %s"
GIT_LOG=10
# colors
RESET=$(echo -ne '\e[0m')
WHITE=$(echo -ne '\e[1;37m')
RED=$(echo -ne '\e[1;31m')
GREEN=$(echo -ne '\e[1;32m')
YELLOW=$(echo -ne '\e[1;33m')
CYAN=$(echo -ne '\e[1;36m')
GRAY=$(echo -ne '\e[1;30m')
DARKRED=$(echo -ne '\e[0;31m')
DARKGREEN=$(echo -ne '\e[0;32m')
DARKYELLOW=$(echo -ne '\e[0;33m')
DARKCYAN=$(echo -ne '\e[0;36m')
# cursor movement
COLUMNS=`tput cols`
LINES=`tput lines`
function main {
unset list showall editall force complete
unset input pattern selection command
unset loading
while [ "${1:0:1}" = "-" ]; do
case "$1" in
-l|--list) list=1;;
-a|--all) showall=1;;
-f|--force) force=1;;
-c|--complete)
complete=1
# redirect stdout to stderr, and open FD 3 to output completions to stdout
exec 3>&1
exec 1>&2
;;
-e|--command)
command=( "$2" )
shift
;;
--spin)
start_loading
trap "echo; exit" 2
read
stop_loading
exit
;;
--)
shift
command_args=( "$@" )
shift $#
break
;;
-*) usage;;
esac
shift
done
original_input=( "$@" )
input="$1"
shift
if [ -f "$1" ]; then
input="$1"
shift
elif [[ "$1" =~ ^[0-9]+$ ]]; then
output_selection=1
selection="$1"
shift
fi
if [ -n "$1" -o -z "$command" ]; then
case "$1" in
edit|edi|ed|"")
shift
command=( sensible-editor "$@" )
;;
diff|dif|di|d)
shift
command=( git -c pager.diff=false diff "$@" )
command_run_each=1
;;
add|ad|a)
shift
command=( git add "$2" )
;;
each|eac|ea)
command_run_each=1
shift
;;
*)
command=( "$@" )
shift $#
;;
esac
fi
if [ -v command_args ]; then
command=( "${command[@]}" "${command_args[@]}" )
fi
pattern="$input"
limit=$((LINES - 4))
# check for trailing numbers
if echo -n "$pattern" | egrep -q '(^|.*#)[0-9]*$'; then
# use the given number as the selection
selection=`echo "$pattern" | sed -r 's/(^|.*#)([0-9]*)$/\2/'`
pattern=`echo "$pattern" | sed -r 's/#?[0-9]*$//'`
fi
# show all matches if the pattern ends with '!'
if [ "${pattern:$((${#pattern} - 1))}" = "!" ]; then
showall=1
editall=1
pattern="${pattern:0:$((${#pattern} - 1))}"
fi
if [ -z "$input" -a -z "$complete" -a -z "$list" -a ! -v __GIT_EDIT_LAST ]; then
exec "${command[@]}" "$@"
elif [ -n "$complete" ]; then
start_loading
fi
match_pattern
[ -z "$count" ] && count=${#matches[@]}
[ $count -eq 0 ] && pretty_matches=
if [ -n "$showall" ]; then
limit="$count"
else
# trim results to number of lines
[ $count -lt $limit ] && limit=$count
[ $limit -le 0 ] && limit=1
if [ $count -gt $limit ]; then
pretty_matches=( "${pretty_matches[@]:0:$limit}" )
fi
fi
# use the selected match
if [ -n "$selection" ]; then
if [ $selection -gt $count ]; then
echo "Only got $count matches."
exit 1
fi
count=1
matches=${matches[$((selection - 1))]}
pretty_matches=${pretty_matches[$((selection - 1))]}
[ "$type" != "log" ] && completion="$matches"
fi
if [ -n "$list" -o ! -t 1 ]; then
output "${matches[@]}"
elif [ -n "$complete" ]; then
stop_loading
show_completion
elif [ "$type" = "log" ]; then
return
elif [ $count -eq 0 ]; then
return
elif [ $count -eq 1 -o -z "$editall" ]; then
matches=${matches[0]}
if [ "${command[0]}" = "sensible-editor" ]; then
echo -e " $GREEN>$WHITE Editing $GREEN$matches$WHITE"
else
echo -e " $GREEN>$WHITE Running $CYAN'${command[@]}'$WHITE with $GREEN$matches$WHITE"
fi
exec "${command[@]}" "$matches"
else
if [ -z "$force" -a "${command[0]}" != "sensible-editor" ]; then
echo -n " $CYAN>>>$WHITE Run $CYAN'${command[@]}'$WHITE on $YELLOW$count$WHITE files?$RESET [Y/n] "
read
[ "$REPLY" = "n" -o "$REPLY" = "N" ] && exit
fi
if [ -n "$command_run_each" ]; then
echo "$matches" | while read match; do
"${command[@]}" "$match"
done
else
exec "${command[@]}" "${matches[@]}"
fi
fi
}
function usage {
if [ -z "$complete" ]; then
cat <<EOF
Usage: git-edit [OPTIONS] INPUT [SELECTION] [each] [COMMAND [ARGS..]]
Options:
-l, --list Output all matches (unformatted)
-a, --all Show all matches (formatted)
-f, --force Assume yes for all questions
-c, --complete Completion mode
Input formats:
@ Show Git log (repeat @ to show more entries)
* Path or fuzzy match pattern
Fuzzy matching:
. Expands to .*
/ Expands to .*/
Selection:
INPUT#NUMBER Select a file by number
INPUT NUMBER Select a file by number (as second argument)
INPUT! Show all files
@! Show all log entries
@NUMBER, @HASH Show changed files for a commit
@INPUT#NUMBER Select file from commit by number
@INPUT NUMBER Select file from commit by number (as second argument)
Commands:
edit Run 'sensible-editor' (default)
diff Run 'git diff'
add Run 'git add'
* Run any command on the files
each * Run command on each file individually
EOF
fi
exit 255
}
function is_git {
git rev-parse --is-inside-work-tree &>/dev/null
}
# output a string to the redirected stdout,
# which will cause the input to be replaced by it
function feed_completion {
echo "$@" | sed -r 's/ /\\ /g' >&3
}
# output array members separated by newlines
function output {
if [ $# -gt 0 ]; then
IFS=$'\n'
echo "$*"
fi
}
# truncate lines to the given length
function truncate {
local length="${1:-$((COLUMNS - 9))}"
if [ "$2" = "head" ]; then
sed -r "s/^.{5,}(.{$((length - 4))})$/ ...\1/"
else
sed -r "s/^(.{$((length - 3))}).{5,}$/\1.../"
fi
}
# collect matches for the given pattern
function match_pattern {
if [ -z "$pattern" ] && is_git; then
match_git_status
if [ ${#matches[@]} -eq 0 ]; then
match_git_log
[ -n "$complete" ] && feed_completion "@$selection"
fi
elif [ "${pattern:0:1}" = "@" ]; then
if ! is_git; then
title="Not a Git repository"
return
fi
commit="$pattern"
GIT_LOG=0
while [ "${commit:0:1}" = "@" ]; do
commit=${commit:1}
let GIT_LOG+=10
done
if [ "${commit:0:1}" = "!" ]; then
showall=1
commit=${commit:1}
fi
if [ -n "$showall" ]; then
GIT_LOG=-1
elif [ -n "$selection" ]; then
GIT_LOG=$((selection + 1))
fi
if [[ "$commit" =~ ^[0-9]{1,5}$ ]]; then
local commit_selection=$commit
GIT_LOG=$commit_selection
fi
local original_limit=$limit
limit=$GIT_LOG
match_git_log
if [ -n "$commit" ]; then
if [ -n "$commit_selection" ]; then
commit=${matches[$((commit_selection - 1))]}
fi
limit=$original_limit
unset count matches pretty_matches
if [ -n "$commit" ]; then
match_git_diff "$commit"
else
title="Invalid commit '$commit_selection'"
fi
fi
else
match_files
fi
}
function match_files {
type="files"
if [ -f "$pattern" ]; then
matches="$pattern"
completion="$pattern"
return
fi
# expand '.' to '.*' and '/' to '.*/
pattern=$(
echo "$pattern" | sed -r -e 's#\.+\**#.*#g' -e 's#/+#.*/#g'
)
if [ "$PWD" = "$HOME" ]; then
local dirs="${__GIT_EDIT_HOME_DIRS:-bin src tmp}"
else
unset dirs
fi
mapfile -t matches < <(
$AG . $dirs -l --file-search-regex "$pattern" 2>/dev/null | awk '{ print length, $0 }' | sort -n | cut -d" " -f2-
)
count=${#matches[@]}
if [ $count -gt 0 ]; then
if [ $count -le $limit ]; then
local prefix=`output "${matches[@]}" | lines prefix`
if [ -n "$prefix" -a ${#prefix} -gt ${#input} ] && echo "$prefix" | egrep -q "$pattern"; then
completion="$prefix"
pattern="$prefix"
fi
fi
mapfile -t pretty_matches < <(
output "${matches[@]}" \
| truncate "" head \
| sed -r "s#($pattern)#$RED\1$RESET#g" \
| sed -r "s/$/$RESET/"
)
title="Found %count matches in %dir"
else
title="${RED}No matches in %dir"
fi
}
function match_git_status {
type="status"
mapfile -t pretty_matches < <(
$GIT status -s | egrep -v '^[^a-z]+mD\b'
)
mapfile -t matches < <(
output "${pretty_matches[@]}" | sed -r 's/.* ([^ ]+)$/\1/'
)
if [ ${#matches[@]} -gt 0 ]; then
title="Found %count changes in %dir"
else
title="No changes in %dir"
fi
}
function match_git_log {
type="log"
local log="$GIT log -n $GIT_LOG --all --no-merges"
mapfile -t matches < <(
$log --pretty=format:"%h"
)
if [ -n "$matches" ]; then
mapfile -t pretty_matches < <(
$log --pretty=format:"%C(yellow)[%h] %C(black bold)%ar:%Creset %s" \
| truncate $((COLUMNS + 5))
)
title="Latest commits in %dir"
else
title="No commits in %dir"
fi
}
function match_git_diff {
type="diff"
local show="$GIT show --pretty=format:"" ${commit:-HEAD}"
mapfile -t matches < <(
$show --name-only 2>/dev/null | tr "\0" "\n" | grep .
)
if [ $? -ne 0 ]; then
title="Unknown or ambiguous commit $YELLOW[$commit]$WHITE"
elif [ -n "$matches" ]; then
mapfile -t pretty_matches < <(
$show --color --stat=$((COLUMNS - 7)) \
--pretty=format:"%C(yellow)[%h]%C(white) %s" \
| sed -r -e 's/^\s*//' \
-e "s/\|(.*[0-9]+)?/$GRAY|$DARKCYAN\1$RESET/"
)
title=`echo "${pretty_matches[0]}" | truncate $((COLUMNS - 3))`
pretty_matches=( "${pretty_matches[@]:1:$((${#pretty_matches[@]} - 2))}" )
editall=1
else
title="${RED}No changed files in commit $YELLOW[$commit]$WHITE"
fi
}
function show_completion {
#tput rc
# overwrite the spinner
echo -n " "
tput cub 3
# if there's a completion backtrack over the input, to make sure
# the completion will end up in the correct position
if [ -n "$completion" ]; then
local args="${original_input[@]}"
tput cub ${#args}
fi
if [ $count -eq 0 ]; then
# output bell if there are no matches
echo -ne "\a"
return
elif [ $count -eq 1 ] && [ -n "$selection" -o "$type" = "files" ]; then
# don't show matches when a number was passed,
# or when a file match only returns one result
unset show_matches
else
local show_matches=1
fi
if [ -n "$show_matches" ]; then
# expand variables in title
if is_git && [ -f /usr/lib/git-core/git-sh-prompt ]; then
. /usr/lib/git-core/git-sh-prompt
branch=" $RESET`__git_ps1 "$DARKGREEN[$GREEN%s$DARKGREEN]$RESET"`"
else
unset branch
fi
title=$(echo "$title" | sed -r \
-e "s|%count|$YELLOW$count$WHITE|g" \
-e "s|%dir|$DARKCYAN[$CYAN$workdir$DARKCYAN]$WHITE$branch|g"
)
if [ $count -eq 0 ]; then
title=" $DARKRED>$WHITE $title$RESET"
else
title=" $GREEN>$WHITE $title$RESET"
fi
echo
echo -ne "\n$title $GRAY[$type"
# show the pattern when no match was found
if [ $count -eq 0 -a -n "$pattern" ]; then
echo -n "~$pattern" | truncate 20 head
fi
echo "]$RESET"
if [ $count -gt 0 ]; then
output "${pretty_matches[@]}" | nl -s ' ' | sed -r "s/^\s{2}(\s*)([0-9]+)/ \1$DARKYELLOW[$YELLOW\2$DARKYELLOW]$RESET/"
if [ $count -gt $limit -a -z "$showall" ]; then
echo " $GRAY($RESET$((count - limit))$GRAY more...)$RESET"
fi
fi
fi
# show the completion prompt
if [ -n "$show_matches" ]; then
echo -n " $GREEN>>>$RESET "
fi
[ $count -gt 0 -a -z "$completion" ] && echo -n "$RED"
if [ -n "$output_selection" ]; then
# output the selected match
echo -n "$selection"
else
# output the input
echo -n "$input"
fi
echo -n "$RESET"
if [ -n "$completion" ]; then
feed_completion "$completion"
fi
if [ -n "$show_matches" ]; then
exit 0
else
# signal for shell completion to indicate that there
# was no output besides the completed word
exit 100
fi
}
function start_loading {
#tput sc
(
echo -n "$RED"
sleep 0.1
while true; do
for char in ◐ ◓ ◑ ◒; do
echo -n " $char"
tput cub 3
sleep 0.1
done
done
) 2>/dev/null &
loading=$!
trap "stop_loading; exit" 0 1 2 3 9 15
}
function stop_loading {
if [ -n "$loading" ]; then
kill $loading 2>/dev/null
echo -n "$RESET"
unset loading
fi
}
main "$@"
Something went wrong with that request. Please try again.