Skip to content

Repository

Bill Sacks edited this page Feb 22, 2023 · 4 revisions

Branching Policy

ESMF uses a branching policy adapted from GitFlow. There are two permanent branches, main and develop as well as branches for feature development and releases. The branching strategy allows developers to continue committing to an integration branch (develop) even while a release is in process. It supports patching previous releases. Finally, it support a branch (main) which offers a high degree of stability.

Description of branches:

  • develop is the main integration branch and undergoes nightly testing. Developers working on new features or fixes should merge changes into develop regularly to ensure timely integration and testing of code. Features need not be complete to merge into develop but should come in logical chunks. (Note: Releases should not contain partial features that are public facing, so merges into develop should in general not introduce a partial feature with a public API.) Regular "beta snapshot" tags are placed on develop as reference points.

  • main is the stable branch and contains tags for major and minor releases of ESMF. Developers are not committing or merging to main for day-to-day development. Instead, main is updated by merging from a release branch during a release process. main does not contain patch release tags since those tags must be applied to a release branch.

  • Feature branches (prefixed with: feature/, e.g., feature/update_io) are where development occurs for new features. In most cases feature branches are branched from the develop branch to stay up to date with the most recent integrated code. Feature branches are merged back into develop when branch testing indicates that the feature is ready for integration testing, or a logical subset of the feature is ready for integration testing.

  • Release branches (prefixed with: release/ e.g., release/8.1) are created at the start of a release cycle. The release branch branches off from develop. Heavy testing occurs on the release branch and only fixes that result from release testing are committed to the branch. These fixes are also merged back to develop so they are not lost (either immediately or post-release). When the release is ready, the release branch is merged into main and then main is tagged. The release branch is also merged back into develop. Release branches are retained to support patches.

  • Patches to previous releases are handled by checking out the release branch in question and committing the fix to the release branch. The patched release must be tagged on the release branch itself since in most cases the main branch will have moved on.

Recommended Git Workflow to Maintain a Clean First-Parent History

Introduction

Git allows a wide variety of workflows. This is both a strength and a weakness: sometimes consistency can be more valuable than flexibility. One area where consistency can help is maintaining a consistent meaning of the first parent of a merge commit, especially for merges to the develop branch. When you perform a git merge (or a git pull) of one branch into another (which could be the upstream/remote branch into your local branch), the first parent is the branch you are currently on and the second parent is the branch you are merging.

The main rule for maintaining a clean first-parent history is to never merge the upstream/remote copy of a branch into your local copy of that branch (especially for key branches like develop), because this reverses the expected meaning of first parent. (These problematic merges typically have log messages like Merge branch 'develop' of github.com:esmf-org/esmf into develop.) Instead, if your local copy and the upstream have diverged, you should either rebase your changes onto the upstream or introduce a temporary branch that you can merge into the upstream.

Rationale for Maintaining a Clean First-Parent History

A number of git sub-commands support a --first-parent option. This option can be very useful in a repository that maintains a clean first-parent history.

One common use of the --first-parent option is with the git log command. When git log --first-parent hits a merge commit, it will follow only the first parent. With the develop branch and other integration branches, if all integrators follow the recommendations in this section, this means that git log --first-parent will show (1) commits made directly to the develop branch (typically small fixes) and (2) the merge commits for any feature branches that were merged to develop. It will not show the individual commits made on feature branches. This can be a very useful way to see a high-level overview of the changes made to the develop branch between two points. Furthermore, even with a more complex graphical view of the full log, following the recommendations in this section will mean that the main-line history of the develop branch will appear along one margin (typically the left margin), with feature branches appearing to one side. Without maintaining a consistent meaning of first-parent, git log --first-parent is useless and a graphical git log takes more effort to understand, since you can no longer rely on the main-line of the develop branch appearing in any consistent place in the graphical log.

Other git sub-commands also support the --first-parent option. For example, if integrators follow the recommendations in this section, git blame --first-parent will show when a given line was introduced into the develop branch, rather than when it was introduced into the history overall.

Some third-party tools also rely on a clean first-parent history. For example, git-when-merged is a very useful tool for determining when a particular commit (e.g., one identified via git blame or git log -G) was merged into a git branch (typically an integration branch like develop). However, it only works well when following the recommendations in this section.

Recommended Git Settings for Integrators

The following git settings will help achieve a clean first-parent history without needing to remember to specify flags on all of your git pull and git merge commands. These settings assume that you use git merge when merging two different branches together and git pull when merging a branch's upstream (remote) copy into your local copy. So, with these settings, you should NOT do something like git merge origin/develop to merge the upstream develop branch into your local copy: instead, you should always use git pull for that scenario. (If this feels too restrictive, you can explicitly specify the corresponding flags to your git merge and git pull commands rather than relying on these configuration settings.)

The following prevents you from using git pull if your local branch has diverged from the upstream; you will be notified of this situation, at which point you can use the workflows documented later in this section; this is equivalent to specifying the --ff-only to git pull, and is important for ensuring that you maintain a clean first-parent history:

git config --global pull.ff only

The following isn't quite as important for a clean first-parent history, but ensures that a merge commit is always created when merging a feature branch into develop; this is equivalent to specifying --no-ff to git merge, and ensures that work on a feature branch always shows up as a single merge commit in the first-parent history (rather than having each individual commit on the feature branch show up in cases where a fast-forward merge is possible):

git config --global merge.ff false

Maintaining a Clean First-Parent History via Consistent Use of Feature Branches

A straightforward way to maintain a clean first-parent history is to create a feature branch for any changes, rather than committing directly to your local copy of develop. Then, when you are ready to integrate your changes into the shared develop, you would run:

git checkout develop
git pull
git merge feature/mybranch
git push

Maintaining a Clean First-Parent History When You Have Made Commits Directly to Develop

If you have made commits directly to your local copy of develop but the upstream develop on GitHub has progressed before you pushed your changes, there are two ways to recover while still maintaining a clean first-parent history:

Option (1): Rebase your changes

If you don't mind rebasing your changes (with all of the history-rewriting caveats that entails), then this is the simplest option. In this case, you can simply run:

git pull --rebase
git push

Option (2): Create a temporary branch and merge that into develop

If you want to avoid rebasing your changes, you can still maintain a clean first-parent history in this situation by running a few extra commands to effectively pretend that you had been using a separate feature branch all along. This is done by introducing a temporary branch, restoring your local copy of develop to match the upstream, then merging your temporary branch into the upstream develop (rather than merging the upstream develop into your local copy); the commands to do this are:

git checkout -b temporary_branch  # If possible, give this a meaningful name that summarizes the work you are merging
git checkout develop
git fetch origin
git reset --hard origin/develop
git merge temporary_branch  # Give this a helpful merge commit message, summarizing the work you are merging. If your local develop had a collection of miscellaneous commits, you can (for example) copy the result of 'git log --oneline develop..temporary_branch' into your commit message
git push

Losing the Race to Integrate

Occasionally, even when following the above recommendations, you will encounter problems due to "losing the race" to push your changes to the upstream. This situation can arise when:

  1. You do a git pull to update your local develop
  2. You merge some changes into develop
  3. Meanwhile, someone else has pushed new changes to the upstream develop on GitHub
  4. You try to do a git push of develop, but it is rejected

If the time between your git pull and git push is short, then this occurrence should be rare. When it does occur, you have a few options:

Option (1): Gracefully lose the race and merge again

The cleanest solution is to undo the merge you just did and start over:

git fetch origin
git reset --hard origin/develop
git merge feature/mybranch
git push

Option (2): Introduce a second merge

If your merge of feature/mybranch had significant conflicts, such that redoing the merge would be onerous, you can avoid losing your work by performing an additional merge. This slightly complicates the git history, but still maintains a clean first-parent history (note that this is essentially the same as the workflow given above for maintaining a clean first-parent history):

git checkout -b temporary_develop
git checkout develop
git fetch origin
git reset --hard origin/develop
git merge temporary_develop  # Give this a helpful merge commit message, saying which branch you are actually merging and anything else that you included in your original merge commit message
git push

Option (3): Do not try to maintain a clean first-parent history in this situation

As long as this is rare, it may be acceptable to allow exceptions to the clean first-parent history in this situation. In this case, you can deal with the issue by doing a non-fast-forward pull:

git pull --no-ff
git push

Other resources

For more details on maintaining a clean first-parent history, see the following resources: