Unit 3 · Lesson 3

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.

# Look at what a branch actually is on disk cat .git/refs/heads/main 3f8a2c1d7e9f4b2a8c6d0e1f3a7b9c2e4d6f8a0b ← just a SHA hash # HEAD points to the current branch cat .git/HEAD ref: refs/heads/main # Creating a branch creates a new pointer file — no file copying git switch -c feature/intake-subsystem Switched to a new branch 'feature/intake-subsystem' cat .git/refs/heads/feature/intake-subsystem 3f8a2c1d7e9f4b2a8c6d0e1f3a7b9c2e4d6f8a0b ← same hash as main, for now

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.

Branch Graph Visualizer
click Next Step to begin
↑ step through the graph to watch branches diverge and merge
step 0 / 9

The Branch Commands

Creating and switching

# Modern syntax — create and switch in one command git switch -c feature/intake-subsystem Switched to a new branch 'feature/intake-subsystem' # Older syntax (still works, widely used in tutorials) git checkout -b feature/intake-subsystem # Switch to an existing branch git switch main Switched to branch 'main' Your branch is up to date with 'origin/main'. # See all local branches — the * marks your current branch git branch feature/intake-subsystem * main

Merging

# Switch to the branch you want to merge INTO git switch main # Merge the feature branch into main git merge feature/intake-subsystem Updating 3f8a2c1..7d4e9b2 Fast-forward src/main/java/frc/robot/subsystems/IntakeSubsystem.java | 52 ++++++++++ 1 file changed, 52 insertions(+) # After a successful merge, delete the feature branch — it's been absorbed git branch -d feature/intake-subsystem Deleted branch feature/intake-subsystem (was 7d4e9b2).
💡 Fast-forward vs. three-way merge

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

// What Git inserts into the file when it finds a conflict <<<<<<< HEAD private static final double INTAKE_SPEED_FORWARD = 0.75; ======= private static final double INTAKE_SPEED_FORWARD = 0.6; >>>>>>> feature/intake-subsystem
  • <<<<<<< HEAD — everything between here and ======= is your current branch's version (the branch you ran git merge from)
  • ======= — 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.

Merge Conflict Scenarios select to resolve →
01 Two programmers changed the same constant in Constants.java
02 Both branches added a different new method to Robot.java
03 Conflicting teleopPeriodic() helper calls — one branch added, one removed
04 Same import line edited differently on each branch
05 CAN ID for a new motor assigned differently on each branch
Conflict as it appears in the file
How do you resolve this?

After resolving: the three-step close

Every conflict resolution follows the same pattern regardless of what the conflict was about:

# 1. Edit the conflicting file(s) — remove all markers, keep what you want # 2. Stage the resolved file(s) git add src/main/java/frc/robot/Constants.java # 3. Complete the merge commit (Git opens your editor for the message) git commit [main a3c7f91] Merge branch 'feature/intake-subsystem' # Verify the log shows the merge commit with two parents git log --oneline --graph -5 * a3c7f91 Merge branch 'feature/intake-subsystem' |\ | * 7d4e9b2 feat(intake): add IntakeSubsystem with beam break detection | * 5c1a3f8 feat(intake): add Constants for intake motor CAN ID and speeds * | 4b8d2e1 fix(drive): correct swerve module array initialization order |/ * 3f8a2c1 chore: initial WPILib project setup
🔍 Event Observation

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:

feature/intake-subsystem New mechanism or capability

Use feature/ for any branch that adds new functionality — a new subsystem, a new autonomous routine, a new sensor integration.

fix/arm-encoder-offset Isolated bug or tuning fix

Use fix/ for a targeted repair — correcting a constant, fixing a null check, adjusting a PID gain that's causing oscillation.

refactor/extract-motor-helpers Restructuring without behavior change

Use refactor/ when you're improving code structure without changing what the robot does — extracting methods, renaming constants, cleaning up periodic logic.

auto/three-piece-center New or experimental auto routine

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.

mybranch No type, no description

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.

kates-branch-2 Named for a person, not the work

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.

🔍 LRI Observation

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.

# ❌ A branch that's lived too long git log --oneline main..feature/old-shooter f3a1d92 tweak shooter again e9b8c71 more shooter work ... a2d4f18 start shooter (47 commits ago) # ✅ Keep branches short — break big features into smaller ones # feature/shooter-subsystem-skeleton → merge when skeleton is done # feature/shooter-velocity-pid → merge when PID is working # feature/shooter-dashboard-logging → merge when logging is complete

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.

💡 VS Code makes conflict resolution visual

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

⚙️ Branching Discipline for Competition
  • 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 — contaminating main with 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 main is 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.

A teammate says "creating a new branch made a copy of all the project files." What is the accurate correction?
  • 1Correct — Git copies all tracked files into a new folder when you create a branch
  • 2Incorrect — a branch is a lightweight pointer (a file containing a SHA hash) that points to an existing commit; no files are copied, and both branches share all commits up to the point of divergence
  • 3Partially correct — Git copies only the files that have changed since the last commit
  • 4Incorrect — branches are stored in GitHub, not locally, so no local files are created
You run 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?
  • 1HEAD refers to the feature/shooter branch — the branch being merged in
  • 2HEAD refers to the very first commit in the repository's history
  • 3HEAD refers to the branch you are currently on — in this case main, which is the branch you ran git merge from; the section between <<<<<<< HEAD and ======= is main's version of that line
  • 4HEAD refers to the version of the file on GitHub — the remote state before either branch changed it
You merged 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?
  • 1Keep it — the branch contains work that would be lost if deleted
  • 2Keep it as an archive — delete it from GitHub but not locally
  • 3Delete it with git branch -d feature/intake — the commits are permanently in main's history, so nothing is lost; the branch pointer is now just noise that misleads the new team member into thinking active work lives there
  • 4Rename it to archive/intake — never delete branches in case you need to revert
💪 Practice Prompt

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.

  1. 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 --oneline shows the same commits on both machines.
  2. Programmer A: create a feature branch. Run git switch -c feature/add-intake-constants. Open Constants.java (or create it if it doesn't exist) and add: static final double INTAKE_SPEED_FORWARD = 0.75; and static final int INTAKE_MOTOR_CAN_ID = 5;. Commit with a proper message but do not push yet.
  3. Programmer B: on main, add a conflicting constant. In their clone on main, add the same INTAKE_SPEED_FORWARD constant but with a different value: 0.6. Also add static final int INTAKE_MOTOR_CAN_ID = 7;. Commit and push to main.
  4. Programmer A: push the feature branch and try to merge. Push feature/add-intake-constants to GitHub. Then switch to main, run git pull origin main to get Programmer B's change, and then run git merge feature/add-intake-constants. Git should report a conflict.
  5. 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 add and git commit.
  6. Push and verify. Push the merge commit. Both programmers should run git pull origin main and then git log --oneline --graph -5. Confirm the graph shows the branching and merge commit.
  7. Clean up. Delete the feature branch locally and on GitHub: git branch -d feature/add-intake-constants and git push origin --delete feature/add-intake-constants.
  8. 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.