66 comments

[ 3.2 ms ] story [ 77.7 ms ] thread
Can anyone give a comparison of the two from experience?

I'm painfully familiar with the problem they solve and am delighted to discover they exist.

I'd love some input about how they differ.

I have only used `git-absorb`, but one difference from reading the article is that it does its own analysis of your previous commits to choose what each line should `fixup`. Because of this, `git-absorb` can only fix up commits up to a specified base (10 commits earlier by default) when you use it.
> does its own analysis of your previous commits

Not really; you can pass any commit ref to git-autofixup, and it will create commits only for commits since that ref. So it's for the user to decide :)

I have looked at both but only used git-autofixup. Some observations:

- git-autofixup has a changelog

- git-autofixup includes sober, technical documentation that actually explains how it works. OTOH, git-absorb has a great elevator pitch for users that are not so familiar with Git.

- git-autofixup does not have unresolved bugs, unlike git-absorb

- git-absorb creates fixup commits for the last 10 commits, which seems really odd. It's better to use symbolic references like @{upstream}, but it looks like they dont' support this yet?

- git-autofixup is stable and mostly done software, while git-absorb has a sizeable list of todos

- git-autofixup is written in Perl (like some tools in Git itself), making it easier to install than git-absorb which uses Rust.

Then again, as author of this article, I'm obviously biased ;)

Great information. Thank you!
git-autofixup definitely changed my daily development life.... I mostly use it with this Git alias

``` autofixup = !"autofixup origin/master --exit-code; test $? -lt 2 && GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash $(git merge-base HEAD origin/master)" ```

Works almost every time perfectly, and when not i always get a comment during a code review.

> when not i always get a comment during a code review.

Why during CR? Is the error not obvious to you? You should be able to check the results of your changes before pushing, right?

I/we do but sometimes you just overlook something especially if there are a lot of changes. But having someone else review what you do almost completely removes that problem.

I usually do not look through all commits again when a basic review already happend and i "just" integrate the review comments. That is where for me personally these problems happen that git-autofixup is sometimes wrong but in over a few thousand commits in the last months this happend only thrice. Which for me just speaks for the tool IMHO. Ow the original author big time :-)

Cool, I'm glad it's working out for you. Due to my workflow at my current job I haven't actually had much occasion to use it since writing it, so I'm not that confident about how good the hueristics are.

Do you generally use the default --strict setting?

If you're an Emacs user, I must recommend magit (https://magit.vc/) - the interactive rebasing (including squashing/fixups) is one of the best UIs (overall, not just git) I've seen in my life.

I'd switch away from Emacs but magit keeps me hooked!

Does Magit have a similar feature to what is discussed in this article? Specifically “magically figure out which previous commit these changes should be combined with”?
It doesn't do it magically, but it makes it a lot easier to do. Easy enough that I've never really wanted a tool like this.

Having said that, if autofixup works well it might be worth integrating it into magit via a plugin as it would certainly reduce the repetition.

Could you share what your workflow would be? I’m assuming you put the point on the line you want to fix, then use Magit to start a rebase at the last commit that changed that line, then apply your changes, then continue?

I’m a casual Magit user, so learning from other users would be very beneficial.

That works. My habit is to make the fix, commit it as "!fixup whatever", start an interactive rebase, move the fixup commit just after the bad commit (M-↑), press f on the fixup commit to tell rebase to merge it with the bad commit, and run the rebase (C-c C-c). This is pretty much the same method as yours, but committing first means the fixup will still be there if you have to cancel & redo the rebase for some reason, such as unexpected and confusing merge conflicts.
> move the fixup commit just after the bad commit (M-↑), press f on the fixup commit to tell rebase to merge it with the bad commit,

That's what --autosquash is for. If you do fixup commits correctly (using --fixup), it will put them in the correct place and mark them as fixups for you.

> using --fixup

If I understand you correctly, this requires that I think about which commit I want to fixup. Once identified, I can mark my fixup with what commit it should be combined with automatically.

My understanding of the article is that it attempts to automatically figure out which commit to combine with. That’s the feature that I’m interested in.

