├── .github └── workflows │ ├── main.yml │ └── shellcheck.yml ├── CHANGELOG.md ├── COPYING.md ├── Makefile ├── README.md ├── completion.fish ├── completion.zsh └── git-fixup /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 'v*' 4 | 5 | jobs: 6 | homebrew: 7 | name: Bump Homebrew formula 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: mislav/bump-homebrew-formula-action@v3 11 | with: 12 | # A PR will be sent to github.com/Homebrew/homebrew-core to update this formula: 13 | formula-name: git-fixup 14 | env: 15 | COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: ShellCheck 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | shellcheck: 11 | name: Shellcheck 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Run ShellCheck 16 | uses: ludeeus/action-shellcheck@master 17 | with: 18 | ignore_paths: ./completion.* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v1.6.0 / 2024-05-20 4 | - [#71](https://github.com/keis/git-fixup/pull/71) Install zsh completion with mode 644, add `install-fish` target (@mattst88) 5 | - [#70](https://github.com/keis/git-fixup/pull/70) Fail script if git-commit fails (@keis) 6 | - [#66](https://github.com/keis/git-fixup/pull/66) Add --all option (@jerome-reybert-tiempo) 7 | - [#67](https://github.com/keis/git-fixup/pull/67) Add --amend option (@guludo) 8 | - [#64](https://github.com/keis/git-fixup/pull/64) Fix tab completion for revisions in base parameter (@pe) 9 | - [#63](https://github.com/keis/git-fixup/pull/63) Remove filenames in tab-completion in fish (@pe) 10 | - [#62](https://github.com/keis/git-fixup/pull/62) Add short option -n for --no-verify (@bbannier) 11 | - [#61](https://github.com/keis/git-fixup/pull/61) Fix default menu markdown link (@glensc) 12 | 13 | ### v1.5.0 / 2022-05-24 14 | - [#60](https://github.com/keis/git-fixup/pull/60) Add new options to completion scripts (@keis) 15 | - [#59](https://github.com/keis/git-fixup/pull/59) Add `--rebase` option that calls rebase after commit (@FdelMazo) 16 | - [#57](https://github.com/keis/git-fixup/pull/57) Recover lost `git fixup ` functionality (@FdelMazo) 17 | - [#56](https://github.com/keis/git-fixup/pull/56) Add --base option (#56) (@guludo) 18 | - [#55](https://github.com/keis/git-fixup/pull/55) Use the option parsing plumbing of git-sh-setup (@keis) 19 | - [#54](https://github.com/keis/git-fixup/pull/54) No-verify: doc and completion (@pe) 20 | 21 | ### v1.4.0 / 2021-08-25 22 | - [#52](https://github.com/keis/git-fixup/pull/52) Add --no-verify option to pass to git commit (@glensc) 23 | - [#50](https://github.com/keis/git-fixup/pull/50) Add tab completion for the fish shell (@pe) 24 | - [#49](https://github.com/keis/git-fixup/pull/49) replace readarray (@pe) 25 | - [#47](https://github.com/keis/git-fixup/pull/47) Make the various options and config behave consistently (@keis) 26 | 27 | ### v1.3.0 / 2020-02-14 28 | - [#45](https://github.com/keis/git-fixup/pull/45) Use bash from PATH for brew (@glensc) 29 | - [#43](https://github.com/keis/git-fixup/pull/43) Updates to zsh completion (@aschrab) 30 | 31 | ### v1.2.0 / 2019-02-28 32 | - [#39](https://github.com/keis/git-fixup/pull/39) Add the --commit option (@guludo) 33 | 34 | ### v1.1.2 / 2018-04-26 35 | - [#36](https://github.com/keis/git-fixup/pull/36) Fix completion for Zsh 5.3 (@eigengrau) 36 | - [#34](https://github.com/keis/git-fixup/pull/34) Don’t use --invert-grep. (@mcepl) 37 | 38 | ### v1.1.1 / 2016-08-11 39 | - [#31](https://github.com/keis/git-fixup/pull/31) Use DESTDIR in Makefile (@Shir0kamii) 40 | - [#28](https://github.com/keis/git-fixup/pull/28) Don't use symmetrical rev range (@keis) 41 | 42 | ### v1.1.0 / 2015-12-22 43 | - [#27](https://github.com/keis/git-fixup/pull/27) Exclude other fixups from recent commits (@keis) 44 | - [#25](https://github.com/keis/git-fixup/pull/25) update zsh completion script (@keis) 45 | - [#24](https://github.com/keis/git-fixup/pull/24) add support for squashing with the `-s` or `--squash` params (@joeshaw) 46 | 47 | ### v1.0.2 / 2015-04-09 48 | - [#22](https://github.com/keis/git-fixup/pull/22) add ISC license text (@keis) 49 | - [#20](https://github.com/keis/git-fixup/pull/20) redirect error message to stderr (@keis) 50 | - [#17](https://github.com/keis/git-fixup/pull/17) Add screenshot to README (@fixe) 51 | 52 | ### v1.0.1 / 2015-02-26 53 | - [#16](https://github.com/keis/git-fixup/pull/16) When installing, create any missing parent dirs. (@Josh-Tilles) 54 | - [#15](https://github.com/keis/git-fixup/pull/15) Add brew install information to README (@nunofgs) 55 | 56 | ### v1.0.0 / 2015-02-24 57 | - [#13](https://github.com/keis/git-fixup/pull/13) no pager on git log please (@keis) 58 | - [#12](https://github.com/keis/git-fixup/pull/12) fallback to all commits when rev range is empty (@keis) 59 | - [#10](https://github.com/keis/git-fixup/pull/10) Update README (@fixe) 60 | - [#8](https://github.com/keis/git-fixup/pull/8) Fix git blame file usage (@fixe) 61 | - [#6](https://github.com/keis/git-fixup/pull/6) Simplify how the commit message is fetched (@fixe) 62 | - [#4](https://github.com/keis/git-fixup/pull/4) git-fixup improvements (@cgiuffr) 63 | - [#3](https://github.com/keis/git-fixup/pull/3) cd to toplevel before doing git blame (@keis) 64 | - [#1](https://github.com/keis/git-fixup/pull/1) Create dir before installing to it (@alde) 65 | -------------------------------------------------------------------------------- /COPYING.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, David Keijser 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | INSTALLDIR?=$(PREFIX) 3 | INSTALL=install 4 | 5 | install: 6 | ${INSTALL} -d ${DESTDIR}${INSTALLDIR}/bin 7 | ${INSTALL} -m755 git-fixup ${DESTDIR}${INSTALLDIR}/bin/git-fixup 8 | 9 | install-fish: 10 | ${INSTALL} -d ${DESTDIR}${INSTALLDIR}/share/fish/vendor_completions.d/ 11 | ${INSTALL} -m644 completion.fish ${DESTDIR}${INSTALLDIR}/share/fish/vendor_completions.d/git-fixup.fish 12 | 13 | install-zsh: 14 | ${INSTALL} -d ${DESTDIR}${INSTALLDIR}/share/zsh/site-functions 15 | ${INSTALL} -m644 completion.zsh ${DESTDIR}${INSTALLDIR}/share/zsh/site-functions/_git-fixup 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-fixup 2 | 3 | Fighting the copy-paste element of your rebase workflow. 4 | 5 | `git fixup ` is simply an alias for `git commit --fixup `. That's 6 | just a convenience feature that can be also be used to trigger tab completion. 7 | 8 | The magic is in plain `git fixup` without any arguments. It finds which 9 | lines/files you have changed, uses git blame/log to find the most recent commits 10 | that touched those lines/files, and displays a list for you to pick from. This 11 | is a convenient alternative to manually searching through the commit log and 12 | copy-pasting the commit hash. 13 | 14 | git fixup 15 | 16 | ## Install 17 | 18 | On **OS X** you can install this script with _homebrew_ 19 | 20 | brew install git-fixup 21 | 22 | On **Arch linux** you can install from AUR using _yaourt_ or a similar tool 23 | 24 | yaourt git-fixup 25 | 26 | For most other systems (as long as they include `install` and `make`) you can 27 | install by cloning this repo and running make 28 | 29 | git clone https://github.com/keis/git-fixup.git 30 | cd git-fixup 31 | make install 32 | make install-zsh 33 | 34 | Or if you don't want to deal with any of that you can simply download the 35 | scripts in anyway you like and make sure to put the program and completion 36 | script into your `$PATH` and `$fpath` respectively. 37 | 38 | ## Usage 39 | 40 | ``` 41 | git-fixup [-s|--squash] [-f|--fixup] [-a|--amend] [-c|--commit] [--no-verify] 42 | [--rebase] [-b|--base ] [-r|--reverse] [] 43 | ``` 44 | 45 | For this tool to make any sense you should enable the `rebase.autosquash` 46 | setting in the git config, or use the `--rebase` option. 47 | 48 | 49 | ```bash 50 | # Select the changes that should be part of the fixup. 51 | $ git add -p 52 | 53 | # Output a list of commits that the staged changes are likely a fixup of. 54 | $ git fixup 55 | 56 | # Create a fixup!- of the given ref. If you have installed the zsh script 57 | # you can cycle through the list of fixup candidates with tab completion. 58 | $ git fixup 59 | 60 | # Commit rebased into the selected commit as a fixup. 61 | $ git rebase -i ... 62 | ``` 63 | 64 | ## Options 65 | 66 | ### -s, --squash 67 | 68 | Instruct `git-fixup` to create a `squash!` commit instead of a `fixup!` commit. 69 | 70 | Squashing gives you the opportunity to edit the commit message before 71 | the commits are squashed together. 72 | 73 | Default action can be configured by setting [fixup.action](#fixupaction) 74 | 75 | ### -f, --fixup 76 | 77 | Instruct `git-fixup` to create `fixup!` commit (This is the default). 78 | 79 | Default action can be configured by setting [fixup.action](#fixupaction) 80 | 81 | ### -a, --amend 82 | 83 | Instruct `git-fixup` to create an `amend!` commit. 84 | 85 | Default action can be configured by setting [fixup.action](#fixupaction) 86 | 87 | ### -c, --commit 88 | 89 | Instead of listing the suggested commits show a menu to pick a commit to 90 | create a fixup/squash commit of. 91 | 92 | A [default menu](#the-default-menu) is provided that is intentionally very 93 | simple and with no advanced features. Instead of using it you can tell `git 94 | fixup` to use an external tool for the menu by defining a command line via 95 | either the [fixup.menu](#fixupmenu) setting in the git config or the `GITFIXUPMENU` 96 | environment variable (the latter overrides the former). 97 | 98 | ```bash 99 | # Use fzf as a menu program 100 | $ GITFIXUPMENU=fzf git fixup -c 101 | ``` 102 | 103 | This option can be enabled by default by setting [fixup.commit](#fixupcommit) 104 | in the git config. 105 | 106 | ### --no-commit 107 | 108 | Don't show the commit menu even if previously instructed to do so. 109 | 110 | ### --rebase 111 | 112 | Call an interactive rebase right after the commit is created, to automatically apply the 113 | fix-up into the target commit. This is merely to avoid doing two commands one after the 114 | other (`git fixup && git rebase`). 115 | 116 | This simply calls `git rebase --interactive --autosquash target~1`, with the target being the 117 | commit to fix-up. 118 | 119 | Default rebase/no-rebase can be configured by setting [fixup.rebase](#fixuprebase) 120 | 121 | ### --no-rebase 122 | 123 | Don't do a rebase even if previously instructed to do so (useful to bypass [fixup.rebase](#fixuprebase)) 124 | 125 | ### --no-verify 126 | 127 | Bypass the pre-commit and commit-msg hooks. (see `git help commit`) 128 | 129 | 130 | ### --base 131 | 132 | This option receives as argument the revision to be used as base commit for 133 | the search of fixup/squash candidates. You can use anything that resolves to a 134 | commit. The special value `closest` resolves to the closest ancestor branch of 135 | the current head. 136 | 137 | If omitted, the default base commit is resolved in the following order: 138 | 139 | 1. The value of the environment variable `GITFIXUPBASE` if present; 140 | 2. The value of the configuration key `fixup.base` if present; 141 | 3. The branch configured as upstream of the current one (i.e. `@{upstream}`) 142 | if existing; 143 | 4. Finally, the root commit (i.e. full history) if nothing of the above is 144 | satisfied. 145 | 146 | ### --reverse 147 | 148 | Commits are sorted by time. `-r` reverses the sort order. 149 | 150 | ## Configuration 151 | 152 | `git-fixup` uses configuration from the ENVIRONMENT or from `git config` 153 | 154 | ### fixup.base 155 | 156 | Or `GITFIXUPBASE` 157 | 158 | The default argument for `--base`. You can set the value `closest` to make 159 | `git-fixup` use the closest ancestor branch by default, for example. 160 | 161 | ### fixup.action 162 | 163 | Or `GITFIXUPACTION` 164 | 165 | Decides if the default actions will be `fixup` or `squash`. 166 | 167 | ### fixup.commit 168 | 169 | Or `GITFIXUPCOMMIT` 170 | 171 | Decides if the commit menu should be displayed instead of the commit list by 172 | default. 173 | 174 | ```bash 175 | # Enable --commit for all my projects 176 | $ git config --global fixup.commit true 177 | ``` 178 | 179 | ### fixup.rebase 180 | 181 | Or `GITFIXUPREBASE` 182 | 183 | Decides if `git rebase` should be called right after the `git commit` call. 184 | 185 | ```bash 186 | # Enable --rebase for all my projects 187 | $ git config --global fixup.rebase true 188 | ``` 189 | 190 | ### fixup.menu 191 | 192 | Or `GITFIXUPMENU` 193 | 194 | Sets the command that will be used to display the commit menu. If not set 195 | a simple [default menu](the-default-menu) will be used. 196 | 197 | See [External menu](#external-menu) for more details and a more advanced 198 | example. 199 | 200 | ### fixup.additionalSortFlags 201 | 202 | Or `GITFIXUPADDITIONALSORTFLAGS` 203 | 204 | Sets the flags that are passed to sort in addition to the default sort flags 205 | that enable sorting by time. 206 | 207 | For example, 208 | 209 | ```bash 210 | # Always sort the commits by time reversed 211 | $ git config --global fixup.additionalSortFlags '-r' 212 | ``` 213 | 214 | ## Tab completion 215 | 216 | Tab completion for zsh/fish is implemented. The suggestions for the tab completion 217 | are the suggested fixup bases as generated by running the tool without any 218 | arguments. 219 | 220 | To be able to tab complete the command itself add a line like this to your zsh 221 | configuration:: 222 | 223 | zstyle ':completion:*:*:git:*' user-commands fixup:'Create a fixup commit' 224 | 225 | 226 | ## External menu 227 | 228 | In order to use an external tool for display the commit menu, you need to 229 | either define the [fixup.menu](#fixupmenu) setting in the git config or set the 230 | `GITFIXUPMENU` environment variable with the command for the menu. The menu 231 | command must receive as input the lines as the options for the user and return 232 | the selected line to the standard output. 233 | 234 | The following example is a fragment of a git config that makes `git fixup 235 | --commit` display a nice menu with [fzf](https://github.com/junegunn/fzf): 236 | 237 | ```ini 238 | [fixup] 239 | menu = fzf --height '60%' \ 240 | --bind 'tab:toggle-preview' \ 241 | --preview 'git show --color {+1}' \ 242 | --preview-window=up:80% \ 243 | --prompt 'Select commit: ' 244 | ``` 245 | 246 | ## The default menu 247 | 248 | If you have not configured an external menu, the default menu is used. See the 249 | example below: 250 | 251 | ```bash 252 | $ git fixup -c 253 | 1) 500be603c66040dd8a9ca18832d6221c00e96184 [F] Add README.md 254 | 2) ddab3b03da529af5303531a3d4127e3663063e08 [F] Add index.js 255 | Which commit should I fixup? 256 | ``` 257 | 258 | Here `` should be the number of the desired commit in the list. 259 | You can use `q` to abort the operation and `h` to see a help message for the 260 | menu. 261 | 262 | If the commit title alone is not enough for you to decide, you can use `show 263 | ` to call `git show` on the ``-th commit of the menu. 264 | 265 | ## Changelog 266 | 267 | See [CHANGELOG.md](CHANGELOG.md) 268 | 269 | ## Authors 270 | 271 | The fine people who have contributed to this script in ASCIIbetical order. 272 | 273 | - Cristiano Giuffrida ([cgiuffr](https://github.com/cgiuffr)) 274 | - David Keijser ([keis](https://github.com/keis)) 275 | - Elan Ruusamäe ([glensc](https://github.com/glensc)) 276 | - Federico del Mazo ([FdelMazo](https://github.com/FdelMazo)) 277 | - Gustavo Sousa ([guludo](https://github.com/guludo)) 278 | - Joe Shaw ([joeshaw](https://github.com/joeshaw)) 279 | - Martin Imre ([mimre25](https://github.com/mimre25)) 280 | - Matěj Cepl ([mcepl](https://github.com/mcepl)) 281 | - Philippe ([pe](https://github.com/pe)) 282 | - Rickard Dybeck ([alde](https://github.com/alde)) 283 | - Tiago Ribeiro ([fixe](https://github.com/fixe)) 284 | -------------------------------------------------------------------------------- /completion.fish: -------------------------------------------------------------------------------- 1 | # fish completion for git fixup 2 | 3 | function __fish_git_fixup_target 4 | git-fixup --no-commit --no-rebase 2>/dev/null | string replace -r '^([0-9a-f]{10})[0-9a-f]* (.*)' '$1\t$2' 5 | end 6 | 7 | complete -c git-fixup -s s -l squash -f -d 'Create a squash commit rather than a fixup' 8 | complete -c git-fixup -s c -l commit -f -d 'Show a menu to pick a commit' 9 | complete -c git-fixup -l no-commit -f -d 'Don\'t show a menu to pick a commit' 10 | complete -c git-fixup -s n -l no-verify -f -d 'Bypass the pre-commit and commit-msg hooks' 11 | complete -c git-fixup -l rebase -f -d 'Do a rebase after commit' 12 | complete -c git-fixup -l no-rebase -f -d 'Don\'t do a rebase after commit' 13 | complete -c git-fixup -s b -l base -x -d 'Use as base of the revision range for the search]' -a '(__fish_git_refs)' 14 | complete -c git-fixup -s A -l all -x -d 'Show all candidates' 15 | complete -c git-fixup -f -k -a '(__fish_git_fixup_target)' 16 | -------------------------------------------------------------------------------- /completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef git-fixup 2 | #description create a fixup commit 3 | 4 | function _fixup_target { 5 | local -a lines commits 6 | 7 | lines=(${(f)"$(git fixup --no-commit --no-rebase 2>&1)"}) 8 | if test $? -ne 0; then 9 | _message ${(F)lines} 10 | return 1 11 | fi 12 | 13 | commits=(${lines[@]%% *}) 14 | compadd -l -d lines -a -- commits 15 | } 16 | 17 | _arguments -A \ 18 | '(-s --squash)'{-s,--squash}'[Create a squash commit rather than a fixup]' \ 19 | '(-a --amend)'{-a,--amend}'[Create an amend commit rather than a fixup]' \ 20 | '(-c --commit --no-commit)'{-c,--commit}'[Create a commit]' \ 21 | '(-c --commit --no-commit)'--no-commit"[Don't create a commit]" \ 22 | '(--rebase --no-rebase)'--rebase'[Do a rebase after commit]' \ 23 | '(--rebase --no-rebase)'--no-rebase"[Don't do a rebase after commit]" \ 24 | '(-b --base)'{-b,--base}+"[Use as base of the revision range for the search]":rev:__git_references \ 25 | '(-n --no-verify)'{-n,--no-verify}'[Bypass the pre-commit and commit-msg hooks]' \ 26 | '(-A --all)'{-a,--all}'[Show all candidates]' \ 27 | ':commit:_fixup_target' 28 | -------------------------------------------------------------------------------- /git-fixup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # git-fixup (https://github.com/keis/git-fixup) 3 | # We cannot set -u, because included git libraries don't support it. 4 | set -e 5 | 6 | # shellcheck disable=SC2034 7 | OPTIONS_SPEC="\ 8 | git fixup [options] [] 9 | -- 10 | h,help Show this help text 11 | s,squash Create a squash! commit 12 | f,fixup Create a fixup! commit 13 | a,amend Create an amend! commit 14 | c,commit Show a menu from which to pick a commit 15 | no-commit Don't show a menu to pick a commit 16 | rebase Do a rebase right after commit 17 | no-rebase Don't do a rebase after commit 18 | n,no-verify Bypass the pre-commit and commit-msg hooks 19 | b,base=rev Use as base of the revision range for the search 20 | A,all Show all candidates 21 | r,reverse Reverse the sort 22 | " 23 | # shellcheck disable=SC2034 24 | SUBDIRECTORY_OK=yes 25 | # shellcheck disable=SC1091 26 | . "$(git --exec-path)/git-sh-setup" 27 | 28 | # Define a sed program that turns `git diff` output into a stream of filenames 29 | # and sections within those files. 30 | grok_diff='/^--- .*/p ; 31 | s/^@@ -\([0-9]*\),\([0-9]*\).*/\1 \2/p' 32 | 33 | # Produce suggestion of commits by finding the sections of files with changes 34 | # staged (U1 to diff is used to give some context for when adding items to 35 | # lists etc) and looking up the previous commits touching those sections. 36 | fixup_candidates_lines () { 37 | git diff --cached -U1 --no-prefix | sed -n "$grok_diff" | ( 38 | file='' 39 | while read -r offs len ; do 40 | if test "$offs" = '---'; then 41 | file="$len" 42 | else 43 | if test "$len" != '0'; then 44 | if test "$file" != '/dev/null' ; then 45 | git blame -sl -L "$offs,+$len" "$rev_range" -- "$file" 46 | fi 47 | fi 48 | fi 49 | done 50 | ) | grep -v "^^" | cut -d' ' -f 1 | sed 's/^/L /g' 51 | } 52 | 53 | # Produce suggestion of commits by taking the latest commit to each file with 54 | # staged changes 55 | fixup_candidates_files () { 56 | git diff --cached --name-only | ( 57 | while read -r file; do 58 | git rev-list -n 1 -E --invert-grep --grep='^(fixup|squash)' "$rev_range" -- "$file" 59 | done 60 | ) | sed 's/^/F /g' 61 | } 62 | 63 | # Produce suggestion of all commits in $rev_range 64 | fixup_candidates_all_commits () { 65 | git rev-list "$rev_range" | sed 's/^/F /g' 66 | } 67 | 68 | # Pretty print details of a commit 69 | print_sha () { 70 | local sha=$1 71 | local type=$2 72 | 73 | git --no-pager log --format="%h %ai [$type] %s <%ae>" -n 1 "$sha" 74 | } 75 | 76 | # Call git commit 77 | call_commit () { 78 | local flag=$op 79 | local target=$1 80 | 81 | if test "$op" = "amend"; then 82 | flag=fixup 83 | target="amend:$target" 84 | fi 85 | 86 | # shellcheck disable=SC2086 87 | git commit "${git_commit_args[@]}" "--$flag=$target" || die 88 | } 89 | 90 | # Call git rebase 91 | call_rebase () { 92 | local target=$1 93 | 94 | # If our target-commit has a parent, we call a rebase with that 95 | # shellcheck disable=SC1083 96 | if git rev-parse --quiet --verify "$target"~1^{commit}; then 97 | git rebase --interactive --autosquash "$target~1" 98 | # If our target-commit exists but has no parents, it must be the very first commit 99 | # the repo. We simply call a rebase with --root 100 | elif git rev-parse --quiet --verify "$target"^{commit}; then 101 | git rebase --interactive --autosquash --root 102 | fi 103 | } 104 | 105 | # Print list of fixup/squash candidates 106 | print_candidates () { 107 | ( 108 | if [ "$show_all" = "false" ]; then 109 | fixup_candidates_lines 110 | fixup_candidates_files 111 | else 112 | fixup_candidates_all_commits 113 | fi 114 | ) | sort -uk2 | while read -r type sha; do 115 | if test -n "$sha"; then 116 | print_sha "$sha" "$type" 117 | fi 118 | done 119 | } 120 | 121 | fallback_menu () { 122 | ( 123 | IFS=$'\n' 124 | read -d '' -ra options 125 | PS3="Which commit should I $op? " 126 | select line in "${options[@]}"; do 127 | if test -z "$line"; then 128 | declare -a args=("$REPLY") 129 | case ${args[0]} in 130 | quit|q) 131 | echo "Alright, no action taken." >&2 132 | break 133 | ;; 134 | show|s) 135 | idx=$((args[1] - 1)) 136 | if test "$idx" -ge 0; then 137 | git show "${options[$idx]%% *}" >&2 138 | fi 139 | ;; 140 | help|h) 141 | local fmt="%s\n %s\n" 142 | # shellcheck disable=SC2059 143 | printf "$fmt" "" "$op the -th commit from the list" >&2 144 | # shellcheck disable=SC2059 145 | printf "$fmt" "s[how] " "show the -th commit from the list" >&2 146 | # shellcheck disable=SC2059 147 | printf "$fmt" "q[uit]" "abort operation" >&2 148 | # shellcheck disable=SC2059 149 | printf "$fmt" "h[elp]" "show this help message" >&2 150 | ;; 151 | esac 152 | else 153 | echo "$line" 154 | break 155 | fi 156 | done < /dev/tty 157 | ) 158 | } 159 | 160 | show_menu () { 161 | if test -n "$fixup_menu"; then 162 | eval command "$fixup_menu" 163 | else 164 | fallback_menu 165 | fi 166 | } 167 | 168 | sort_commits () { 169 | # shellcheck disable=SC2086 170 | sort $sort_flags $additional_sort_flags 171 | } 172 | 173 | git_commit_args=() 174 | target= 175 | op=${GITFIXUPACTION:-$(git config --default=fixup fixup.action)} 176 | rebase=${GITFIXUPREBASE:-$(git config --default=false fixup.rebase)} 177 | fixup_menu=${GITFIXUPMENU:-$(git config --default="" fixup.menu)} 178 | create_commit=${GITFIXUPCOMMIT:-$(git config --default=false --type bool fixup.commit)} 179 | base=${GITFIXUPBASE:-$(git config --default="" fixup.base)} 180 | show_all=false 181 | sort_flags="-k2 -k3 -k4" # default flags to sort by time (eg 2025-01-03 10:04:43 +0100, hence 3 fields) 182 | additional_sort_flags=${GITFIXUPADDITIONALSORTFLAGS:-$(git config --default="" fixup.additionalSortFlags)} 183 | while test $# -gt 0; do 184 | case "$1" in 185 | -s|--squash) 186 | op="squash" 187 | ;; 188 | -f|--fixup) 189 | op="fixup" 190 | ;; 191 | -a|--amend) 192 | op="amend" 193 | ;; 194 | -c|--commit) 195 | create_commit=true 196 | ;; 197 | --no-commit) 198 | create_commit=false 199 | ;; 200 | --rebase) 201 | rebase=true 202 | ;; 203 | --no-rebase) 204 | rebase=false 205 | ;; 206 | -n|--no-verify) 207 | git_commit_args+=("$1") 208 | ;; 209 | -b|--base) 210 | shift 211 | if [ $# -eq 0 ]; then 212 | die "--base requires an argument" 213 | fi 214 | base="$1" 215 | ;; 216 | -A|--all) 217 | show_all=true 218 | ;; 219 | -r|--reverse) 220 | additional_sort_flags="-r" 221 | ;; 222 | --) 223 | shift 224 | break 225 | ;; 226 | esac 227 | shift 228 | done 229 | 230 | target="$1" 231 | if test $# -gt 1; then 232 | die "Pass only one ref, please" 233 | fi 234 | 235 | if test -n "$target"; then 236 | call_commit "$target" 237 | if test "$rebase" = "true"; then 238 | call_rebase "$target" 239 | fi 240 | exit 241 | fi 242 | 243 | if git diff --cached --quiet; then 244 | die 'No staged changes. Use git add -p to add them.' 245 | fi 246 | 247 | cd_to_toplevel 248 | 249 | if test "$base" = "closest"; then 250 | base=$(git for-each-ref \ 251 | --merged HEAD~1 \ 252 | --sort=-committerdate \ 253 | --count 1 \ 254 | --format='%(objectname)' \ 255 | refs/heads/ \ 256 | ) 257 | if test -z "$base"; then 258 | die "Could not find the ancestor branch" 259 | fi 260 | fi 261 | 262 | if test -z "$base"; then 263 | upstream=$(git rev-parse "@{upstream}" 2>/dev/null || true) 264 | head=$(git rev-parse HEAD 2>/dev/null) 265 | if test -n "$upstream" && test "$upstream" != "$head"; then 266 | base="$upstream" 267 | fi 268 | fi 269 | 270 | if test -n "$base"; then 271 | rev_range="$base..HEAD" 272 | else 273 | rev_range="HEAD" 274 | fi 275 | 276 | if test "$create_commit" = "true"; then 277 | target=$(print_candidates | sort_commits | show_menu) 278 | if test -z "$target"; then 279 | exit 280 | fi 281 | call_commit "${target%% *}" 282 | if test "$rebase" = "true"; then 283 | call_rebase "${target%% *}" 284 | fi 285 | else 286 | print_candidates | sort_commits 287 | fi 288 | --------------------------------------------------------------------------------