What git commit practice is better?
Asked Answered
E

2

10

I truly believe that to have one commit on one issue is a good practice. I'm sure I read it somewhere in an article like “Best practices”.

As such, my workflow has been the following:

  • For a new issue, I create a new local branch with git checkout -b new-issue.
  • Commit all changes into it. Sometimes this involves lots of commits.
  • When done, I squash the commits and rebase them into current thematic branch.
  • If something goes wrong, I can git revert the commit, find the bug, fix it, and commit new patch into the thematic branch. I won't change the remote repository’s history.

But today, I was surprised to hear the following workflow:

  • Create new branch for the new issue.
  • Commit everything into it.
  • Use merge --no-ff to merge the issue branch with thematic branch (so we’ll have “merge-commit” that we can revert).
  • If something goes wrong, we can use git bisect to find the bug.

According to the 1st approach, we’ll have a clean git history, and no idea about overhead branches used during development.

According to the 2nd approach, we’ll have a very messy history, with a lot of ugly, unnecessary merges and commits for just one issue. However, we can use git bisect to find bugs. (Perhaps this is better for refactoring?)


  • What pros and cons do you see for both approaches?

  • Which approach do you use, and why?

  • In practice, have you actually used git bisect to find bugs? (I haven't…)

Elkeelkhound answered 21/3, 2013 at 16:37 Comment(1)
You can use the --first-parent option of git log to hide individual commits on merged-in branches. Not messy at all.Cly
C
5

In the end, it is largely a personal taste issue... can only explain my taste (and give a bit of justification for it).

I tend to keep the individual commits around, even when they are just "Fix dumb typos". Any "history rewriting" creates commits that never before existed, so are guaranteed never to have been tested. Besides, minimal commits make git bisect immensely useful when a bug surfaces later. Better to be able to narrow it down to a few changed lines than to a week's work, squashed together.

If a development branch gets a mess of a history, I clean it up (minimally, i.e., reverted commits just never happened, general fixes like whitespace or variable renamings might get applied earlier, some reordering to place related changes together). Commits still stay small, are rarely squashed. This cleanup I do incrementally mostly. Then I merge (or rebase) the cleaned up branch with the "official" one.

Cymoid answered 21/3, 2013 at 19:10 Comment(2)
Have you ever used git bisect? Also, let's say I'm developing new module for one of the projects, I made a lot of commits and save them at history. When I will need this module on other prject, I will copy it and create only one commit.Elkeelkhound
@viakondratiuk, not often. But when I needed to find out what broke a perfectly fine build, it allowed me to do the job of finding the culprit in a few tries. I'd never even tried looking by hand.Cymoid
I
8

The second approach doesn't have to have a lot of ugly and unnecessary merges and commits. This is what I prefer to do:

  1. create a new topic branch
  2. make a bunch of commits
  3. just before merging back to the parent branch, clean up the commits:
    • rebase onto the latest version of the parent branch
    • squash typo fix commits
    • split commits doing multiple things at once into separate commits
    • reorder the commits to make it easier for a reviewer to understand the sequence of changes
    • etc.
  4. merge with --no-ff into the parent branch

The above steps result in a history that looks like this:

*   354b644 Merge branch 'topic3'
|\
| * 54527e0 remove foo now that it is no longer used
| * 1ef3dad stop linking against foo
| * 7dfc7e5 wrap lines longer than 80 characters, no other changes
| * b45fbcf delete end-of-line whitespace, fix indendataion
|/
*   db13612 Merge branch 'topic2'
|\
| * 961eebf unbreak build by adding a missing semicolon
|/
*   a5b6b16 Merge branch 'topic1'
|\
... (more history not shown)

The above graph has all the same advantages of approach #1:

  • You can use the --first-parent argument to git log to get a concise summary that resembles what you would get with approach #1:

    * 354b644 Merge branch 'topic3'
    * db13612 Merge branch 'topic2'
    * a5b6b16 Merge branch 'topic1'
    ... (more history not shown)
    
  • You can still easily examine the entirety of changes made in a topic branch. For example, git diff 354b644^..354b644 will show you what was changed for topic #3.

But you get benefits that approach #1 can't give you:

  • The history is much easier to review: commits b45fbcf and 7dfc7e5 (for the topic3 branch) introduce a lot of noise but no actual logic changes. Someone trying to answer the question, "What logic changes were made for topic #3?" might have a hard time digging through the noise if all of those commits were squashed into one.
  • The merge commits nicely identify the context for the series of commits on the merged branch (e.g., this group of commits were made to address topic #3).
  • The finer granularity of commits makes it easier to figure out why a particular change was made, which can help distinguish accidental changes from intentional-but-subtle.
  • If multiple people collaborated on the branch, you can see who they all were and how much each person contributed.
  • The number of commits on the merged topic branch gives you a rough idea about how much was changed.
  • The time range of the commits can provide useful context.
  • You can easily cherry-pick a specific change made onto a different branch (e.g., cherry-pick the minimal change needed to fix a bug onto a release branch).

There is one disadvantage I can think of: It may be hard to configure your software development tools to only follow the first-parent path and ignore all of those intermediate commits. For example, there is no --first-parent argument to git bisect. Also, I'm not familiar enough with Jenkins to know how easy it is to configure it to prioritize building and testing the first-parent path over all the other commits.

Idomeneus answered 30/3, 2013 at 17:36 Comment(0)
C
5

In the end, it is largely a personal taste issue... can only explain my taste (and give a bit of justification for it).

I tend to keep the individual commits around, even when they are just "Fix dumb typos". Any "history rewriting" creates commits that never before existed, so are guaranteed never to have been tested. Besides, minimal commits make git bisect immensely useful when a bug surfaces later. Better to be able to narrow it down to a few changed lines than to a week's work, squashed together.

If a development branch gets a mess of a history, I clean it up (minimally, i.e., reverted commits just never happened, general fixes like whitespace or variable renamings might get applied earlier, some reordering to place related changes together). Commits still stay small, are rarely squashed. This cleanup I do incrementally mostly. Then I merge (or rebase) the cleaned up branch with the "official" one.

Cymoid answered 21/3, 2013 at 19:10 Comment(2)
Have you ever used git bisect? Also, let's say I'm developing new module for one of the projects, I made a lot of commits and save them at history. When I will need this module on other prject, I will copy it and create only one commit.Elkeelkhound
@viakondratiuk, not often. But when I needed to find out what broke a perfectly fine build, it allowed me to do the job of finding the culprit in a few tries. I'd never even tried looking by hand.Cymoid

© 2022 - 2024 — McMap. All rights reserved.