Guides for SE student projects »

Working with Git

Organizing commits

Commits in a branch or a PR is said to be well-organized if they have the following qualities:

  • Each commit contains a single logical change, and this change must stand on its own. i.e. each commit has a single responsibility, and that responsibility must be fully carried out.
    For example, if the commit message says Move delete() from Person class to Address class, the commit cannot contain the addition of delete() to Address class only; it should also contain the deletion of delete() from the Person class for it to be a complete implementation what is stated in the commit message.

  • Each commit has a well-written commit message i.e., it follows these guidelines.

  • Commits are ordered in a bottom-up fashion, each commit building on top of the previous one towards the end goal of the PR.

    Rationale: Reviewers should be able to review one commit at a time.

  • Ideally, a commit does not modify more than 100 lines of code.

    Rationale: Bigger commits are harder to review.

    "Ask a programmer to review 10 lines of code, he'll find 10 issues. Ask him to do 500 lines and he'll say it looks good." --[source]

    Commits containing mechanical changes (e.g. automated refactorings, cut-paste type code movements, file renames, etc.),

    • should include only one mechanical change per commit e.g., rename a single variable across the code base.
    • should not contain other non-mechanical changes, unless unavoidable.
    • can exceed 100 LoC.
    • should have the description of the change in the commit message (so that the results can be reproduced).
  • Every commit pass CI. when you merge a series of commits (without squashing), every commit in your push (not just the last commit) should pass CI.

    Rationale: Build-breaking commits in the version history hinder the ability to use git bisect for locating bugs.

Here is an example PR of commits that are organized as described above.

Refactor commits before pushing. It is unlikely that you can produce a series of commits that meet all the above criteria in the first try. In such cases, refactor commits until they meet the criteria. This S/O post describes how to refactor commits (even easier to do with visual tools such as SourceTree -- see this video).

Merging branches

When merging branch, the aim is to keep the version history neat so that it is easy to do things such as the following:

  • Find which commit introduced a bug using git bisect.
  • Undo a specific change by reverting a commit in the history without breaking anything else.
  • The default strategy is to do a squash-merge. This is suitable when the branch tackles one task but multiple commits that are not well-organized (as per the definition of 'well-organized' in the panel below).

  • Use a merge commit if the commits are well-organized, and the branch tackles only one task. In this case the commit message of the merge commit should explain the full task.

  • Use a rebase-merge if the commits are well-organized and each commit is an independent task (as opposed to steps or a bigger tasks).

  • In other cases, consider reorganizing/splitting the branch to match one of the above.