Yeah, it does require you to think about it. But magit has really quick and easy diffing and blame so it can make it significantly easier. In practice I rarely have to think very hard before knowing which commit to fixup.
So I guess what I should be doing is make fix → magit-blame to get the commit ID for the last prior edit → copy commit ID or message to clipboard → magit commit with `c F`, C-s to the commit ID I just copied, and C-c C-c to set up and run the autosquash rebase.
This process is what I think the article describes and what I’m curious if can be automated by magit itself.
Magit appears to have an integration with 'git absorb'. The issue that tracked the integration discussed git absorb as providing similar function to git autofixup. I don't particularly desire unsupervised changes to my commit history though so don't plan on looking into it further.

https://github.com/magit/magit/issues/3053

Right, that process is what I do now, outside of Magit. The point of this article is a tool that (attempts to) automatically find the correct commit to combine with. Conceptually it removes the effort taken to move the fixup commit to the right spot; both keystroke-wise and mental effort wise.

That’s what I’m looking for: a Magit-native way to make my little fixups without having to manually track which fixup should be applied where in history.

I typed it out but HN seems to be rate limiting me or something and tells me I'm "posting too fast". I seem to be able to post shorter comments, though.

I've been meaning to write an article on a fixup workflow for a while now. Just need to find somewhere to host it.

You could throw it in a gist to start with.
If you start doing a commit (via `c` in the magit status buffer, with the standard semantics of "you're going to commit everything that's currently staged") you can press capital F for an instant fixup, or capital S for instant squash.

When you press either of those, magit pops up a commit picker which shows the current git log. Selecting a commit will then instantaneously apply your staged changes to the selected commit. It's much simpler than any of the other workflows I've seen in response to your question.

The gif in this repo (for a tool I made that simulates this behavior as a cli tool for some jealous coworkers) tries to show the workflow: https://github.com/quodlibetor/git-fixup

That said, this _doesn't_ support the "automatically figure out which commits to apply hunks to" workflow. I personally find that I use both workflows depending on the nature of my changes.

Magit incorporates git-autofixup directly via `magit-commit-autofixup`; also available is `magit-commit-absorb` for git-absorb.
How does Vim's fugitive (https://github.com/tpope/vim-fugitive/) plugin compare to this, in case anyone here has used them both? I've only used it for some basic things so far, and it seems nice enough, but I'm wondering if it's good with more advanced git-fu like the above.
Sublime Merge do the same thing in just a couple of clicks, just saying
I doubt it. There's an open feature request currently for adding fixup commits. This article goes a step beyond that. Are you sure you understood the article?
Select two commits. Edit Commit -> Squash Selected Commits, ignoring new messages (fixup)
That's not a fixup commit, it's a rebase. It's missing the point of fixup commits which is to commit now and defer the rebase until later.

It's also completely missing the point of the feature under discussion here which is to automatically decide which commit to squash the fix into.

For this issue and so many others, everyone should just get comfortable with interactive rebase. Crutches like this trap you in this space of never really learning git. And that's a tragedy. All because you're impatient? Afraid?
Learning rebasing is one thing but wasting hours every week on rebasing because you are doing the same thing over and over again is another. When i do a one line fixup i just do not want to figure out the correct commit and then rebase i just want it to happen because i already know how i would rebase that: you just use git-autofixup or other similar tools discussed here to get it done.
It's possible the other reason is trying to manipulate git history too frequently. Keep in mind this comes from a prolific git history abuser and rebaser... if hacking on a branch, a trail of messy commits is fine, it's your work in progress, it's also useful if you messed up otherwise you are forced to use reflog. Rebase when you are at the final stage of tidying things up and squashing commits down into a legible history, not on every single amendment... for this style of history rewriting rebase is perfect because you have the full power to rearrange rewrite and squash commits.
Totally agree. I guess what i am trying to say is that learning rebase is a must and powerful tool but if you are doing the same things again and again you should think about automating them and use your thinking-power and time for non-automated things.
I'd disagree. The longer the branch to rebase the larger the chance of conflicts occuring.
Rebase early and often. Always keep a close eye on upstream while you're working downstream. Otherwise you're making it harder on yourself when you do that final cleanup.
That's a fair point, if you are working against a fast moving target that forces you to rebase onto it often then you really want a short and simple history... a trail of WIPs or changes on top of changes tend blow up into a cascade of conflicts.

However in my original comment I was actually just referring to the scenario where you only want to rewrite history onto the same base, in which case I stand by my suggestion... In fact I find this can be a good strategy if your history has grown and you also need to rebase onto a new upstream - i.e first clean up your history onto the same base with a `git rebase -i current-base` and squash it all down and tidy it up, then do the `git rebase upstream` so you can do conflict resolution with more holistic commit diffs.

