Git Branching and Pull Requests: The GitHub Team Workflow

Working alone, you can commit straight to main forever. Add one teammate and that habit collapses the first time you both edit the same file. Git branching is the fix: grow each change on its own branch, review it in a pull request, and merge only when it is ready. This guide walks the whole workflow, real merge conflict and git bisect included, with every command’s actual output.

“A branch is a cheap experiment. Make it, break it, throw it away if you must.”

Last Updated: July 2026 | Tested on: Git 2.45, Python 3.14.6 | Difficulty: Intermediate | Reading Time: 17 minutes

Think of a branch like a photocopy of a shared recipe book. Say a cook named Aditi wants to try adding cardamom to the kheer recipe. She does not scribble in the master book that everyone else is cooking from. She photocopies the page, experiments on her copy, and only when the new version tastes right does she paste it back into the master. A Git branch is that photocopy: a private line of work that leaves main untouched until you are sure. Merging is pasting the finished page back in. And a merge conflict is what happens when two cooks edited the very same line and Git needs you to decide which words survive.

CONFLICTCommit 1Add AccountCommit 2add_interest methodmain branchRound to 2 decimalsfeature/monthlyApply interest monthlygit mergesame line changedon both sidesOpen account.py, read themarkersHEAD vs feature, edit to onefinal line, delete themarkersgit add + git commitrecords a merge commitmain now has roundedmonthlyinterest, full historypreservedGit Branch and Merge Topology: How a Conflict Forms and Resolves

The diagram traces the whole life of a conflict. Two commits build the shared history, then main and a feature branch each change the same line of account.py. When you merge, Git cannot guess which version wins, so it pauses and marks the clash. You open the file, keep the one line you actually want, delete the markers, and commit. The merge commit stitches both lines of history back together with nothing lost.

Prerequisites

You need Git installed and the basics from the Git basics tutorial: init, add, commit, and log. A free GitHub account is enough for the pull request and fork sections. We reuse the tiny account.py from the OOP project, so nothing new to install. If you want the automated git bisect at the end, have pytest available too.

Create a Branch for a New Feature

Here is our starting point: a bank Account class with deposit and withdraw, already committed on main. We want to add interest calculation, but we do not want half-finished code sitting on main where a teammate might pull it. So we branch. That is the heart of git branching: half-finished work lives on its own line of history. The command git switch -c creates a new branch and moves you onto it in one step (the -c stands for create).

📄 Terminal: create and switch to a feature branch

git switch -c feature/interest
git branch

▶ Output

Switched to a new branch 'feature/interest'
* feature/interest
  main

What happened here: The asterisk in git branch marks where you are standing right now, on feature/interest. The main branch still exists, frozen exactly as you left it. Nothing you do here touches it. The naming style feature/interest is a common convention: a category prefix, a slash, then a short description of the work. Now add the method and commit it on this branch.

📄 Terminal: add the feature, then inspect and commit

# after adding an add_interest() method to account.py
git status -s
git diff
git commit -am "Add add_interest method"

▶ Output

 M account.py

diff --git a/account.py b/account.py
index b0bfcd1..b3350de 100644
--- a/account.py
+++ b/account.py
@@ -6,3 +6,7 @@ class Account:
     def deposit(self, amount):
         self.balance += amount
         return self.balance
+
+    def add_interest(self, rate):
+        self.balance += self.balance * rate
+        return self.balance

What happened here: git status -s shows a short view: M means account.py is modified. git diff shows the exact lines that changed, with + in front of every added line. The four new lines are our interest method, and none of the old lines were touched. The commit records this snapshot on feature/interest only. Switch back to main and the interest method vanishes, because on main it was never added.

Merge the Feature Back Into main

The feature works and is committed. Time to fold it back into main. You switch to the branch you want to merge into, then run git merge naming the branch you want to pull from. Because main has not moved since we branched, Git can simply slide its pointer forward. That shortcut is called a fast-forward.

📄 Terminal: merge the branch into main

git switch main
git merge feature/interest
git branch -d feature/interest

▶ Output

Switched to branch 'main'
Updating 7bf6d40..512cba0
Fast-forward
 account.py | 4 ++++
 1 file changed, 4 insertions(+)
Deleted branch feature/interest (was 512cba0).

What happened here: “Fast-forward” means main had no new commits of its own, so Git moved main forward to the branch tip with no extra merge commit needed. The interest method is now part of main. Once a branch is merged, git branch -d safely deletes it; the -d refuses to delete anything that has not been merged yet, so it doubles as a safety check. Feature branches are meant to be disposable, so deleting them after merge keeps your branch list clean.

Fixing a Real Merge Conflict

Fast-forwards are the easy case. The moment two branches change the same line, Git cannot decide for you, and this is the half of git branching people fear. Let us cause one on purpose. On a branch called feature/monthly we change interest to apply monthly, and separately on main we change the same line to round the result. Then we merge.

📄 Terminal: two branches edit the same line, then merge

git merge feature/monthly

▶ Output

Auto-merging account.py
CONFLICT (content): Merge conflict in account.py
Automatic merge failed; fix conflicts and then commit the result.

Do not panic when you see the word CONFLICT. It is not an error, it is Git asking you a question. Open account.py and it now contains both versions wrapped in markers.

📄 account.py: the conflict markers Git inserted

    def add_interest(self, rate):
<<<<<<< HEAD
        self.balance = round(self.balance + self.balance * rate, 2)
=======
        self.balance += self.balance * (rate / 12)
>>>>>>> feature/monthly
        return self.balance

What happened here: The three markers split the clash into two sides. Everything between <<<<<<< HEAD and ======= is the version already on your current branch (main, which rounds). Everything from ======= to >>>>>>> feature/monthly is the incoming version (monthly). Your job is to delete the markers and leave the one line you actually want. Often the best answer combines both ideas: round and apply monthly.

📄 account.py: resolved by keeping both ideas

    def add_interest(self, rate):
        self.balance = round(self.balance + self.balance * (rate / 12), 2)
        return self.balance

📄 Terminal: mark it resolved and commit

git add account.py
git commit --no-edit
git log --oneline --graph

▶ Output

*   be2e0d7 Merge branch 'feature/monthly'
|\
| * 9af0422 Interest applied monthly
* | 2333e0b Round interest to 2 decimals
|/
* 512cba0 Add add_interest method
* 7bf6d40 Add Account with deposit

What happened here: Once the markers are gone, git add tells Git “this file is fixed,” and the commit records a merge commit that ties both lines of history together. The graph shows it perfectly: the two branches split apart, each made its own commit, and the merge commit at the top rejoins them. Nothing was thrown away, and anyone reading the history later can see exactly how the two changes came together.

Push to GitHub and Open a Pull Request

On a team you rarely merge into main yourself. Git branching at work runs through review: you push your branch to GitHub and open a pull request (PR), which is a polite way of saying “please review my branch and merge it if it looks good.” The GitHub Command-Line Interface (CLI), gh, does this from the terminal. At the time of writing gh is version 2.x, and if it ever disappears the pure-web flow below does the exact same thing.

Quick rewind before you type anything: we deleted feature/interest after merging it earlier, so either recreate it with git switch -c feature/interest or picture yourself back at the moment just before that merge. On a team this is exactly the point where you would push the branch and open a PR instead of merging locally.

📄 Terminal: push the branch and open a PR with the GitHub CLI

git push -u origin feature/interest
gh pr create --title "Add interest calculation" \
             --body "Adds add_interest() to Account. Tests pass."
gh pr view --web

▶ Example output (needs a GitHub remote and gh login)

branch 'feature/interest' set up to track 'origin/feature/interest'.
Creating pull request for feature/interest into main in aditi/bank
https://github.com/aditi/bank/pull/1

What happened here: git push -u origin feature/interest uploads your branch to GitHub and remembers the link, so future pushes are just git push. gh pr create opens the PR and prints its URL. If you would rather not touch the CLI, the web flow is identical: push the branch, open the repo on github.com, and GitHub shows a yellow “Compare & pull request” button. Click it, write the same title and body, and press Create. Add a reviewer from the right-hand sidebar and they get notified.

When the PR is approved, GitHub offers three merge buttons. The one you pick shapes your history. A quick guide:

Merge optionWhat it doesBest for
Create a merge commitKeeps every branch commit plus a merge commitLong-lived branches where full history matters
Squash and mergeFlattens all branch commits into one tidy commitMost feature branches; a clean, readable main
Rebase and mergeReplays branch commits onto main, no merge commitTeams that want a strictly linear history

For everyday feature work, Squash and merge is the friendly default. Your branch might have ten messy “wip” and “fix typo” commits, and squash turns them into one clear commit on main. Reviewers reading history later see intent, not your keystrokes.

Fork, Contribute, and Keep Your Fork Synced

You cannot push branches to a stranger’s repository, so open source git branching works through a fork: your own copy of someone else’s project under your account. You change your copy, then send a PR back to the original. The flow, from the project’s GitHub page: click Fork, clone your fork, branch, commit, push, and open a PR against the original repo. The one habit beginners miss is keeping that fork current. The original keeps moving, and your fork goes stale within days.

📄 Terminal: point at the original and pull its updates

# one-time: name the original repo "upstream"
git remote add upstream https://github.com/original-owner/project.git

# every time you start new work: sync your main with theirs
git switch main
git fetch upstream
git merge upstream/main
git push origin main

What happened here: Your fork has two remotes now. origin is your copy on GitHub, and upstream is the original project. git fetch upstream downloads their latest commits without changing your files, and git merge upstream/main folds them into your local main. Push that to origin and your fork matches the original again. Always branch off a freshly synced main so your PR does not arrive full of conflicts.

Find the Breaking Commit with git bisect

A test that passed last week is red today, and there are forty commits in between. Reading them one by one is slow. git bisect does a binary search through history: it checks out a commit halfway back, you tell it good or bad, and it halves the range each time. Forty commits become about six checks. Better still, if you have a test command, git bisect run automates every step. Here is a repo where one commit quietly flipped a minus into a plus in a discount function.

📄 Terminal: let Git hunt the bad commit for you

git bisect start
git bisect bad HEAD              # today's commit is broken
git bisect good e5cd514          # this old commit was known good
git bisect run python -m pytest -q
git bisect reset                 # always end by leaving bisect mode

▶ Output

Bisecting: 1 revision left to test after this (roughly 1 step)
running 'python' '-m' 'pytest' '-q'
F                                                            [100%]
1 failed in 0.14s
Bisecting: 0 revisions left to test after this (roughly 0 steps)
running 'python' '-m' 'pytest' '-q'
.                                                            [100%]
1 passed in 0.02s
a267d12 is the first bad commit
    Refactor discount math
bisect found first bad commit

What happened here: You marked one known-bad commit and one known-good one, then handed Git a test command. git bisect run checked out the midpoint, ran pytest, read the exit code (non-zero means bad, zero means good), and repeated. In two steps it pinned the culprit: “Refactor discount math.” That is the commit to open and inspect. The final git bisect reset returns you to where you started; forgetting it leaves you stranded on an old commit, which trips up a lot of first-timers.

A README and License Turn a Repo Into a Portfolio Piece

A public repo with no README is a locked shop with the lights off. The README is the first thing a recruiter or teammate reads, so it is where your project earns a second look. Keep it short and answer three questions: what is this, how do I run it, and what can it do. A LICENSE file matters just as much, because without one, legally no one may reuse your code. MIT is the friendliest choice for a portfolio project.

📄 README.md: the front door of your repo

# Bank Account

A small Python class modelling a bank account with deposits,
withdrawals, and monthly interest. Built while learning OOP and Git.

## Run it

    python account.py

## Features

- Deposit and withdraw with an overdraft guard
- Monthly interest, rounded to two decimals
- Fully unit tested with pytest

## License

MIT, see [LICENSE](LICENSE).

What happened here: GitHub renders README.md automatically on the repo home page, so this becomes your project’s landing page with zero extra work. To add a license, GitHub has a built-in picker: open the repo, click Add file, name it LICENSE, and GitHub offers to fill in the full MIT text with your name and year. This account.py repo, with a README, a license, tests, and a clean branch history, is a real portfolio artifact you can link on a resume.

GitLab, Bitbucket, and Other Hosts

The Git commands are universal. Only the website wrapper around them changes, and mostly just the vocabulary. If your job uses GitLab instead of GitHub, this table maps the words so you are never lost:

GitHub termGitLab termSame underlying idea
Pull Request (PR)Merge Request (MR)Propose merging one branch into another
ActionsCI/CD PipelinesRun tests automatically on push
ForkForkYour own copy of a project
gh CLIglab CLIDrive the host from the terminal

Bitbucket also uses “Pull Request.” The point to remember: git switch, git merge, and git push behave identically no matter who hosts your repo. Learn Git once and every host feels familiar.

Common Mistakes

Mistake 1: Committing straight to main

Why it hurts: Half-finished work on main blocks the whole team, because anyone who pulls gets your broken code. Always branch first. On real projects, a branch protection rule on GitHub can even forbid direct pushes to main, forcing every change through a reviewed PR.

Mistake 2: Deleting the conflict markers wrong

Why it hurts: If you leave even one ======= or >>>>>>> line behind, you commit broken syntax that will not even parse. After resolving, search the file for <<<< to be sure none survived, and run your tests before committing the merge.

Mistake 3: Letting a fork rot

Why it hurts: Branch off a fork you synced three weeks ago and your PR arrives buried in conflicts the maintainer has to untangle. Sync upstream into your main before you start any new contribution, every single time.

