Using Git amend on a commit instead of creating multiple commits

Viewed 295

For my understanding, git commit --amend can be used to modify an incomplete commit to reach an understandable state, fix typos, etc. However, sometimes it might be (wrongly?) used as a silver bullet for keeping the commit history cleaner (similar to squashed merge).

An engineer just postulated that when developing a complex feature locally (on a branch) they use git commit --amend on one and the same commit over and over again until the feature is done. E.g.:

  • 1st commit: Stubs for functions A and B created.
  • 1st amend on 1st commit: Function A implemented.
  • 2nd amend on 1st commit: Function B implemented.
  • 3rd amend on 1st commit: Add a service X.

The reason for that is their attempt to keep the commit history clean and having as less commits as possible).

The depicted amend-focused approach was new to me since I usually trying keeping the development process more transparent by adding one commit for each logically finished state. E.g.:

  • 1st commit: Stubs for functions A and B created.
  • 2nd commit: Function A implemented.
  • 3rd commit: Function B implemented.
  • 4th commit: Add a service X.

For my understanding, having multiple commits might facilitate the review process, contribute to the understandability of a single line changed (even years later) as well as gives the possibility for resetting to an older intermediate state during development (e.g. in the case of a dead end during the development) since it's not getting "overridden" over and over again.

Besides, due to usually disabled git push --force on the upstream, --amend will not longer work once the commits are pushed. Most likely it will also cause issues when working in a team.

Do you think using "amend" for keeping the history clean is a valid approach or can it be considered being an anti-pattern for a given example? Could squashed merge understood as more accurate approach for cleaner history.

3 Answers

What git commit --amend really does is very simple. It does not actually change any commit and anyone using it is still making multiple commits. Unfortunately, getting to the point of truly understanding this is a bit complicated. What you are asking, though, is really a different question entirely: When is this a good idea and when is it not a good idea?

The question you intended to ask does not have a hard and fast answer: it's a matter of opinion and thus not really on topic on StackOverflow. Besides this, it's part of an even larger question, that is even fuzzier: What is the appropriate granularity for work in one commit?

We really cannot answer this question either, but we can take a technical approach to answer subsidiary questions that will feed into one's final opinion-based answer. Specifically, we can look at what each commit is, and does for us. This gets us into a bit of Git-specific-ness, although commits in other version control systems are generally pretty similar in terms of effect, at least (even if the underlying mechanisms are very different):

  • A commit represents a snapshot of all of the files that go into making up the system.

  • A commit carries some metadata: who did this, when, and—perhaps most importantly—why. The why part is in the form of a commit message.

  • In Git, the history is commits. Git achieves this by shoving its own Git-specific metadata into the commit along with the user-provided why this commit exists message.

  • Using the history—the commits in the repository, in the case of Git—we can have our computers do several automated things. The two I would specifically call out here are these:

    • Generate logs (for whatever purpose: just to read to remind ourselves of things, or to build change-logs for release notes, or whatever).

    • Automate tests and do bisection to look for bugs.

The above is not necessarily a complete list, but I think it is a pretty good starting point.

Note that to some extent, the last two bullet points argue in opposite directions: the one that generates logs argues for fewer commits, so that the logs are easier to read and digest, giving a better high-level overview. Meanwhile, the bisection idea argues for commits that are as small and granular as possible, so that when we do introduce a bug, the individual commit that first creates the bug is small enough that the problem will be obvious when we read the diff. So we should have as few commits as possible, while having as many commits as possible.1

How to resolve this is, well, a matter of opinion. We can, however, note that the smallest sensible commit is one that stands alone: the software works before the commit, and the software still works after the commit too. If a larger set of changes, to add some feature for instance, can be broken down into two or more smaller changes, and we prefer (by opinion) the "more granular" mode, we should break this commit into more pieces.

In some cases, a small commit might not actually improve the function of the code at all, but still be justified as a small commit simply because it allows the next highly-granular commit to stand alone. That is, we break off a small chunk of infrastructure work and do it and commit, writing a good commit message that explains that this particular change is preparatory. It will enable our future change.

Usually, getting to this point—commits that are this granular—requires extra time and effort: we might write the new feature as a series of half a dozen (or more) messy, invasive, and/or low-quality commits, just to make sure the idea really works and is useful. Then we go back and restructure the pieces into sensible commits. Time pressures may prevent this, but that's an ideal way to break up a large commit into small, sensible pieces.

In the end, though, we must keep in mind the goal of using a version control system in the first place: it is to make our jobs, now and in the future, easier; to make our software more reliable; to make our software more maintainable; to meet quality control goals; and so on. One often-overriding goal from management is time, which can smash all other arguments to bits. Commit granularity has to be chosen with all of this in mind, and not everyone will agree on the correct granularity.


1Using git log's filtering mechanisms, which allow us to view only some of the commits that are the history, we can actually get the best of both worlds here: have highly detailed individual commits and good overviews. This requires discipline that Git itself does not enforce, though.

Consider this scenario.

  1. You refactor some code to a function name doThisThing, replacing 3 blocks of mostly identical code with calls to doThisThing, and you commit the change.

  2. On testing, you noticed one of the calls was misspelled doThsiThing, so you correct the typo.

Do you really think changing doThsiThing to doThisThing merits a separate commit, rather than just amending the commit in step 1 to correct the spelling?

If you share the commit, nobody looking at it can or needs to know if you originally misspelled the function name. A separate second commit would convey no useful information to anybody looking at the commit history.

Which approach is better depends on a number of factors, including the tooling you're using for review, whether your project has certain policies on commits or commit messages, and the preferences of your team.

The way Git and Linux do commit messages is typically considered the ideal: each commit describes a single logical change; has a thorough, descriptive commit message; and is bisectable (the testsuite passes both before and after it). However, this is a lot of work and discipline and many teams and projects don't want to implement it.

Some projects prefer that each change end up as a single commit because this avoids a lot of small “fixup” commits that contain unhelpful commit messages and lead to an untidy history. That can be done in one of a couple of ways:

  1. The continued use of git commit --amend.
  2. The building of a single commit and then additional commits tacked on with git commit --squash or git commit --fixup, squashed before merge with git rebase -i --autosquash.
  3. Multiple commits with a manual squash operation with git rebase -i before merge.
  4. A squash merge.

If you are using any workflow where you need to merge two long-running branches together repeatedly, squash merges are definitely the wrong choice and will only lead to breakage and sadness. Otherwise, any of these options are legitimate. You may prefer an approach like in 2 or 3 because it aids reviewability but still results in a single commit.

Since I contribute to Git, I write commits like the Git project requires and I see the value in it. I've more than once answered a colleague's question about something I'd worked on in the recent past by referring to a commit message.

When I'm developing, I do frequently use git commit --squash and git commit --fixup (or git commit --amend) to fix my previous mistakes and then squash those changes in to keep a tidy history. If I'm trying to provide context to a reviewer over the course of a pull request, I may push up those fixup commits for review before squashing them in later.

I sometimes implement a commit series with a generic new function or set of functions that introduce new functionality and then use that functionality later in the series. Sometimes it's not worth splitting out into a separate commit. I do generally try to avoid stub functions in commits.

If you're looking for inspiration about what one logical commit might contain, Git's history is a reasonable place to look. You need not understand the gory details to look at the overall structure of the code and commits.

I have some colleagues who are very tidy with their commit histories and others who are not. That's not a priority where I work, so people are free to do as they please provided they provide a helpful pull request message. However, if you do write nice commit messages, they are often helpful as a good pull request message as well.

Ultimately, the right decision is the one that your team can agree on. Git is a versatile tool and can adapt successfully to many workflows. If your team and tooling are okay with multiple commits and you think they're preferable, go for it.

Related