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.
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:
- the head branch, with your proposed changes (
add-tests); - the base branch, which receives them (usually
main).
"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.
| Tab | What you use it for |
|---|---|
| Conversation | Description, review comments, the merge button |
| Commits | Each commit on the head branch |
| Checks | "Automated tests, builds, and other validations." This is CI, in Lesson 5.4 |
| Files changed | The 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
| Method | What lands on main |
|---|---|
| Merge commit (default) | All branch commits plus a merge commit; GitHub uses --no-ff. |
| Squash and merge | The branch's commits "squashed into a single commit." |
| Rebase and merge | Each 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.
-
$ git switch main $ git pull $ git switch -c add-tests -
If you have pandas and pytest installed, runimport 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) == 2python -m pytest -qand expect1 passed. -
$ git add test_clean.py $ git commit -m "Add test for drop_missing" $ git push -u origin add-tests -
$ gh pr create --fill # title and body from your commit $ gh pr view --web # open it in the browser--filluses "commit info for title and body"; add--draftto open it as a draft.6 Click through the four tabs. On Files changed, confirm you see onlytest_clean.py: that's the three-dot diff. -
$ gh pr merge --squash --delete-branch -
You should see one new commit for the whole PR, and$ git switch main $ git pull $ git log --oneline -3git branchshould no longer listadd-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
- Create a branch
- Make changes
- Create a pull request
- Address review comments
- Merge your pull request
- 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?
- A bookmark of GitHub's main
- A second branch you edit
- The branch HEAD names now
- A backup Git makes nightly
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?
- main, which receives the changes
- add-tests, which holds the changes
- origin, which stores both branches
- HEAD, which marks the merge
The head branch proposes changes; the base branch receives them.
You squash-merged clean-dates. What should you do next?
- Delete it; start fresh next time
- Keep committing on it for later
- Merge main into it every day
- Rename it so nobody reuses it
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?
- Nothing; issue twelve stays open
- Issue twelve closes right away
- GitHub blocks the merge entirely
- The issue moves to dev
Closing keywords only work when the PR targets the default branch; otherwise "these keywords are ignored."
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
- Chacon & Straub, Pro Git, §3.5 Remote Branches.
- Git reference, git-push --set-upstream.
- GitHub Docs, Branches: comparing branches in pull requests.
- GitHub Docs, Pull requests (tabs; draft pull requests).
- GitHub Docs, About pull request merges.
- GitHub CLI manual, gh pr create and gh pr merge.
- GitHub Docs, Linking a pull request to an issue.
- GitHub Docs, Managing the automatic deletion of branches.
- GitHub Docs, GitHub flow.