Branching, Merging & Pull Requests

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

In the last lesson, Git commits became deliberate snapshots. Branches are how you make those snapshots in a focused line of work without disturbing main, and pull requests are how teams review that line before it becomes part of the shared project.

Branches are cheap lines of work

A branch is not a second copy of your project folder. It is a movable name pointing at a commit. When you create a branch, Git can do it instantly because it mostly creates a new pointer into the same history.

Start from the weather-widget project after the first Git lesson:

$ git log --oneline --decorate
c9e8a10 (HEAD -> main) Ignore local and generated files
f7b4d91 Show fallback text when weather data is missing
a13f2c4 Add weather widget starter

HEAD means "where your working tree is currently checked out." Right now it points to main.

Create a branch for one focused change:

$ git switch -c add-refresh-button
Switched to a new branch 'add-refresh-button'

That branch name is boring in the best way: it says what the work is for. Branch names like stuff, new-work, and ada-test become hard to understand as soon as more than one branch exists.

Now work normally. Edit files, check status, stage the coherent snapshot, and commit:

$ git status --short
 M src/renderWeather.js
 M src/styles.css
 
$ git add src/renderWeather.js src/styles.css
$ git commit -m "Add refresh button to weather widget"
[add-refresh-button 6d2a8f3] Add refresh button to weather widget
 2 files changed, 18 insertions(+), 3 deletions(-)

The branch moved forward. main did not:

* 6d2a8f3 (HEAD -> add-refresh-button) Add refresh button to weather widget
* c9e8a10 (main) Ignore local and generated files
* f7b4d91 Show fallback text when weather data is missing
* a13f2c4 Add weather widget starter

This is why teams use branches for nearly everything: a bug fix, a small UI change, a refactor, a documentation update. A branch gives the work a name, keeps it separate while it is incomplete, and gives other people something concrete to review.

Before switching branches, get in the habit of checking git status. A clean working tree is easiest to move around with. If you have half-finished edits, commit a real checkpoint or finish the thought before switching.

When histories diverge

While you are working, main can move too. Imagine Sam commits a small naming change on main:

* 6d2a8f3 (HEAD -> add-refresh-button) Add refresh button to weather widget
| * b4c19ad (main) Rename clear sky label to sunny
|/
* c9e8a10 Ignore local and generated files
* f7b4d91 Show fallback text when weather data is missing
* a13f2c4 Add weather widget starter

Your branch and main now have different commits after the shared c9e8a10 commit. That is normal. Git gives you two common ways to bring histories back together: merge and rebase.

Merge preserves the fork

A merge combines two lines of history and, when both sides have new commits, usually creates a merge commit. It preserves the fact that the branch split and later came back together.

If you merge main into add-refresh-button, the history shape looks like this after the merge commit:

*   e31c0b6 (HEAD -> add-refresh-button) Merge main into refresh button work
|\
| * b4c19ad (main) Rename clear sky label to sunny
* | 6d2a8f3 Add refresh button to weather widget
|/
* c9e8a10 Ignore local and generated files
* f7b4d91 Show fallback text when weather data is missing
* a13f2c4 Add weather widget starter

Merge is honest about what happened: two lines of work existed at the same time. For beginners, it is the safest default when you need to update a branch that other people can see. It does not rewrite existing commit IDs.

The tradeoff is that long-running branches can collect several merge commits. That is not automatically bad. It is just a more detailed history.

Rebase replays your work

A rebase takes the commits from your branch and replays them on top of a new base commit. Instead of preserving the fork, it makes the history look as if you started your branch from the newer main.

If you rebased add-refresh-button onto main before sharing it, the history could look like this:

* 9a45e21 (HEAD -> add-refresh-button) Add refresh button to weather widget
* b4c19ad (main) Rename clear sky label to sunny
* c9e8a10 Ignore local and generated files
* f7b4d91 Show fallback text when weather data is missing
* a13f2c4 Add weather widget starter

Notice the hash changed from 6d2a8f3 to 9a45e21. That is not a typo. Rebase creates a new commit with the same patch and message, but a different parent, so Git gives it a different identity.

Merge and rebase are not teams in a holy war. They answer different history questions:

  • Use merge when you want to preserve the true branch shape, or when the branch is shared and you do not want to rewrite commit IDs.
  • Use rebase when you have a private local branch and want to replay your work on top of the latest main before opening a clean pull request.
  • If you are unsure, merge. Then ask what convention your team uses.

The beginner safety rule is simple: do not rebase commits that other people may already be building on unless your team has explicitly agreed to that workflow.

Pull requests wrap a branch in review

A pull request is a request to merge one branch into another, usually a feature branch into main. Git itself has branches and commits. Hosting tools add the pull request page around them: discussion, file-by-file diffs, comments, approval, and automated checks.

Push the branch so the hosting tool can see it:

$ git push -u origin add-refresh-button
branch 'add-refresh-button' set up to track 'origin/add-refresh-button'.

Then open a pull request:

Pull request #14
Title: Add refresh button to weather widget
Base: main
Compare: add-refresh-button
 
Commits:
  6d2a8f3 Add refresh button to weather widget
  e31c0b6 Merge main into refresh button work
 
Files changed:
  src/renderWeather.js
  src/styles.css
 
Checks:
  lint       passed
  typecheck  passed
  test       passed

The pull request is not just a button that says "ship." It is a conversation attached to code. A teammate can ask why the label says "refresh," suggest a clearer name, or point out a missing test. Automated checks can run the same scripts every time so a broken branch does not quietly enter main.

When you make another commit on the same branch and push it, the pull request updates:

$ git add src/renderWeather.js
$ git commit -m "Tighten refresh button label"
[add-refresh-button d0f6a22] Tighten refresh button label
 1 file changed, 1 insertion(+), 1 deletion(-)
 
$ git push

That means review is not a separate document you email around. It stays attached to the branch, the commits, the diff, and the checks.

A safe beginner workflow

You can get very far with this loop:

  1. Start clean: git status.
  2. Create a focused branch: git switch -c add-refresh-button.
  3. Make one coherent change.
  4. Stage and commit useful snapshots.
  5. Push the branch and open a pull request.
  6. Respond to review with more commits on the same branch.
  7. Let the team merge the pull request according to project convention.

Branches give your work a contained place to happen. Merge and rebase are just two ways to arrange the history. Pull requests turn that branch into a reviewable unit.

Next lesson, you will focus on the human side of that review: how to read someone else's code carefully, give feedback kindly, and use team conventions without losing your own judgment.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion