├── .gitignore ├── LICENSE ├── README.md ├── bin └── git-fuzzy ├── gifs ├── diff.gif ├── log.gif └── status.gif └── lib ├── core.sh ├── debug.sh ├── load-configs.sh ├── modules ├── branch.sh ├── diff-checkout.sh ├── diff-direct.sh ├── diff.sh ├── helpers.sh ├── helpers │ ├── branch.sh │ ├── diff-checkout.sh │ ├── diff-direct.sh │ ├── diff.sh │ ├── generic.sh │ ├── hub │ │ └── pr.sh │ ├── log.sh │ ├── reflog.sh │ ├── stash.sh │ └── status.sh ├── hub │ └── pr.sh ├── log.sh ├── main.sh ├── reflog.sh ├── stash.sh └── status.sh ├── patch-index.awk ├── patch-selector.awk ├── snapshot.sh └── utils.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # I use this file to keep my private TODO list 2 | TODO.md 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Hiren Hiranandani 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `git-fuzzy` 2 | 3 | A CLI interface to git that relies heavily on [`fzf`](https://github.com/junegunn/fzf) (version `0.21.0` or higher). 4 | 5 | You can run `git add` and `git reset` by selecting or cursoring. You can commit interactively. 6 | 7 | ![status manager](gifs/status.gif) 8 | 9 | You can search the diff from the query bar and the RHS diff will be highlighted accordingly. 10 | 11 | ![diff viewer](gifs/diff.gif) 12 | 13 | Search the log and corresponding diff at once. Notice that when you use `|` the left hand side is sent to `log` while the right hand side is sent to `diff`. 14 | 15 | ![log viewer](gifs/log.gif) 16 | 17 | ## Installing 18 | 19 | `fzf` is **required**: 20 | ```bash 21 | brew install fzf 22 | ``` 23 | 24 | ### Bash 25 | ```bash 26 | git clone https://github.com/bigH/git-fuzzy.git 27 | 28 | # add the executable to your path 29 | echo "export PATH=\"$(pwd)/git-fuzzy/bin:\$PATH\"" >> ~/.bashrc 30 | ``` 31 | 32 | ### Zsh 33 | ```bash 34 | git clone https://github.com/bigH/git-fuzzy.git 35 | 36 | # add the executable to your path 37 | echo "export PATH=\"$(pwd)/git-fuzzy/bin:\$PATH\"" >> ~/.zshrc 38 | ``` 39 | 40 | Alternatively, you can use a plugin manager: 41 | 42 | #### Antibody 43 | Update your `.zshrc` file with the following line: 44 | ``` 45 | antibody bundle bigH/git-fuzzy path:bin kind:path 46 | ``` 47 | 48 | #### Znap 49 | Run the following on the command line: 50 | ``` 51 | znap install bigH/git-fuzzy 52 | ``` 53 | 54 | #### zplug 55 | ``` 56 | zplug "bigH/git-fuzzy", as:command, use:"bin/git-fuzzy" 57 | ``` 58 | 59 | #### zinit 60 | ``` 61 | zinit ice as"program" pick"bin/git-fuzzy" 62 | zinit light bigH/git-fuzzy 63 | ``` 64 | 65 | ### Fish 66 | ``` 67 | git clone https://github.com/bigH/git-fuzzy.git 68 | 69 | # add the executable to your path 70 | echo "set -x PATH (pwd)\"/git-fuzzy/bin:\$PATH\"" >> ~/.config/fish/config.fish 71 | ``` 72 | 73 | ## Usage 74 | 75 | Simply install and run `git fuzzy` and you can begin using the menu. 76 | 77 | **Supported sub-commands**: 78 | 79 | - `git fuzzy status` (or `git fuzzy` -> `status`) 80 | 81 | Interact with staged and unstaged changes. 82 | 83 | - `git fuzzy branch` (or `git fuzzy` -> `branch`) 84 | 85 | Search for, checkout and look at branches. 86 | 87 | - `git fuzzy log` (or `git fuzzy` -> `log`) 88 | 89 | Look for commits in `git log`. Typing in the search simply filters in the usual `fzf` style. 90 | 91 | - `git fuzzy reflog` (or `git fuzzy` -> `reflog`) 92 | 93 | Look for entries in `git reflog`. Typing in the search simply filters in the usual `fzf` style. 94 | 95 | - `git fuzzy stash` (or `git fuzzy` -> `stash`) 96 | 97 | Look for entries in `git stash`. Typing in the search simply filters in the usual `fzf` style. 98 | 99 | - `git fuzzy diff` (or `git fuzzy` -> `diff`) 100 | 101 | Interactively select diff subjects. Drilling down enables searching through diff contents in a diff browser. 102 | 103 | - `git fuzzy pr` (or `git fuzzy` -> `pr`) 104 | 105 | Interactively select and open/diff GitHub pull requests. 106 | 107 | ## Useful Information 108 | 109 | All items from the menu can be accessed via the CLI by running `git fuzzy `. Many of the commands simply pass on additional CLI args to the underlying commands. (e.g. `git fuzzy diff a b -- XYZ` uses the args you provided in the listing and preview) 110 | 111 | Any time `git` command output is used in preview or listing, there is a header with the command run (useful for copy-pasting or just knowing what's happening). You can optionally [enable debugging switches](#stability--hacking) to see other commands being run in the background or how commands are routed. 112 | 113 | ## Customizing 114 | 115 | For the ideal experience, install the following optional tools to your `PATH`: 116 | 117 | - [`delta`](https://github.com/dandavison/delta) or [`diff-so-fancy`](https://github.com/so-fancy/diff-so-fancy) for nicer looking diffs 118 | - [`bat`](https://github.com/sharkdp/bat) for a colorized alternative to `cat` 119 | - [`eza`](https://github.com/eza-community/eza) for a `git`-enabled, and better colorized alternative to `ls` 120 | 121 | `git fuzzy diff` uses `grep` to highlight your search term. The default may clash with `diff` formatting or just not be to your liking. You can configure `git fuzzy` without affecting the global setting. 122 | 123 | ```bash 124 | export GF_GREP_COLOR='1;30;48;5;15' 125 | ``` 126 | 127 | If provided, `GF_PREFERRED_PAGER` is used as a way to decorate diffs. Otherwise, `diff-so-fancy`, then `delta` are tried before using raw diffs. **Remember to adequately quote this value as it's subject to string splitting.** 128 | 129 | ```bash 130 | export GF_PREFERRED_PAGER="delta --theme=gruvbox --highlight-removed -w __WIDTH__" 131 | ``` 132 | 133 | If present, `bat` is used for highlighting. You can choose different defaults in `git fuzzy` if you so desire. 134 | 135 | ```bash 136 | # set them for `git fuzzy` only 137 | export GF_BAT_STYLE=changes 138 | export GF_BAT_THEME=zenburn 139 | 140 | # OR set these globally for all `bat` instances 141 | export BAT_STYLE=changes 142 | export BAT_THEME=zenburn 143 | ``` 144 | 145 | You may often want to use a different branch and remote to use as your "merge-base" in `git fuzzy`. _The default is `origin/main`._ 146 | 147 | ```bash 148 | export GF_BASE_REMOTE=upstream 149 | export GF_BASE_BRANCH=trunk 150 | ``` 151 | 152 | **FOOTGUN**: If you work in a repository that's changed it's default `HEAD` (e.g. from `master` to `main`) since your initial `clone`, you may need to run `git remote set-head `. Use `git symbolic-ref -q "refs/remotes//HEAD"` to check what the current value is. 153 | 154 | For some repos, it can be useful to turn off the remote branch listing in `git fuzzy branch`. _By default, `git fuzzy` displays remote branches._ 155 | 156 | ```bash 157 | # any non-empty value will result in skipping remotes (including 'no') 158 | export GF_BRANCH_SKIP_REMOTE_BRANCHES="yes" 159 | ``` 160 | 161 | You may want the diff search to behave differently in `git fuzzy diff` (this doesn't apply to `log` or any other command that uses `diff`). The query will be quoted by `fzf` and provided as the next argument. In the default case, that means `-G `. _The default is `-G`._ 162 | 163 | ```bash 164 | export GF_DIFF_SEARCH_DEFAULTS="--pickaxe-regex -S" 165 | ``` 166 | 167 | You may want custom formats for your `log` and/or `reflog` experience. This is hidden from the command headers to save room and enable freedom in formatting parameters. **Remember to adequately quote this value as it's subject to string splitting.** If you have trouble quoting formats, you can use a pretty format alias (see `man git-config`) _The default is `--pretty=oneline --abbrev-commit`._ 168 | 169 | ```bash 170 | # for `git fuzzy log` 171 | export GF_LOG_MENU_PARAMS='--pretty="%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --topo-order' 172 | 173 | # for `git fuzzy reflog` 174 | export GF_REFLOG_MENU_PARAMS='--pretty=fuzzyformat' 175 | ``` 176 | 177 | You can also configure various `git` commands' default args in various contexts. This is hidden from the command headers to save room and enable freedom in formatting parameters. **Remember to adequately quote this value as it's subject to string splitting.** _These are not set by default._ 178 | 179 | ```bash 180 | # when diffing with branches or commits for preview 181 | export GF_DIFF_COMMIT_PREVIEW_DEFAULTS="--patch-with-stat" 182 | 183 | # when diffing with branches or commits for preview 184 | export GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS="--summary" 185 | 186 | # when diffing individual files 187 | export GF_DIFF_FILE_PREVIEW_DEFAULTS="--indent-heuristic" 188 | ``` 189 | 190 | If you use vertical terminals/windows often, you may want to configure the threshold for switching to a vertical view. This ratio is calculated by running `"__WIDTH__ / __HEIGHT__ > $GF_VERTICAL_THRESHOLD"`. This is calculated using GNU `bc`. _The default is `2.0`._ 191 | 192 | ```bash 193 | export GF_VERTICAL_THRESHOLD="1.7 * __HEIGHT__ / 80" 194 | ``` 195 | 196 | You can also configure how the size of the preview window is calculated. They're calculated using GNU `bc`. Try using [Desmos](https://www.desmos.com/calculator) to tweak the calculation. _The defaults are more complex than shown below._ 197 | 198 | ```bash 199 | # use __WIDTH__ for horizontal scenarios 200 | export GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION='max(50, min(80, 100 - (7000 / __WIDTH__)))' 201 | 202 | # use __HEIGHT__ for horizontal scenarios 203 | export GF_VERTICAL_PREVIEW_PERCENT_CALCULATION='max(50, min(80, 100 - (5000 / __HEIGHT__)))' 204 | ``` 205 | 206 | In cases where you are using a particularly small terminal, you can configure the following calculations to determine when to hide extraneous things. Note that both defaults use `__HEIGHT__`, but `__WIDTH__` is also available. 207 | 208 | ```bash 209 | # use __HEIGHT__ for horizontal scenarios 210 | export GF_HORIZONTAL_SMALL_SCREEN_CALCULATION='__HEIGHT__ <= 30' 211 | 212 | # use __HEIGHT__ for horizontal scenarios 213 | export GF_VERTICAL_SMALL_SCREEN_CALCULATION='__HEIGHT__ <= 60' 214 | ``` 215 | 216 | You may want to customize the default keyboard shortcuts. There are [many configuration options available](https://github.com/bigH/git-fuzzy/pull/16/files). Here's an example: 217 | 218 | ```bash 219 | export GIT_FUZZY_STATUS_ADD_KEY='Ctrl-A' 220 | ``` 221 | 222 | If you are using nano as your default editor, you need to pass `/dev/tty` as stdin otherwise you may receive an error similar to `Too many errors from stdintor to close the file...`: 223 | 224 | ```bash 225 | git config --global core.editor 'nano < /dev/tty' 226 | ``` 227 | 228 | `git fuzzy` appends a static list of defaults to your `FZF_DEFAULT_OPTIONS`. If you want to use your own set of `git fuzzy`-specific fzf defaults, you can set `GIT_FUZZY_FZF_DEFAULT_OPTS` which will be used in place. Note that `FZF_DEFAULT_OPTS` is merged with this variable. 229 | 230 | ## Backups 231 | 232 | `git fuzzy` takes a backup of your current sha, branch, index diff, unstaged diff and new files. This is helpful in case you take an action hastily (like discarding a file you meant to stage) or there is a bug. If you'd like snapshots, simply set the variable below. I have the following entry in my `.zshrc` (with corresponding `.gitignore_global`): 233 | 234 | ```bash 235 | export GF_SNAPSHOT_DIRECTORY='.git-fuzzy-snapshots' 236 | ``` 237 | 238 | Alternatively, if you'd like to avoid having these files in your repo directory, you can simply set the snapshot location like so: 239 | 240 | ```bash 241 | export GF_SNAPSHOT_DIRECTORY="$HOME/.git-fuzzy-snapshots" 242 | ``` 243 | 244 | ## `bc` Usage 245 | 246 | `bc` programs are all run with some useful functions defined (`min` and `max`). If you'd like to add any others, you can do so. _This is not set by default._ 247 | 248 | ```bash 249 | # defining your own function: 250 | export GF_BC_LIB='my_favorite_variable = 3.14159;' 251 | ``` 252 | 253 | ## Project-Specific Settings 254 | 255 | `git fuzzy` sources `./git-fuzzy-config` if it's present. You can add the following to your `~/.gitignore_global` to avoid having to worry about `git` picking it up: 256 | 257 | ```gitignore 258 | .git-fuzzy-config 259 | ``` 260 | 261 | This file is sourced at the end, so you can build on top of existing or default configurations: 262 | 263 | ```bash 264 | # make the preview bigger, but keep the flexibility 265 | export GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION='(80 + $GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION) / 2' 266 | ``` 267 | 268 | ## Questions 269 | 270 | **Why does the UI flash?** 271 | 272 | `execute` from the `fzf` man page states that `fzf` switches to the alternate screen when executing a command. I've filed [this issue](https://github.com/junegunn/fzf/issues/2028), which should enable making the transitions smoother. 273 | 274 | ## Stability & Hacking 275 | 276 | I built this for myself and it's working reasonably well. 277 | 278 | That being said, I've gone through great pains to polish existing functionality to work pretty nicely. I've made it easy to develop or change features by using debug output to check behavior. All debug output goes to `/dev/stderr`, so you can hack on this while using it in pipes and in your `zsh` or `bash` readline shortcuts. 279 | 280 | **These variables are considered `true` if they are non-empty.** 281 | 282 | ```bash 283 | # debugging information 284 | export GF_DEBUG_MODE="YES" 285 | 286 | # commands run by the program (those without headers) 287 | export GF_COMMAND_DEBUG_MODE="YES" 288 | 289 | # fzf commands run by the program 290 | export GF_COMMAND_FZF_DEBUG_MODE="YES" 291 | 292 | # log output of commands run by `git fuzzy` 293 | export GF_COMMAND_LOG_OUTPUT="YES" 294 | 295 | # log internal commands (pretty noisy) 296 | export GF_INTERNAL_COMMAND_DEBUG_MODE="YES" 297 | ``` 298 | 299 | Or, log everything: 300 | 301 | ``` 302 | GF_DEBUG_MODE="YES" GF_COMMAND_DEBUG_MODE="YES" GF_COMMAND_FZF_DEBUG_MODE="YES" GF_COMMAND_LOG_OUTPUT="YES" GF_INTERNAL_COMMAND_DEBUG_MODE="YES" git fuzzy 303 | ``` 304 | -------------------------------------------------------------------------------- /bin/git-fuzzy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # NB: get the absolute path to the directory containing this script 4 | # https://stackoverflow.com/a/246128 5 | 6 | script_source="${BASH_SOURCE[0]}" 7 | 8 | while [ -h "$script_source" ]; do 9 | target="$(readlink "$script_source")" 10 | if [[ $target == /* ]]; then 11 | script_source="$target" 12 | else 13 | script_dir="$( dirname "$script_source" )" 14 | script_source="$script_dir/$target" 15 | fi 16 | done 17 | 18 | script_dir="$( cd -P "$( dirname "$script_source" )" >/dev/null 2>&1 && pwd )" 19 | 20 | # NB: all sourcing happens here. 21 | # ----------------------------------------- 22 | git_fuzzy_dir="$script_dir/.." 23 | 24 | . "$git_fuzzy_dir/lib/load-configs.sh" 25 | 26 | . "$git_fuzzy_dir/lib/snapshot.sh" 27 | 28 | . "$git_fuzzy_dir/lib/utils.sh" 29 | 30 | . "$git_fuzzy_dir/lib/debug.sh" 31 | . "$git_fuzzy_dir/lib/core.sh" 32 | 33 | . "$git_fuzzy_dir/lib/modules/main.sh" 34 | 35 | . "$git_fuzzy_dir/lib/modules/branch.sh" 36 | . "$git_fuzzy_dir/lib/modules/diff-checkout.sh" 37 | . "$git_fuzzy_dir/lib/modules/diff-direct.sh" 38 | . "$git_fuzzy_dir/lib/modules/diff.sh" 39 | . "$git_fuzzy_dir/lib/modules/log.sh" 40 | . "$git_fuzzy_dir/lib/modules/reflog.sh" 41 | . "$git_fuzzy_dir/lib/modules/status.sh" 42 | . "$git_fuzzy_dir/lib/modules/stash.sh" 43 | 44 | . "$git_fuzzy_dir/lib/modules/hub/pr.sh" 45 | 46 | . "$git_fuzzy_dir/lib/modules/helpers.sh" 47 | # ----------------------------------------- 48 | 49 | # -- Invariants -- 50 | 51 | if ! type git >/dev/null 2>&1; then 52 | # shellcheck disable=2016 53 | gf_log_error '`git` not found; it is required for `git fuzzy` to work.' 54 | exit 1 55 | fi 56 | 57 | if ! gf_is_in_git_repo; then 58 | gf_log_error "not in git repo" 59 | exit 1 60 | fi 61 | 62 | if ! type fzf >/dev/null 2>&1; then 63 | # shellcheck disable=2016 64 | gf_log_error '`fzf` not found; it is required for `git fuzzy` to work.' 65 | exit 1 66 | fi 67 | 68 | FZF_VERSION="$(fzf --version)" 69 | MIN_FZF_VERSION="0.21.0" 70 | if [ "$FZF_VERSION" = "$(echo -e "$FZF_VERSION\n$MIN_FZF_VERSION" | sort -V | head -n1)" ]; then 71 | # shellcheck disable=2016 72 | gf_log_warning '`fzf` is too old and may not work properly' 73 | fi 74 | 75 | # NB: checking for actually _unset_ 76 | # shellcheck disable=2016 77 | if [ -z "${HUB_AVAILABLE+X}" ]; then 78 | if type hub >/dev/null 2>&1; then 79 | export HUB_AVAILABLE="YES" 80 | export GIT_CMD="hub" 81 | gf_log_debug '`hub` found, enabling GitHub support.' 82 | else 83 | export HUB_AVAILABLE="" 84 | export GIT_CMD="git" 85 | gf_log_debug '`hub` not found, disabling GitHub support.' 86 | fi 87 | fi 88 | 89 | gf_run() { 90 | COMMAND="$1" 91 | shift 92 | if [ "$COMMAND" = 'helper' ]; then 93 | SUB_COMMAND="$1" 94 | shift 95 | FUNC="gf_${COMMAND}_${SUB_COMMAND}" 96 | PARAMETERS_QUOTED="$(quote_params "$@")" 97 | if type "$FUNC" >/dev/null 2>&1; then 98 | gf_log_internal "$FUNC $PARAMETERS_QUOTED" 99 | eval "$FUNC $PARAMETERS_QUOTED" 100 | else 101 | gf_log_error "\`$COMMAND\` named \`${SUB_COMMAND}\` not found" 102 | fi 103 | else 104 | FORCE_INTERACTIVE="" 105 | if [ "$COMMAND" = 'interactive' ]; then 106 | COMMAND="$1" 107 | FORCE_INTERACTIVE="YES" 108 | shift 109 | fi 110 | 111 | PARAMETERS_QUOTED="$(quote_params "$@")" 112 | INTERACTIVE_FUNC="gf_${COMMAND}" 113 | NON_INTERACTIVE_FUNC="gf_${COMMAND}_direct" 114 | 115 | if [ -z "$FORCE_INTERACTIVE" ] && type "$NON_INTERACTIVE_FUNC" >/dev/null 2>&1; then 116 | gf_log_internal "$NON_INTERACTIVE_FUNC $PARAMETERS_QUOTED" 117 | eval "$NON_INTERACTIVE_FUNC $PARAMETERS_QUOTED" 118 | elif type "$INTERACTIVE_FUNC" >/dev/null 2>&1; then 119 | gf_log_internal "$INTERACTIVE_FUNC $PARAMETERS_QUOTED" 120 | eval "$INTERACTIVE_FUNC $PARAMETERS_QUOTED" 121 | else 122 | gf_log_error "\`$COMMAND\` not found" 123 | fi 124 | fi 125 | } 126 | 127 | git_fuzzy() { 128 | if [ "$#" = '0' ]; then 129 | gf_menu 130 | else 131 | gf_run "$@" 132 | fi 133 | } 134 | 135 | git_fuzzy "$@" 136 | -------------------------------------------------------------------------------- /gifs/diff.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigH/git-fuzzy/9b6846f25f33c82a1b7af6e7c9a5a013eeb9b702/gifs/diff.gif -------------------------------------------------------------------------------- /gifs/log.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigH/git-fuzzy/9b6846f25f33c82a1b7af6e7c9a5a013eeb9b702/gifs/log.gif -------------------------------------------------------------------------------- /gifs/status.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigH/git-fuzzy/9b6846f25f33c82a1b7af6e7c9a5a013eeb9b702/gifs/status.gif -------------------------------------------------------------------------------- /lib/core.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_FUZZY_SELECT_ALL_KEY="${GIT_FUZZY_SELECT_ALL_KEY:-Alt-A}" 4 | GIT_FUZZY_SELECT_NONE_KEY="${GIT_FUZZY_SELECT_NONE_KEY:-Alt-D}" 5 | 6 | if [ -z "$GF_FZF_DEFAULTS_SET" ]; then 7 | export GF_FZF_DEFAULTS_SET="YES" 8 | 9 | # override the default global fzf configuration options to fit the git-fuzzy use case 10 | # allow the user to override the global fzf defaults by setting GIT_FUZZY_FZF_DEFAULT_OPTS 11 | 12 | if [ -z "$GIT_FUZZY_FZF_DEFAULT_OPTS" ]; then 13 | export FZF_DEFAULT_OPTS="\ 14 | $FZF_DEFAULT_OPTS \ 15 | --border \ 16 | --layout=reverse \ 17 | --bind 'ctrl-space:toggle-preview' \ 18 | --bind 'ctrl-j:down' \ 19 | --bind 'ctrl-k:up' \ 20 | --bind 'ctrl-d:half-page-down' \ 21 | --bind 'ctrl-u:half-page-up' \ 22 | --bind 'ctrl-s:toggle-sort' \ 23 | --bind 'ctrl-e:preview-down' \ 24 | --bind 'ctrl-y:preview-up' \ 25 | --bind 'change:top' \ 26 | --no-height" 27 | else 28 | export FZF_DEFAULT_OPTS="\ 29 | $FZF_DEFAULT_OPTS \ 30 | $GIT_FUZZY_FZF_DEFAULT_OPTS" 31 | fi 32 | 33 | export FZF_DEFAULT_OPTS_MULTI="\ 34 | $FZF_DEFAULT_OPTS_MULTI \ 35 | --bind \"$(lowercase "$GIT_FUZZY_SELECT_NONE_KEY"):deselect-all\" \ 36 | --bind \"$(lowercase "$GIT_FUZZY_SELECT_ALL_KEY"):select-all\"" 37 | fi 38 | 39 | 40 | GF_BC_STL=' 41 | define min(a, b) { if (a < b) return a else return b } 42 | define max(a, b) { if (a > b) return a else return b } 43 | ' 44 | 45 | WIDTH="$(tput cols)" 46 | HEIGHT="$(tput lines)" 47 | 48 | run_bc_program() { 49 | WIDTH_SUBSTITUTED="${1//__WIDTH__/$WIDTH}" 50 | echo "${GF_BC_STL} ${GF_BC_LIB} ${WIDTH_SUBSTITUTED//__HEIGHT__/$HEIGHT}" | bc -l 51 | } 52 | 53 | is_vertical() { 54 | run_bc_program "__WIDTH__ / __HEIGHT__ < $GF_VERTICAL_THRESHOLD" 55 | } 56 | 57 | particularly_small_screen() { 58 | if [ "$(is_vertical)" = '1' ]; then 59 | run_bc_program "$GF_VERTICAL_SMALL_SCREEN_CALCULATION" 60 | else 61 | run_bc_program "$GF_HORIZONTAL_SMALL_SCREEN_CALCULATION" 62 | fi 63 | } 64 | 65 | preview_window_size_and_direction() { 66 | if [ "$(is_vertical)" = '1' ]; then 67 | PREVIEW_DIRECTION="$GF_VERTICAL_PREVIEW_LOCATION" 68 | PREVIEW_SIZE="$(run_bc_program "$GF_VERTICAL_PREVIEW_PERCENT_CALCULATION")" 69 | else 70 | PREVIEW_DIRECTION="$GF_HORIZONTAL_PREVIEW_LOCATION" 71 | PREVIEW_SIZE="$(run_bc_program "$GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION")" 72 | fi 73 | 74 | # NB: round the `bc -l` result 75 | echo "--preview-window=$PREVIEW_DIRECTION:${PREVIEW_SIZE%%.*}%" 76 | } 77 | 78 | preview_window_settings() { 79 | echo "$(preview_window_size_and_direction):nohidden" 80 | } 81 | 82 | hidden_preview_window_settings() { 83 | echo "$(preview_window_size_and_direction):hidden" 84 | } 85 | 86 | gf_is_in_git_repo() { 87 | git -C . rev-parse > /dev/null 2>&1 88 | } 89 | 90 | gf_merge_base(){ 91 | git merge-base HEAD "$@" 92 | } 93 | 94 | gf_git_root_directory() { 95 | git rev-parse --show-toplevel 96 | } 97 | 98 | gf_go_to_git_root_directory() { 99 | local git_root_directory 100 | git_root_directory="$(gf_git_root_directory)" 101 | gf_log_debug "going to git root at ${git_root_directory} from $(pwd)" 102 | cd "${git_root_directory}" 103 | } 104 | 105 | gf_diff_renderer() { 106 | if [ -n "$GF_PREFERRED_PAGER" ]; then 107 | if [ -n "$FZF_PREVIEW_COLUMNS" ]; then 108 | cat - | ${GF_PREFERRED_PAGER//__WIDTH__/$FZF_PREVIEW_COLUMNS} 109 | else 110 | cat - | ${GF_PREFERRED_PAGER//__WIDTH__/$WIDTH} 111 | fi 112 | elif type delta >/dev/null 2>&1; then 113 | if [ -n "$FZF_PREVIEW_COLUMNS" ]; then 114 | cat - | delta --width "$FZF_PREVIEW_COLUMNS" 115 | else 116 | cat - | delta 117 | fi 118 | elif type diff-so-fancy >/dev/null 2>&1; then 119 | cat - | diff-so-fancy 120 | else 121 | cat - 122 | fi 123 | } 124 | 125 | gf_command_logged() { 126 | gf_log_command "$@" 127 | if [ -n "$GF_COMMAND_LOG_OUTPUT" ]; then 128 | "$@" >> "$GF_LOG_LOCATION" 2>&1 129 | else 130 | "$@" >/dev/null 2>&1 131 | fi 132 | } 133 | 134 | gf_interactive_command_logged() { 135 | gf_log_command "$@" 136 | "$@" 137 | } 138 | 139 | gf_fzf() { 140 | local gf_command="fzf --ansi --no-sort --no-info --multi \ 141 | $FZF_DEFAULT_OPTS_MULTI \ 142 | $(preview_window_settings) \ 143 | $(quote_params "$@")" 144 | 145 | if [ -n "$GF_COMMAND_FZF_DEBUG_MODE" ]; then 146 | gf_log_command_string "$gf_command" 147 | fi 148 | 149 | eval "$gf_command" 150 | } 151 | 152 | gf_fzf_one() { 153 | local gf_command="fzf +m --ansi --no-sort --no-info \ 154 | $(preview_window_settings) \ 155 | $(quote_params "$@")" 156 | if [ -n "$GF_COMMAND_FZF_DEBUG_MODE" ]; then 157 | gf_log_command_string "$gf_command" 158 | fi 159 | eval "$gf_command" 160 | } 161 | 162 | gf_command_with_header() { 163 | NUM="$1" 164 | shift 165 | printf "%s" "$GRAY" "$BOLD" '$ ' "$CYAN" "$BOLD" 166 | # shellcheck disable=2046 167 | printf "%s " $(printf '%q ' "$@") 168 | printf "%s" "$NORMAL" 169 | # shellcheck disable=2034 170 | for i in $(seq 1 "$NUM"); do 171 | echo 172 | done 173 | "$@" 174 | } 175 | 176 | gf_git_command() { 177 | "$GIT_CMD" -c color.ui=always "$@" 178 | } 179 | 180 | gf_git_command_with_header() { 181 | NUM="$1" 182 | shift 183 | printf "%s" "$GRAY" "$BOLD" '$ ' "$CYAN" "$BOLD" "$GIT_CMD $(quote_params "$@")" "$NORMAL" 184 | # shellcheck disable=2034 185 | for i in $(seq 1 "$NUM"); do 186 | echo 187 | done 188 | "$GIT_CMD" -c color.ui=always "$@" 189 | } 190 | 191 | gf_git_command_with_header_default_parameters() { 192 | NUM="$1" 193 | shift 194 | DEFAULT_SUBCOMMAND_PARAMETERS="$1" 195 | shift 196 | SUB_COMMAND="$1" 197 | shift 198 | printf "%s" "$GRAY" "$BOLD" '$ ' "$CYAN" "$BOLD" "$GIT_CMD $SUB_COMMAND $(quote_params "$@")" "$NORMAL" 199 | # shellcheck disable=2034 200 | for i in $(seq 0 "$NUM"); do 201 | [ "$i" -gt 0 ] && echo 202 | done 203 | gf_log_command_string "$GIT_CMD -c color.ui=always '$SUB_COMMAND' $DEFAULT_SUBCOMMAND_PARAMETERS $(quote_params "$@")" 204 | eval "$GIT_CMD -c color.ui=always '$SUB_COMMAND' $DEFAULT_SUBCOMMAND_PARAMETERS $(quote_params "$@")" 205 | } 206 | 207 | gf_quit() { 208 | gf_log_debug "exiting" 209 | exit 0 210 | } 211 | -------------------------------------------------------------------------------- /lib/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=2129 3 | 4 | gf_emit_log_and_reset() { 5 | >&2 cat "$GF_LOG_LOCATION" 6 | } 7 | 8 | if [ -z "$GF_LOG_LOCATION" ]; then 9 | if [ -d "$TMPDIR" ]; then 10 | # NB: TMPDIR on macOS has a trailing slash 11 | GF_LOG_LOCATION="${TMPDIR%/}/git-fuzzy-log" 12 | else 13 | GF_LOG_LOCATION="$HOME/.git-fuzzy-log" 14 | fi 15 | export GF_LOG_LOCATION 16 | 17 | rm -f "$GF_LOG_LOCATION" 18 | touch "$GF_LOG_LOCATION" 19 | 20 | trap gf_emit_log_and_reset EXIT 21 | fi 22 | 23 | gf_log_warning() { 24 | printf "[%s%sWRN%s]" "$YELLOW" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 25 | printf " %s" "$@" >> "$GF_LOG_LOCATION" 26 | echo >> "$GF_LOG_LOCATION" 27 | } 28 | 29 | gf_log_error() { 30 | printf "[%s%sERR%s]" "$RED" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 31 | printf " %s" "$@" >> "$GF_LOG_LOCATION" 32 | echo >> "$GF_LOG_LOCATION" 33 | } 34 | 35 | gf_log_debug() { 36 | if [ -n "$GF_DEBUG_MODE" ]; then 37 | printf "[%s%sDBG%s]" "$GRAY" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 38 | printf " %s" "$@" >> "$GF_LOG_LOCATION" 39 | echo >> "$GF_LOG_LOCATION" 40 | fi 41 | } 42 | 43 | gf_log_command() { 44 | if [ -n "$GF_COMMAND_DEBUG_MODE" ]; then 45 | if [ "$#" -gt 0 ]; then 46 | printf "[%s%sCMD%s]" "$GREEN" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 47 | printf '%s' "$GRAY" "$BOLD" ' $ ' "$NORMAL" "$CYAN" "$BOLD" "$(quote_params "$1")" "$NORMAL" >> "$GF_LOG_LOCATION" 48 | shift 49 | printf ' %s' "$GREEN" "$(quote_params "$@")" >> "$GF_LOG_LOCATION" 50 | printf '%s' "$NORMAL" >> "$GF_LOG_LOCATION" 51 | echo >> "$GF_LOG_LOCATION" 52 | else 53 | # shellcheck disable=2016 54 | error_exit '`gf_log_command` requires an actual command' 55 | fi 56 | fi 57 | } 58 | 59 | gf_log_command_string() { 60 | if [ -n "$GF_COMMAND_DEBUG_MODE" ]; then 61 | if [ "$#" -gt 0 ]; then 62 | printf "[%s%sCMD%s]" "$GREEN" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 63 | printf '%s%s%s%s' "$GRAY" "$BOLD" ' $ ' "$NORMAL" >> "$GF_LOG_LOCATION" 64 | printf '%s%s%s' "$CYAN" "$1" "$NORMAL" >> "$GF_LOG_LOCATION" 65 | echo >> "$GF_LOG_LOCATION" 66 | else 67 | # shellcheck disable=2016 68 | error_exit '`gf_log_command_string` requires an actual command' 69 | fi 70 | fi 71 | } 72 | 73 | gf_log_internal() { 74 | if [ -n "$GF_INTERNAL_COMMAND_DEBUG_MODE" ]; then 75 | if [ "$#" -gt 0 ]; then 76 | printf "[%s%sCMD%s] (internal)" "$GRAY" "$BOLD" "$NORMAL" >> "$GF_LOG_LOCATION" 77 | printf '%s' "$GRAY" "$BOLD" ' $ ' "$CYAN" "$BOLD" "$1" "$NORMAL" >> "$GF_LOG_LOCATION" 78 | shift 79 | printf '%s' "$GREEN" >> "$GF_LOG_LOCATION" 80 | printf '%s' "$(quote_params "$@")" >> "$GF_LOG_LOCATION" 81 | printf '%s' "$NORMAL" >> "$GF_LOG_LOCATION" 82 | echo >> "$GF_LOG_LOCATION" 83 | else 84 | # shellcheck disable=2016 85 | error_exit '`gf_log_internal` requires an actual command' 86 | fi 87 | fi 88 | } 89 | -------------------------------------------------------------------------------- /lib/load-configs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -- Configuring FZF UI: -- 4 | 5 | if [ -z "$GF_VERTICAL_THRESHOLD" ]; then 6 | export GF_VERTICAL_THRESHOLD="2.0" 7 | fi 8 | 9 | if [ -z "$GF_VERTICAL_PREVIEW_LOCATION" ]; then 10 | export GF_VERTICAL_PREVIEW_LOCATION="bottom" 11 | fi 12 | 13 | if [ -z "$GF_HORIZONTAL_PREVIEW_LOCATION" ]; then 14 | export GF_HORIZONTAL_PREVIEW_LOCATION="right" 15 | fi 16 | 17 | if [ -z "$GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION" ]; then 18 | export GF_HORIZONTAL_PREVIEW_PERCENT_CALCULATION='max(50, min(80, 100 - ((7000 + (11 * __WIDTH__)) / __WIDTH__)))' 19 | fi 20 | 21 | if [ -z "$GF_HORIZONTAL_SMALL_SCREEN_CALCULATION" ]; then 22 | export GF_HORIZONTAL_SMALL_SCREEN_CALCULATION='__HEIGHT__ <= 30' 23 | fi 24 | 25 | if [ -z "$GF_VERTICAL_PREVIEW_PERCENT_CALCULATION" ]; then 26 | export GF_VERTICAL_PREVIEW_PERCENT_CALCULATION='max(50, min(80, 100 - ((4000 + (5 * __HEIGHT__)) / __HEIGHT__)))' 27 | fi 28 | 29 | if [ -z "$GF_VERTICAL_SMALL_SCREEN_CALCULATION" ]; then 30 | export GF_VERTICAL_SMALL_SCREEN_CALCULATION='__HEIGHT__ <= 60' 31 | fi 32 | 33 | # -- Configuring External Command Style: -- 34 | 35 | # NB: used for highlighting search terms in diff output 36 | if [ -n "$GF_GREP_COLOR" ]; then 37 | # NB: needs exporting as `grep` runs as a subprocess 38 | export GREP_COLOR="$GF_GREP_COLOR" 39 | fi 40 | 41 | if [ -n "$GF_BAT_STYLE" ]; then 42 | # NB: needs exporting as `bat` runs as a subprocess 43 | export BAT_STYLE="$GF_BAT_STYLE" 44 | fi 45 | 46 | if [ -n "$GF_BAT_THEME" ]; then 47 | # NB: needs exporting as `bat` runs as a subprocess 48 | export BAT_THEME="$GF_BAT_THEME" 49 | fi 50 | 51 | # -- Configuring `$EDITOR`: -- 52 | 53 | if [ -z "$GF_EDITOR_ARGS" ]; then 54 | export GF_EDITOR_ARGS='' 55 | fi 56 | 57 | # -- Configuring `git`: -- 58 | 59 | if [ -z "$GF_DIFF_SEARCH_DEFAULTS" ]; then 60 | export GF_DIFF_SEARCH_DEFAULTS="-G" 61 | fi 62 | 63 | if [ -z "$GF_HUB_PR_FORMAT" ]; then 64 | export GF_HUB_PR_FORMAT='%pC%>(8)%I%Creset %t% l%n' 65 | fi 66 | 67 | if [ -z "$GF_LOG_MENU_PARAMS" ]; then 68 | export GF_LOG_MENU_PARAMS='--pretty=oneline --abbrev-commit' 69 | fi 70 | 71 | if [ -z "$GF_REFLOG_MENU_PARAMS" ]; then 72 | export GF_REFLOG_MENU_PARAMS='--pretty=oneline --abbrev-commit' 73 | fi 74 | 75 | if [ -z "$GF_BASE_REMOTE" ]; then 76 | export GF_BASE_REMOTE=origin 77 | fi 78 | 79 | if [ -z "$GF_BASE_BRANCH" ]; then 80 | GF_BASE_BRANCH="$(git symbolic-ref -q "refs/remotes/${GF_BASE_REMOTE}/HEAD" || echo -n "main" | sed "s@^refs/remotes/${GF_BASE_REMOTE}/@@")" 81 | export GF_BASE_BRANCH 82 | fi 83 | 84 | if [ -r "./.git-fuzzy-config" ]; then 85 | # shellcheck disable=1091 86 | . "./.git-fuzzy-config" 87 | fi 88 | -------------------------------------------------------------------------------- /lib/modules/branch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_FUZZY_BRANCH_WORKING_COPY_KEY="${GIT_FUZZY_BRANCH_WORKING_COPY_KEY:-Ctrl-P}" 4 | GIT_FUZZY_BRANCH_MERGE_BASE_KEY="${GIT_FUZZY_BRANCH_MERGE_BASE_KEY:-Alt-P}" 5 | GIT_FUZZY_BRANCH_COMMIT_LOG_KEY="${GIT_FUZZY_BRANCH_COMMIT_LOG_KEY:-Alt-L}" 6 | GIT_FUZZY_BRANCH_CHECKOUT_FILE_KEY="${GIT_FUZZY_BRANCH_CHECKOUT_FILE_KEY:-Alt-F}" 7 | GIT_FUZZY_BRANCH_CHECKOUT_KEY="${GIT_FUZZY_BRANCH_CHECKOUT_KEY:-Alt-B}" 8 | GIT_FUZZY_BRANCH_DELETE_BRANCH_KEY="${GIT_FUZZY_BRANCH_DELETE_BRANCH_KEY:-Alt-D}" 9 | 10 | GF_BRANCH_RELOAD="reload(git fuzzy helper branch_menu_content)" 11 | 12 | GF_BRANCH_CHECKOUT="git fuzzy helper branch_checkout {1}" 13 | 14 | GF_BRANCH_ATTEMPT_DELETE="git fuzzy helper branch_delete {1}" 15 | GF_BRANCH_DELETE_BINDING="execute-silent($GF_BRANCH_ATTEMPT_DELETE)+$GF_BRANCH_RELOAD" 16 | 17 | gf_fzf_branch() { 18 | BRANCH_HEADER=' 19 | Type to filter. '"${WHITE}Enter${NORMAL} to ${GREEN}ACCEPT${NORMAL}"' 20 | Select an entry with '"${WHITE}${NORMAL}"' to use as basis for comparison in the preview. 21 | ' 22 | BRANCH_CHECKOUT_BINDING="$(lowercase "$GIT_FUZZY_BRANCH_CHECKOUT_KEY"):execute-silent($GF_BRANCH_CHECKOUT)+$GF_BRANCH_RELOAD" 23 | BRANCH_HEADER_BRANCH_CHECKOUT=" ${GREEN}${BOLD}checkout ${YELLOW}${BOLD}${NORMAL} ${WHITE}${GIT_FUZZY_BRANCH_CHECKOUT_KEY}${NORMAL}" 24 | BRANCH_HEADER_FILE_CHECKOUT=" ${GREEN}${BOLD}checkout ${YELLOW}${BOLD}📁${NORMAL} ${WHITE}${GIT_FUZZY_BRANCH_CHECKOUT_FILE_KEY}${NORMAL}" 25 | if [ -n "$(git status --short)" ]; then 26 | # files are dirty - warn that checkout is potentially undesired 27 | BRANCH_HEADER_BRANCH_CHECKOUT="${GRAY}(${RED}${BOLD}*${GRAY}) ${RED}${BOLD}checkout ${YELLOW}${BOLD}${NORMAL} ${WHITE}${GIT_FUZZY_BRANCH_CHECKOUT_KEY}${NORMAL}" 28 | BRANCH_HEADER_FILE_CHECKOUT="${GRAY}(${RED}${BOLD}*${GRAY}) ${RED}${BOLD}checkout ${YELLOW}${BOLD}📁${NORMAL} ${WHITE}${GIT_FUZZY_BRANCH_CHECKOUT_FILE_KEY}${NORMAL}" 29 | BRANCH_HEADER="$BRANCH_HEADER"' 30 | '"${GRAY}(${RED}${BOLD}*${GRAY}) ${NORMAL}working copy is dirty; changes may be ${RED}${BOLD}destructive${NORMAL}"' 31 | ' 32 | fi 33 | 34 | BRANCH_HEADER="$BRANCH_HEADER"' 35 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}working copy${NORMAL} ${WHITE}$GIT_FUZZY_BRANCH_WORKING_COPY_KEY${NORMAL} $BRANCH_HEADER_BRANCH_CHECKOUT"' 36 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}merge-base${NORMAL} ${WHITE}$GIT_FUZZY_BRANCH_MERGE_BASE_KEY${NORMAL} $BRANCH_HEADER_FILE_CHECKOUT"' 37 | '"${GREEN}commit log${NORMAL} ${WHITE}$GIT_FUZZY_BRANCH_COMMIT_LOG_KEY${NORMAL} ${RED}${BOLD}delete branch ✗${NORMAL} ${WHITE}$GIT_FUZZY_BRANCH_DELETE_BRANCH_KEY${NORMAL}"' 38 | 39 | ' 40 | 41 | if [ "$(particularly_small_screen)" = '1' ]; then 42 | BRANCH_HEADER='' 43 | fi 44 | 45 | # shellcheck disable=2046,2016,2090,2086 46 | gf_fzf_one -m \ 47 | --header "$BRANCH_HEADER" \ 48 | --bind 'click-header:reload(git fuzzy helper branch_menu_content)' \ 49 | --bind 'backward-eof:reload(git fuzzy helper branch_menu_content)' \ 50 | --bind "$BRANCH_CHECKOUT_BINDING" \ 51 | --bind "$(lowercase "$GIT_FUZZY_BRANCH_CHECKOUT_FILE_KEY")"':execute(git fuzzy helper branch_checkout_files {1})' \ 52 | --bind "$(lowercase "$GIT_FUZZY_BRANCH_DELETE_BRANCH_KEY"):$GF_BRANCH_DELETE_BINDING" \ 53 | --bind "$(lowercase "$GIT_FUZZY_BRANCH_COMMIT_LOG_KEY")"':execute(git fuzzy log {1})' \ 54 | --bind "$(lowercase "$GIT_FUZZY_BRANCH_WORKING_COPY_KEY")"':execute(git fuzzy diff {1})' \ 55 | --bind "$(lowercase "$GIT_FUZZY_BRANCH_MERGE_BASE_KEY")"':execute(git fuzzy diff "$(git merge-base "'"$GF_BASE_BRANCH"'" {1})" {1})' \ 56 | --preview 'git fuzzy helper branch_preview_content {1} {+1}' | \ 57 | awk '{ print $1 }' 58 | } 59 | 60 | gf_branch() { 61 | gf_snapshot "branch" 62 | git fuzzy helper branch_menu_content | gf_fzf_branch 63 | } 64 | -------------------------------------------------------------------------------- /lib/modules/diff-checkout.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=2016 4 | GF_DIFF_CHECKOUT_HEADER=' 5 | Query above is with these args '"'${MAGENTA}${GF_DIFF_SEARCH_DEFAULTS} query${NORMAL}'"' 6 | Press '"${WHITE}Enter${NORMAL}"' to checkout selected file(s) 7 | 8 | '"${GRAY}-- (files will not disappear) --${NORMAL}"' 9 | 10 | ' 11 | 12 | if [ "$(particularly_small_screen)" = '1' ]; then 13 | GF_DIFF_CHECKOUT_HEADER='' 14 | fi 15 | 16 | gf_fzf_diff_checkout() { 17 | # shellcheck disable=2016 18 | RELOAD_COMMAND="git fuzzy helper diff_direct_menu_content {q} '$1' '$2'" 19 | PREVIEW_COMMAND="git fuzzy helper diff_direct_preview_content {q} {} '$1'" 20 | 21 | gf_fzf -m --phony \ 22 | --header-lines=2 \ 23 | --header "$GF_DIFF_CHECKOUT_HEADER" \ 24 | --preview "$PREVIEW_COMMAND" \ 25 | --bind "click-header:reload(git fuzzy helper diff_direct_menu_content {q} '$1' '$2')" \ 26 | --bind "backward-eof:reload(git fuzzy helper diff_direct_menu_content {q} '$1' '$2')" \ 27 | --bind "change:reload($RELOAD_COMMAND)" \ 28 | --bind "enter:execute-silent(git fuzzy helper diff_checkout_file '$1' {+})+down" 29 | } 30 | 31 | gf_diff_checkout() { 32 | gf_snapshot "diff-checkout" 33 | gf_go_to_git_root_directory 34 | MERGE_BASE="$(gf_merge_base "$1")" 35 | git fuzzy helper diff_direct_menu_content '' "$1" "$MERGE_BASE" | gf_fzf_diff_checkout "$1" "$MERGE_BASE" 36 | } 37 | -------------------------------------------------------------------------------- /lib/modules/diff-direct.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=2016 4 | GF_DIFF_DIRECT_HEADER=' 5 | Query above is with these args '"'${MAGENTA}${GF_DIFF_SEARCH_DEFAULTS} query${NORMAL}'"' 6 | 7 | ' 8 | 9 | if [ "$(particularly_small_screen)" = '1' ]; then 10 | GF_DIFF_DIRECT_HEADER='' 11 | fi 12 | 13 | gf_fzf_diff_direct() { 14 | PARAMETERS_QUOTED="$(quote_params "$@")" 15 | 16 | # shellcheck disable=2016 17 | RELOAD_COMMAND="git fuzzy helper diff_direct_menu_content {q} $PARAMETERS_QUOTED" 18 | PREVIEW_COMMAND="git fuzzy helper diff_direct_preview_content {q} {} $PARAMETERS_QUOTED" 19 | 20 | gf_fzf -m --phony \ 21 | --header-lines=2 \ 22 | --header "$GF_DIFF_DIRECT_HEADER" \ 23 | --preview "$PREVIEW_COMMAND" \ 24 | --bind "click-header:reload(git fuzzy helper diff_direct_menu_content {q} $PARAMETERS_QUOTED)" \ 25 | --bind "backward-eof:reload(git fuzzy helper diff_direct_menu_content {q} $PARAMETERS_QUOTED)" \ 26 | --bind "change:reload($RELOAD_COMMAND)" 27 | } 28 | 29 | gf_diff_direct() { 30 | gf_go_to_git_root_directory 31 | if ! git diff --quiet "$@"; then 32 | git fuzzy helper diff_direct_menu_content '' "$@" | gf_fzf_diff_direct "$@" 33 | else 34 | gf_log_debug "empty diff" 35 | exit 1 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /lib/modules/diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=2016 4 | GF_DIFF_HEADER=' 5 | Type to filter. '"${WHITE}Enter${NORMAL} to ${GREEN}ACCEPT${NORMAL}."' 6 | 7 | '"${GREEN}mark${NORMAL} ${WHITE}Tab${NORMAL}"' 8 | 9 | ' 10 | 11 | if [ "$(particularly_small_screen)" = '1' ]; then 12 | GF_DIFF_HEADER='' 13 | fi 14 | 15 | GF_DIFF_PREVIEW=' 16 | [ {1} != "nothing" ] && 17 | git fuzzy helper diff_preview_content {2} || 18 | echo "nothing to show" 19 | ' 20 | 21 | gf_fzf_diff_select() { 22 | gf_fzf -m 2 \ 23 | --with-nth=2.. \ 24 | --header "$GF_DIFF_HEADER" \ 25 | --preview "$GF_DIFF_PREVIEW" \ 26 | --bind 'click-header:reload(git fuzzy helper diff_menu_content)' \ 27 | --bind 'backward-eof:reload(git fuzzy helper diff_menu_content)' \ 28 | --bind 'enter:execute([ {1} != "nothing" ] && git fuzzy helper diff_select {+2})' 29 | } 30 | 31 | gf_diff() { 32 | if [ $# -gt 0 ]; then 33 | gf_diff_direct "$@" 34 | else 35 | git fuzzy helper diff_menu_content | gf_fzf_diff_select 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /lib/modules/helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$git_fuzzy_dir/lib/modules/helpers/generic.sh" 4 | 5 | . "$git_fuzzy_dir/lib/modules/helpers/branch.sh" 6 | . "$git_fuzzy_dir/lib/modules/helpers/diff.sh" 7 | . "$git_fuzzy_dir/lib/modules/helpers/diff-checkout.sh" 8 | . "$git_fuzzy_dir/lib/modules/helpers/diff-direct.sh" 9 | . "$git_fuzzy_dir/lib/modules/helpers/log.sh" 10 | . "$git_fuzzy_dir/lib/modules/helpers/reflog.sh" 11 | . "$git_fuzzy_dir/lib/modules/helpers/status.sh" 12 | . "$git_fuzzy_dir/lib/modules/helpers/stash.sh" 13 | 14 | . "$git_fuzzy_dir/lib/modules/helpers/hub/pr.sh" 15 | -------------------------------------------------------------------------------- /lib/modules/helpers/branch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_branch_menu_content() { 4 | echo "${RED}$(git branch --show-current)${NORMAL}" 5 | 6 | # locals sorted by last checkout 7 | echo 8 | git --no-pager reflog --decorate=full --pretty="%D|%cr|%an" | \ 9 | grep -v '^|' | \ 10 | awk ' 11 | BEGIN { 12 | FS="|" 13 | } 14 | { 15 | split($1, aliases, ", "); 16 | for (i in aliases) { 17 | if (aliases[i] ~ /^refs\/heads\//) { 18 | alias = substr(aliases[i], 12) 19 | if (counters[alias]++ == 0) { 20 | print \ 21 | "'"$ORANGE$BOLD"'" \ 22 | alias \ 23 | "'"$NORMAL"'" \ 24 | "|" \ 25 | "'"$MAGENTA"'" \ 26 | $2 \ 27 | "'"$NORMAL"'" \ 28 | "|" \ 29 | "'"$CYAN"'" \ 30 | $3 \ 31 | "'"$NORMAL"'"; 32 | } 33 | } 34 | } 35 | } 36 | ' | \ 37 | column -t -s'|' \ 38 | 39 | if [ -z "$GF_BRANCH_SKIP_REMOTE_BRANCHES" ]; then 40 | echo 41 | git for-each-ref --sort='-committerdate' \ 42 | --format="$YELLOW$BOLD%(refname:short)$NORMAL|$MAGENTA%(committerdate:relative)$NORMAL|$CYAN%(committername)$NORMAL" \ 43 | refs/remotes | \ 44 | column -t -s'|' 45 | fi 46 | } 47 | 48 | gf_helper_branch_preview_content() { 49 | if [ -z "$1" ]; then 50 | echo "nothing to show" 51 | else 52 | if [ "$#" -eq 2 ] && [ "$1" != "$2" ]; then 53 | # use $2 as the "base" and $1 as the "change" 54 | # shellcheck disable=2086 55 | gf_git_command_with_header 1 diff $GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS "$2" "$1" | gf_diff_renderer 56 | else 57 | # check against merge-base of the branch under cursor 58 | # shellcheck disable=2086 59 | gf_git_command_with_header 1 diff $GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS "$(git merge-base "$GF_BASE_BRANCH" "$1")" "$1" | gf_diff_renderer 60 | fi 61 | fi 62 | } 63 | 64 | gf_helper_branch_is_local() { 65 | BRANCH_IF_LOCAL="$(git for-each-ref --format='%(refname:short)' "refs/heads/$1")" 66 | test -n "$BRANCH_IF_LOCAL" 67 | } 68 | 69 | gf_helper_branch_checkout() { 70 | if [ -z "$1" ]; then 71 | gf_log_debug "no branch chosen for checkout" 72 | else 73 | if git fuzzy helper branch_is_local "$1"; then 74 | gf_command_logged git checkout "$1" 75 | else 76 | STRIPPED_REMOTE="${1#*/}" 77 | gf_command_logged git checkout -b "$STRIPPED_REMOTE" "$1" 78 | fi 79 | fi 80 | } 81 | 82 | gf_helper_branch_checkout_files() { 83 | if [ "$#" -eq 0 ]; then 84 | gf_log_debug "no branch chosen for checkout" 85 | else 86 | git fuzzy diff_checkout "$@" 87 | fi 88 | } 89 | 90 | gf_helper_branch_delete() { 91 | if [ -z "$1" ]; then 92 | gf_log_debug "no branch chosen for deletion" 93 | else 94 | if git fuzzy helper branch_is_local "$1"; then 95 | gf_command_logged git branch -D "$1" 96 | else 97 | STRIPPED_REMOTE="${1#*/}" 98 | REMOTE="${1%%/*}" 99 | gf_command_logged git push "$REMOTE" --delete "$STRIPPED_REMOTE" 100 | fi 101 | fi 102 | } 103 | -------------------------------------------------------------------------------- /lib/modules/helpers/diff-checkout.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_diff_checkout_file() { 4 | BRANCH="$1" 5 | shift 6 | if [ -n "$BRANCH" ] && [ "$#" -gt 0 ]; then 7 | gf_command_logged git checkout "$BRANCH" -- "$@" 8 | else 9 | gf_log_error "invalid args for \`checkout\`: '$BRANCH' -- $(quote_params "$@")" 10 | fi 11 | } 12 | -------------------------------------------------------------------------------- /lib/modules/helpers/diff-direct.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_diff_direct_menu_content() { 4 | QUERY="$1" 5 | shift 6 | if [ -z "$QUERY" ]; then 7 | gf_git_command_with_header 2 diff --name-only "$@" 8 | else 9 | # shellcheck disable=2086 10 | gf_git_command_with_header 2 diff $GF_DIFF_SEARCH_DEFAULTS "$QUERY" --name-only "$@" 11 | fi 12 | } 13 | 14 | gf_helper_diff_direct_preview_content() { 15 | QUERY="$1" 16 | shift 17 | 18 | FILE_PATH="$1" 19 | shift 20 | 21 | FOUND= 22 | for INDEX_OF_ARG in $(seq 0 $#); do 23 | VALUE="${!INDEX_OF_ARG}" 24 | if [ "$VALUE" = '--' ]; then 25 | FOUND=yes 26 | break 27 | fi 28 | done 29 | 30 | ARGS_TO_PASS=("$@") 31 | if [ -n "$FOUND" ]; then 32 | ARGS_TO_PASS=("${@:1:$((INDEX_OF_ARG - 1))}") 33 | fi 34 | 35 | if [ -z "$FILE_PATH" ]; then 36 | echo "nothing to show" 37 | elif [ -z "$QUERY" ]; then 38 | # shellcheck disable=2086,2090 39 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_FILE_PREVIEW_DEFAULTS" diff "${ARGS_TO_PASS[@]}" -- "$FILE_PATH" | gf_diff_renderer 40 | else 41 | # shellcheck disable=2086,2090 42 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_FILE_PREVIEW_DEFAULTS" diff "${ARGS_TO_PASS[@]}" -- "$FILE_PATH" | gf_diff_renderer | grep --color=always -E "$QUERY|$" 43 | fi 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/helpers/diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_diff_menu_content() { 4 | # locals sorted by last commit 5 | GF_BRANCH_LOCAL_BRANCHES="$(git for-each-ref --sort='-committerdate' \ 6 | --format="local $GREEN$BOLD%(refname:short)$NORMAL|$MAGENTA%(committerdate:relative)$NORMAL|$CYAN%(committername)$NORMAL" \ 7 | refs/heads | \ 8 | column -t -s'|' \ 9 | )" 10 | 11 | echo "nothing ${CYAN}-- LOCAL --${NORMAL}" 12 | echo "nothing" 13 | 14 | if [ -n "$GF_BRANCH_LOCAL_BRANCHES" ]; then 15 | echo "$GF_BRANCH_LOCAL_BRANCHES" 16 | else 17 | echo "nothing ${RED}no local branches${NORMAL}" 18 | fi 19 | 20 | echo "nothing" 21 | echo "nothing ${CYAN}-- REMOTE --${NORMAL}" 22 | echo "nothing" 23 | 24 | # locals sorted by last commit 25 | GF_BRANCH_REMOTE_BRANCHES="$(git for-each-ref --sort='-committerdate' \ 26 | --format="remote $YELLOW$BOLD%(refname:short)$NORMAL|$MAGENTA%(committerdate:relative)$NORMAL|$CYAN%(committername)$NORMAL" \ 27 | refs/remotes | \ 28 | column -t -s'|' \ 29 | )" 30 | 31 | if [ -n "$GF_BRANCH_REMOTE_BRANCHES" ]; then 32 | echo "$GF_BRANCH_REMOTE_BRANCHES" 33 | else 34 | echo "nothing ${RED}no remote branches${NORMAL}" 35 | fi 36 | } 37 | 38 | gf_helper_diff_preview_content() { 39 | if [ -z "$1" ]; then 40 | echo "nothing to show" 41 | elif [ "$1" = '.' ]; then 42 | echo "nothing to show" 43 | else 44 | REF="$1" 45 | # shellcheck disable=2086 46 | gf_git_command_with_header 1 diff $GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS "$(git merge-base "$GF_BASE_BRANCH" "$REF")" "$REF" | gf_diff_renderer 47 | fi 48 | } 49 | 50 | gf_helper_diff_select() { 51 | git fuzzy diff "$@" 52 | } 53 | -------------------------------------------------------------------------------- /lib/modules/helpers/generic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_fzf_log_line_interpreter() { 4 | cat - | awk '{ print $1 }' 5 | } 6 | -------------------------------------------------------------------------------- /lib/modules/helpers/hub/pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_pr_menu_content() { 4 | gf_git_command_with_header 2 pr list --format "$GF_HUB_PR_FORMAT" 5 | } 6 | 7 | gf_helper_pr_preview_content() { 8 | if [ -n "$1" ]; then 9 | DIFF_PARAMS="$(hub pr show --format='%sB %sH' "$1")" 10 | if [ -n "$DIFF_PARAMS" ]; then 11 | # shellcheck disable=2086 12 | gf_git_command_with_header 1 diff $DIFF_PARAMS 13 | fi 14 | fi 15 | } 16 | 17 | gf_helper_pr_select() { 18 | if [ -n "$1" ]; then 19 | DIFF_PARAMS="$(hub pr show --format='%sB %sH' "$1")" 20 | if [ -n "$DIFF_PARAMS" ]; then 21 | # shellcheck disable=2086 22 | gf_git_command_with_header 1 fuzzy diff $DIFF_PARAMS 23 | fi 24 | fi 25 | } 26 | 27 | gf_helper_pr_show() { 28 | if [ -n "$1" ]; then 29 | gf_git_command_with_header 1 pr show "$1" 30 | fi 31 | } 32 | 33 | gf_helper_pr_log() { 34 | if [ -n "$1" ]; then 35 | DIFF_PARAMS="$(hub pr show --format='%sB..%sH' "$1")" 36 | if [ -n "$DIFF_PARAMS" ]; then 37 | # shellcheck disable=2086 38 | gf_git_command_with_header 1 fuzzy log $DIFF_PARAMS 39 | fi 40 | fi 41 | } 42 | -------------------------------------------------------------------------------- /lib/modules/helpers/log.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # `*` means use both parts, otherwise `log` gets only the first part 4 | gf_helper_log_log_query() { 5 | if [ "${1:0:1}" = '*' ]; then 6 | echo "$(query_part_one "$1") $(query_part_two "$1")" 7 | else 8 | query_part_one "$1" 9 | fi 10 | } 11 | 12 | # `#` means use both parts, otherwise `diff` gets only the first part 13 | gf_helper_log_diff_query() { 14 | if [ "${1:0:1}" = '#' ]; then 15 | echo "$(query_part_one "$1") $(query_part_two "$1")" 16 | else 17 | query_part_two "$1" 18 | fi 19 | } 20 | 21 | query_part_one() { 22 | echo "$1" | sed -E 's/[#*]?([^|]*)([|](.*))?/\1/' 23 | } 24 | 25 | query_part_two() { 26 | echo "$1" | sed -E 's/[#*]?(([^|]*)[|])?(.*)/\3/' 27 | } 28 | 29 | # TODO faithfully pass `log` params to `log_menu_content` (e.g. branch name) 30 | gf_helper_log_menu_content() { 31 | QUERY="$(git fuzzy helper log_log_query "$1")" 32 | shift 33 | if [ -n "$QUERY" ]; then 34 | # shellcheck disable=2086 35 | gf_git_command_with_header_default_parameters 2 "$GF_LOG_MENU_PARAMS" log $QUERY "$@" 36 | else 37 | gf_git_command_with_header_default_parameters 2 "$GF_LOG_MENU_PARAMS" log "$@" 38 | fi 39 | } 40 | 41 | gf_helper_log_preview_content() { 42 | if [ -z "$1" ]; then 43 | echo "nothing to show (empty line)" 44 | else 45 | REF="$(extract_commit_hash_from_first_line "$1")" 46 | if [ -n "$REF" ]; then 47 | QUERY="$(git fuzzy helper log_diff_query "$2")" 48 | BASE="$(extract_commit_hash_from_first_line "$3")" 49 | 50 | if [ "$BASE" == "$REF" ]; then 51 | # only show header when no commits selected 52 | if [ "$(particularly_small_screen)" = '1' ]; then 53 | # NB: `fold` is not aware of color codes; however, folding over whitespace seems fine 54 | gf_git_command show --decorate --oneline --no-patch "$REF" | fold -s -w "$FZF_PREVIEW_COLUMNS" 55 | else 56 | # NB: `fold` is not aware of color codes; however, folding over whitespace seems fine 57 | gf_git_command show --decorate --no-patch "$REF" | fold -s -w "$FZF_PREVIEW_COLUMNS" 58 | echo 59 | fi 60 | 61 | # shellcheck disable=2086 62 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_COMMIT_PREVIEW_DEFAULTS" diff "$REF^" "$REF" $QUERY | gf_diff_renderer 63 | else 64 | # shellcheck disable=2086 65 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_COMMIT_PREVIEW_DEFAULTS" diff "$BASE" "$REF" $QUERY | gf_diff_renderer 66 | fi 67 | else 68 | echo "nothing to show (no commit found on line)" 69 | fi 70 | fi 71 | } 72 | 73 | gf_helper_log_open_diff() { 74 | if [ -z "$1" ]; then 75 | echo "nothing to show (no mode selected)" 76 | else 77 | MODE="$1" 78 | shift 79 | if [ -z "$1" ]; then 80 | echo "nothing to show (empty line)" 81 | else 82 | REF="$(extract_commit_hash_from_first_line "$1")" 83 | if [ -n "$REF" ]; then 84 | case "$MODE" in 85 | commit) 86 | git fuzzy diff "$REF^" "$REF" 87 | ;; 88 | working_copy) 89 | git fuzzy diff "$REF" 90 | ;; 91 | merge_base) 92 | git fuzzy diff "$(git merge-base "$GF_BASE_BRANCH" "$REF")" "$REF" 93 | ;; 94 | *) 95 | log_error "mode unknown: $MODE" 96 | ;; 97 | esac 98 | else 99 | echo "nothing to show (no commit found on line)" 100 | fi 101 | fi 102 | fi 103 | } 104 | -------------------------------------------------------------------------------- /lib/modules/helpers/reflog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # get the first token by `|` 4 | gf_helper_reflog_log_query() { 5 | echo "$1" | cut -d'|' -f1 6 | } 7 | 8 | # get the _last_ token by `|` 9 | # NB: this supports using the same query for both chunks 10 | gf_helper_reflog_diff_query() { 11 | echo "$1" | rev | cut -d'|' -f1 | rev 12 | } 13 | 14 | # TODO faithfully pass `reflog` params to `reflog_menu_contents` 15 | gf_helper_reflog_menu_content() { 16 | QUERY="$(git fuzzy helper reflog_log_query "$1")" 17 | shift 18 | if [ -n "$QUERY" ]; then 19 | # shellcheck disable=2086 20 | gf_git_command_with_header_default_parameters 2 "$GF_REFLOG_MENU_PARAMS" reflog $QUERY "$@" 21 | else 22 | gf_git_command_with_header_default_parameters 2 "$GF_REFLOG_MENU_PARAMS" reflog "$@" 23 | fi 24 | } 25 | 26 | gf_helper_reflog_preview_content() { 27 | if [ -z "$1" ]; then 28 | echo "nothing to show" 29 | else 30 | REF="$(extract_commit_hash_from_first_line "$1")" 31 | QUERY="$(git fuzzy helper reflog_diff_query "$2")" 32 | BASE="$(extract_commit_hash_from_first_line "$3")" 33 | 34 | if [ "$BASE" == "$REF" ]; then 35 | MERGE_BASE="$(git merge-base "$GF_BASE_BRANCH" "$REF")" 36 | if [ "$(particularly_small_screen)" = '0' ]; then 37 | # shellcheck disable=2086 38 | gf_git_command log $GF_LOG_MENU_PARAMS "$MERGE_BASE..$REF" 39 | echo 40 | fi 41 | # shellcheck disable=2086 42 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS" diff "$MERGE_BASE..$REF" $QUERY | gf_diff_renderer 43 | else 44 | # shellcheck disable=2086 45 | gf_git_command_with_header_default_parameters 1 "$GF_DIFF_COMMIT_RANGE_PREVIEW_DEFAULTS" diff "$BASE" "$REF" $QUERY | gf_diff_renderer 46 | fi 47 | fi 48 | } 49 | -------------------------------------------------------------------------------- /lib/modules/helpers/stash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_helper_stash_menu_content() { 4 | printf "%s%s%s%s%s%s%s\n\n" "$GRAY" "$BOLD" '$ ' "$CYAN" "$BOLD" "git stash list" "$NORMAL" 5 | git stash list 6 | } 7 | 8 | gf_helper_stash_preview_content() { 9 | if [ -z "$1" ]; then 10 | echo "nothing to show" 11 | else 12 | STASH_ID="$(echo "$1" | cut -d':' -f1)" 13 | 14 | git stash show -p "$STASH_ID" | gf_diff_renderer 15 | fi 16 | } 17 | 18 | gf_helper_stash_drop() { 19 | if [ -z "$1" ]; then 20 | echo "nothing to show" 21 | else 22 | STASH_ID="$(echo "$1" | cut -d':' -f1)" 23 | 24 | git stash drop "$STASH_ID" 25 | fi 26 | 27 | } 28 | 29 | gf_helper_stash_pop() { 30 | if [ -z "$1" ]; then 31 | echo "nothing to show" 32 | else 33 | STASH_ID="$(echo "$1" | cut -d':' -f1)" 34 | 35 | git stash pop "$STASH_ID" 36 | fi 37 | 38 | } 39 | 40 | gf_helper_stash_apply() { 41 | if [ -z "$1" ]; then 42 | echo "nothing to show" 43 | else 44 | STASH_ID="$(echo "$1" | cut -d':' -f1)" 45 | 46 | git stash apply "$STASH_ID" 47 | fi 48 | 49 | } 50 | -------------------------------------------------------------------------------- /lib/modules/helpers/status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GF_STATUS_DIRECTORY_PREVIEW_COMMAND='ls -l --color=always' 4 | if type eza >/dev/null 2>&1; then 5 | GF_STATUS_DIRECTORY_PREVIEW_COMMAND='eza -l --color=always' 6 | elif type exa >/dev/null 2>&1; then 7 | GF_STATUS_DIRECTORY_PREVIEW_COMMAND='exa -l --color=always' 8 | elif type lsd >/dev/null 2>&1; then 9 | GF_STATUS_DIRECTORY_PREVIEW_COMMAND='lsd -l --color=always' 10 | fi 11 | 12 | GF_STATUS_FILE_PREVIEW_COMMAND='cat' 13 | if type bat >/dev/null 2>&1; then 14 | GF_STATUS_FILE_PREVIEW_COMMAND='bat --color=always' 15 | fi 16 | 17 | gf_helper_status_preview_content() { 18 | STATUS_CODE="$1" 19 | FILE_PATH="$2" 20 | RENAMED_FILE_PATH="$3" 21 | 22 | # NB: git status will quote paths with whitespace. currently that's not supported 23 | 24 | if [ "??" = "$STATUS_CODE" ]; then 25 | # new files/dirs get special treatment since `git diff` won't handle them 26 | if [ -d "$FILE_PATH" ]; then 27 | # shellcheck disable=2086 28 | gf_command_with_header 2 $GF_STATUS_DIRECTORY_PREVIEW_COMMAND "$FILE_PATH" 29 | else 30 | # shellcheck disable=2086 31 | gf_command_with_header 2 $GF_STATUS_FILE_PREVIEW_COMMAND "$FILE_PATH" 32 | fi 33 | elif [ ! -e "$FILE_PATH" ] && [ -n "$RENAMED_FILE_PATH" ]; then 34 | gf_git_command_with_header 1 diff HEAD -M -- "$FILE_PATH" "$RENAMED_FILE_PATH" | gf_diff_renderer 35 | else 36 | gf_git_command_with_header 1 diff HEAD -M -- "$FILE_PATH" | gf_diff_renderer 37 | fi 38 | } 39 | 40 | gf_helper_status_menu_content() { 41 | gf_git_command_with_header 2 status --short 42 | } 43 | 44 | gf_helper_status_add() { 45 | gf_command_logged git add -- "$@" 46 | } 47 | 48 | gf_helper_status_amend() { 49 | gf_command_logged git commit --amend --reuse-message=HEAD 50 | } 51 | 52 | gf_helper_status_add_patch() { 53 | if [ "$#" = 0 ]; then 54 | gf_log_error 'tried to git add --patch with no file(s)' 55 | else 56 | gf_interactive_command_logged git add --patch -- "$@" < /dev/tty 57 | fi 58 | 59 | # if there's more to commit, loop right back into the status view 60 | if [ -n "$(git status -s)" ]; then 61 | gf_interactive_command_logged git fuzzy status 62 | fi 63 | } 64 | 65 | gf_helper_status_reset() { 66 | gf_command_logged git reset -- "$@" 67 | } 68 | 69 | gf_helper_status_discard() { 70 | if [ "$#" = 0 ]; then 71 | gf_log_error 'tried to CHECKOUT in status with no file(s)' 72 | else 73 | if git ls-files --error-unmatch "$1" > /dev/null 2>&1; then 74 | gf_command_logged git checkout HEAD -- "$@" 75 | else 76 | gf_command_logged rm -rf "$@" 77 | fi 78 | fi 79 | } 80 | 81 | gf_helper_status_edit() { 82 | if [ "$#" = 0 ]; then 83 | gf_log_error 'tried to EDIT in status with no file(s)' 84 | else 85 | # shellcheck disable=2086 86 | gf_interactive_command_logged "$EDITOR" $GF_EDITOR_ARGS "$@" < /dev/tty 87 | fi 88 | } 89 | 90 | gf_helper_status_commit() { 91 | # shellcheck disable=2086 92 | gf_interactive_command_logged git commit 93 | 94 | # if there's more to commit, loop right back into the status view 95 | if [ -n "$(git status -s)" ]; then 96 | gf_interactive_command_logged git fuzzy status 97 | fi 98 | } 99 | -------------------------------------------------------------------------------- /lib/modules/hub/pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=2016 4 | GF_PR_HEADER=' 5 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}diff${NORMAL} ${WHITE}Enter${NORMAL}"' 6 | '"${BOLD}🌐${NORMAL} ${GREEN}web${NORMAL} ${WHITE}Alt-O${NORMAL}"' 7 | '" ${GREEN}log${NORMAL} ${WHITE}Alt-L${NORMAL}"' 8 | 9 | ' 10 | 11 | if [ "$(particularly_small_screen)" = '1' ]; then 12 | GF_PR_HEADER='' 13 | fi 14 | 15 | gf_fzf_pr_select() { 16 | gf_fzf -m 2 \ 17 | --header="$GF_PR_HEADER" \ 18 | --header-lines 2 \ 19 | --preview 'git fuzzy helper pr_preview_content {1}' \ 20 | --bind 'alt-o:execute(git fuzzy helper pr_show {1})' \ 21 | --bind 'alt-l:execute(git fuzzy helper pr_log {1})' \ 22 | --bind 'enter:execute(git fuzzy helper pr_select {1})' 23 | } 24 | 25 | gf_pr() { 26 | if [ "$HUB_AVAILABLE" = 'YES' ]; then 27 | gf_command_logged git fetch "$GF_BASE_REMOTE" 28 | if [ $# -eq 0 ]; then 29 | git fuzzy helper pr_menu_content | gf_fzf_pr_select 30 | elif [[ "$1" =~ [0-9]+ ]]; then 31 | DIFF_PARAMS="$(hub pr show --format='%sB %sH' "$1")" 32 | if [ -z "$DIFF_PARAMS" ]; then 33 | eval "git fuzzy diff $DIFF_PARAMS" 34 | fi 35 | else 36 | gf_log_error '`git fuzzy pr` only accepts one numeric parameter, the PR number' 37 | fi 38 | else 39 | gf_log_error '`hub` not available; this command requires `hub` to be installed' 40 | return 1 41 | fi 42 | } 43 | -------------------------------------------------------------------------------- /lib/modules/log.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_FUZZY_LOG_WORKING_COPY_KEY="${GIT_FUZZY_LOG_WORKING_COPY_KEY:-Ctrl-P}" 4 | GIT_FUZZY_MERGE_BASE_KEY="${GIT_FUZZY_MERGE_BASE_KEY:-Alt-P}" 5 | GIT_FUZZY_LOG_COMMIT_KEY="${GIT_FUZZY_LOG_COMMIT_KEY:-Alt-D}" 6 | 7 | # shellcheck disable=2016 8 | GF_LOG_HEADER=' 9 | '"${YELLOW}|${NORMAL} to split CLI args for ${MAGENTA}git log${NORMAL} vs ${MAGENTA}git diff${NORMAL} ${WHITE}Enter${NORMAL} to ${GREEN}PRINT SHA(s)${NORMAL}"' 10 | '"${YELLOW}*${NORMAL} as prefix to send all content to ${MAGENTA}git log${NORMAL} ${WHITE}${NORMAL} to ${GREEN}SELECT SHA${NORMAL} for ${MAGENTA}diff${NORMAL}"' 11 | '"${YELLOW}#${NORMAL} as prefix to send all content to ${MAGENTA}git diff${NORMAL} ${GRAY}(cannot select >1 SHA)${NORMAL}"' 12 | 13 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}working copy${NORMAL} ${WHITE}$GIT_FUZZY_LOG_WORKING_COPY_KEY${NORMAL} ${GRAY}-- search messages${NORMAL} ${MAGENTA}--grep=Foo${NORMAL}"' 14 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}merge-base${NORMAL} ${WHITE}$GIT_FUZZY_MERGE_BASE_KEY${NORMAL} ${GRAY}-- search patch${NORMAL} ${MAGENTA}-G 'Foo'${NORMAL}"' 15 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}commit${NORMAL} ${WHITE}$GIT_FUZZY_LOG_COMMIT_KEY${NORMAL} ${GRAY}-- customize patch${NORMAL} ${MAGENTA}-G 'Foo' | -W -- foo.c${NORMAL}"' 16 | 17 | ' 18 | 19 | if [ "$(particularly_small_screen)" = '1' ]; then 20 | GF_LOG_HEADER='' 21 | fi 22 | 23 | gf_fzf_log() { 24 | PARAMS_FOR_SUBSTITUTION='' 25 | if [ "$#" -gt 0 ]; then 26 | PARAMS_FOR_SUBSTITUTION="$(quote_params "$@")" 27 | fi 28 | 29 | # shellcheck disable=2016 30 | gf_fzf_one -m \ 31 | --phony \ 32 | --header-lines=2 \ 33 | --header "$GF_LOG_HEADER" \ 34 | --preview 'git fuzzy helper log_preview_content {..} {q} {+..}' \ 35 | --bind "click-header:reload(git fuzzy helper log_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 36 | --bind "backward-eof:reload(git fuzzy helper log_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 37 | --bind "change:reload(git fuzzy helper log_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 38 | --bind "$(lowercase "$GIT_FUZZY_LOG_COMMIT_KEY"):execute(git fuzzy helper log_open_diff commit {..})" \ 39 | --bind "$(lowercase "$GIT_FUZZY_LOG_WORKING_COPY_KEY"):execute(git fuzzy helper log_open_diff working_copy {..})" \ 40 | --bind "$(lowercase "$GIT_FUZZY_MERGE_BASE_KEY")"':execute(git fuzzy helper log_open_diff merge_base {..})' 41 | } 42 | 43 | gf_log() { 44 | # NB: first parameter is the "query", which is empty right now 45 | git fuzzy helper log_menu_content '' "$@" | gf_fzf_log "$@" | gf_fzf_log_line_interpreter 46 | } 47 | -------------------------------------------------------------------------------- /lib/modules/main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_menu_item() { 4 | printf 'choice %s%s%-10s%s %s%s%s' "$MAGENTA" "$BOLD" "$1" "$NORMAL" "$GRAY" "$2" "$NORMAL" 5 | echo 6 | } 7 | 8 | # shellcheck disable=2016 9 | gf_menu_content() { 10 | gf_menu_item 'status' '`status`, `add`, `reset`, and other status-related tools' 11 | echo 12 | gf_menu_item 'branch' 'list branches, `checkout`, `diff`, etc.' 13 | gf_menu_item 'log' 'browse the log and search diffs' 14 | gf_menu_item 'reflog' 'browse the reflog and search diffs' 15 | gf_menu_item 'stash' 'browse stashed changes' 16 | echo 17 | gf_menu_item 'diff' 'compare up to two branches (remote or local)' 18 | 19 | if [ -n "$HUB_AVAILABLE" ]; then 20 | echo 21 | echo "header ${YELLOW}-- 🚧 ${CYAN}${BOLD}GitHub${NORMAL}${YELLOW} 🚧 --${NORMAL}" 22 | echo 23 | gf_menu_item "pr" "browse and see diffs of pull requests" 24 | fi 25 | } 26 | 27 | gf_fzf_main() { 28 | gf_fzf_one \ 29 | "$(hidden_preview_window_settings)" \ 30 | --with-nth=2.. \ 31 | --bind "enter:execute([ {1} = 'choice' ] && git fuzzy interactive {2})" 32 | } 33 | 34 | gf_menu() { 35 | gf_menu_content | gf_fzf_main 36 | } 37 | -------------------------------------------------------------------------------- /lib/modules/reflog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_FUZZY_REFLOG_WORKING_COPY_KEY="${GIT_FUZZY_REFLOG_WORKING_COPY_KEY:-Ctrl-P}" 4 | GIT_FUZZY_REFLOG_MERGE_BASE_KEY="${GIT_FUZZY_REFLOG_MERGE_BASE_KEY:-Alt-P}" 5 | GIT_FUZZY_REFLOG_COMMIT_KEY="${GIT_FUZZY_REFLOG_COMMIT_KEY:-Alt-D}" 6 | 7 | # shellcheck disable=2016 8 | GF_REFLOG_HEADER=' 9 | Use '"${YELLOW}|${NORMAL} to separate CLI args for ${MAGENTA}git reflog${NORMAL} vs ${MAGENTA}git diff${NORMAL}. ${WHITE}Enter${NORMAL} to ${GREEN}ACCEPT${NORMAL}"' 10 | Select an entry with '"${WHITE}${NORMAL}"' to use as basis for comparison in the preview. 11 | 12 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}working copy${NORMAL} ${WHITE}$GIT_FUZZY_REFLOG_WORKING_COPY_KEY${NORMAL} ${GRAY}-- search messages${NORMAL} ${MAGENTA}--grep=Foo${NORMAL}"' 13 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}merge-base${NORMAL} ${WHITE}$GIT_FUZZY_REFLOG_MERGE_BASE_KEY${NORMAL} ${GRAY}-- search patch${NORMAL} ${MAGENTA}-G 'Foo'${NORMAL}"' 14 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}commit${NORMAL} ${WHITE}$GIT_FUZZY_REFLOG_COMMIT_KEY${NORMAL} ${GRAY}-- customize patch${NORMAL} ${MAGENTA}-G 'Foo' | -W -- foo.c${NORMAL}"' 15 | 16 | ' 17 | 18 | if [ "$(particularly_small_screen)" = '1' ]; then 19 | GF_REFLOG_HEADER='' 20 | fi 21 | 22 | gf_fzf_reflog() { 23 | PARAMS_FOR_SUBSTITUTION='' 24 | if [ "$#" -gt 0 ]; then 25 | PARAMS_FOR_SUBSTITUTION="$(quote_params "$@")" 26 | fi 27 | 28 | # shellcheck disable=2016 29 | gf_fzf_one -m \ 30 | --phony \ 31 | --header-lines=2 \ 32 | --header "$GF_REFLOG_HEADER" \ 33 | --preview 'git fuzzy helper reflog_preview_content {1} {q} {+..}' \ 34 | --bind "change:reload(git fuzzy helper reflog_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 35 | --bind "click-header:reload(git fuzzy helper reflog_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 36 | --bind "backward-eof:reload(git fuzzy helper reflog_menu_content {q} $PARAMS_FOR_SUBSTITUTION)" \ 37 | --bind "$(lowercase "$GIT_FUZZY_REFLOG_COMMIT_KEY"):execute(git fuzzy diff {1}^ {1})" \ 38 | --bind "$(lowercase "$GIT_FUZZY_REFLOG_WORKING_COPY_KEY"):execute(git fuzzy diff {1})" \ 39 | --bind "$(lowercase "$GIT_FUZZY_REFLOG_MERGE_BASE_KEY"):"'execute(git fuzzy diff "$(git merge-base "'"$GF_BASE_BRANCH"'" {1})" {1})' 40 | } 41 | 42 | gf_reflog() { 43 | # NB: first parameter is the "query", which is empty right now 44 | git fuzzy helper reflog_menu_content '' "$@" | gf_fzf_reflog "$@" | gf_fzf_log_line_interpreter 45 | } 46 | -------------------------------------------------------------------------------- /lib/modules/stash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GIT_FUZZY_DROP_KEY="${GIT_FUZZY_DROP_KEY:-Alt-D}" 4 | GIT_FUZZY_POP_KEY="${GIT_FUZZY_POP_KEY:-Alt-P}" 5 | GIT_FUZZY_APPLY_KEY="${GIT_FUZZY_APPLY_KEY:-Alt-A}" 6 | 7 | # shellcheck disable=2016 8 | GF_STASH_HEADER=' 9 | '"${WHITE}Enter${NORMAL} to ${GREEN}QUIT${NORMAL}"' 10 | 11 | '"${YELLOW}${BOLD}∆${NORMAL} ${GREEN}drop${NORMAL} ${WHITE}$GIT_FUZZY_DROP_KEY${NORMAL} ${GRAY}-- drop the selected stash${NORMAL}"' 12 | '"${YELLOW}${BOLD}⇧${NORMAL} ${GREEN}pop ${NORMAL} ${WHITE}$GIT_FUZZY_POP_KEY${NORMAL} ${GRAY}-- pops the selected stash${NORMAL}"' 13 | '"${GREEN}${BOLD}⇧${NORMAL} ${GREEN}apply${NORMAL} ${WHITE}$GIT_FUZZY_APPLY_KEY${NORMAL} ${GRAY}-- applies the selected stash${NORMAL}"' 14 | 15 | ' 16 | 17 | if [ "$(particularly_small_screen)" = '1' ]; then 18 | GF_STASH_HEADER='' 19 | fi 20 | 21 | gf_fzf_stash() { 22 | gf_fzf_one -m \ 23 | --header-lines=2 \ 24 | --header "$GF_STASH_HEADER" \ 25 | --preview 'git fuzzy helper stash_preview_content {1}' \ 26 | --bind 'click-header:reload(git fuzzy helper stash_menu_content '"$(quote_params "$@")"')' \ 27 | --bind 'backward-eof:reload(git fuzzy helper stash_menu_content '"$(quote_params "$@")"')' \ 28 | --bind "$(lowercase "$GIT_FUZZY_DROP_KEY"):execute(git fuzzy helper stash_drop {1})+reload(git fuzzy helper stash_menu_content)" \ 29 | --bind "$(lowercase "$GIT_FUZZY_POP_KEY"):execute(git fuzzy helper stash_pop {1})+reload(git fuzzy helper stash_menu_content)" \ 30 | --bind "$(lowercase "$GIT_FUZZY_APPLY_KEY"):execute(git fuzzy helper stash_apply {1})+reload(git fuzzy helper stash_menu_content)" 31 | } 32 | 33 | gf_stash() { 34 | # NB: first parameter is the "query", which is empty right now 35 | git fuzzy helper stash_menu_content "$@" | gf_fzf_stash "$@" 36 | } 37 | -------------------------------------------------------------------------------- /lib/modules/status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=2016 3 | 4 | GIT_FUZZY_STATUS_AMEND_KEY="${GIT_FUZZY_STATUS_AMEND_KEY:-Alt-M}" 5 | GIT_FUZZY_STATUS_ADD_KEY="${GIT_FUZZY_STATUS_ADD_KEY:-Alt-S}" 6 | GIT_FUZZY_STATUS_ADD_PATCH_KEY="${GIT_FUZZY_STATUS_ADD_PATCH_KEY:-Alt-P}" 7 | GIT_FUZZY_STATUS_EDIT_KEY="${GIT_FUZZY_STATUS_EDIT_KEY:-Alt-E}" 8 | GIT_FUZZY_STATUS_COMMIT_KEY="${GIT_FUZZY_STATUS_COMMIT_KEY:-Alt-C}" 9 | GIT_FUZZY_STATUS_RESET_KEY="${GIT_FUZZY_STATUS_RESET_KEY:-Alt-R}" 10 | GIT_FUZZY_STATUS_DISCARD_KEY="${GIT_FUZZY_STATUS_DISCARD_KEY:-Alt-U}" 11 | 12 | GF_STATUS_HEADER=' 13 | Type to filter. '"${WHITE}Enter${NORMAL} to ${GREEN}ACCEPT${NORMAL}"' 14 | 15 | '"${GRAY}-- (${NORMAL}*${GRAY}) editor: ${MAGENTA}${EDITOR} ${NORMAL}${GF_EDITOR_ARGS}${NORMAL}"' 16 | '""' 17 | '"${GREEN}amend ✁${NORMAL} ${WHITE}${GIT_FUZZY_STATUS_AMEND_KEY}${NORMAL} ${GREEN}stage -p ${BOLD}⇡ ${NORMAL}${WHITE}${GIT_FUZZY_STATUS_ADD_PATCH_KEY}${NORMAL} * ${GREEN}${BOLD}edit ✎${NORMAL} ${WHITE}$GIT_FUZZY_STATUS_EDIT_KEY${NORMAL}"' 18 | '"${GREEN}all ☑${NORMAL} ${WHITE}${GIT_FUZZY_SELECT_ALL_KEY}${NORMAL} ${GREEN}stage ${BOLD}⇡${NORMAL} ${WHITE}$GIT_FUZZY_STATUS_ADD_KEY${NORMAL} ${RED}${BOLD}discard ✗${NORMAL} ${WHITE}$GIT_FUZZY_STATUS_DISCARD_KEY${NORMAL}"' 19 | '"${GREEN}none ☐${NORMAL} ${WHITE}${GIT_FUZZY_SELECT_NONE_KEY}${NORMAL} ${GREEN}reset ${RED}${BOLD}⇣${NORMAL} ${WHITE}$GIT_FUZZY_STATUS_RESET_KEY${NORMAL} * ${RED}${BOLD}commit ${NORMAL}${RED}⇧${NORMAL} ${WHITE}$GIT_FUZZY_STATUS_COMMIT_KEY${NORMAL}"' 20 | 21 | ' 22 | 23 | if [ "$(particularly_small_screen)" = '1' ]; then 24 | GF_STATUS_HEADER='' 25 | fi 26 | 27 | gf_fzf_status() { 28 | RELOAD="reload:git fuzzy helper status_menu_content" 29 | 30 | gf_fzf -m --header "$GF_STATUS_HEADER" \ 31 | --header-lines=2 \ 32 | --expect="$(lowercase "$GIT_FUZZY_STATUS_EDIT_KEY"),$(lowercase "$GIT_FUZZY_STATUS_COMMIT_KEY"),$(lowercase "$GIT_FUZZY_STATUS_ADD_PATCH_KEY")" \ 33 | --nth=2 \ 34 | --preview 'git fuzzy helper status_preview_content {1} {2} {4}' \ 35 | --bind 'click-header:reload(git fuzzy helper status_menu_content)' \ 36 | --bind 'backward-eof:reload(git fuzzy helper status_menu_content)' \ 37 | --bind "$(lowercase "$GIT_FUZZY_STATUS_AMEND_KEY"):execute-silent(git fuzzy helper status_amend {+2..})+down+$RELOAD" \ 38 | --bind "$(lowercase "$GIT_FUZZY_STATUS_ADD_KEY"):execute-silent(git fuzzy helper status_add {+2..})+down+$RELOAD" \ 39 | --bind "$(lowercase "$GIT_FUZZY_STATUS_RESET_KEY"):execute-silent(git fuzzy helper status_reset {+2..})+down+$RELOAD" \ 40 | --bind "$(lowercase "$GIT_FUZZY_STATUS_DISCARD_KEY"):execute-silent(git fuzzy helper status_discard {+2..})+$RELOAD" 41 | } 42 | 43 | gf_status_interpreter() { 44 | CONTENT="$(cat -)" 45 | HEAD="$(echo "$CONTENT" | head -n1)" 46 | TAIL="$(echo "$CONTENT" | tail -n +2)" 47 | 48 | if [ "$(lowercase "$HEAD")" = "$(lowercase "$GIT_FUZZY_STATUS_EDIT_KEY")" ]; then 49 | local selected_file=$(echo "$TAIL" | cut -c4- | join_lines_quoted) 50 | eval "git fuzzy helper status_edit $selected_file" 51 | elif [ "$(lowercase "$HEAD")" = "$(lowercase "$GIT_FUZZY_STATUS_COMMIT_KEY")" ]; then 52 | eval "git fuzzy helper status_commit" 53 | elif [ "$(lowercase "$HEAD")" = "$(lowercase "$GIT_FUZZY_STATUS_ADD_PATCH_KEY")" ]; then 54 | local selected_file=$(echo "$TAIL" | cut -c4- | join_lines_quoted) 55 | eval "git fuzzy helper status_add_patch $selected_file" 56 | else 57 | echo "$TAIL" | cut -c4- 58 | fi 59 | } 60 | 61 | gf_status() { 62 | gf_snapshot "status" 63 | if [ -n "$(git status -s)" ]; then 64 | # shellcheck disable=2086 65 | git fuzzy helper status_menu_content | gf_fzf_status | gf_status_interpreter 66 | else 67 | gf_log_debug "nothing to commit, working tree clean" 68 | exit 1 69 | fi 70 | } 71 | -------------------------------------------------------------------------------- /lib/patch-index.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # Array utilities 4 | 5 | function push(A,B) { 6 | A[length(A)] = B; 7 | } 8 | 9 | function new_array(A) { 10 | split("", A); 11 | } 12 | 13 | function set_file_name() { 14 | if (preamble[1] ~ /^--- \/dev\/null$/) { 15 | file_name = ENVIRON["GREEN"] substr(preamble[2], 7) ENVIRON["NORMAL"] 16 | } else if (preamble[2] ~ /^\+\+\+ \/dev\/null$/) { 17 | file_name = ENVIRON["RED"] substr(preamble[1], 7) ENVIRON["NORMAL"] 18 | } else { 19 | remove_file_name = substr(preamble[1], 7) 20 | add_file_name = substr(preamble[2], 7) 21 | if (remove_file_name == add_file_name) { 22 | file_name = ENVIRON["YELLOW"] add_file_name ENVIRON["NORMAL"] 23 | } else { 24 | file_name = ENVIRON["RED"] remove_file_name ENVIRON["GRAY"] " -> " ENVIRON["GREEN"] add_file_name ENVIRON["NORMAL"] 25 | } 26 | } 27 | } 28 | 29 | # hunk management 30 | 31 | function print_hunk_line() { 32 | if (preamble[1] ~ /^--- \/dev\/null$/) { 33 | file_marker = ENVIRON["GREEN"] ENVIRON["BOLD"] "+++" ENVIRON["NORMAL"]; 34 | } else if (preamble[2] ~ /^\+\+\+ \/dev\/null$/) { 35 | file_marker = ENVIRON["RED"] ENVIRON["BOLD"] "---" ENVIRON["NORMAL"]; 36 | } else { 37 | file_marker = ENVIRON["YELLOW"] ENVIRON["BOLD"] "+/-" ENVIRON["NORMAL"]; 38 | } 39 | 40 | file_hunk_number_str = ENVIRON["GRAY"] "#" ENVIRON["MAGENTA"] ENVIRON["BOLD"] file_hunk_number ENVIRON["NORMAL"] 41 | 42 | set_file_name() 43 | printf("%s %s %s %s\n", hunk_number, file_marker, file_hunk_number_str, file_name); 44 | 45 | file_hunk_number ++; 46 | hunk_number ++; 47 | } 48 | 49 | BEGIN { 50 | hunk_number = 0; 51 | } 52 | 53 | /^diff --git/ { 54 | file_hunk_number = 1; 55 | new_array(preamble); 56 | push(preamble, $0); 57 | } 58 | 59 | /^---/ { 60 | push(preamble, $0); 61 | } 62 | 63 | /^\+\+\+/ { 64 | push(preamble, $0); 65 | } 66 | 67 | /^@@/ { 68 | print_hunk_line(); 69 | } 70 | -------------------------------------------------------------------------------- /lib/patch-selector.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # Array utilities 4 | 5 | function push(A,B) { 6 | idx = length(A); 7 | A[idx] = B; 8 | } 9 | 10 | function new_array(A) { 11 | split("", A); 12 | } 13 | 14 | # hunk management 15 | 16 | function new_hunk() { 17 | if (length(hunk) > 0) { 18 | if (hunk_number == desired_hunk_number) { 19 | for (preamble_index = 0; preamble_index < length(preamble); preamble_index ++) { 20 | print preamble[preamble_index]; 21 | } 22 | for (hunk_index = 0; hunk_index < length(hunk); hunk_index ++) { 23 | print hunk[hunk_index]; 24 | } 25 | } 26 | hunk_number++; 27 | } 28 | 29 | new_array(hunk); 30 | } 31 | 32 | BEGIN { 33 | hunk_number = 0; 34 | } 35 | 36 | # diff marker detection 37 | /^diff --git/ { 38 | new_hunk() 39 | in_hunk = 0; 40 | new_array(preamble); 41 | push(preamble, $0); 42 | } 43 | 44 | /^---/ { 45 | in_hunk = 0 46 | push(preamble, $0); 47 | } 48 | 49 | /^\+\+\+/ { 50 | in_hunk = 0 51 | push(preamble, $0); 52 | } 53 | 54 | /^@@/ { 55 | new_hunk() 56 | in_hunk = 1; 57 | } 58 | 59 | # treatment for every line 60 | { 61 | if (in_hunk) { 62 | push(hunk, $0); 63 | } 64 | } 65 | 66 | # ensure we print last hunk 67 | END { 68 | new_hunk(); 69 | } 70 | -------------------------------------------------------------------------------- /lib/snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gf_snapshot() { 4 | if [ -n "$GF_SNAPSHOT_DIRECTORY" ]; then 5 | NAME="snapshot" 6 | if [ -z "$1" ]; then 7 | NAME="$1" 8 | fi 9 | if [ ! -d "$GF_SNAPSHOT_DIRECTORY" ]; then 10 | mkdir -p "$GF_SNAPSHOT_DIRECTORY" 11 | fi 12 | if [ ! -w "$GF_SNAPSHOT_DIRECTORY" ]; then 13 | gf_log_error "GF_SNAPSHOT_DIRECTORY not writeable" 14 | exit 1 15 | fi 16 | 17 | GF_SNAPSHOT_LOCATION="$(cd "$GF_SNAPSHOT_DIRECTORY" && pwd)" 18 | 19 | NOW="$(date +'%Y-%m-%d-%H-%M-%S')" 20 | 21 | NOW_DIR="$GF_SNAPSHOT_LOCATION/$NAME-$NOW" 22 | 23 | if [ ! -d "$NOW_DIR" ]; then 24 | mkdir "$NOW_DIR" 25 | 26 | if git rev-parse HEAD >/dev/null 2>&1 ; then 27 | git rev-parse HEAD > "$NOW_DIR/HEAD" 28 | git rev-parse --abbrev-ref HEAD > "$NOW_DIR/HEAD-branch" 29 | else 30 | echo 'no HEAD' > "$NOW_DIR/HEAD" 31 | echo 'no HEAD' > "$NOW_DIR/HEAD-branch" 32 | fi 33 | 34 | git diff > "$NOW_DIR/working-copy" 35 | git diff --staged > "$NOW_DIR/index" 36 | 37 | mkdir "$NOW_DIR/new" 38 | 39 | # NB: creates appropriate directory structure and only copies files in the list 40 | rsync --files-from <(git status --short | grep '^??' | cut -c4-) . "$NOW_DIR/new/" 41 | fi 42 | fi 43 | } 44 | -------------------------------------------------------------------------------- /lib/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DARK_GRAY="${DARK_GRAY:-$(tput setaf 0)}" 4 | RED="${RED:-$(tput setaf 1)}" 5 | GREEN="${GREEN:-$(tput setaf 2)}" 6 | YELLOW="${YELLOW:-$(tput setaf 3)}" 7 | BLUE="${BLUE:-$(tput setaf 4)}" 8 | MAGENTA="${MAGENTA:-$(tput setaf 5)}" 9 | CYAN="${CYAN:-$(tput setaf 6)}" 10 | WHITE="${WHITE:-$(tput setaf 7)}" 11 | GRAY="${GRAY:-$(tput setaf 8)}" 12 | BOLD="${BOLD:-$(tput bold)}" 13 | UNDERLINE="${UNDERLINE:-$(tput sgr 0 1)}" 14 | INVERT="${INVERT:-$(tput sgr 1 0)}" 15 | NORMAL="${NORMAL:-$(tput sgr0)}" 16 | 17 | export DARK_GRAY 18 | export RED 19 | export GREEN 20 | export YELLOW 21 | export BLUE 22 | export MAGENTA 23 | export CYAN 24 | export WHITE 25 | export GRAY 26 | export BOLD 27 | export UNDERLINE 28 | export INVERT 29 | export NORMAL 30 | 31 | # Mispellings 32 | export DARKGRAY="$DARK_GRAY" 33 | export DARK_GREY="$DARK_GRAY" 34 | export DARKGREY="$DARK_GRAY" 35 | export GREY="$GRAY" 36 | 37 | quote_params() { 38 | if [ "$#" -eq 0 ]; then 39 | printf '' 40 | else 41 | printf '%q ' "$@" 42 | fi 43 | } 44 | 45 | lowercase() { 46 | echo "$1" | tr '[:upper:]' '[:lower:]' 47 | } 48 | 49 | join_lines_quoted() { 50 | IFS=$'\r\n' eval 'LINES_TO_BE_QUOTED=($(cat -))' 51 | 52 | if [ "${#LINES_TO_BE_QUOTED[@]}" -gt 0 ]; then 53 | printf ' %q' "${LINES_TO_BE_QUOTED[@]}" 54 | else 55 | printf '' 56 | fi 57 | } 58 | 59 | extract_commit_hash_from_first_line() { 60 | # shellcheck disable=2001 61 | echo "$1" | awk '{ 62 | for(i=1; i<=NF; i++) { 63 | if(match($i, /^[0-9a-f]{7,40}$/)){ print $i } 64 | } 65 | }' 66 | } 67 | --------------------------------------------------------------------------------