Hacker News new | comments | show | ask | jobs | submit login
Show HN: 'git inject' – amend commits other than HEAD
71 points by koreno 895 days ago | hide | past | web | favorite | 36 comments
'git inject' is a git alias (see code at the bottom). It is similar to 'git commit --amend', but it allows you to 'inject' your changes into commits further back in history (using 'rebase').

If you're as pedantic as I am about the git history you're about to push into master (as far as I can control it, I strive to keep each commit conceptually coherent), you'll often come to a situation where you have a modification that really belongs in an older commit. It takes a few git commands to stash other changes away, commit the modification, then interactively rebase over some previous commit, move your commit into place, change to 'fixup', save, exit, and then unstash whatever else you had lying around. Not fun.

Here's how 'git inject' makes it fun:

>> git inject <commit-ref> <patch-ref>

>> git inject HEAD^^ -a # inject all work-dir modifications

>> git inject a28kd8 -p # interactively select patches to inject

>> git inject HEAD~4 file1 # inject the modifications in file1

Just put this into your .gitconfig under 'aliases':

inject = "!f() { set -e; HASH=`git show $1 --pretty=format:\"%H\" -q`; shift; git commit -m \"fixup! $HASH\" $*; [ -n \"$(git diff-files)\" ] && git stash && DIRTY=1; git rebase $HASH^^ -i --autosquash; [ -n \"$DIRTY\" ] && git stash pop;}; f"




This can be easily solved without aliases by adding a new commit on top, then using `git rebase -i` to re-order and squash the commit on top of another one.

It wasn't immediately obvious to me that you can also re-order patches using `rebase -i`. I've used for squashing and dropping temporary code before but realizing it can also reorder commits was really useful.


git rebase is just a mechanized cherry picker. (Agricultural implement, really, when you think about it.)

In fact, you can add arbitrary commits to the list, not only re-order the existing ones!

I sometimes do this:

    $ git log --reverse --oneline here..there > arbitrary-picks
Then we edit the arbitrary-picks file so that it has the interactive rebase syntax: i.e. putting the word "pick" in front of every line. Then:

    $ git rebase -i HEAD   # noop!
In the editor, we delete the "noop" line and read in the arbitrary-picks:

    :r arbitrary-picks
Wee, now we are picking all those commits into this branch, with the interactive rebase workflow.


Even cooler is the "exec" directive. I converted a darcs repo to git and then used rebase to separate the huge mass of commits in the dev repo into their own branches. It went something like this:

    exec git branch new_master
    exec git checkout -b branch-a
    pick a
    pick b
    pick c
    exec git checkout new_master
    pick d
    exec git checkout -b branch-b
    pick e
    exec git checkout new_master
    exec git checkout -b branch-c
    pick f
    ...
    exec git checkout new_master
The only trick was that you're doing stuff behind rebase's back, so whatever branch you end the script on gets `git reset` to the last pick. So you have to remember to switch back to master or end on a dummy branch that you can delete. Also, since the branch you are rebasing is not changed until the end, you can't rely on its name (since at the start it's pointing to the current state of things, not the new state), so you have to create your own branch if you want to keep popping back and forth.

It was a neat trick and when it was done all the commits were nicely sorted into feature branches, as if we'd been doing that all along.


There's also a feature called autosquash that lets you use 'git commit --fixup sha' to write a commit preformatted to be recognized by interactive rebase. You have to turn it on in your config, but it's very handy for this sort of thing.


Yep, git inject uses this feature :)


The point of the alias is to make it more user-friendly and less error-prone.


If you find that this is a regular part of your workflow, then you should consider trying Stacked Git:

http://procode.org/stgit/

