#!/usr/bin/env bash
# MIT (c) Wenxuan Zhang

# This file is meant to be executed directly. If it's available on the PATH,
# it can also be used as a subcommand of git, which then forwards all arguments
# on to forgit. So, all of these commands will work as expected:
#
# `git forgit log`
# `git forgit checkout_file`
# `git forgit checkout_file README.md`
#
# This gives users the choice to set aliases inside of their git config instead
# of their shell config if they prefer.

# Set shell for fzf preview commands
# Disable shellcheck for "which", because it suggests "command -v xxx" instead,
# which is not a working replacement.
# See https://github.com/koalaman/shellcheck/issues/1162
# shellcheck disable=2230
SHELL="$(which bash)"
export SHELL

# Get absolute forgit path
FORGIT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/$(basename -- "${BASH_SOURCE[0]}")

FORGIT_FZF_DEFAULT_OPTS="
$FZF_DEFAULT_OPTS
--ansi
--height='80%'
--bind='alt-k:preview-up,alt-p:preview-up'
--bind='alt-j:preview-down,alt-n:preview-down'
--bind='ctrl-r:toggle-all'
--bind='ctrl-s:toggle-sort'
--bind='?:toggle-preview'
--bind='alt-w:toggle-preview-wrap'
--preview-window='right:60%'
+1
$FORGIT_FZF_DEFAULT_OPTS
"

_forgit_warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; }
_forgit_info() { printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$@" >&2; }
_forgit_inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null; }
# tac is not available on OSX, tail -r is not available on Linux, so we use either of them
_forgit_reverse_lines() { tac 2>/dev/null || tail -r; }

_forgit_previous_commit() {
	# "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead
	if [[ "$(git rev-parse "$1")" == "$(git rev-list --max-parents=0 HEAD)" ]]; then
		echo "--root"
	else
		echo "$1~"
	fi
}

_forgit_contains_non_flags() {
	while (("$#")); do
		case "$1" in
		-*) shift ;;
		*)
			return 0
			;;
		esac
	done
	return 1
}

# optional render emoji characters (https://github.com/wfxr/emoji-cli)
hash emojify &>/dev/null && _forgit_emojify='|emojify'

# extract the first git sha occurring in the input and strip trailing newline
_forgit_extract_sha="grep -Eo '[a-f0-9]+' | head -1 | tr -d '[:space:]'"

_forgit_pager=${FORGIT_PAGER:-$(git config core.pager || echo 'cat')}
_forgit_show_pager=${FORGIT_SHOW_PAGER:-$(git config pager.show || echo "$_forgit_pager")}
_forgit_diff_pager=${FORGIT_DIFF_PAGER:-$(git config pager.diff || echo "$_forgit_pager")}
_forgit_ignore_pager=${FORGIT_IGNORE_PAGER:-$(hash bat &>/dev/null && echo 'bat -l gitignore --color=always' || echo 'cat')}
_forgit_blame_pager=${FORGIT_BLAME_PAGER:-$(git config pager.blame || echo "$_forgit_pager")}
_forgit_enter_pager=${FORGIT_ENTER_PAGER:-"LESS='-r' less"}

_forgit_log_format=${FORGIT_LOG_FORMAT:-%C(auto)%h%d %s %C(black)%C(bold)%cr%Creset}
_forgit_log_preview_options="--graph --pretty=format:'$_forgit_log_format' --color=always --abbrev-commit --date=relative"
_forgit_fullscreen_context=${FORGIT_FULLSCREEN_CONTEXT:-10}
_forgit_preview_context=${FORGIT_PREVIEW_CONTEXT:-3}
_forgit_is_file_tracked="(git ls-files {} --error-unmatch) &> /dev/null"