Practice. You'll get better and faster. I do it tens of times daily and each fixup takes ten or fifteen seconds.
Yep. I have that practice too and i thought that was good enough but with git-autofixup and my posted alias i am down to under one second. So it will save you 1-14 sesconds of your life with every fixup you do... unless it cannot be done automatically. Try it and let us know what you think :-)
Challenge accepted.
I tried git-absorb (git-autofixup is Perl? Installing things with CPAN needs sudo? Sorry... I don't have a good Perl situation and don't care to have one.) and it did not immediately work correctly. Maybe git-autofixup is better; idk. Also I misunderstood; I'm expected to `git add` before using these tools - after that, the commit/rebase only takes me a few seconds; I already knew what commit it was bound for when I staged the changes.
git-autofixup can also be installed by simply downloading the script[1], giving it execute permissions, and putting it somewhere in your PATH. It needs perl 5.8.4+, which is very old, and only depends on the standard library. Git ships with a Perl interpreter on Windows.

If there are any staged changes, git-autofixup only fixes those up and ignores any unstaged ones; otherwise it tries to autofixup all unstaged changes.

[1]: https://github.com/torbiak/git-autofixup/blob/master/git-aut...

Coming from the other direction, I am very familiar with interactive rebase, and I use it on my working branch all the time before pushing.

I am struggling to understand the use case for autofixup and absorb. The descriptions talk about post-code-review changes, which means the branch has been pushed. So are these tools only for a flow that uses force-pushing regularly?

I don't think that would be a feature for my teams. We think of code review as an event in time, and changes post-code review should be clearly differentiated from changes pre-code review.

Am I missing the point?

I generally use autosquash when a code review of a PR turns up things I missed, which should have been part of a particular commit on that PR's branch. However, I don't want to amend the previous commit right away if I'd like another review for the new changes. Thus, I do a `git commit --fixup <HEAD|hash>` and push it. I can then ask the review to only look at the new commit, I won't break their local checkouts of the branch, and only when everything's been approved I run `git rebase --autosquash` and then merge it in.

(Before I started using autosquash for this, I'd do the same, except I had to figure out at the time of merging onto which commit every new commit had to be fixed up, rather than at the time of writing it.)

I rely on code review tools that can show me the difference between two patch sets. I really love Phabricator for that ability - push a set of commits and each gets a review. Absorb any fixes, push and suddenly each commit has a new version that you can diff against old ones if you care. It makes reviewing the patch set in totality trickier. However, since I’m practice you want individual commits within a set to be standalone ones, I’ve rarely missed the ability.
<plug>In https://reviweable.io, you can have both matching-commit diffs and whole-PR diffs, all in the same review.</plug>
I think you typo'ed your plugged domain name. :)
Dammit! I mean, uh, it was a clever marketing ploy to attract attention and you totally fell for it. Yeah, that's the ticket.
This whole article seems to hinge on the fact that you already know how to run an interactive rebase though?

As in, I'm very comfortable doing an interactive rebase, but have to figure out the correct commit to fixup on is tedious, so it sounds very useful to me if a tool can help me with that.

Unfortunately I think fixup commits are one of the least understood parts of git. People seem to confuse them with rebasing. They support rebasing but don't replace it.
An excerpt from my ~/.gitconfig, showing a related approach (piggy-backing on git-revise):

  [alias]
   # Revise into the commit that last changed File
   rf = "!f() { if [ $# -eq 0 ]; then REV=\"$(git status --porcelain --untracked-files=no | sed '/^ /d;s/^.. //' | xargs -n1 git rev-list -1 HEAD --)\"; NUM_REVS=\"$(echo \"$REV\" | wc -l)\"; if [ $NUM_REVS -ne 1 ]; then >&2 echo Files in the index were not all last modified in the same commit; exit 1; fi; else REV=\"$(git rev-list -1 HEAD -- \"$1\")\"; shift; fi; git revise \"$REV\" \"$@\"; }; f"
(This alias is three times as long as my next longest aliases, which are ports of Mercurial’s id, tip, incoming and outgoing commands.)

This is more coarse-grained than the technique in this article, as it only goes down to the file level—because that was sufficient for me when I wrote the alias, and probably easier to implement. I’ve been vaguely contemplating trying git-autofixup and git-absorb for a while too, which include what are essentially more polished and powerful versions of my alias.

You know that can go in $PATH/git-rf, right.
If it grew much bigger I’d do that, but until it’s definitely too large, keeping all of this stuff in ~/.gitconfig is simpler, especially for sharing across heterogeneous machines.
You can add a version-controlled directory to your PATH and put scripts in it. This can be in the same repository as your .gitconfig. So you can graduate aliases to scripts.
Copying a single file is regularly easier, and more flexible when working on machines not set up with your SSH keys or whatever.
hg incoming and hg outgoing are really useful.

When I am in git, I use this version from https://github.com/sympy/sympy/wiki/Git-hg-rosetta-stone#set...:

    [alias]
    outgoing = !git fetch && git log FETCH_HEAD..
    incoming = !git fetch && git log ..FETCH_HEAD
Feature-wise, the mercurial command is way more powerful: https://www.mercurial-scm.org/doc/hg.1.html#incoming
My aliases for those two deals with remote-tracking branches (see git-rev-parse(1) for documentation of the @{upstream} part if you’re not familiar with it). Might as well provide a more full excerpt:

  # I almost always use glog rather than log.
  glog = log --graph
  # “Short log”
  slog = log --graph --oneline
  # `git id` = `git rev-list --max-count=1` with default refspec HEAD.
  id = "!f() { case \"x$1\" in x-*|x) refspec=HEAD;; *) refspec=\"$1\"; shift;; esac; git rev-list --max-count=1 \"$refspec\" \"$@\"; }; f"
  # `git tip` = `git log --max-count=1` with default refspec HEAD.
  tip = "!f() { case \"x$1\" in x-*|x) refspec=HEAD;; *) refspec=\"$1\"; shift;; esac; git log --max-count=1 \"$refspec\" \"$@\"; }; f"
  # `git out` = glog commits that exist locally but not on the upstream branch. No way to refer to different origins, sorry.
  out = "!f() { case \"x$1\" in x-*|x) branch=;; *) branch=\"$1\"; shift;; esac; git glog \"$branch@{upstream}..$branch\" \"$@\"; }; f"
  sout = "!f() { case \"x$1\" in x-*|x) branch=;; *) branch=\"$1\"; shift;; esac; git slog \"$branch@{upstream}..$branch\" \"$@\"; }; f"
  # `git in` = glog commits that exist on the upstream branch (remember to fetch them first) but not locally.
  in  = "!f() { case \"x$1\" in x-*|x) branch=;; *) branch=\"$1\"; shift;; esac; git glog \"$branch..$branch@{upstream}\" \"$@\"; }; f"
  sin  = "!f() { case \"x$1\" in x-*|x) branch=;; *) branch=\"$1\"; shift;; esac; git slog \"$branch..$branch@{upstream}\" \"$@\"; }; f"
Is there one of these `@{}` tags that means "the point where this branch split off from master"? That'd be very useful, because that's usually the scope of my rebases.
I recommend reading the git-rev-parse(1) man page in full, it’ll give you good ideas for this sort of thing.

What you’re looking for could be spelled $(git merge-base HEAD master) (that’s a subshell invocation, outputting a single commit ID). In some circumstances dotted range notation might do: master.. includes all the commits after master and HEAD diverge. That’s good for logging, for example, but not so useful for the sort of rebasing you’re describing, where you may wish to use the merge base instead.

That's helpful, thanks! I already do use merge-base, I was wondering if there's a builtin shortcut as I hadn't seen anything in the rev-parse manage.
Hmm. This sounds like my workflow with gerrit, which goes like this:

- each work item has its own branch. When pushed, gerrit turns this into a review.

- a git alias, "alias.fixlast=commit -a --amend --no-edit" which amends the last commit to match the working directory

Plus these configurations:

branch.autosetuprebase=local

branch.autosetupmerge=always

pull.rebase=true

The effect is that every branch I create off the main "develop" branch is automatically set up to rebase from that branch when I do "git pull". So when I get some review comments for review "xyz", I just do checkout - pull - make changes - fixup - push.

Gerrit's workflow is slighly unusual in that each review must be a single git change, but it keeps a history of its own of previous changes you made to that review.

I wrote my own tool for this which allows you to fix up a file based on the last time you changed that file, or on a specific commit. It will then execute an interactive rebase to that point as well as taking care of stashing and then restoring unrelated files.

https://github.com/alblue/scripts/blob/master/git-fixup

I like the idea of having the editor definition return “true” instead of showing it; I’ll have to add that later.