A Practical Git Workflow for Solo Developers and Small Teams
A Practical Git Workflow for Solo Developers and Small Teams
A clean Git workflow does not need heavy branching or ceremony; it needs a repeatable way to keep work small, reviewable, and easy to undo. This guide shows a lightweight process built around short-lived branches, fast feedback, safe history editing, and deliberate cleanup, which aligns with modern trunk-based practices and Git’s own merge and bisect tooling.
Why this workflow works
The goal is to make each change easy to understand, test, and revert. Trunk-based development emphasizes a single long-lived branch plus short-lived branches when needed, which reduces merge pain and keeps the main branch deployable. Git’s bisect, revert, and branch cleanup tools give you escape hatches when something goes wrong.
A good workflow should optimize for four things:
- Small commits that are easy to review.
- A main branch that stays green.
- A clear path to fix mistakes without rewriting shared history.
- A fast way to find the commit that introduced a bug.
The core loop
Use this loop for most tasks:
- Sync your local main branch.
- Create a short-lived branch.
- Make small commits as you go.
- Rebase on main if needed.
- Open a pull request.
- Merge with a strategy that preserves clarity.
- Delete the branch after merge.
A simple version looks like this:
git switch main
git pull rebase origin main
git switch -c feature/login-form
Then work in small slices:
git add src/login-form.tsx
git commit -m "Add login form shell"
git add src/login-form.tsx src/auth.ts
git commit -m "Validate login input"
When the branch is ready, push and open a PR:
git push -u origin feature/login-form
Branching rules
Keep branches short-lived and purpose-specific. A branch should usually map to one task, one bug, or one small experiment, not a vague pile of unrelated work. That keeps review manageable and makes it easier to delete the branch after merging.
A useful naming pattern is:
-
feature/...for user-visible work. -
fix/...for bug fixes. -
chore/...for maintenance. -
spike/...for temporary investigation.
If a branch grows too large, split it. Long-lived branches are where merge conflicts, stale assumptions, and “big bang” reviews tend to appear.
Commit style
Each commit should represent one coherent step. That makes git log useful, makes reviews easier, and helps git bisect pinpoint problems faster. Git’s bisect workflow works best when history contains meaningful checkpoints rather than random WIP snapshots.
Good commit messages usually read like this:
Add login form shellValidate email inputHandle auth API errorsShow loading state while submitting
Avoid commits like:
fixchangeswipstuff
A simple rule: if you can’t explain the commit in one sentence, it’s probably too big.
Rebasing safely
Rebase is useful when you need to bring a branch up to date with main and keep history linear. It rewrites commit hashes, so it is best used on your own branch before merge, not on shared branches other people are already using.
Typical workflow:
git switch feature/login-form
git fetch origin
git rebase origin/main
If conflicts appear, resolve them, stage the fixes, and continue:
git add .
git rebase continue
Use rebase to keep your branch current, but avoid force-pushing shared history unless your team explicitly agrees on that practice.
Merge strategy
For small teams, squash merge is often the simplest default because it turns a branch into one clean commit on main. Rebase preserves a linear history, while merge commits preserve branch structure; squash is often best when you want the final main branch to stay readable and compact.
A practical rule:
- Use squash merge for small, self-contained work.
- Use merge commit when the branch history itself matters.
- Use rebase before merge to keep the branch current and conflicts manageable.
Example GitHub-style workflow:
- Open PR.
- Request review.
- Fix comments in the same branch.
- Rebase if main has moved.
- Squash merge after approval.
- Delete the branch.
Undoing mistakes
Not all mistakes are equal. If a bad commit has not been pushed, you can rewrite local history with git reset; if it is already shared, use git revert to create a new commit that safely undoes the old one without rewriting history.
Examples:
git reset soft HEAD~1
This keeps your changes staged so you can recommit them differently.
git revert HEAD
This adds a new commit that reverses the last one while preserving history.
A good habit is to prefer revert on shared branches and reset only on local work or private branches.
Finding broken commits
When a bug appears and you do not know where it came from, git bisect is one of the best debugging tools in Git. You mark one commit as good and one as bad, and Git uses binary search to narrow down the exact first bad commit.
Basic bisect session:
git bisect start
git bisect bad HEAD
git bisect good v1.4.2
Then test each checkout Git gives you:
git bisect good
### or
git bisect bad
When finished:
git bisect reset
If you have an automated test, you can often run bisect almost hands-free. That is one of the strongest reasons to keep commits small and testable.
Parallel work with worktrees
If you frequently need to juggle two tasks at once, Git worktrees are cleaner than stashing everything. A worktree lets you check out another branch into a separate directory while sharing the same repository data, so you can debug one issue and build another without context switching chaos.
Example:
git worktree add ../repo-hotfix hotfix/payment-fix
git worktree add ../repo-experiment experiment/new-nav
That gives you two independent working folders backed by one repository. It is especially useful when one branch is waiting on review and you need to start something urgent without disturbing the first task.
Pre-commit checks
Use hooks to catch obvious problems before they leave your machine. A pre-commit hook can run formatting, linting, or tests so bad code does not become a needless review cycle.
Example .git/hooks/pre-commit:
#!/bin/bash
set -e
npm run lint
npm test
Make it executable:
chmod +x .git/hooks/pre-commit
For team use, store the hook script in the repo and link or install it consistently so everyone gets the same checks.
A complete example
Imagine you need to add a password reset feature. Start by syncing main, then create a focused branch:
git switch main
git pull rebase origin main
git switch -c feature/password-reset
Make the work in logical commits:
- Add the reset form UI.
- Add validation.
- Wire the API call.
- Add tests.
If main changes while you are working:
git fetch origin
git rebase origin/main
Push the branch and open a PR:
git push -u origin feature/password-reset
After review, squash merge it, delete the branch, and move on. If the change later breaks login flows, use git bisect to isolate the bad commit or git revert to undo the damage safely.
What to avoid
Avoid huge branches, because they turn review into archaeology. Avoid committing broken code to shared branches, because it makes bisect and collaboration harder. Avoid using reset on public history unless your team has explicitly agreed that rewriting shared commits is acceptable.
Also avoid treating Git as just storage. A good workflow is a communication system: each commit, branch, and merge tells teammates what changed and why.
A simple team policy
If you want a policy that works for many small teams, use this:
- main is always releasable.
- Branches are short-lived.
- Commits are small and meaningful.
- Rebase before merge if needed.
- Squash merge by default.
- Revert shared mistakes.
- Use bisect for regressions.
- Delete merged branches quickly.
That gives you a workflow that is easy to teach, easy to debug, and hard to accidentally misuse.
-
Rizwan Saleem | https://rizwansaleem.co
Top comments (0)