Lesson 4 of 9 · about 15 minutes

Push and pull requests

Your branch leaves your laptop, gets a review page, and lands on main in one clean commit.

You will push a branch to GitHub, open a pull request from the terminal, squash-merge it, and leave both your laptop and GitHub tidy.

Two copies, one name: origin

So far every branch has lived only on your laptop. GitHub holds another copy of the repository, called a remote. origin isn't a special word. It "is the default name for a remote when you run git clone."1

Git keeps a read-only bookmark for each branch it has seen on the remote, such as origin/main. These remote-tracking branches are "local references that you can't move; Git moves them for you whenever you do any network communication."1 They tell you where GitHub's branches were the last time you pushed or fetched.

Publishing a branch: push -u

A new branch doesn't exist on GitHub until you push it. The first push should set its upstream:

$ git push -u origin add-tests   # create add-tests on GitHub and link to it
$ git push                       # every later push: no arguments needed

The -u (--set-upstream) flag adds an "upstream (tracking) reference, used by argument-less git-pull and other commands."2 After that, git status tells you whether you are ahead of origin/add-tests, and plain git push and git pull know where to go.

Try it: publish a branch in the simulator

main has already been pushed. The italic origin/… labels show where GitHub thinks each branch is.

Notice what happened at task 3: plain git push refused because the branch had no upstream yet. That error message is one you'll see for real. Its fix is always git push -u origin <branch>.

The pull request: a review page for a branch

"Pull requests are proposals to merge code changes into a project."4 Every pull request has two ends:

"A pull request compares the proposed changes on the head branch with the base branch," and GitHub shows a three-dot diff: only what changed on your branch since it split from main.3 That is the same view as git diff main...add-tests from Lesson 2.

TabWhat you use it for
ConversationDescription, review comments, the merge button
CommitsEach commit on the head branch
Checks"Automated tests, builds, and other validations." This is CI, in Lesson 5.4
Files changedThe line-by-line diff. Review every line here, especially an agent's.

Not finished yet? Open a draft pull request. "Draft pull requests cannot be merged," and they are "useful when you want to share work-in-progress without formally requesting reviews."4

Choosing a merge method

GitHub offers three buttons.5

MethodWhat lands on main
Merge commit (default)All branch commits plus a merge commit; GitHub uses --no-ff.
Squash and mergeThe branch's commits "squashed into a single commit."
Rebase and mergeEach commit re-applied individually, with new SHAs, no merge commit.

Our recommendation for agent branches: squash. GitHub suggests squash "when a pull request represents one logical change, especially if the branch includes many small fixup commits."5 That describes a Claude Code session well: one task, many small commits. It comes with one rule:

"Squash merging works best for short-lived branches. If you keep working on the same head branch after a squash merge, later pull requests can include commits that were already squashed into the base branch."5 After a squash merge, delete the branch and start the next task on a fresh one.

Link the issue

Put Closes #12 in the PR description and issue #12 closes automatically on merge. But keywords only work when the PR targets the default branch: "If the pull request targets any other branch, then these keywords are ignored."7

Do it for real in penguins-lab

Work in the practice repository from Lessons 2–3. Its main already contains clean.py with drop_missing(df, cols). Tick each step as you go. Your progress is saved in this browser.

  1. $ git switch main
    $ git pull
    $ git switch -c add-tests
  2. import pandas as pd
    from clean import drop_missing
    
    def test_drop_missing_removes_nan_rows():
        df = pd.DataFrame({"bill_length_mm": [39.1, None, 40.3], "species": ["Adelie"] * 3})
        out = drop_missing(df, ["bill_length_mm"])
        assert len(out) == 2
    If you have pandas and pytest installed, run python -m pytest -q and expect 1 passed.
  3. $ git add test_clean.py
    $ git commit -m "Add test for drop_missing"
    $ git push -u origin add-tests
  4. $ gh pr create --fill          # title and body from your commit
    $ gh pr view --web             # open it in the browser
    --fill uses "commit info for title and body"; add --draft to open it as a draft.6 Click through the four tabs. On Files changed, confirm you see only test_clean.py: that's the three-dot diff.
  5. $ gh pr merge --squash --delete-branch
  6. $ git switch main
    $ git pull
    $ git log --oneline -3
    You should see one new commit for the whole PR, and git branch should no longer list add-tests.

Want GitHub to always tidy up for you? Settings → General → Pull Requests → Automatically delete head branches (needs admin rights).8

You just did GitHub flow

GitHub calls this "a lightweight, branch-based workflow," and its six steps are exactly what you did:9

  1. Create a branch
  2. Make changes
  3. Create a pull request
  4. Address review comments
  5. Merge your pull request
  6. Delete your branch

Every Claude Code task in Lessons 6–9 follows this same loop. The only change is who makes the commits in step 2.

Check yourself

Type each command from memory.

Publish the new local branch add-tests to GitHub and set its upstream.

Open a pull request for the current branch, taking the title and body from your commits.

Squash-merge the current branch's pull request and delete the branch locally and on GitHub.

What is origin/main on your laptop?

It's a remote-tracking branch: a read-only pointer to where main was on the remote when you last pushed or fetched.

In a pull request from add-tests into main, which is the base branch?

The head branch proposes changes; the base branch receives them.

You squash-merged clean-dates. What should you do next?

GitHub warns that continuing on a squashed head branch makes later PRs re-include commits already squashed into the base. Short-lived branches, deleted after merge.

Your PR targets dev, not main, and says Closes #12. What happens on merge?

Closing keywords only work when the PR targets the default branch; otherwise "these keywords are ignored."

Read GitHub's GitHub flow (5 minutes). It's the workflow behind the rest of the course. Then skim About pull request merges for the details of each merge button.
Anything fuzzy? Ask Claude: "Show me what git branch -vv prints in my penguins-lab repo and explain each column," or "When would I choose a merge commit over squash and merge?"

Sources

  1. Chacon & Straub, Pro Git, §3.5 Remote Branches.
  2. Git reference, git-push --set-upstream.
  3. GitHub Docs, Branches: comparing branches in pull requests.
  4. GitHub Docs, Pull requests (tabs; draft pull requests).
  5. GitHub Docs, About pull request merges.
  6. GitHub CLI manual, gh pr create and gh pr merge.
  7. GitHub Docs, Linking a pull request to an issue.
  8. GitHub Docs, Managing the automatic deletion of branches.
  9. GitHub Docs, GitHub flow.