gitshellzshgit-aliasgit-revision

Git alias to rebase onto commit before message


Here's a common workflow:

  1. Make some changes and commit, e.g. git commit -m 'add action buttons'
  2. Make a bunch of interim commits
  3. Realize I want to fix that first commit
  4. Search for the commit SHA in the log and copy it
  5. Rebase onto the commit before the SHA, e.g. git rebase -i asdf123^
  6. Edit the todo list so it pauses on the commit I want to fix
  7. Make the change, amend the commit, and continue rebasing

I'd like an alias to automate steps 4-5, e.g. git fixup add action buttons. The rebase command is:

$ git rebase -i 'HEAD^{/Add action buttons}^'

I think this alias should work, but it doesn't:

[alias]
    fixup = !sh -c 'git rebase --interactive HEAD^{/$@}^'

Curiously, it works if I do git fixup add action, but git fixup add action buttons throws an error: fatal: invalid upstream 'HEAD^{/action'.

I think the problem might have to do with quoting, but I can't figure out how to escape the quotes in the alias. I tried this:

[alias]
    fixup = !sh -c 'git rebase --interactive \'HEAD^{/$@}^\''

But this throws another error: fatal: bad config line 2 in file /home/dave/.gitconfig.


Solution

  • This quote from the git-config(1) man page is useful here:

    Shell command aliases always receive any extra arguments provided to the Git command-line as positional arguments.

    • Care should be taken if your shell alias is a "one-liner" script with multiple commands (e.g. in a pipeline), references multiple arguments, or is otherwise not able to handle positional arguments added at the end. For example: alias.cmd = "!echo $1 | grep $2" called as git cmd 1 2 will be executed as echo $1 | grep $2 1 2, which is not what you want.
    • A convenient way to deal with this is to write your script operations in an inline function that is then called with any arguments from the command-line. For example `alias.cmd = "!c() { echo $1 | grep $2 ; }; c" will correctly execute the prior example.

    With that in mind, we can write your alias like this:

    [alias]
      fixup = "!f() { git rebase -i \"HEAD^{/$*}^\" ; }; f"
    

    If I have a commit history like this:

    $ git log --oneline -8
    423e981 (HEAD -> main) Reformat with gofmt
    b9de0cb (origin/main) Refactor Makefile
    be4ac65 Integrate options and common_options
    042d122 Rename precommit workflow
    d0f41ef Fully qualify package name
    829bda3 Remove --exact-match from git describe command line
    c8de23e Simplify pre-commit execution of golangci-lint
    40b071a Update krew index manifest
    

    Then I can write:

    git fixup precommit workflow
    

    And end up with a pick list like this:

    pick 042d122 Rename precommit workflow
    pick be4ac65 Integrate options and common_options
    pick b9de0cb Refactor Makefile
    pick 423e981 Reformat with gofmt
    

    Which is what you want.


    By trying to call out to the shell, like this...

    !sh -c '...'
    

    You are calling a shell from a shell, so you have, effectively:

    sh -c "sh -c '...'"
    

    And that makes quoting a royal PITA. You end up with something like:

    [alias]
      fixup = "!bash -c \"git rebase -i \\\"HEAD^{/\\$*}\\\"\" -- "
    

    Lastly, you should really take a look at stacked git, which sits on top of git and makes a workflow like this ("go back and make changes to an older commit") super trivial. For the above example, I might do something like:

    # pop patches after the named patch so that the named patch becomes HEAD
    stg goto rename-precommit-workflow
    <make changes>
    
    # update the patch
    stg refresh
    
    # push all the patches back so that I return the original HEAD of the branch
    stg push -a