Best Practices

  • One branch, one purpose. A branch that adds interest should not also rename three files. Small, focused branches are reviewed faster and revert cleanly.
  • Write commit messages in the imperative. “Add interest method,” not “added” or “adding.” It reads like a command Git is carrying out, which matches Git’s own messages.
  • Pull before you push. Sync main before opening a PR so your branch merges cleanly and you meet fewer conflicts.
  • Delete branches after merge. A merged branch has served its purpose. Clearing it keeps git branch readable.
  • Squash noisy feature branches. Ten “wip” commits become one clear story on main.

Conclusion

You just ran the full team workflow on a real project: branch a feature, merge it, resolve an honest conflict by reading the markers, push to GitHub and open a pull request, contribute through a fork, and let git bisect find a broken commit in seconds. You also gave the repo a README and a license, which turns a folder of code into something you can proudly link on a resume.

The one git branching habit that carries all of this: branch for every change, keep main always working, and let reviewed pull requests be the only door into it. Next, learn to give and receive good feedback in the code review tutorial. For every other topic in this series, from beginner basics to AI/ML projects, visit the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is the difference between git branch and git switch?

git branch by itself lists your branches, and git branch name creates one without moving to it. git switch actually moves you onto a branch, and git switch -c name both creates and moves in one step. On older Git you may see git checkout used for both jobs; switch and restore were introduced to split those two roles clearly.

How do I abort a merge that went wrong?

If you are mid-conflict and want to bail out entirely, run git merge –abort. It throws away the half-done merge and returns your files to exactly how they were before you ran git merge, so you can try again with a clear head.

What is the difference between a pull request and a merge?

A merge is the local Git command that combines two branches. A pull request is a GitHub feature that wraps a merge in a review: you propose the merge, teammates comment and approve, and then the merge happens through the website. PRs add discussion and approval on top of the plain merge.

When should I use squash and merge instead of a merge commit?

In everyday git branching, use squash and merge for feature branches, since it flattens messy work-in-progress commits into one clean commit on main. Use a full merge commit when the individual commits tell an important story you want to keep, such as a long-running branch with distinct milestones.

Does git bisect work without automated tests?

Yes. You can run git bisect manually: after each checkout Git hands you, test the behavior by hand and type git bisect good or git bisect bad. The automated git bisect run is just a convenience for when you have a test command that exits non-zero on failure.

Interview Questions on Git Branching

Scenario questions, not trivia: this is the form this topic takes in a real interview.

Q: What actually is a branch in Git, under the hood?

A branch is just a lightweight, movable pointer to one commit. It is a single line in a file under .git/refs/heads/ holding a commit hash. That is why creating a branch is instant and cheap: Git copies nothing, it only writes a new pointer. When you commit on a branch, the pointer moves forward to the new commit. This is the mental model that makes everything else about branching click.

Q: What is the difference between a fast-forward merge and a merge commit?

A fast-forward happens when the target branch has not moved since you branched, so Git just slides its pointer forward with no new commit. A merge commit is created when both branches have new commits, because Git needs one commit with two parents to tie the diverged histories together. You can force a merge commit even in the fast-forward case with git merge --no-ff, which some teams do so every feature is visible as a distinct merge in the history.

Q: A teammate says “just rebase your branch onto main.” What does that do and what is the risk?

Rebasing replays your branch’s commits one by one on top of the latest main, giving a clean linear history with no merge commit. The catch is that it rewrites commit hashes, so you must never rebase a branch other people have already pulled, because their history and yours diverge and pushes turn ugly. The safe rule: rebase your own local, unshared branches to tidy them; merge anything already shared.

Q: How would you find which of the last 200 commits introduced a performance regression?

git bisect, with a script as the test. Mark a known-good old commit and the bad current one, then write a small script that runs the slow operation and exits non-zero if it takes longer than a threshold. Hand it to git bisect run and Git binary-searches the 200 commits in about eight steps instead of two hundred. The key insight is that bisect works on any yes/no question, not only pass/fail unit tests.

Q: Why do teams protect the main branch and require pull requests?

Because main is what ships, so it must always be working. Branch protection blocks direct pushes and forces every change through a PR that runs the test suite and gets a human review before merge. This catches bugs and bad design early, spreads knowledge across the team, and leaves an auditable trail of who approved what. It trades a little speed for a lot of safety, which is the right trade for shared code.

Go deeper: Official Git documentation covers every edge case of this topic.

Previous: Git Basics for Python Developers: Commits, Diffs, and Undo

Next: Code Review in Python: Reading and Improving Other People’s Code

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *