10 ms·
Show HN: 'git inject' – amend commits other than HEAD
'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"
- jasonlotito 11y agoThanks! I'd use this. Usually for updating previous commits that are up in Gerrit.
- kazinator 11y agoDo 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.
- johnkeeping 11y ago`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.
- kazinator 11y ago> 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.
- pstadler 11y agoThis 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.
- exDM69 11y agoAdd --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.
- exDM69 11y agoThis 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.
- stormbrew 11y agoThere'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.
- koreno 11y agoYep, git inject uses this feature :)
- koreno 11y agoThe point of the alias is to make it more user-friendly and less error-prone.
- kazinator 11y agogit 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.
- __david__ 11y agoEven 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.
- dr4g0n 11y agoIt's not exactly hard to do by hand: git add ... git commit --fixup $SHA git stash git rebase -i --autosquash $SHA~ git stash pop
- rlpb 11y agoOr, 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.
- johnkeeping 11y agoOr simply: git add ... git commit --fixup $SHA git rebase -i --autosquash --autostash $SHA~
- koreno 11y agoIt 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.
- johnkeeping 11y agoThat'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.
- Ninfoo 11y agoNice idea! But, be aware that this doesn't work to inject into the initial commit. I hope you don't mind that I streamlined your original code a bit to make it more readable. This can be put directly into the .bashrc, .zshrc, etc: inject() { local HASH=$1; shift 1 git commit --fixup=$HASH if [ -n "$(git diff-files)" ] then git stash trap "git stash pop" EXIT fi git rebase $HASH^ -i --autosquash }
- milkworsethan 11y agoImo if you were as pedantic about the history you wouldn't be tampering with it.
- artursapek 11y agoThere'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.
- bpicolo 11y agoThen do a commit and then use rebase interactive (-i) and squash them together.
- artursapek 11y agoThat's what I would do, and that's what "git inject" seems to be after.
- nothrabannosir 11y ago"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?
- kazinator 11y agoEven (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".
- mdemare 11y agoDoes this work when using the fish shell, which is not bash compatible?
- davidcelis 11y agogit aliases aren't run in fish-shell, even if you've set that to be your default shell.
- piinbinary 11y agoI 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.
- michaelmior 11y agoWouldn't the stash be easily handled with --autostash?
- xenadu02 11y agoEh 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.
- krupan 11y agoWith 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
- evandrix 11y agoyou mean under `[alias]`, rather than 'aliases'
- zb 11y agoIf you find that this is a regular part of your workflow, then you should consider trying Stacked Git: http://procode.org/stgit/ 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.