commit a43305a18096d04392a9558ef957fd577253a511 Author: Lucas Burns <burnsac@me.com> Date: Fri, 5 Feb 2021 13:12:39 -0600 initial commit Diffstat:
A | .gitignore | | | 2 | ++ |
A | Makefile | | | 54 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README.md | | | 135 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fadd | | | 70 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fcheckout | | | 92 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fedit | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fgit_helper.sh | | | 290 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | flog | | | 116 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | freset | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fstash | | | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fstat | | | 75 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | funtrack | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fzgrep | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | preview.sh | | | 118 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
14 files changed, 1302 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +**/.DS_Store diff --git a/Makefile b/Makefile @@ -0,0 +1,54 @@ +PROG ?= fzfgit +PREFIX ?= /usr/local +DESTDIR ?= $(PREFIX)/bin +# DESTDIR ?= $(PREFIX)/fzfgit +# SHELL := /bin/zsh +# export PATH=$(DESTDIR):$$PATH; echo $$PATH; + +all: + @echo "To install $(PROG) scripts run \"make install\"." + @echo "The $(PROG) scripts require some other packages:" + @echo " brew install fzf bat" + +install: + @install -vd "$(DESTDIR)/" + @install -vm755 fadd "$(DESTDIR)/fadd" + @echo "fadd installed successfully" + @install -vm755 fcheckout "$(DESTDIR)/fcheckout" + @echo "fcheckout installed successfully" + @install -vm755 fedit "$(DESTDIR)/fedit" + @echo "fedit installed successfully" + @install -vm755 flog "$(DESTDIR)/flog" + @echo "flog installed successfully" + @install -vm755 freset "$(DESTDIR)/freset" + @echo "freset installed successfully" + @install -vm755 fstash "$(DESTDIR)/fstash" + @echo "fstash installed successfully" + @install -vm755 fstat "$(DESTDIR)/fstat" + @echo "fstat installed successfully" + @install -vm755 funtrack "$(DESTDIR)/funtrack" + @echo "funtrack installed successfully" + @install -vm755 fzgrep "$(DESTDIR)/fzgrep" + @echo "fzgrep installed successfully" + @install -vm755 fgit_helper.sh "$(DESTDIR)/fgit_helper.sh" + @echo "fgit_helper.sh installed successfully" + @install -vm755 preview.sh "$(DESTDIR)/preview.sh" + @echo "preview.sh installed successfully" + @echo + @echo "fzfgit successfully completed installation " + @echo + +uninstall: + @rm -vrf \ + "$(DESTDIR)/fadd" \ + "$(DESTDIR)/fcheckout" \ + "$(DESTDIR)/fedit" \ + "$(DESTDIR)/flog" \ + "$(DESTDIR)/freset" \ + "$(DESTDIR)/freset" \ + "$(DESTDIR)/fstash" \ + "$(DESTDIR)/fstat" \ + "$(DESTDIR)/funtrack" \ + "$(DESTDIR)/fzgrep" \ + "$(DESTDIR)/fgit_helper.sh" \ + "$(DESTDIR)/preview.sh" diff --git a/README.md b/README.md @@ -0,0 +1,135 @@ +fzfgit scripts +============== + +These scripts come from [dotbare](https://github.com/kazhala/dotbare) and are modified to work with a regular git directory. + +Installation: +------------ + +```sh +git clone https://github.com/burnsac5040/fzfgit.git +cd fzfgit +make install +``` + +Dependencies: +------------ +- [`fzf`](https://github.com/junegunn/fzf) + - macOS: `brew install fzf` +- [`bat`](https://github.com/sharkdp/bat) + - macOS: `brew install bat` + +Scripts (from [dotbare](https://github.com/kazhala/dotbare)) +------- +fadd +---- +Select files/directories or modified files through fzf and stage the selected files/directories. + + - Default: list all modified files and stage selected files. Support multi selection. + - `-h, --help`: show the help message of `fadd` and exit. + - `-f, --file`: list all files in current directory and stage selected files. Support multi selection. (Used for staging new files to index). + - `-d, --dir`: list all directory under current directory and stage selected directory. Support multi selection. (Used for staging new files to index). + + +fcheckout +--------- +Checkout files/commit/branch interactively through fzf. + + - Default: list all modified files and reset selected files back to HEAD. Support multi selection. (Discard all changes) **Note**: if your file is staged, you will need to unstage first before running fcheckout to make it work. + - `-h, --help`: show the help message of `fcheckout` and exit. + - `-s, --select`: list all tracked files and select a commit to checkout the selected files. Support multi selection. + - `-b, --branch`: list all branch and switch/checkout the selected branch. + - `-c, --commit`: list all commits and checkout selected commit. + - `-y, --yes`: acknowledge all actions that will be taken and skip confirmation. + + +fedit +----- +Select files/commits through fzf and edit selected files/commits in `$EDITOR`. Editing commits will perform a interactive rebase. + + - Default: list all tracked files and open `$EDITOR` to edit the selected files. Support multi selection. + - `-h, --help`: show the help message of `fedit` and exit. + - `-m, --modified`: list all modified files and open `$EDITOR` to edit the selected files. Support multi selection. + - `-c, --commit`: list all commits and edit the selected commit through interactive rebase. + + +flog +---- +Interactive log viewer that will prompt you with a menu after selecting a commit. The action menu contains options including edit, reset, revert and checkout the selected commits. + + - Default: list all commits and then prompt menu to select action to perform. + - `-h, --help`: show the help message of `flog` and exit. + - `-r, --revert`: revert the selected commit and skip action menu. + - `-R, --reset`: reset HEAD back to the selected commit and skip action menu. + - `-e, --edit`: edit selected commit through interactive rebase and skip action menu. + - `-c, --checkout`: checkout selected commit and skip action menu. + - `-y, --yes`: acknowledge all actions that will be taken and skip confirmation. + + +freset +------ +Select staged files or commits through fzf and then reset(unstage) staged files or reset HEAD back to certain commits. Also supports reset HEAD back to certain commits using either `--soft`, `--hard`, `--mixed` flags. More information on differences between flags [here](https://git-scm.com/docs/git-reset#Documentation/git-reset.txt-emgitresetemltmodegtltcommitgt). + + - Default: list all staged files and unstage the selected files. Support multi selection. + - `-h, --help`: show the help message of `freset` and exit. + - `-c, --commit`: list all commits and then reset HEAD back to the selected commits. (Default: --mixed, put all changes into modified state). + - `-S, --soft`: use --soft flag instead of --mixed flag, reset HEAD to certain commit without modifying working tree. + - `-H, --hard`: use --hard flag instead of --mixed flag, reset HEAD to certain commit discard all changes from the working tree. + - `-y, --yes`: acknowledge all actions that will be taken and skip confirmation. + + +fstash +------ +View and manage stash interactively. + + - Default: list all stashes and apply the selected stash. (Default: `apply`). + - `-h, --help`: show the help message of `fstash` and exit. + - `-s, --select`: list modified files and stash the selected files. Support multi selection. + - `-d, --delete`: list all stashes and delete selected stash. Support multi selection. + - `-p, --pop`: use `pop` instead of `apply`. (`pop` would remove the stash while `apply` preserve the stash). + + +fstat +----- +Display interactive git status menu. Toggle file stage/unstage status interactively. + + - `-h, --help`: show the help message of `fstat` and exit. + + +funtrack +-------- +Stop tracking the selected git files. It could also be used to temporarily stop tracking changes for files and then later on resume tracking changes. + +**Note**: This command has severe limitations. + +By default, selected files are permanently untracked from git. Selected files will be remove from index while keeping the file in your current system. However, when your other machines pull down the changes, the untracked files will be deleted by git. This is a limitation with git. + +`funtrack` does come with capabilities to temporarily untrack files, which will not remove the untracked files from other system. However, this is NOT recommended way to untrack files, explained [here](https://www.git-scm.com/docs/git-update-index#_notes). + + - Default: list all tracked files and permanently untrack the selected files. Support multi selection. + - `-h, --help`: show the help message of `funtrack` and exit. + - `-t, --temp`: list all tracked files and temporarily untrack changes of the selected files. Support multi selection. + - `-r, --resume`: list all tracked files and resume tracking changes of the selected files. + - `-y, --yes`: acknowledge all actions that will be taken and skip confirmation. + + +fzgrep +------ +Grep words within tracked files and select to edit them through fzf. The words are listed as lines in fzf and is separated by columns. First column is the file name, second column is the line number and the third column and beyond are the content of the lines. + + - Default: start searching from 3rd column (excluding the file name and line number during search). + - `-h, --help`: show the help message of `fstat` and exit. + - `-c COL, --col COL`: specify the column number to start searching (e.g. `fgrep --col 2`). + - `-f, --full`: include all column during search, as if using `--col 1`. + + +Helper scripts +============== + +fgit_helper.sh +-------------- +Provides variables and functions used by some of the scripts mentioned above. + +preview.sh +---------- +Fzf preview window coloring. diff --git a/fadd b/fadd @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# +# Stage the selected file to git bare repo +# +# @params +# Globals +# ${mydir}: string, current directory of the executing script +# ${stage_type}: modified, new file, or directory to stage +# ${selected_files}: bash array of user selected files to stage +# Arguments +# -h|--help: show help message +# -f|--file: select a file in PWD to stage +# -d|--dir: select a directory in PWD to stage + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fadd [-h] [-f] [-d] ... + +Select files/directories or modified files through fzf. +Stage the selected file to the dotfile gitbare repo. + +Default: list all modified files and stage the selected files. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -f, --file\t\tselect files in current directory and stage the selected files. + -d, --dir\t\tselect folders in current directory and stage the selected folders." +} + +####################################### +# stage file +# Arguments: +# $1: array of files to stage +####################################### + +function stage_file() { + local files=("$@") + [[ "${#files[@]}" -eq 0 ]] && exit 1 + git add "${files[@]}" +} + +stage_type="modified" +selected_files=() + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -f|--file) stage_type="file" shift ;; + -d|--dir) stage_type="dir" shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +while IFS= read -r line; do + selected_files+=("${line}") +done < <( + if [[ "${stage_type}" == "file" ]]; then + search_file 'f' + elif [[ "${stage_type}" == "dir" ]]; then + search_file 'd' + else + get_modified_file "select files to stage" "unstaged" + fi +) + +stage_file "${selected_files[@]}" diff --git a/fcheckout b/fcheckout @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# checkout files/commit/branches using fzf +# +# @params +# Globals +# ${mydir}: current directory of the script, used for imports +# ${action_type}: what type of git commands to use (branch|select|commit|modified) +# ${selected_branch}: selected_branch to switch +# ${selected_files}: selected_files to checkout +# ${selected_commit}: selected commit to checkout +# ${confirm}: confirm status of the user +# Arguments +# -h|--help: show help message +# -s|--select: search all files instead of just the modified files +# -b|--branch: search branch and checkout branch +# -c|--commit: search commit and checkout commit + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fcheckout [-h] [-s] [-b] [-c] [-y] ... + +Select files/commit/branch through fzf and checkout the selected objects. +Files: checkout the version in HEAD or in a specific commit (reset files content back to the selected commit). +Branch: switch to the selected branch. +Commit: switch to a specific commit. + +Default: list all modified files and reset selected files back to HEAD. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -s, --select\t\tlist all tracked files and select a commit to checkout the selected files. + -b, --branch\t\tlist all branch and checkout/switch the selected branch. + -c, --commit\t\tlist all commits and checkout selected commit. + -y, --yes\t\tacknowledge all actions that will be taken and skip confirmation." +} + +action_type="modified" +selected_files=() +confirm="" +selected_commit="" +selected_branch="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -s|--select) action_type="select" shift ;; + -b|--branch) action_type="branch" shift ;; + -c|--commit) action_type="commit" shift ;; + -y|--yes) confirm="y" shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1;; + esac +done + +if [[ "${action_type}" == "branch" ]]; then + # checkout branch + selected_branch=$(get_branch 'select a branch to checkout') + [[ -z "${selected_branch}" ]] && exit 1 + git checkout "${selected_branch}" +elif [[ "${action_type}" == "commit" ]]; then + # checkout commit + selected_commit=$(get_commit 'select a commit to checkout') + [[ -z "${selected_commit}" ]] && exit 1 + git checkout "${selected_commit}" +elif [[ "${action_type}" == "modified" ]]; then + # checkout modified file back to version in HEAD + while IFS= read -r line; do + selected_files+=("${line}") + done < <(get_modified_file 'select files to checkout version in HEAD') + [[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + [[ -z "${confirm}" ]] && echo "(dryrun) git checkout --" "${selected_files[@]}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Confirm?") + [[ "${confirm}" != 'y' ]] && exit 1 + git checkout -- "${selected_files[@]}" +elif [[ "${action_type}" == "select" ]]; then + # checkout selected files to a selected commit + while IFS= read -r line; do + selected_files+=("${line}") + done < <(get_git_file 'select files to checkout to previous commit') + [[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + # continue select a commit and then checkout the file back to the selected commit + selected_commit=$(get_commit 'select the target commit' "${selected_files[@]}") + [[ -z "${selected_commit}" ]] && exit 1 + [[ -z "${confirm}" ]] && echo "(dryrun) git checkout ${selected_commit} --" "${selected_files[@]}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Confirm?") + [[ "${confirm}" != 'y' ]] && exit 0 + git checkout "${selected_commit}" "${selected_files[@]}" +fi diff --git a/fedit b/fedit @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# interactive menu to select file/commit to edit +# +# @params +# Globals +# ${mydir}: current directory of the script +# ${edit_type}: which type to edit, all files, modified files, commit +# ${selected_commit}: selected commit to edit +# ${selected_files}: arrays of selected file to edit +# Arguments +# -m|--modified: display modified file only +# -c|--commit: edit commit using interactive rebase +# -h|--help: show helpe message and exit + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fedit [-h] [-m] [-c] ... + +Select files/commits through fzf and edit selected files/commits in EDITOR. + +Default: list all tracked dotfiles and edit the selected files. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -m, --modified\tonly list and edit selected modified files. + -c, --commit\t\tlist commit and edit the selected commit through interactive rebase." +} + +edit_type="all" +selected_files=() +selected_commit="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -m|--modified) edit_type="modified" shift ;; + -c|--commit) edit_type="commit" shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +if [[ "${edit_type}" == "commit" ]]; then + selected_commit=$(get_commit "select a commit to edit") + [[ -z "${selected_commit}" ]] && exit 1 + git rebase -i "${selected_commit}"~ +else + while IFS= read -r line; do + selected_files+=("${line}") + done < <( + if [[ "${edit_type}" == "modified" ]]; then + get_modified_file "select files to edit" + else + get_git_file "select files to edit" + fi + ) + [[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + exec "${EDITOR}" "${selected_files[@]}" +fi diff --git a/fgit_helper.sh b/fgit_helper.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash + +####################################### +########## SETTING VARIABLES ########## +####################################### +export GTREE="$(git rev-parse --show-toplevel)" +export G_DIFF_PAGER="$(git config core.pager || echo 'cat')" +export EDITOR="${EDITOR:-vim}" + +[[ -z "${FZF_DEFAULT_OPTS}" ]] && export FZF_DEFAULT_OPTS='--cycle' + +FZF_DEFAULT_OPTS=" + $FZF_DEFAULT_OPTS + --ansi + --cycle + --exit-0" + +[[ -z "${COLUMNS}" ]] \ + && COLUMNS=$(stty size < /dev/tty | cut -d' ' -f2) +[[ "${COLUMNS}" -lt 80 ]] \ + && FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview-window=hidden" + +####################################### +# determine to set multi selection or not +# Globals: +# ${FZF_DEFAULT_OPTS}: fzf options +# Arguments: +# $1: if exists, disable multi, set single +####################################### +function set_fzf_multi() { + local no_multi="$1" + if [[ -z "${no_multi}" ]]; then + export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS} --multi" + else + export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS} --no-multi" + fi +} + +####################################### +# Helper function to get user confirmation +# Globals: +# None +# Locals: +# ${confirm}: user confirmation status +# Arguments: +# $1: confirmation message to show during confirm +# Outputs: +# ${confirm}: y or n indicating user response +####################################### +function get_confirmation() { + local confirm + local message="${1:-Confirm?}" + while [ "${confirm}" != 'y' ] && [ "${confirm}" != 'n' ]; do + read -r -p "${message}(y/n): " confirm + done + echo "${confirm}" +} + + +####################################### +# let user select a commit interactively +# credit to forgit for the git log format +# Arguments: +# $1: the helper message to display in the fzf header +# $2: files to show diff against HEAD +# Outputs: +# the selected commit 6 char code +# e.g. b60b330 +####################################### +function get_commit() { + local header="${1:-select a commit}" + local files=("${@:2}") + if [[ "${#files[@]}" -eq 0 ]]; then + git \ + log --color=always --format='%C(auto)%h%d %s %C(black)%C(bold)%cr' \ + | fzf --header="${header}" --no-multi \ + --preview "echo {} \ + | awk '{print \$1}' \ + | xargs -I __ git \ + show --color=always __ \ + | ${G_DIFF_PAGER}" \ + | awk '{print $1}' + else + git \ + log --oneline --color=always --decorate=short \ + | fzf --header="${header}" --no-multi --preview "echo {} \ + | awk '{print \$1}' \ + | xargs -I __ git \ + diff --color=always __ ${files[*]} \ + | ${G_DIFF_PAGER}" \ + | awk '{print $1}' + fi +} + +####################################### +# let user select a branch interactively +# Arguments: +# $1: the helper message to display in the fzf header +# Outputs: +# the selected branch name +# e.g. master +####################################### +function get_branch() { + local header="${1:-select a branch}" + git branch -a \ + | awk '{ + if ($0 ~ /\*.*\(HEAD.*/) { + gsub(/\* /, "", $0) + print "\033[32m" $0 "\033[0m" + } else if (match($1, "\\*") != 0) { + print "\033[32m" $2 "\033[0m" + } else if ($0 ~ /^[ \t]+remotes\/.*/) { + gsub(/[ \t]+/, "", $0) + print "\033[31m" $0 "\033[0m" + } else { + gsub(/^[ \t]+/, "", $0) + print $0 + } + }' \ + | fzf --no-multi --header="${header}" \ + --preview="echo {} \ + | awk '{ + if (\$0 ~ /.*HEAD.*/) { + print \"HEAD\" + } else { + print \$0 + } + }' \ + | xargs -I __ git \ + log --color=always --color=always --format='%C(auto)%h%d %s %C(black)%C(bold)%cr' __" +} + +####################################### +# let user select a git tracked file interactively +# Arguments: +# $1: the helper message to display in the fzf header +# $2: print option, values (full|raw) +# $3: if exist, don't do multi selection, do single +# Outputs: +# the selected file path +# e.g.$HOME/.config/nvim/init.vim +####################################### +function get_git_file() { + local mydir + local header="${1:-select tracked file}" + local print_opt="${2:-full}" + mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + set_fzf_multi "$3" + git \ + ls-files --full-name --directory "${GTREE}" \ + | fzf --header="${header}" \ + --preview preview.sh ${GTREE}/{}" \ + | awk -v home="${GTREE}" -v print_opt="${print_opt}" '{ + if (print_opt == "full") { + print home "/" $0 + } else { + print $0 + } + }' +} + +####################################### +# let user select a modified file interactively +# Arguments: +# $1: the helper message to display in the fzf header +# $2: display mode of modified files. +# default: true +# all: display all modified, include staged and unstaged +# staged: display only staged files +# unstaged: display only unstaged files +# $3: output_format +# default: name +# name: formatted name of the file +# raw: raw file name with status +# $4: if exists, don't do multi selection, do single +# Outputs: +# the selected file path +# e.g.$HOME/.config/nvim/init.vim +####################################### +function get_modified_file() { + local header="${1:-select a modified file}" + local display_mode="${2:-all}" + local output_format="${3:-name}" + set_fzf_multi "$4" + git \ + status --porcelain \ + | awk -v display_mode="${display_mode}" '{ + if ($0 ~ /^[A-Za-z][A-Za-z].*$/) { + print "\033[32m" substr($0, 1, 1) "\033[31m" substr($0, 2) "\033[0m" + } else if ($0 ~ /^[A-Za-z][ \t].*$/) { + if (display_mode == "all" || display_mode == "staged") { + print "\033[32m" $0 "\033[0m" + } + } else { + if (display_mode == "all" || display_mode == "unstaged") { + print "\033[31m" $0 "\033[0m" + } + } + }' \ + | fzf --header="${header}" --preview "echo {} \ + | awk '{sub(\$1 FS,\"\");print \$0}' \ + | xargs -I __ git \ + diff HEAD --color=always -- ${GTREE}/__ \ + | ${G_DIFF_PAGER}" \ + | awk -v home="${GTREE}" -v format="${output_format}" '{ + if (format == "name") { + $1="" + gsub(/^[ \t]/, "", $0) + gsub(/"/, "", $0) + print home "/" $0 + } else { + print $0 + } + }' +} + +####################################### +# let user select a stash interactively +# Arguments: +# $1: the help message to display in header +# $2: if exists, don't do multi select, only allow single selection +# Outputs: +# the selected stash identifier +# e.g. stash@{0} +####################################### +function get_stash() { + local header="${1:-select a stash}" + set_fzf_multi "$2" + git \ + stash list \ + | fzf --header="${header}" --preview "echo {} \ + | awk '{ + gsub(/:/, \"\", \$1) + print \$1 + }' \ + | xargs -I __ git \ + stash show -p __ --color=always \ + | ${G_DIFF_PAGER}" \ + | awk '{ + gsub(/:/, "", $1) + print $1 + }' +} + +####################################### +# Using git grep to find word within +# all tracked files in the bare repo. +# Arguments: +# $1: the help message to display in header +# $2: the fzf delimiter to start searching, default is 3 +# $3: if exists, don't do multi select, only allow single selection +# Outputs: +# the selected file name with it's line number and line, separated by ":" +# e.g. .bash_profile:1:echo hello +####################################### +function grep_words() { + local header="${1:-select matches to edit}" + local delimiter="${2:-3}" + mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + set_fzf_multi "$2" + cd "${GTREE}" || exit + git \ + grep --line-number -- . \ + | fzf --delimiter : --nth "${delimiter}".. --header="${header}" \ + --preview "${mydir}/preview.sh ${GTREE}/{}" \ + | awk -F ":" -v home="${GTREE}" '{ + print home "/" $1 ":" $2 + }' +} + +####################################### +# search local file +# Arguments: +# $1: string, f or d, search file or directory +# Outputs: +# A user selected file path +####################################### +function search_file() { + local search_type="$1" mydir + mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + if [[ "${search_type}" == "f" ]]; then + find . -maxdepth 1 -type f | sed "s|\./||g" | fzf --multi --preview "${mydir}/preview.sh {}" + elif [[ "${search_type}" == "d" ]]; then + if command -v tree &>/dev/null; then + find . -maxdepth 1 -type d | awk '{if ($0 != "." && $0 != "./.git"){gsub(/^\.\//, "", $0);print $0}}' | fzf --multi --preview "tree -L 1 -C --dirsfirst {}" + else + find . -maxdepth 1 -type d | awk '{if ($0 != "." && $0 != "./.git"){gsub(/^\.\//, "", $0);print $0}}' | fzf --multi + fi + fi +} diff --git a/flog b/flog @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# git log interactive viewer +# +# @params +# Globals +# ${mydir}: current dir of the script +# ${selected_action}: action to take on the selected commit +# ${selected_commit}: user selected commit +# ${confirm}: confirm status of user +# Arguments +# -h|--help: display help message +# -r|--revert: revert the selected commit +# -R|--reset: reset HEAD back to the selected commit +# -e|--edit: edit commmit (interactive rebase) +# -c|--checkout: checkout selected commmit +# -y|--yes: confirm action by default and skip confirmation + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: flog [-h] [-r] [-R] [-e] [-c] [-y] ... + +Interactive log viewer with action menu. +Action menu contains options including revert|reset|edit|checkout|exit. + +Default: list all commits and prompt a menu to select action to perform. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -r, --revert\t\trevert the selected commit and skip action menu. + -R, --reset\t\treset HEAD back to selected commit and skip action menu. + -e, --edit\t\tedit selected commit through interactive rebase and skip action menu. + -c, --checkout\tcheckout selected commit and skip action menu. + -y, --yes\t\tacknowledge all actions that will be taken and skip confirmation." +} + +####################################### +# draw action menu for selected commit +# Arguments: +# $1: selected commit hash, used to display commit message in fzf header +# $2: selected action, if selected, skip menu, return action +# Outputs: +# ${selected_action}: user selected action +####################################### +function draw_menu() { + local selected_commit="$1" + local selected_action="$2" + local menu header message + if [[ -n "$selected_action" ]]; then + echo "${selected_action}" + else + menu="revert: revert the selected commit\n" + menu="${menu}reset: reset HEAD to the selected commit using --mixed flag\n" + menu="${menu}edit: edit selected commit through interactive rebase\n" + menu="${menu}checkout: checkout the selected commit\n" + menu="${menu}exit: quit flog" + message=$( + git \ + log --format=%B -n 1 "${selected_commit}" + ) + header="commit ${selected_commit}: ${message}" + selected_action=$(echo -e "${menu}" \ + | fzf --no-multi --header="${header}" \ + | awk -F ":" '{ + print $1 + }' + ) + echo "${selected_action}" + fi +} + +selected_action="" +selected_commit="" +confirm="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -r|--revert) selected_action="revert" shift ;; + -R|--reset) selected_action="reset" shift ;; + -e|--edit) selected_action="edit" shift ;; + -c|--checkout) selected_action="checkout" shift ;; + -y|--yes) confirm='y' shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +while :; do + selected_commit=$(get_commit) + [[ -z "${selected_commit}" ]] && exit 1 + selected_action=$(draw_menu "${selected_commit}" "${selected_action}") + [[ -n "${selected_action}" ]] && break +done + +if [[ "${selected_action}" != 'exit' ]]; then + if [[ "${selected_action}" == "reset" ]] && [[ -z "${confirm}" ]]; then + echo "(dryrun) reset HEAD to ${selected_commit}" + elif [[ -z "${confirm}" ]]; then + echo "(dryrun) ${selected_action} ${selected_commit}" + fi + [[ -z "${confirm}" ]] && confirm=$(get_confirmation) + [[ "${confirm}" != 'y' ]] && exit 1 +fi + +case "${selected_action}" in + revert) git revert "${selected_commit}" ;; + reset) git reset "${selected_commit}" ;; + edit) git rebase -i "${selected_commit}"~ ;; + checkout) git checkout "${selected_commit}" ;; + exit) exit 0 ;; + *) exit 1 ;; +esac diff --git a/freset b/freset @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# unstage the selected staged file +# or reset the commit to certain point +# +# @params +# Globals +# ${mydir}: current directory of the script +# ${reset_type}: reset type, modified files or commit +# ${reset_option}: git reset flag, --mixed | --soft | --hard +# ${selected_files}: selected file to reset +# ${selected_commit}: selected commit to reset +# ${confirm}: confirmation status of the user +# Arguments +# -h|--help: show help message and quit +# -c|--commit: reset commit +# -S|--soft: use --soft flag +# -H|--hard: use --hard flag +# -y|--yes: confirm action by default and skip confirmation + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: freset [-h] [-c] [-S] [-H] [-y] ... + +Reset(unstage) the selected staged files. +Reset the HEAD to certain commits by using -c flag. + +Default: unstage the selected files. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -c, --commit\t\treset HEAD to certain commit, default --mixed flag, reset HEAD to certain commit put all changes into modified state. + -S, --soft\t\treset commit using --soft flag, reset HEAD to certain commit without modify working tree. + -H, --hard\t\treset commit using --hard flag, reset HEAD to certain commit discard all changes from the working tree. + -y, --yes\t\tacknowledge all actions that will be taken and skip confirmation." +} + +reset_option="--mixed" +reset_type="modified" +selected_files=() +confirm="" +selected_commit="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -c|--commit) reset_type="commit" shift ;; + -S|--soft) reset_option="--soft" shift ;; + -H|--hard) reset_option="--hard" shift ;; + -y|--yes) confirm='y' shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +if [[ "${reset_type}" == "commit" ]]; then + selected_commit=$(get_commit "select the target commit") + [[ -z "${selected_commit}" ]] && exit 1 + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Reset HEAD to ${selected_commit} ${reset_option}?") + [[ "${confirm}" != 'y' ]] && exit 1 + git reset "${selected_commit}" "${reset_option}" +else + while IFS= read -r line; do + selected_files+=("${line}") + done < <(get_modified_file 'select files to unstage' 'staged') + [[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + git reset "${selected_files[@]}" +fi diff --git a/fstash b/fstash @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# +# stash operation using fzf +# +# @params +# Globals +# ${mydir}: current dir of the script, source purpose +# ${stash_command}: stash command, pop, apply or file/delete to stash file or delete stash +# ${selected_files}: selected files to stash +# ${selected_stash}: selected stash to apply +# ${confirm}: user confirm status + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fstash [-h] [-s] [-d] [-p] ... + +View and manage stash interactively. + +Default: list all stashes and apply the selected stash. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -s, --select\t\tlist modified files and stash the selected files. + -d, --delete\t\tlist all stashes and delete the selected stash from stash list. + -p, --pop\t\tuse 'stash pop' instead of 'stash apply'." +} + +stash_command="apply" +selected_files=() +selected_stash="" +confirm="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -p|--pop) stash_command="pop" shift ;; + -s|--select) stash_command="select" shift ;; + -d|--delete) stash_command="delete" shift ;; + -y|--yes) confirm="y" shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +if [[ "${stash_command}" == "select" ]]; then + while IFS= read -r line; do + selected_files+=("${line}") + done < <(get_modified_file "select files to add to a stash") + [[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + git stash -- "${selected_files[@]}" +elif [[ "${stash_command}" == "delete" ]]; then + selected_stash=$(get_stash "select stash to delete") + [[ -z "${selected_stash}" ]] && exit 1 + [[ -z "${confirm}" ]] && \ + while IFS= read -r line; do + echo "(dryrun) drop ${line}" + done <<< "${selected_stash}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation) + [[ "${confirm}" != 'y' ]] && exit 1 + while IFS= read -r line; do + git stash drop "${line}" + done <<< "${selected_stash}" +else + selected_stash=$(get_stash "select stash to apply" "true") + [[ -z "${selected_stash}" ]] && exit 1 + [[ -z "${confirm}" ]] && echo "(dryrun) ${stash_command} ${selected_stash}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation) + [[ "${confirm}" != 'y' ]] && exit 1 + git stash "${stash_command}" "${selected_stash}" +fi diff --git a/fstat b/fstat @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# interactive git status menu +# toggle stage and unstage +# +# @params +# Globals +# ${mydir}: current directory of where the script is running +# ${selected_files}: raw selected file (with current git status prepend) +# ${selected_filenames}: bash array of names for the selected_files +# ${stage_file}: determine if current operation should be staging file or unstage + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fstat [-h] ... + +Display interactive git status menu. +Toggle file stage/unstage interactively. + +Optional arguments: + -h, --help\t\tshow this help message and exit." +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +while :; do + # reset all variable and arrays for each loop + selected_files=() + selected_filenames=() + stage_file="" + + while IFS= read -r line; do + selected_files+=("${line}") + done < <(get_modified_file "select files to stage/unstage" "all" "raw") + [[ "${#selected_files[@]}" -eq 0 ]] && break + + # check if current operation should stage file or unstage file + # if any file start with M but has char immediately follow it, new changes are made, stage file + # if any file start with a space or tab, the file is not staged, stage file + # otherwise, we unstage + stage_file=$(printf '%s\n' "${selected_files[@]}" | awk '{ + if ($0 ~ /^[A-Za-z][A-Za-z].*$/) { + print "stage" + } else if ($0 ~ /^[ \t].*$/) { + print "stage" + } + }') + + while IFS= read -r line; do + selected_filenames+=("${line}") + done < <( + printf '%s\n' "${selected_files[@]}" \ + | awk -v home="${GTREE}" '{ + $1="" + gsub(/^[ \t]/, "", $0) + gsub(/"/, "", $0) + print home "/" $0 + }' + ) + + if [[ -z "${stage_file}" ]]; then + git reset --quiet HEAD "${selected_filenames[@]}" + else + git add "${selected_filenames[@]}" + fi +done diff --git a/funtrack b/funtrack @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# untrack selected files +# +# @params +# Globals +# ${mydir}: current dir of the script +# ${track_type}: determine method to use for untrack, possible values: untrack, temp, resume +# ${confirm}: user confirm status +# ${selected_files}: arrays of user selected_files for operation + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: funtrack [-h] [-t] [-r] [-y] ... + +Untrack selected files from git. + +Default: list all tracked files and permanently untrack the selected files (using git rm --cached filename). + +Files will be remove from index while keeping the file in your current system. +However, when your other computers pull down the changes, the untracked files will be deleted. +Make sure to run fbackup before pulling down the changes. + +Alternatively use the -t flag (using git update-index --assume-unchanged [path]) to temporarily +untrack a file but keeping the files when other computers pull down the changes. + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -t, --temp\t\tlist all tracked files and temporarily ignore changes of the selected files. + -r, --resume\t\tlist all tracked files and resume tracking changes of the selected files. + -y, --yes\t\tacknowledge all actions that will be taken and skip confirmation." +} + +track_type="untrack" +selected_files=() +confirm="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -t|--temp) track_type="temp" shift ;; + -r|--resume) track_type="retrack" shift ;; + -y|--yes) confirm='y' shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +while IFS= read -r line; do + selected_files+=("${line}") +done < <(get_git_file "select files to untrack") +[[ "${#selected_files[@]}" -eq 0 ]] && exit 1 + +if [[ "${track_type}" == "temp" ]]; then + [[ -z "${confirm}" ]] && echo "(dryrun) git update-index --assume-unchanged" "${selected_files[@]}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Files will be temporarily stop being tracked for changes, continue?") + [[ "${confirm}" != 'y' ]] && exit 1 + git update-index --assume-unchanged "${selected_files[@]}" + echo -e " " + echo "Selected files are temporarily untracked by git, use funtrack -r to continue tracking changes." + echo "Although funtrack -t won't delete the files on other machines, it is not the recommended way to untrack files." + echo "funtrack -t is using git update-index --assume-unchanged under the hood" + echo "Please refer to git update-index official documentation for more details" +elif [[ "${track_type}" == "retrack" ]]; then + [[ -z "${confirm}" ]] && echo "(dryrun) git update-index --no-assume-unchanged" "${selected_files[@]}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Files will resume being tracked by git, continue?") + [[ "${confirm}" != 'y' ]] && exit 1 + git update-index --no-assume-unchanged "${selected_files[@]}" + echo " " + echo "Selected files are being resumed for tracking by git." + echo "Although funtrack -t won't delete the files on other machines, it is not the recommended way to untrack files." + echo "funtrack -t is using git update-index --assume-unchanged under the hood" + echo "Please refer to git update-index official documentation for more details" +else + [[ -z "${confirm}" ]] && echo "(dryrun) git rm --cached" "${selected_files[@]}" + [[ -z "${confirm}" ]] && confirm=$(get_confirmation "Untrack the selected files?") + [[ "${confirm}" != 'y' ]] && exit 1 + git rm --cached "${selected_files[@]}" + echo -e " " + echo "Selected files are being untracked by git, make sure to run fbackup on your other systems." + echo "When other systems pull down this change, selected files will be deleted on other systems." + echo "This is the default behavior of git rm --cached." + echo "Please refer to git rm official documentation for more details" +fi diff --git a/fzgrep b/fzgrep @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Grep for words within all dotfiles +# +# @params +# Globals +# mydir: path to this current script +# selected_lines: selected lines to edit +# fzf_search_delimiter: the col to start searching in fzf + +set -ef + +mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "${mydir}"/fgit_helper.sh + +function usage() { + echo -e "Usage: fgrep [-h] [-c] [-f] ... + +Grep words within tracked files and select to edit matches. + +Default: start searching from 3rd column (excluding the file name during search). + +Optional arguments: + -h, --help\t\tshow this help message and exit. + -c COL, --col COL\tspecify the column number to start searching. + -f, --full\t\tinclude all column during search, as if using '--col 1'." +} + +selected_lines=() +fzf_search_delimiter=3 + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -c|--col) fzf_search_delimiter="$2" shift 2 ;; + -f|--full) fzf_search_delimiter=1 shift ;; + -h|--help) usage && exit 0 ;; + *) echo "Invalid option: $1" >&2 && usage && exit 1 ;; + esac +done + +while IFS= read -r line; do + case "${EDITOR}" in + vim|nvim|nano) + # line number = "${line##*:}" + # file name = "${line%%:*}" + selected_lines+=(+"${line##*:}" "${line%%:*}") + ;; + *) + selected_lines+=("${line}") + ;; + esac +done < <(grep_words "select matches to edit" "${fzf_search_delimiter}") + +[[ "${#selected_lines[@]}" -eq 0 ]] && exit 1 + +"${EDITOR}" "${selected_lines[@]}" diff --git a/preview.sh b/preview.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# dynamic preview command for git +# borrowed and modified from fzf.vim +# can export G_PREVIEW='bat -Pf --style auto' +# +# @params +# Globals +# reverse_seq: reverse highlight sequence for easier print +# reset_seq: reset highlight sequence for eaiser print +# input_option: the array containing informaiton from cmd argument +# preview_file: the file path to be previewed +# preview_center: the center/highlight line of the preview, if not set, display entire file +# preview_lines: total lines to be previed, used to determine the first line and last line to be printed +# preview_first: the first line number of the file to be printed +# preview_last: the last line number of the file to be printed +# file_mime: mime of the file, used to display information for binary file +# Arguments +# $1: The filename and line info to be previewed +# Format: filepath[:lineno][:ignored] +# Example +# preview "$HOME/.bashrc:15" + +####################################### +# display the preview of the file +# construct argument for the action, execute and exit +# Arguments: +# $1: the file path to be previewed +# $2: the first line number to be previewd, optional +# $3: the center/highlight line in the preview, optional +# $4: the last line number to be previewed, optional +# Note: +# if the line number information such as $2 $3 $4 is not given, the entire +# file will be printed. +# Otherwise only the $2-$4 parts of the file will be printed and $3 will be highlighted +####################################### +function display_preview() { + local fallback_cmd preview_cmd preview_file preview_first preview_last preview_center + preview_file="$1" + preview_first="$2" + preview_center="$3" + preview_last="$4" + fallback_cmd="highlight -O ansi -l {} || coderay {} || rougify {} || cat {}" + preview_cmd=${fallback_cmd} + preview_cmd=${preview_cmd//{\}/$(printf %q "${preview_file}")} + + if [[ -z "${preview_first}" ]] || [[ -z "${preview_center}" ]] || [[ -z "${preview_last}" ]]; then + if [[ -z "${G_PREVIEW}" ]] && command -v bat > /dev/null; then + bat --color=always --pager=never "${preview_file}" + exit $? + fi + eval "${preview_cmd}" 2> /dev/null + exit 0 + else + if [ -z "${G_PREVIEW}" ] && command -v bat > /dev/null; then + bat --color=always --pager=never \ + --line-range="${preview_first}":"${preview_last}" --highlight-line="${preview_center}" "${preview_file}" + exit $? + fi + eval "${preview_cmd}" 2> /dev/null \ + | awk "NR >= ${preview_first} && NR <= ${preview_last} { \ + if (NR == ${preview_center}) \ + { gsub(/\x1b[[0-9;]*m/, \"&${reverse_seq}\"); printf(\"${reverse_seq}%s\n${reset_seq}\", \$0); } \ + else \ + printf(\"${reset_seq}%s\n\", \$0); \ + }" + exit 0 + fi +} + +reverse_seq="\x1b[7m" +reset_seq="\x1b[m" + +IFS=':' read -r -a input_option <<< "$1" +preview_file=${input_option[0]} +preview_center=${input_option[1]} + +if [[ $1 =~ ^[A-Z]:\\ ]]; then + preview_file=$preview_file:${input_option[1]} + preview_center=${input_option[2]} +fi + +preview_file="${preview_file/#\~\//$HOME/}" +if [ ! -r "${preview_file}" ]; then + echo "File not found ${preview_file}" + exit 1 +fi + +# if binary, display binary info and exit +file_mime=$(file --dereference --mime "${preview_file}") +[[ "${file_mime}" =~ binary ]] \ + && echo "${file_mime}" \ + && exit 0 + +# if no line number was given, just preview the entire file +[[ -z "${preview_center}" ]] && display_preview "${preview_file}" + +# if invalid line number was given, just preview the entire file +[[ -n "${preview_center}" && ! "${preview_center}" =~ ^[0-9] ]] && display_preview "${preview_file}" + +# get the size of the termianl window and determine the first line and last line +preview_center=${preview_center/[^0-9]*/} + +if [ -n "${FZF_PREVIEW_LINES}" ]; then + preview_lines="${FZF_PREVIEW_LINES}" +else + if [ -r /dev/tty ]; then + preview_lines=$(stty size < /dev/tty | awk '{print $1}') + else + preview_lines=40 + fi +fi + +preview_first=$((preview_center-preview_lines/3)) +preview_first=$((preview_first < 1 ? 1 : preview_first)) +preview_last=$((preview_first+preview_lines-1)) + +display_preview "${preview_file}" "${preview_first}" "${preview_center}" "${preview_last}"