← Back to Blog

Git Workflow on a Real Team: Branches, PRs, and Merge Conflicts

TL;DR

  • Never commit directly to main. Always work on a feature branch and open a pull request.
  • PRs should be small and focused. A PR that does one thing is reviewed faster and merged with fewer problems.
  • The review cycle is: push, wait for review, address feedback, get approval, merge. Understand where you are in that cycle at all times.
  • Merge conflicts are not emergencies. They're a normal part of collaborative work. Learn to resolve them calmly.
  • Rebasing and merging both have their place. Most teams have a preference. Know yours.

Git is one of those tools where the tutorials show you enough to work alone, but not enough to work on a team. Cloning a repo, committing, pushing: those parts transfer. But feature branches, PR workflows, rebase versus merge, handling conflicts on a shared codebase: these patterns are mostly learned on the job, usually under the mild stress of not wanting to break something.

This article covers how git actually works in a professional engineering context, including the patterns that matter most and the commands that tutorials usually skip.


Why You Never Commit Directly to Main

On a solo project, committing to main is fine. You're the only one working on it. If something breaks, you can roll it back.

On a shared codebase, committing directly to main creates problems.

Main (or master, or whatever your team calls the primary branch) is typically the branch that maps to production. Or it's the branch that staging and release branches are cut from. Changes that go to main without review can break things for every other engineer on the team. They skip the safety mechanism that code review provides. They make the git history harder to understand.

Most teams protect main with a branch protection rule: you literally cannot push directly to it. You can only get code to main by opening a pull request, getting it reviewed, and merging it through the standard process.

Even if your team doesn't have this protection enforced, follow it anyway. Committing directly to main on a team codebase is a habit that causes incidents.


Feature Branches: The Basic Pattern

The standard workflow is:

  1. Pull the latest main to your local machine: git pull origin main
  2. Create a new branch from main: git checkout -b your-feature-branch-name
  3. Do your work, committing as you go
  4. Push your branch to the remote: git push origin your-feature-branch-name
  5. Open a pull request from your branch to main

Branch naming conventions vary by team, but common patterns are: feature/ticket-id-short-description, fix/bug-description, or yourname/feature-description. Look at recent branches in your repo and follow the pattern.

Work in progress commits are fine while you're on your feature branch. But before opening a PR, it's worth reviewing your own commit history. If you have commits like "wip", "fix", "fix again", "actually fix", consider cleaning those up with git rebase -i to produce a cleaner history before others see it. More on this in a moment.


What Makes a Good Pull Request

A PR is not just a code submission. It's a communication artifact. How you write a PR description affects how fast it gets reviewed, how useful the feedback is, and how easy it is to understand the change in the future.

Keep PRs small and focused. The single biggest thing you can do to improve your code review experience is to make PRs that do one thing. Small PRs are reviewed faster, generate more specific feedback, and are easier to revert if something goes wrong. If your feature requires multiple logical changes (database migration, API endpoint, frontend integration), consider whether they can be separate PRs that build on each other.

As a rough rule: if your PR takes more than a few minutes to skim, it's probably too large. The engineers reviewing your code are doing this on top of their own work. A manageable PR gets reviewed today. A sprawling PR gets pushed to tomorrow and the day after.

Write a useful description. Your PR description should answer: what does this change do, why is it needed, and how did you approach it? It doesn't need to be an essay. A few sentences and a link to the relevant ticket is often enough. If there's something non-obvious about the implementation, explain it briefly. If there are known tradeoffs or things you'd like feedback on specifically, say so.

A minimal but solid PR description looks like:

Adds email validation on the registration form before the API call is made.

Related ticket: PROJ-142

I added a regex check on the client side to catch malformed emails before they hit the backend. The pattern handles the common cases (no @ symbol, no domain, etc.) but not every edge case, since the server validates too. Let me know if the pattern here looks right.

That's enough. It's specific, it explains the tradeoff, and it invites the exact type of feedback that's most useful.

Link to the ticket. Most teams track work in a project management tool. Link your PR to the relevant ticket. This creates a navigable history: from ticket to PR to merged code to deployment. It also helps reviewers understand the context of the change.


The Review Cycle

Understanding the review cycle lets you know what to do at each stage, rather than waiting and wondering.

You push and open the PR. At this point, your job is to assign reviewers (or let the team's conventions handle that) and wait. Don't ping people immediately unless it's urgent. Most teams check for open PRs as part of their daily flow.

Reviewers leave comments. This might take hours or a day, depending on the team's pace. When comments come in, address them promptly. For each comment: fix it, explain why you didn't fix it, or ask a clarifying question. Respond to every comment so the reviewer knows you've seen it. More detail on this in how to handle code review feedback.

You push additional commits to address the feedback. The PR stays open. Each commit you push is visible in the PR timeline. Reviewers can re-review and see exactly what changed.

The reviewer approves. Depending on your team's settings, one approval might be enough to merge. Others require two approvals. Check what your team's process is.

You merge. Most teams give the PR author the job of merging once the approvals are in. Don't merge someone else's PR without asking. Use the merge strategy your team prefers (squash, merge commit, or rebase merge). If you're unsure, ask once and then default to that going forward.


Merge Conflicts: How to Stay Calm and Resolve Them

Merge conflicts happen when two people change the same part of the same file on different branches. When you try to merge (or rebase), git doesn't know which version to keep, so it stops and asks you to decide.

This is not an emergency. It's not a sign that you did something wrong. It's a normal artifact of collaborative work.

When you open a PR and see a merge conflict, the standard approach is to update your branch against main:

git fetch origin
git rebase origin/main

Or if your team uses merges rather than rebases:

git fetch origin
git merge origin/main

Either way, git will pause at the conflict and mark the affected files. The markers look like this:

<<<<<<< HEAD
Your version of the code
=======
The version on main
>>>>>>> origin/main

You need to edit the file to contain the correct final version (sometimes your version, sometimes the other version, sometimes a combination of both), remove the conflict markers, save the file, and then run git add on the resolved file and continue the rebase or merge.

git add path/to/resolved/file
git rebase --continue   # if rebasing
# or
git commit              # if merging

A few things that help:

  • If the conflict is in a file you don't understand well, ask a teammate who knows that area before resolving. A wrong resolution can cause bugs that are hard to trace.
  • Most code editors and IDEs have conflict resolution UIs that make this easier to visualize. Use them.
  • If the rebase gets complicated (many conflicts, many files), it's sometimes cleaner to abort with git rebase --abort and take a different approach.

Rebasing vs. Merging

Teams have strong opinions about this. The difference is in how the history looks.

Merging creates a merge commit that records the point where two branches came together. The history shows that your branch existed, main continued in parallel, and they were joined. This preserves the full picture of what happened.

Rebasing replays your commits on top of the latest main, as if you had branched off after the latest changes. The history is linear: it looks like your work happened sequentially after everything on main.

Both approaches are valid. Most teams standardize on one to keep the history consistent.

If your team uses rebase-heavy workflows: git pull --rebase when pulling, git rebase origin/main before opening a PR, and a preference for squash commits on merge. The history will be clean and linear.

If your team uses merge-heavy workflows: git merge origin/main to update your branch, and merge commits are preserved. The history shows the full branching structure.

When you join a new team, look at the git log and PR merge history to see what commits look like. That will tell you which pattern the team uses. Then follow it. This isn't a place to introduce your preferences.


Git Commands That Tutorials Skip

Beyond the basics, a few commands come up regularly on real teams that most tutorials don't cover well.

git stash: Temporarily shelves uncommitted changes so you can switch branches or pull without conflicts.

git stash           # saves your uncommitted changes
git stash pop       # restores them
git stash list      # shows all stashed changesets

Useful when you're partway through something and need to quickly switch to fix something else.

git log with useful flags: The default git log output is verbose. More useful forms:

git log --oneline --graph --decorate   # compact visual history
git log --oneline -10                  # last 10 commits
git log --author="your name"           # your commits only

git diff: Understanding what changed before committing.

git diff                    # unstaged changes
git diff --staged           # staged changes (what will be committed)
git diff main...HEAD        # all changes on your branch vs main

Reverting a commit: When something needs to be undone.

git revert <commit-hash>    # creates a new commit that undoes a previous one

This is safer than git reset for commits that have already been pushed, because it doesn't rewrite history. git revert is the right tool when you've merged something that needs to be undone. git reset is for local work that hasn't been shared.

git bisect: For tracking down which commit introduced a bug.

git bisect start
git bisect bad              # current commit is broken
git bisect good <commit>    # this older commit was working

Git will binary-search through the history, checking out commits for you to test. When you find the first bad commit, run git bisect reset to exit. This is a niche but powerful tool for debugging regressions.

git blame: For understanding why a line of code was written the way it was.

git blame path/to/file

This shows the last commit that touched each line of a file. When you're onboarding to a new codebase and wondering why something is written strangely, git blame often leads to a commit message that explains it.


Keeping Your Branch in Sync

One thing that trips up junior engineers: opening a PR and then not touching the branch for a week while other work merges to main. By the time you address the feedback, your branch is far behind and the conflicts are extensive.

The fix is to keep your branch relatively current. While your PR is in review, periodically rebase or merge in the latest main. Not obsessively, but often enough that the gap doesn't grow large.

git fetch origin
git rebase origin/main

If there are conflicts during this rebase, resolve them the same way you would for a PR merge. Then force-push the rebased branch:

git push --force-with-lease origin your-branch-name

--force-with-lease is safer than --force because it will fail if someone else has pushed to your branch, preventing you from accidentally overwriting their changes.


Working with Git as a Team Practice

Good git hygiene is part of how teams maintain shared codebases over time. Commit hygiene matters beyond your portfolio: on a real team, clear commits make code review easier and make it possible to understand the history of a change six months later.

The patterns covered here aren't just conventions. They're the mechanisms by which multiple engineers can work on the same codebase without constantly stepping on each other. Understanding how they work and why they exist makes you a more reliable teammate.

When you're in your first 90 days, git workflow is one of the things that looks simple from the outside and turns out to have depth. The sooner you get comfortable with it, the more smoothly your work will move through the team's process.

If you want structured support getting comfortable with these workflows before your first job, here's how the Globally Scoped program works.

Interested in the program?