(That page is actually out of date; it's actually somewhat better maintained than it appears there.) It seems to have fallen out of favour somewhat as Git's own native porcelain has improved over the years, but I know there are still some kernel developers using (and maintaining) it.

Personally, I find it allows me to do things trivially and routinely that others find difficult or impossible. I still prefer it even though I know how to accomplish the same thing using native Git commands (like rebase -i).

One of the things I like most is the lack of global (as opposed to per-branch) state of the kind that Git rebase relies on. So if I want to go switch to another branch in the middle of a rebase, I can. If I have unapplied patches that I haven't deleted, they'll still be there in a month or a year. I never have to worry about whether I am in Git's weird rebase mode or not, or decide whether I have to use "git commit --amend" or "git rebase --continue" (it depends on whether you specifically asked to edit the patch or there was an error applying it, and if you forget and guess badly Git does the Wrong Thing) because the commands are always the same, and the list of applied and unapplied patches is always right there to see.


Eh most of the time I just commit blindly as I'm working (WIP #1, #2, etc) so I can see how the code has evolved or revert to an earlier approach if I end up in a blind alley.

When the code is tested and ready I just reset the index but leave the working directory, then commit different hunks to make coherent commits.

During reviews we use fixup commits so it doesn't blast the comment history on github, then the final merge does a rebase -i --autosquash.


It's not exactly hard to do by hand:

    git add ...
    git commit --fixup $SHA
    git stash
    git rebase -i --autosquash $SHA~
    git stash pop


Or simply:

    git add ...
    git commit --fixup $SHA
    git rebase -i --autosquash --autostash $SHA~


It gets more complicated if you have some other modified content that you wanna leave untouched. You then need to stash it or commit it separately.

The point of the alias is to make it all more user-friendly and less error-prone.


That's what the `--autostash` is for. It also has the advantage that the stash will be restored at the end of the rebase even if it hits problems and needs to be continued or aborted.


You can even use --autostash to avoid the manual stash, and if you're doing this a lot then the config options rebase.autosquash and rebase.autostash are your friends.


Nice! wasn't familiar was autostash.


Or, alternatively, just git rebase -i, mark the comment for amending with "e", then when it stops for amending amend what you need and commit with "git commit --amend", then "git rebase --continue" to finish. Wrap with stash if you don't have a clean working directory to start with.


That's what I do right now, leading to temporary commits like "Squash me" followed by the kinda awkward interactive rebase flow, which is the only instance where I use Vim, and awkwardly and with some resistance at that.


My method does not lead to any temporary commits.


Do not expand arguments with unquoted

  $*
Use:

  "$@"
You probably want

  local HASH=...
and

  local DIRTY=
in there.

Also, why mix the old style

   `command ...`
and

   $(command ...)
expansion.


`local` is a bash-ism so it's best avoided in this context, and the command is running in its own shell anyway so the variables can't pollute anything else.

The other comments are 100% correct.


> so the variables can't pollute anything else

The variables can already exist in the environment. One of them is tested by a statement that is reachable without that variable being initialized in that script.

A fix that keeps the script POSIX would be to initialize the variable.


I have a similar function that's a bit simpler:

    gfixup () {
        REVISION=$(git rev-parse $1) 
        git commit --fixup=$REVISION && git rebase -i --autosquash $REVISION~
    }
It doesn't handle stashing anything, but other than that it's basically the same.


Wouldn't the stash be easily handled with --autostash?


This is neat. Let me add that I use `git commit --amend` relatively often. Useful not only for adding "forgotten" files and changes, but also for editing the commit message.


Add --reset-author to update the timestamp to avoid getting confused with two versions of the "same" commit. It also updates the author information, which may or may not be what you want.


With the mercurial evolve extension this is simply:

hg commit # "stash" away whatever you had lying around

hg update <rev of older commit>

# make your changes

hg amend

hg evolve -a


Thanks! I'd use this. Usually for updating previous commits that are up in Gerrit.


Does this work when using the fish shell, which is not bash compatible?


git aliases aren't run in fish-shell, even if you've set that to be your default shell.


you mean under `[alias]`, rather than 'aliases'


Imo if you were as pedantic about the history you wouldn't be tampering with it.


There's no issue if you haven't pushed to any remotes (origin) yet. I often wish I could amend a few commits back before pushing.


Then do a commit and then use rebase interactive (-i) and squash them together.


That's what I would do, and that's what "git inject" seems to be after.


Mercurial Evolve handles this edit-history debate well. There are draft commits, public commits, and non-publishing servers where people can collaboratively edit commits. This is all encoded into hg itself rather than relying purely on social convention.

https://www.youtube.com/watch?v=4OlDm3akbqg


"history" can be "shared history" or "private history". The part of history that only lives on your computer is more a "patch-set in the making" than actual history.

If a commit is appended to a tree and nobody has checked it out yet, does it really exist?


Even (semi-) public history can be rewritten. At work, I'm working on a bug with several people. We created a "wild repo" that we share. The branches of test commits get rewritten regularly. You just send an e-mail "Heads up; I rewrote the test-branch". If you know what you're doing in git, it's not hard to pick up rewritten branches.

I introduced the convention of preserving the previous version of the branch as "<branch-name>.1", and the previous-previous as "<branch-name>.2" similar to rotating logs.

You obviously don't want to be doing this on a repo with thousands of downstreams. The point is that "private" can have a somewhat larger scope than "just my single local repo".




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact

Search: