Git development strategies

Software development is almost universally coupled to version management today, but how and when a version control system like git is used still depends on chosen workflow, development/deployment targets and internal team structure.

Development workflows are building blocks

The following list contains fundamental workflows for using git during software development. Complete project development approaches often combine multiple strategies into a coherent philosophy like Git Flow (feature branches with long-lived branches) and Github Flow (trunk-based development with feature branches). There is no clearly defined or enforced "industry standard" git workflow across organizations, with many customizing an existing one to fit their needs. View this as a mix-and-match list of base building blocks, not a mutually exclusive list.

Pushing directly to main

If you have ever worked alone on a software project, you are likely familiar with this approach. You simply have a single master or main branch and commit changes directly to it, without feature branches or pull requests. Git works primarily as a history and code distribution mechanism, allowing the developer to easily revert to older versions, save code to a remote git server and optionally trigger an automated remote ci pipeline.

It has virtually no overhead and is extremely easy to set up, but does not scale well beyond a single developer and fails to isolate changes from each other.

Feature branch workflow

The most commonly used way to use git involves a primary development branch called main or master, while developing any changes in separate feature branches (often using pull requests to create/merge from a web UI). The separate feature branch serves as a way to review and adjust code until it is ready to be merged back into the main branch.

How changes are merged into the main branch history varies, with one of three common approaches chosen based on preference.

The easiest is simply merging feature branches into main with a normal merge commit. This retains all feature branch commits and allows finding the exact commit/author of a regression or bug, but is noisier during traces as the branching history is retained even when branches are deleted.

Squashing commits when merging into main solves the history branching and produces a main branch containing a sequence of complete features/changes, but loses commit-level granularity when later tracing the codebase issues.

Lastly, first rebasing the feature branch then fast-forward merging it into main tries to strike a middle ground, where all commits from the feature branch are retained conceptually (they are not exactly the original commits before and after rebasing, but informationally identical) and the history in main remains linear without any branching or merge commits. However, since rebasing alters the original feature branch history, teams creating sub-branches from feature branches usually avoid this approach.

Trunk based development

Software projects that want to heavily lean into CI automation, frequent continuous deployments and feature flags often favor trunk based development. This workflow uses a single primary main/master branch during development and either integrates changes directly or uses very short-lived feature branches. The idea is to make the primary branch reflect the current state of software development as closely as possible, while shifting deployment stability to automated (server-side) CI pipelines, often coupled with build/runtime feature flags.

This workflow aims to keep merge conflicts and resulting history diffs tiny to make the history easy to understand in both directions while continuously delivering small incremental changes to the main branch.

Trunk based development requires a very solid automated CI pipeline to catch errors in a fast moving codebase, alongside excellent testing discipline from developers to ensure logic errors are caught before deployment.

Long-lived branches

Software companies that ship long-term-supported (LTS) releases are forced to treat each release as an ongoing development effort of the codebase. A long-lived branch workflow exists exactly for this purpose, creating a new branch for each release rather than a single commit. Future fixes get incorporated into the current development version and then cherry picked (aka backported) into older LTS release branches. This workflow is typically combined with feature branches/PRs and frequently used by linux distribution repositories or commercial software vendors offering a fixed amount of support/security patches after purchasing a product license.

Fork workflow

In the open source world, collaboration typically works by forking an existing project repository, making changes to that private copy and opening a pull request to merge the changes back into the parent project when done (merge upstream). While conceptually very similar to the classic feature branch workflow, this version removes management overhead from maintainers as every interested party can independently decide to fork the repository without requiring prior authorization or write access to a branch in the parent repository. This decentralized collaboration feature is the primary reason why fork workflows have remained in use for open source projects even today.

More articles

Running OCI containers on incus

And how it differs from docker/podman

Understanding the linux tr utility

Mapping characters one by one