Articles › Git & DevOps

Git Workflow for Teams: Branching, Commits, Merge vs Rebase and Pull Requests

A practical Git workflow for real projects: branching strategies, writing good commits, merge vs rebase, resolving conflicts, pull requests and how to undo mistakes safely.

Every developer uses Git, but many only know add, commit and push. When a team grows — or when something goes wrong — you need a clear workflow and a few more tools. This guide covers a branching model that works for most teams, how to write commits people can understand, when to merge or rebase, and how to undo almost any mistake.

Git in one picture

Git stores snapshots of your project. Each commit points to its parent, forming a graph. A branch is just a movable label pointing at a commit, and HEAD is "where you are now".

main:      A ── B ── C ─────────── F (merge)
                      \           /
feature/login:         D ── E ──┘

Because branches are cheap labels, creating them for every piece of work costs nothing.

A branching strategy that works

  • main is always deployable.
  • Every change happens in a short-lived branch (hours to a few days).
  • Branches are merged through pull requests with review and CI.
  • Incomplete features hide behind feature flags instead of long-lived branches.

Git Flow (for scheduled releases)

Git Flow adds develop, release/* and hotfix/* branches. It's useful for products with versioned releases (desktop software, libraries), but it's heavy for web apps that deploy continuously.

Branch naming

Consistent names make history readable:

feature/article-search
fix/jwt-expiry-check
chore/upgrade-dotnet-9
docs/api-readme

Writing good commits

A commit should be one logical change with a message that explains why:

fix(auth): reject expired refresh tokens

Refresh tokens were validated only by signature, so expired tokens
could still issue new access tokens. Check ExpiresAt and revoke the
token family when an expired token is reused.

The Conventional Commits format (type(scope): summary) makes history scannable and enables automatic changelogs:

Type Meaning
feat new feature
fix bug fix
refactor code change without behavior change
perf performance improvement
docs, test, chore, ci documentation, tests, maintenance, pipelines

Tips:

  • Use the imperative mood: "add", "fix", "remove".
  • Keep the summary under ~70 characters.
  • Stage selectively with git add -p to keep commits focused.

The daily workflow

git switch main
git pull --rebase                       # get the latest main
git switch -c feature/article-search    # new branch

# ...work...
git add -p
git commit -m "feat(articles): add full-text search endpoint"

git fetch origin
git rebase origin/main                  # replay your work on top of latest main
git push -u origin feature/article-search
# open a pull request

Merge vs rebase

Both integrate changes from one branch into another — differently.

Merge creates a merge commit that joins two histories:

git switch main
git merge feature/article-search
  • ✅ Preserves the exact history.
  • ❌ Many merge commits can make the log noisy.

Rebase replays your commits on top of another branch, producing a linear history:

git switch feature/article-search
git rebase main
  • ✅ Clean, linear history; easier git bisect.
  • ❌ Rewrites commit hashes.

Warning: The golden rule — never rebase commits that others have already pulled (shared branches like main). Rebase your own local or feature branches freely; after rebasing a pushed feature branch, use git push --force-with-lease, never plain --force.

A common team policy: rebase your feature branch on main while working, then squash merge the pull request so main gets one clean commit per feature.

Cleaning up history with interactive rebase

Before opening a PR, tidy your commits:

git rebase -i origin/main

In the editor you can pick, reword, squash, fixup or drop commits. Combine "wip" and "fix typo" commits into meaningful ones.

Resolving conflicts

Conflicts happen when two branches change the same lines:

<<<<<<< HEAD
const pageSize = 12;
=======
const pageSize = 20;
>>>>>>> feature/pagination
  1. Decide the correct final code (sometimes a combination of both).
  2. Remove the markers, save, run the tests.
  3. git add <file> and continue: git rebase --continue or git commit for a merge.
  4. Unsure? git rebase --abort / git merge --abort brings you back to safety.

Small, frequent integrations keep conflicts small.

Undoing mistakes

Situation Command
Discard changes in a file git restore file.cs
Unstage a file git restore --staged file.cs
Fix the last commit's message or content git commit --amend
Undo the last commit, keep changes git reset --soft HEAD~1
Undo a pushed commit safely git revert <hash>
Throw away local commits completely git reset --hard origin/main ⚠️
Find a "lost" commit git reflog
Save work temporarily git stash / git stash pop

git reflog is your safety net: it records where HEAD has been, so even "deleted" commits can usually be recovered for a while.

Pull request best practices

  • Keep PRs small (ideally under ~400 changed lines). Small PRs get better reviews, faster.
  • Write a description: what changed, why, how to test, screenshots for UI.
  • Make CI run build, tests and linters on every PR.
  • Protect main: require reviews and passing checks before merging.
  • Review the code, not the person; suggest, don't command.

Useful extras

git log --oneline --graph --decorate --all   # visual history
git blame -L 40,60 Program.cs                # who changed these lines
git bisect start                             # binary-search the commit that introduced a bug
git cherry-pick <hash>                       # apply one commit to another branch
git tag -a v1.4.0 -m "Release 1.4.0"         # mark releases

And a .gitignore for .NET + React projects should include at least bin/, obj/, node_modules/, dist/, .env and IDE folders. Never commit secrets — if you do, rotate them immediately; deleting the file doesn't remove it from history.

Summary

A good Git workflow is simple: keep main deployable, work in short-lived branches, write focused commits with meaningful messages, rebase your own branches and merge through reviewed pull requests. Learn revert, reset, stash and reflog, and there's almost no mistake you can't recover from.

FAQ

Should I squash commits when merging?

For most product teams, yes: one commit per feature keeps main readable. Libraries that care about detailed history may prefer regular merges.

What's the difference between git fetch and git pull?

fetch downloads remote changes without touching your branch. pull is fetch plus a merge (or rebase with --rebase).

How often should I commit?

Whenever you reach a small, working step. Frequent commits are cheap and make it easy to go back.