A solid git workflow is the cheapest investment you can make in a team's codebase. Working alone, pushing everything straight to main causes no harm; but the moment two people touch the same file, the question of who changed what and when goes unanswered. A workflow built on feature branches and pull requests exists to prevent exactly that chaos: every piece of work grows on its own branch, and the main branch never breaks before anyone has seen the change.
In this guide I describe a simple but disciplined flow that works for every kind of team, from Metin2 server projects to Laravel web apps. The goal isn't to memorize flashy commands; it's to work knowing what you're doing and why.
Why a feature branch?
The main branch (main or master) must always be releasable. Anything you check out from it should build and run. The way to protect that is to keep half-finished work out of the main branch. Every new feature, bug fix, or experiment lives on its own branch:
- The main branch stays intact; production can be deployed from it at any time.
- Two people working in parallel never see each other's half-written code.
- If a feature is cancelled, deleting the branch is enough; no trace remains on main.
- Each branch is a self-contained set of changes that can be reviewed independently.
Creating and naming a branch
Before you start, update the main branch, then create a new branch off it. In modern Git it's a single command:
git switch main
git pull --ff-only
git switch -c feature/user-login
git switch -c creates the branch and moves onto it; the older git checkout -b does the same thing. Using a consistent prefix in branch names instantly shows the type of work when you scan the list:
feature/— a new featurefix/— a bug fixchore/— dependencies, configuration, cleanuprefactor/— internal restructuring that doesn't change behavior
Keep the name short but meaningful: fix/login-csrf, feature/payment-webhook. If there's an issue number, add it: feature/142-pdf-export.
Commit discipline
A commit should describe one logical change. "Everything in one commit" and "a separate commit for every line" are the two extremes to avoid. A good message answers why rather than what, because the "what" is already visible in the diff:
git add app/Http/Controllers/AuthController.php
git commit -m "fix: redirect to login instead of 419 when session expires"
The Conventional Commits format (feat:, fix:, chore:, docs:) is both readable and makes automated changelog generation easy. Keep the first line to 50–72 characters and, if needed, add detail after a blank line. Avoid committing half-finished code that doesn't compile; if you genuinely need an interim save, use git stash or a marked "WIP" commit and clean it up later with git rebase.
Keeping the branch current: merge or rebase?
While you work on your branch, the main branch moves ahead. There are two ways to update your branch. Merge integrates the changes from main into your branch and leaves a merge commit; the history is honest but looks tangled. Rebase replays your commits as if they had started on top of the latest main; the history stays flat and readable.
git switch feature/user-login
git fetch origin
git rebase origin/main
The practical rule: rebase freely to tidy the history of a local branch you haven't shared yet. But don't rebase a branch others have already pulled and force-push it (--force) — that corrupts your collaborators' history. In that case --force-with-lease is safer, because it refuses to push if something changed since the state you last saw.
The pull request: your change made visible
When the branch is ready, push it to the remote and open a pull request (PR):
git push -u origin feature/user-login
The PR is where the code is discussed before it merges. A good PR is small — changes under 400 lines are reviewed substantially better, because the reviewer can hold the whole thing in their head. The description should include:
- What changed and why: one sentence of context is usually enough.
- How it was tested: a screenshot, command output, or test steps.
- A link to the related issue (e.g.
Closes #142).
Automated checks (CI) — tests, linter, build — should run on the PR. Don't merge until it's green. This largely eliminates the "it worked on my machine" problem.
Code review and merging
Review isn't a gate; it's shared ownership. The reviewer doesn't just hunt for bugs — they ask about naming, edge cases, and whether a simpler solution is possible. Comments target the problem, not the person; "what happens in this case?" keeps the collaboration healthy in a way that "why did you do it like this?" doesn't.
Once approved, choose a merge strategy:
- Squash merge: collapses all of the branch's commits into one clean commit. The main history stays readable; this is the default for most teams.
- Merge commit: preserves the branch's full history. Good for large, meaningful commit sequences.
- Rebase merge: adds the commits onto main in a flat form, leaving no merge commit.
After merging, delete the branch — both remotely and locally. Dead branches piling up make it harder to read the real state of the repository.
git switch main
git pull --ff-only
git branch -d feature/user-login
git push origin --delete feature/user-login
Frequently Asked Questions
Is pushing directly to main never okay?
On a small solo project it's fine. But once more than one person is involved, make the main branch protected and route every change through a PR. That enforces both review and the CI safety net.
Should I use merge or rebase?
Both are valid; it's a team decision. A practical balance: rebase to keep your local branch current and to tidy commits, and squash merge to bring the PR into main. Never force-rebase a shared branch.
How big should a PR be?
As small as possible. Let it cover a single logical task. If a feature is large, split it into small PRs that can merge independently; it becomes easier to review and to roll back.
Struggling to settle your team's git flow? If you need help with branch strategy, CI setup, and a code-review culture, get in touch — let's build a simple workflow that fits your project.