Branching and Merging
Branching is how two programmers work on the shooter and the intake at the same time without breaking each other's code. It's also how you test a risky autonomous change before eliminations without touching the version that's currently winning matches. This lesson explains what branches actually are — and what to do when two branches disagree.
By the end of this lesson, you will:
- Explain what a branch is in terms of Git's commit graph — a pointer, not a copy
- Create, switch between, and delete branches using
git switch - Identify the difference between a fast-forward merge and a three-way merge
- Read a merge conflict in a file — understand what the markers mean and where each version came from
- Resolve a merge conflict in a
Constants.java-style file and complete the merge - Apply Team 2910's branch naming convention to a season workflow
What a Branch Actually Is
The word "branch" implies a copy — a separate folder, a parallel universe of files. That's not what Git branches are. A Git branch is a lightweight pointer to a single commit. Creating a new branch doesn't copy any files. It creates a 41-byte text file containing a SHA hash. That's it.
Every time you make a commit on a branch, the pointer moves forward to the new commit. main is a branch just like any other — it just happens to be the one everyone agrees is the "official" version. HEAD is a special pointer that tracks which branch you're currently on.
Right after creation, the new branch points to the exact same commit as the branch you branched from. The divergence happens when you make new commits on one branch but not the other — each pointer moves independently from that point forward.
Branching and Merging: The Commit Graph
The best way to understand branching is to watch the commit graph evolve as you run commands. Step through the visualizer below — it shows a real FRC session where one programmer builds an intake subsystem on a feature branch while main receives an unrelated bugfix from a teammate.
The Branch Commands
Creating and switching
Merging
When main hasn't moved since you branched, Git can simply slide the main pointer forward to your feature branch's latest commit — this is a fast-forward merge, and it leaves no merge commit in the history. When main has advanced (a teammate pushed something), Git must combine two diverging histories with a three-way merge commit — a new commit that has two parents. Both are valid. The visualizer above shows both types as you step through it.
Merge Conflicts: When Git Asks for Help
A merge conflict happens when two branches each modified the same lines of the same file. Git is good at merging changes to different parts of a file automatically, but when two people changed the same line, it has no way to know which version is correct — so it stops and asks you to decide.
Conflicts feel alarming the first time. They're not a sign that something is broken — they're Git being honest with you instead of silently picking a winner. Understanding the markers is the whole skill.
Reading the conflict markers
<<<<<<< HEAD— everything between here and=======is your current branch's version (the branch you rangit mergefrom)=======— the separator between the two competing versions>>>>>>> feature/intake-subsystem— everything between=======and here is the incoming branch's version
To resolve: edit the file to contain exactly what you want, then delete all three marker lines. You can keep HEAD's version, keep the incoming version, combine them, or write something entirely new. After editing, stage the file and commit.
Conflict Resolver
Each scenario below shows a real conflict from an FRC codebase. Select one to see the conflict markers and explore the resolution options.
Constants.java
▶
Robot.java
▶
teleopPeriodic() helper calls — one branch added, one removed
▶
After resolving: the three-step close
Every conflict resolution follows the same pattern regardless of what the conflict was about:
The most common conflict I see in FRC repos is in Constants.java — specifically CAN IDs. Two programmers each added a new motor to their respective branches without coordinating on the ID number. Both assigned CAN ID 12. The conflict surfaces at merge time, which is exactly when you want it to surface. The alternative — both committing directly to main — means one programmer's motor silently gets ID 12 while the other's disappears from the code entirely and nobody notices until the robot is on the field and one mechanism doesn't work. A conflict is better than silent overwrite every time.
Branch Naming: The 2910 Convention
Branch names should describe the work on that branch in a way that's immediately readable in a branch list. Team 2910 uses a two-part format: type/short-description. The same types from commit messages apply:
Use feature/ for any branch that adds new functionality — a new subsystem, a new autonomous routine, a new sensor integration.
Use fix/ for a targeted repair — correcting a constant, fixing a null check, adjusting a PID gain that's causing oscillation.
Use refactor/ when you're improving code structure without changing what the robot does — extracting methods, renaming constants, cleaning up periodic logic.
Use auto/ for autonomous path development. These branches often need extended testing before merging to main — the naming makes them easy to identify in the branch list.
A name like this tells no one — including you in three weeks — what work happened on this branch or whether it's safe to delete.
People move on. Naming branches after people tells you who made it but not what it contains. A branch should outlive its author's tenure on the team in terms of readability.
During code reviews with teams at competition, the branch list tells me a lot about how a team works. A team whose branches are named feature/shooter-velocity-control, fix/swerve-module-order, and auto/two-piece-amp is a team that communicates through their tooling. A team with branches named dev2, new, and test-thing is a team where only the person who made those branches knows what's on them — and when that person isn't at the competition, nobody does. Branch naming is free documentation.
Three Patterns That Create Problems
Pattern 1: Committing directly to main
When everyone commits directly to main, there's no safe place to experiment, no isolation between half-finished features, and no structured review before changes reach the "official" version. Lesson 4 covers protecting main with GitHub's branch protection rules. For now: treat main as a branch you merge into, not a branch you work on.
Pattern 2: Long-lived branches that diverge for weeks
A branch that exists for two weeks while main receives dozens of commits is accumulating merge debt. When it finally merges, the conflict set is enormous and the programmer resolving it has to understand weeks of work that happened on a parallel track. The 2910 rule: branches should live for hours to days, not weeks. Merge early and often.
Pattern 3: Pushing unresolved conflicts
Git will not let you push a file with unresolved conflict markers — a build that contains <<<<<<< in a Java file won't compile, and the error is obvious. But the pattern I see is programmers who "resolve" conflicts by deleting one side wholesale without reading what they're removing. The intake speed that one branch tuned to 0.75 during hours of testing gets silently replaced with 0.6 from the other branch because the resolver just kept HEAD's version without checking which was the tested one.
WPILib VS Code displays conflict markers inline with "Accept Current Change / Accept Incoming Change / Accept Both Changes / Compare Changes" buttons above each conflict block. You don't have to edit the raw markers manually. The buttons do the deletion for you. For complex conflicts, the "Compare Changes" view opens a two-panel diff that makes it clear exactly what each version contains before you choose.
🔌 System Check
- Create a branch before you start any new feature or fix. The command is six words:
git switch -c feature/your-description. There is no good reason not to. The cost of not doing it — contaminatingmainwith half-finished work — is paid at the worst possible time. - Name branches for the work, not the person. Branch names are team communication. If the branch name requires knowing who made it to understand what's on it, rename it.
- Merge before branches accumulate more than a day of divergence. Short-lived branches mean small conflict sets. Small conflict sets mean conflicts that take five minutes to resolve, not an hour.
- Read both sides of a conflict before accepting either. Never keep HEAD's version purely because it's yours. The incoming version may contain tuned values, tested logic, or fixes that your version doesn't have. Every conflict resolution is a judgment call that requires reading both sides.
- Build and deploy after resolving conflicts, before pushing the merge. A resolved conflict is only correct if the robot still compiles and the mechanism still behaves correctly. Conflicts in
Constants.java— especially motor IDs and current limits — can produce valid Java that makes hardware behave dangerously. - Delete merged branches. A branch that has been merged into
mainis history — it lives in the commit graph. Keeping the branch pointer around creates a list of stale names that nobody knows whether to touch. Delete on merge; the history is preserved.
Knowledge Check
Click an answer to check your understanding.
git merge feature/shooter from main and Git reports a conflict in Constants.java. You open the file and see <<<<<<< HEAD followed by your version and >>>>>>> feature/shooter followed by the incoming version. What does "HEAD" refer to in this context?feature/intake into main two weeks ago. The branch still exists locally. A new team member sees it in the branch list and asks if they should keep working on it. What should you do and why?Branch, Conflict, and Merge
This prompt requires two people — you and a teammate — or you can simulate both programmers by using two terminal windows on the same machine. The goal is to create a real merge conflict in a robot project and resolve it correctly.
- Set up. Start from the repo you pushed in Lesson 2's practice prompt. Both programmers should have a clone of it. Confirm
git log --onelineshows the same commits on both machines. - Programmer A: create a feature branch. Run
git switch -c feature/add-intake-constants. OpenConstants.java(or create it if it doesn't exist) and add:static final double INTAKE_SPEED_FORWARD = 0.75;andstatic final int INTAKE_MOTOR_CAN_ID = 5;. Commit with a proper message but do not push yet. - Programmer B: on main, add a conflicting constant. In their clone on
main, add the sameINTAKE_SPEED_FORWARDconstant but with a different value:0.6. Also addstatic final int INTAKE_MOTOR_CAN_ID = 7;. Commit and push tomain. - Programmer A: push the feature branch and try to merge. Push
feature/add-intake-constantsto GitHub. Then switch tomain, rungit pull origin mainto get Programmer B's change, and then rungit merge feature/add-intake-constants. Git should report a conflict. - Resolve the conflict. Open the conflicted file. Read both sides of each conflict. Make a deliberate decision for each one: which speed is correct? Which CAN ID? Write the resolved values, delete the markers, and complete the merge with
git addandgit commit. - Push and verify. Push the merge commit. Both programmers should run
git pull origin mainand thengit log --oneline --graph -5. Confirm the graph shows the branching and merge commit. - Clean up. Delete the feature branch locally and on GitHub:
git branch -d feature/add-intake-constantsandgit push origin --delete feature/add-intake-constants. - Bonus: Look at Team 2910's GitHub repository. Find any merged pull request and click on it. Read the "Files changed" tab. Can you find a merge commit in the history? What does the graph shape look like compared to a fast-forward? Write two sentences explaining the difference based on what you see.