nanda mochammad
Engineering Notes

Git workflow for teams: the daily habits a shared repo runs on

7 min read
Tagged Git

Welcome back. Today we look at the daily Git habits that keep a shared repo free of weekly merge conflicts and a commit log nobody can read.

By the end of this article, you will know the six habits that take a change from a fresh branch to a clean landing on main: starting from an up-to-date branch, writing commits that explain themselves, opening small pull requests, reviewing well on both sides, staying in sync with rebase, and picking a merge style on purpose.

A branching strategy is a map. A workflow is how you actually walk it. Two teams can both say “we use GitHub Flow” and have completely different days. One ships calmly. The other drowns in merge conflicts, half-finished reviews, and a commit log nobody can read six months later.

The difference is a handful of habits, repeated, more than the tooling. Here is the loop I teach every engineer I mentor, from the moment they branch to the moment their work lands on main.

Step 01: Start clean

Branch from an up-to-date main, name it for a human

Half of all conflicts start at the very first command. Someone branches off an old main. Pull first, always.

git switch main
git pull --ff-only

git switch -c feat/transfer-retry

Use a short, common prefix so anyone scanning the branch list knows what they are looking at: feat/… for features, fix/… for bug fixes, chore/… for housekeeping. Keep the branch about one thing. “Add transfer retry” is a branch. “Q3 improvements” is a swamp.

Step 02: Commit so the log explains itself

Write commits for the person debugging this at 2am

That person is usually you. A commit message has two jobs. The subject says what changed. The body says why. The diff already shows the how.

Conventional Commits give this a small format that tools can read: type(scope): subject. That same format also powers automatic changelogs and version bumps.

feat(payments): retry failed top-ups with exponential backoff

A flaky gateway was dropping ~2% of top-ups on the first attempt.
Retries now back off 1s/2s/4s before surfacing an error to the user.

Closes #482

Common types: feat, fix, refactor, chore, docs, test. Keep the subject under about 50 characters. Write it as a command (“add”, not “added”).

Step 03: Open a reviewable pull request

Small, described, and green before you ask for eyes

A pull request is a request for someone’s time. Respect it by making the PR easy to say yes to. Keep it small (a few hundred lines beats a few thousand). Write a description that explains the why and how to test it. Make sure CI is green before you tag a reviewer.

Diagram: the pull request lifecycle: branch off main, make small commits, open a PR, pass review and CI, then merge into main. Branch off latest main Commits small, scoped Open PR describe the why Review + CI people + checks Merge into main
The pull request loop. Each handoff is a chance to keep changes small and the main branch always releasable: branch, commit, open the PR, pass review and automated checks, then merge.

A good PR description answers three questions in a few lines: what does this change, why now, and how did you verify it? Add screenshots for UI work. Add a one-line way to reproduce the bug for bug fixes. The reviewer should never have to guess your intent from the diff.

Step 04: Review with care, on both sides

Review the code, support the coder

Code review is one of the most useful habits a team has. It is where bugs die young and where knowledge spreads. It only works if both sides play their part.

  • As the author: keep it small, respond to every comment (even a “done”), and don’t take feedback on the code as feedback on you.
  • As the reviewer: be quick. A PR sitting unreviewed for two days blocks a person. Comment on what matters, and separate “this is a bug” from “I’d prefer”. Approve when it’s good enough to ship, not when it’s how you’d have written it.
Step 05: Stay in sync without rewriting shared history

Update your branch with rebase; never rebase what others have pulled

While your PR is open, main keeps moving. Pull those changes into your branch before the gap grows. Rebasing replays your commits on top of the latest main. That keeps history in a straight line and keeps conflicts small:

git switch feat/transfer-retry
git fetch origin
git rebase origin/main
# resolve any conflicts, then update the PR safely
git push --force-with-lease

Note --force-with-lease, not --force. It refuses to overwrite the remote if someone else pushed in the meantime.

Step 06: Land it

Merge commit, squash, or rebase: pick on purpose

How a PR lands shapes your history for good. Three options, three different shapes:

Diagram: three ways a pull request lands: a merge commit keeps full history, a squash collapses the branch into one commit, and a rebase replays commits onto a linear history. Merge commit merge node Keeps full history Squash 1 commit One tidy commit per PR Rebase replayed Linear, no merge node
The same pull request, landed three ways. A merge commit preserves the branch and adds a join node; a squash collapses the whole PR into one commit; a rebase replays commits onto a straight line with no merge node.

Most product teams I’ve worked with settle on squash merge. Each PR becomes exactly one tidy commit on main. The messy WIP history disappears. Reverting a feature is a single git revert. Choose merge commits when the individual commits are meaningful and worth keeping. Choose rebase-merge when you want a strictly straight-line history and your commits are already atomic.

 Merge commitSquashRebase merge
History shapeBranch preserved + merge nodeOne commit per PRLinear, no merge node
Easy to revert?The whole merge, yesYes, one commitPer commit
Log readabilityShows true branchingCleanest summaryClean, detailed
Best when…Branch context mattersDefault for most teamsCommits are already atomic

Whatever you pick, enforce it with branch protection: no direct pushes to main, required reviews, and required green CI. The rule that can be skipped at 5pm on a Friday is the rule that will be.

How should this PR land?

Choosing a merge style

Branch has messy WIP commitsjust want the outcome→ Squash(safe default)
Every commit is atomicwant a linear log→ Rebase merge
The branch’s shape is the storypreserve context→ Merge commit

Recap

The goal was to give you six habits that take a change from a fresh branch to a clean landing on main. Together they keep changes small, history honest, and your teammates unblocked.

  • Branch from an up-to-date main, and keep each branch about one thing.
  • Write commits where the subject says what changed and the body says why.
  • Keep pull requests small, described, and green before you ask for review.
  • Review quickly and kindly, on both sides.
  • Rebase your own branch to stay in sync, but never rebase a branch others have pulled. Then pick squash, merge commit, or rebase merge on purpose and lock it in with branch protection.

Not sure which branching model these habits sit inside? Start with Git branching strategies that scale.

I hope this writing helps you. Thanks for reading.

Cited sources