# git commit viewer
_forgit_log() {
	_forgit_inside_work_tree || return 1
	local opts graph files log_format preview_cmd enter_cmd
	files=$(sed -nE 's/.*-- (.*)/\1/p' <<<"$*") # extract files parameters for `git show` command
	preview_cmd="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always -U$_forgit_preview_context % -- $files | $_forgit_show_pager"
	enter_cmd="echo {} | $_forgit_extract_sha | xargs -I% ${FORGIT} diff %^! $files"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"enter:execute($enter_cmd)\"
--bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview_cmd\"
$FORGIT_LOG_FZF_OPTS
"
	graph=--graph
	[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
	log_format=${FORGIT_GLO_FORMAT:-$_forgit_log_format}
	eval "git log $graph --color=always --format='$log_format' $FORGIT_LOG_GIT_OPTS $* $_forgit_emojify" |
		FZF_DEFAULT_OPTS="$opts" fzf
	fzf_exit_code=$?
	# exit successfully on 130 (ctrl-c/esc)
	[[ $fzf_exit_code == 130 ]] && return 0
	return $fzf_exit_code
}

# git diff viewer
_forgit_diff() {
	_forgit_inside_work_tree || return 1
	local files opts commits repo get_files preview_cmd enter_cmd
	[[ $# -ne 0 ]] && {
		if git rev-parse "$1" -- &>/dev/null; then
			if [[ $# -gt 1 ]] && git rev-parse "$2" -- &>/dev/null; then
				commits="$1 $2" && files=("${@:3}")
			else
				commits="$1" && files=("${@:2}")
			fi
		else
			files=("$@")
		fi
	}
	repo="$(git rev-parse --show-toplevel)"
	# Construct a null-terminated list of the filenames
	# The input looks like one of these lines:
	#   [R100]  file  ->  another file
	#   [A]     file with spaces
	#   [D]     oldfile
	# And we transform it to this representation for further usage with "xargs -0":
	#   file\0another file\0
	#   file with spaces\0
	#   oldfile\0
	# We have to do a two-step sed -> tr pipe because OSX's sed implementation does
	# not support the null-character directly.
	get_files="echo {} | sed 's/.*] *//' | sed 's/  ->  /\\\n/' | tr '\\\n' '\\\0'"
	# Git stashes are named "stash@{x}", which contains the fzf placeholder "{x}".
	# In order to support passing stashes as arguments to _forgit_diff, we have to
	# prevent fzf from interpreting this substring by escaping the opening bracket.
	# The string is evaluated a few subsequent times, so we need multiple escapes.
	escaped_commits=${commits//\{/\\\\\{}
	git_diff="git diff --color=always $FORGIT_DIFF_GIT_OPTS $escaped_commits"
	preview_cmd="cd '$repo' && $get_files | xargs -0 $git_diff -U$_forgit_preview_context -- | $_forgit_diff_pager"
	enter_cmd="cd '$repo' && $get_files | xargs -0 $git_diff -U$_forgit_fullscreen_context -- | $_forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+m -0 --bind=\"enter:execute($enter_cmd | $_forgit_enter_pager)\"
--preview=\"$preview_cmd\"
--bind=\"alt-e:execute-silent($EDITOR \$\($get_files\) >/dev/tty </dev/tty)+refresh-preview\"
$FORGIT_DIFF_FZF_OPTS
--prompt=\"$commits > \"
"
	eval "git diff --name-status $FORGIT_DIFF_GIT_OPTS $commits -- ${files[*]} | sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1]	\2/'" |
		sed 's/	/  ->  /2' | expand -t 8 |
		FZF_DEFAULT_OPTS="$opts" fzf
	fzf_exit_code=$?
	# exit successfully on 130 (ctrl-c/esc)
	[[ $fzf_exit_code == 130 ]] && return 0
	return $fzf_exit_code
}

# git add selector
_forgit_add() {
	_forgit_inside_work_tree || return 1
	local git_add changed unmerged untracked files opts preview extract
	git_add="git add $FORGIT_ADD_GIT_OPTS"
	# Add files if passed as arguments
	[[ $# -ne 0 ]] && {
		$git_add "$@" && git status -su
		return $?
	}

	changed=$(git config --get-color color.status.changed red)
	unmerged=$(git config --get-color color.status.unmerged red)
	untracked=$(git config --get-color color.status.untracked red)
	# NOTE: paths listed by 'git status -su' mixed with quoted and unquoted style
	# remove indicators | remove original path for rename case | remove surrounding quotes
extract="
sed 's/^.*]  //' |
sed 's/.* -> //' |
sed -e 's/^\\\"//' -e 's/\\\"\$//'"
preview="
file=\$(echo {} | $extract)
if (git status -s -- \\\"\$file\\\" | grep '^??') &>/dev/null; then  # diff with /dev/null for untracked files
    git diff --color=always --no-index -- /dev/null \\\"\$file\\\" | $_forgit_diff_pager | sed '2 s/added:/untracked:/'
else
    git diff --color=always -- \\\"\$file\\\" | $_forgit_diff_pager
    fi"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -0 -m --nth 2..,..
    --preview=\"$preview\"
    --bind=\"alt-e:execute-silent($EDITOR \$\(echo {} | $extract\) >/dev/tty </dev/tty)+refresh-preview\"
    $FORGIT_ADD_FZF_OPTS
    "
	files=$(git -c color.status=always -c status.relativePaths=true status -su |
		grep -F -e "$changed" -e "$unmerged" -e "$untracked" |
		sed -E 's/^(..[^[:space:]]*)[[:space:]]+(.*)$/[\1]  \2/' |
		FZF_DEFAULT_OPTS="$opts" fzf |
		sh -c "$extract")
	[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | $git_add --pathspec-file-nul --pathspec-from-file - && git status -su && return
	echo 'Nothing to add.'
}

# git reset HEAD (unstage) selector
_forgit_reset_head() {
	_forgit_inside_work_tree || return 1
	local git_reset_head cmd files opts rootdir
	git_reset_head="git reset -q $FORGIT_RESET_HEAD_GIT_OPTS HEAD"
	[[ $# -ne 0 ]] && {
		$git_reset_head "$@" && git status --short
		return $?
	}
	rootdir=$(git rev-parse --show-toplevel)
	cmd="git diff --staged --color=always -- $rootdir/{} | $_forgit_diff_pager "
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -m -0
    --preview=\"$cmd\"
    $FORGIT_RESET_HEAD_FZF_OPTS
    "
	files="$(git diff --staged --name-only | FZF_DEFAULT_OPTS="$opts" fzf)"
	# shellcheck disable=2086
	[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% $git_reset_head "$rootdir"/% && git status --short && return
	echo 'Nothing to unstage.'
}

# git stash viewer
_forgit_stash_show() {
	_forgit_inside_work_tree || return 1
	local git_stash_show git_stash_list cmd opts
	git_stash_show="git stash show --color=always --ext-diff"
	[[ $# -ne 0 ]] && {
		$git_stash_show "$@"
		return $?
	}
	git_stash_list="git stash list $FORGIT_STASH_SHOW_GIT_OPTS"
	cmd="echo {} |cut -d: -f1 |xargs -I% $git_stash_show % |$_forgit_diff_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m -0 --tiebreak=index --bind=\"enter:execute($cmd | $_forgit_enter_pager)\"
    --bind=\"ctrl-y:execute-silent(echo {} | cut -d: -f1 | tr -d '[:space:]' | ${FORGIT_COPY_CMD:-pbcopy})\"
    --preview=\"$cmd\"
    $FORGIT_STASH_FZF_OPTS
    "
	$git_stash_list | FZF_DEFAULT_OPTS="$opts" fzf
	fzf_exit_code=$?
	# exit successfully on 130 (ctrl-c/esc)
	[[ $fzf_exit_code == 130 ]] && return 0
	return $fzf_exit_code
}

# git stash push selector
_forgit_stash_push() {
	_forgit_inside_work_tree || return 1
	local git_stash_push msg args
	git_stash_push="git stash push $FORGIT_STASH_PUSH_GIT_OPTS"
	args=("$@")
	while (("$#")); do
		case "$1" in
		# allow message as argument
		-m | --message)
			msg="$2"
			shift 2
			;;
		# ignore -u as it's used implicitly
		-u | --include-untracked) shift ;;
		# pass to git directly when encountering anything else
		*)
			$git_stash_push "${args[@]}"
			return $?
			;;
		esac
	done
	local opts preview files
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -m
    $FORGIT_STASH_PUSH_FZF_OPTS
    "
    preview="
    if $_forgit_is_file_tracked; then
        git diff --color=always {} | $_forgit_diff_pager
    else
        git diff --color=always /dev/null {} | $_forgit_diff_pager
    fi
    "
	# Show both modified and untracked files
	files=$(git ls-files --exclude-standard --modified --others | FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
	[[ -z "$files" ]] && return 1
	echo "${files[@]}" | tr '\n' '\0' | $git_stash_push ${msg:+-m "$msg"} -u --pathspec-file-nul --pathspec-from-file -
}

# git clean selector
_forgit_clean() {
	_forgit_inside_work_tree || return 1
	_forgit_contains_non_flags "$@" && {
		git clean -q "$@"
		return $?
	}
	local git_clean files opts
	git_clean="git clean $FORGIT_CLEAN_GIT_OPTS"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -m -0
    $FORGIT_CLEAN_FZF_OPTS
    "
	# Note: Postfix '/' in directory path should be removed. Otherwise the directory itself will not be removed.
	files=$(git clean -xdffn "$@" | sed 's/^Would remove //' | FZF_DEFAULT_OPTS="$opts" fzf | sed 's#/$##')
	# shellcheck disable=2086
	[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% $git_clean -xdff '%' && git status --short && return
	echo 'Nothing to clean.'
}

_forgit_cherry_pick() {
	local git_cherry_pick base target preview opts fzf_selection fzf_exitval

	git_cherry_pick="git cherry-pick $FORGIT_CHERRY_PICK_GIT_OPTS"

	base=$(git branch --show-current)
	[[ -z "$base" ]] && echo "Current commit is not on a branch." && return 1

	[[ -z $1 ]] && echo "Please specify target branch" && return 1
	target="$1"

	# in this function, we do something interesting to maintain proper ordering as it's assumed
	# you generally want to cherry pick oldest->newest when you multiselect
	# The instances of "cut", "nl" and "sort" all serve this purpose
	# Please see https://github.com/wfxr/forgit/issues/253 for more details

	preview="echo {} | cut -f2- | $_forgit_extract_sha | xargs -I% git show --color=always % | $_forgit_show_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    --preview=\"$preview\"
    --multi --ansi --with-nth 2.. -0 --tiebreak=index
    $FORGIT_CHERRY_PICK_FZF_OPTS
    "
	# Note: do not add any pipe after the fzf call here, otherwise the fzf_exitval is not propagated properly.
	# Any eventual post processing can be done afterwards when the "commits" variable is assigned below.
	fzf_selection=$(git log --right-only --color=always --cherry-pick --oneline "$base"..."$target" | nl |
		FZF_DEFAULT_OPTS="$opts" fzf)
	fzf_exitval=$?
	[[ $fzf_exitval != 0 ]] && return $fzf_exitval
	[[ -z "$fzf_selection" ]] && return $fzf_exitval

	${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
	IFS=$'\n'
	# shellcheck disable=2207
	commits=($(echo "$fzf_selection" | sort --numeric-sort --key=1 | cut -f2 | cut -d' ' -f1 | _forgit_reverse_lines))
	${old_IFS+"false"} && unset IFS || IFS="$old_IFS"
	[ ${#commits[@]} -eq 0 ] && return 1

	$git_cherry_pick "${commits[@]}"
}

_forgit_cherry_pick_from_branch() {
	_forgit_inside_work_tree || return 1
	local cmd preview opts branch exitval input_branch args base

	base=$(git branch --show-current)
	[[ -z "$base" ]] && echo "Current commit is not on a branch." && return 1

	args=("$@")
	if [[ $# -ne 0 ]]; then
		input_branch=${args[0]}
	fi
	cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
	preview="git log --right-only --color=always --cherry-pick --oneline $base...{1}"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index --header-lines=1
    --preview=\"$preview\"
    $FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS
    "
	# loop until either the branch selector is closed or a commit to be cherry
	# picked has been selected from within a branch
	while true; do
		if [[ -z $input_branch ]]; then
			branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
		else
			branch=$input_branch
		fi

		unset input_branch
		[[ -z "$branch" ]] && return 1

		_forgit_cherry_pick "$branch"

		exitval=$?
		[[ $exitval != 130 ]] || [[ $# -ne 0 ]] && return $exitval
	done
}

_forgit_rebase() {
	_forgit_inside_work_tree || return 1
	local git_rebase cmd preview opts graph files target_commit prev_commit
	git_rebase="git rebase -i $FORGIT_REBASE_GIT_OPTS"
	graph=--graph
	[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
	cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
	files=$(sed -nE 's/.* -- (.*)/\1/p' <<<"$*") # extract files parameters for `git show` command
	preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index
    --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
    --preview=\"$preview\"
    $FORGIT_REBASE_FZF_OPTS
    "
	target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
	if [[ -n "$target_commit" ]]; then
		prev_commit=$(_forgit_previous_commit "$target_commit")

		$git_rebase "$prev_commit"
	fi
}

_forgit_fixup() {
	_forgit_inside_work_tree || return 1
	git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1
	local git_fixup cmd preview opts graph files target_commit prev_commit
	git_fixup="git commit --fixup $FORGIT_FIXUP_GIT_OPTS"
	graph=--graph
	[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
	cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
	files=$(sed -nE 's/.* -- (.*)/\1/p' <<<"$*")
	preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index
    --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
    --preview=\"$preview\"
    $FORGIT_FIXUP_FZF_OPTS
    "
	target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
	if [[ -n "$target_commit" ]] && $git_fixup "$target_commit"; then
		prev_commit=$(_forgit_previous_commit "$target_commit")
		# rebase will fail if there are unstaged changes so --autostash is needed to temporarily stash them
		# GIT_SEQUENCE_EDITOR=: is needed to skip the editor
		GIT_SEQUENCE_EDITOR=: git rebase --autostash -i --autosquash "$prev_commit"
	fi

}

# git checkout-file selector
_forgit_checkout_file() {
	_forgit_inside_work_tree || return 1
	local git_checkout cmd files opts
	git_checkout="git checkout $FORGIT_CHECKOUT_FILE_GIT_OPTS"
	[[ $# -ne 0 ]] && {
		$git_checkout -- "$@"
		return $?
	}
	cmd="git diff --color=always -- {} | $_forgit_diff_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -m -0
    --preview=\"$cmd\"
    $FORGIT_CHECKOUT_FILE_FZF_OPTS
    "
	files="$(git ls-files --modified "$(git rev-parse --show-toplevel)" | FZF_DEFAULT_OPTS="$opts" fzf)"
	[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | $git_checkout --pathspec-file-nul --pathspec-from-file -
}

# git checkout-branch selector
_forgit_checkout_branch() {
	_forgit_inside_work_tree || return 1
	# if called with arguments, check if branch exists, else create a new one
	if [[ $# -ne 0 ]]; then
		if [[ "$*" == "-" ]] || git show-branch "$@" &>/dev/null; then
			git switch "$@"
		else
			git switch -c "$@"
		fi
		checkout_status=$?
		git status --short
		return $checkout_status
	fi

	local git_checkout cmd preview opts branch
	cmd="git branch --color=always ${FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS:---all} | LC_ALL=C sort -k1.1,1.1 -rs"
	preview="git log {1} $_forgit_log_preview_options"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index --header-lines=1
    --preview=\"$preview\"
    $FORGIT_CHECKOUT_BRANCH_FZF_OPTS
    "
	branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
	[[ -z "$branch" ]] && return 1

	git_checkout="git checkout $FORGIT_CHECKOUT_BRANCH_GIT_OPTS"
	# track the remote branch if possible
	if [[ "$branch" == "remotes/origin/"* ]]; then
		if git branch | grep -qw "${branch#remotes/origin/}"; then
			# hack to force creating a new branch which tracks the remote if a local branch already exists
			$git_checkout -b "track/${branch#remotes/origin/}" --track "$branch"
		elif ! $git_checkout --track "$branch" 2>/dev/null; then
			$git_checkout "$branch"
		fi
	else
		$git_checkout "$branch"
	fi
}

# git checkout-tag selector
_forgit_checkout_tag() {
	_forgit_inside_work_tree || return 1
	local git_checkout cmd opts preview
	git_checkout="git checkout $FORGIT_CHECKOUT_TAG_GIT_OPTS"
	[[ $# -ne 0 ]] && {
		$git_checkout "$@"
		return $?
	}
	cmd="git tag -l --sort=-v:refname"
	preview="git log {1} $_forgit_log_preview_options"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index
    --preview=\"$preview\"
    $FORGIT_CHECKOUT_TAG_FZF_OPTS
    "
	tag="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf)"
	[[ -z "$tag" ]] && return 1
	$git_checkout "$tag"
}

# git checkout-commit selector
_forgit_checkout_commit() {
	_forgit_inside_work_tree || return 1
	local git_checkout cmd opts graph
	git_checkout="git checkout $FORGIT_CHECKOUT_COMMIT_GIT_OPTS"
	[[ $# -ne 0 ]] && {
		$git_checkout "$@"
		return $?
	}
	cmd="echo {} | $_forgit_extract_sha |xargs -I% git show --color=always % | $_forgit_show_pager"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s +m --tiebreak=index
    --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
    --preview=\"$cmd\"
    $FORGIT_CHECKOUT_COMMIT_FZF_OPTS
    "
	graph=--graph
	[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
	# shellcheck disable=2086
	eval "git log $graph --color=always --format='$_forgit_log_format' $_forgit_emojify" |
		FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha" | xargs -I% $git_checkout % --
}

_forgit_branch_delete() {
	_forgit_inside_work_tree || return 1
	local git_branch preview opts cmd branches
	git_branch="git branch $FORGIT_BRANCH_DELETE_GIT_OPTS"
	[[ $# -ne 0 ]] && {
		$git_branch -D "$@"
		return $?
	}
	preview="git log {1} $_forgit_log_preview_options"

    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s --multi --tiebreak=index --header-lines=1
    --preview=\"$preview\"
    $FORGIT_BRANCH_DELETE_FZF_OPTS
    "

	cmd="git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs"
	branches=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
	# shellcheck disable=2086
	echo -n "$branches" | tr '\n' '\0' | xargs -I{} -0 $git_branch -D {}
}

# git revert-commit selector
_forgit_revert_commit() {
	_forgit_inside_work_tree || return 1
	local git_revert cmd opts files preview commits IFS
	git_revert="git revert $FORGIT_REVERT_COMMIT_GIT_OPTS"
	[[ $# -ne 0 ]] && {
		$git_revert "$@"
		return $?
	}

	cmd="git log --graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    +s --tiebreak=index
    --ansi --with-nth 2..
    $FORGIT_REVERT_COMMIT_FZF_OPTS
    "

	# in this function, we do something interesting to maintain proper ordering as it's assumed
	# you generally want to revert newest->oldest when you multiselect
	# The instances of "cut", "nl" and "sort" all serve this purpose
	# Please see https://github.com/wfxr/forgit/issues/253 for more details

	files=$(sed -nE 's/.* -- (.*)/\1/p' <<<"$*") # extract files parameters for `git show` command
	preview="echo {} | cut -f2- | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"

	${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
	IFS=$'\n'
	# shellcheck disable=2207
	commits=($(eval "$cmd" |
		nl |
		FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview" -m |
		sort --numeric-sort --key=1 |
		cut -f2- |
		sed 's/^[^a-f^0-9]*\([a-f0-9]*\).*/\1/'))
	${old_IFS+"false"} && unset IFS || IFS="$old_IFS"

	[ ${#commits[@]} -eq 0 ] && return 1

	$git_revert "${commits[@]}"
}

# git blame viewer
_forgit_blame() {
	_forgit_inside_work_tree || return 1
	local git_blame opts flags preview file
	git_blame="git blame $FORGIT_BLAME_GIT_OPTS"
	_forgit_contains_non_flags "$@" && {
		$git_blame "$@"
		return $?
	}
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    $FORGIT_BLAME_FZF_OPTS
    "
	flags=$(git rev-parse --flags "$@")
    preview="
    if $_forgit_is_file_tracked; then
        git blame {} --date=short $FORGIT_BLAME_GIT_OPTS $flags | $_forgit_blame_pager
    else
        echo File not tracked
    fi
    "
	file=$(FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
	[[ -z "$file" ]] && return 1
	# shellcheck disable=2086
	eval $git_blame "$file" "$flags"
}

# git ignore generator
export FORGIT_GI_REPO_REMOTE=${FORGIT_GI_REPO_REMOTE:-https://github.com/dvcs/gitignore}
export FORGIT_GI_REPO_LOCAL="${FORGIT_GI_REPO_LOCAL:-${XDG_CACHE_HOME:-$HOME/.cache}/forgit/gi/repos/dvcs/gitignore}"
export FORGIT_GI_TEMPLATES=${FORGIT_GI_TEMPLATES:-$FORGIT_GI_REPO_LOCAL/templates}

_forgit_ignore() {
	[ -d "$FORGIT_GI_REPO_LOCAL" ] || _forgit_ignore_update
	local IFS cmd args opts
	cmd="$_forgit_ignore_pager $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null"
    opts="
    $FORGIT_FZF_DEFAULT_OPTS
    -m --preview-window='right:70%'
    --preview=\"eval $cmd\"
    $FORGIT_IGNORE_FZF_OPTS
    "
	${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
	IFS=$'\n'
	# shellcheck disable=SC2206,2207
	args=($@) && [[ $# -eq 0 ]] && args=($(_forgit_ignore_list | nl -nrn -w4 -s'  ' |
		FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}'))
	${old_IFS+"false"} && unset IFS || IFS="$old_IFS"
	[ ${#args[@]} -eq 0 ] && return 1
	# shellcheck disable=SC2068
	_forgit_ignore_get ${args[@]}
}
_forgit_ignore_update() {
	if [[ -d "$FORGIT_GI_REPO_LOCAL" ]]; then
		_forgit_info 'Updating gitignore repo...'
		(cd "$FORGIT_GI_REPO_LOCAL" && git pull --no-rebase --ff) || return 1
	else
		_forgit_info 'Initializing gitignore repo...'
		git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL"
	fi
}
_forgit_ignore_get() {
	local item filename header
	for item in "$@"; do
		if filename=$(find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "${item}.gitignore" -o -iname "${item}" \) -print -quit); then
			[[ -z "$filename" ]] && _forgit_warn "No gitignore template found for '$item'." && continue
			header="${filename##*/}" && header="${header%.gitignore}"
			echo "### $header" && cat "$filename" && echo
		fi
	done
}
_forgit_ignore_list() {
	find "$FORGIT_GI_TEMPLATES" -print | sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu
}
_forgit_ignore_clean() {
	setopt localoptions rmstarsilent
	[[ -d "$FORGIT_GI_REPO_LOCAL" ]] && rm -rf "$FORGIT_GI_REPO_LOCAL"
}

valid_commands=(
	"add"
	"blame"
	"branch_delete"
	"checkout_branch"
	"checkout_commit"
	"checkout_file"
	"checkout_tag"
	"cherry_pick"
	"cherry_pick_from_branch"
	"clean"
	"diff"
	"fixup"
	"ignore"
	"log"
	"rebase"
	"reset_head"
	"revert_commit"
	"stash_show"
	"stash_push"
)

cmd="$1"
shift

# shellcheck disable=SC2076
if [[ ! " ${valid_commands[*]} " =~ " ${cmd} " ]]; then
	if [[ -z "$cmd" ]]; then
		printf "forgit: missing command\n\n"
	else
		printf "forgit: '%s' is not a valid forgit command.\n\n" "$cmd"
	fi
	printf "The following commands are supported:\n"
	printf "\t%s\n" "${valid_commands[@]}"
	exit 1
fi

_forgit_"${cmd}" "$@"
