Git and GitHub the Safe Way, Ch. 5: Branches and Pull Requests
Part of Git and GitHub the Safe Way, a short book on git, GitHub, and the habits that keep a repository from hurting you.
Until now every commit went straight onto main. That is fine alone, on a project nobody depends on. The moment a second person, or a deployed copy, or a future you depends on main being right, changes need somewhere to live while they are half done and somewhere to be read before they count. That is a branch, and a pull request is the reading.
The chapter follows two people, Ada and Mo, sharing a recipe repository. Where the examples push and pull, the remote is a real git repository on the same disk, so the conflict can be shown line by line; GitHub behaves identically, with a web page on top. The pull request itself is then done for real on the book’s practice repository, and that is also your exercise: fork it, branch, change the recipe or add your name to CONTRIBUTORS.md, and open a pull request against it.
A branch per change
A branch is a name for a line of commits. main is one. Starting another costs nothing:
git switch -c fix-serving-size
Switched to a new branch 'fix-serving-size'
Name it after the change, not after yourself or the date: fix-serving-size, add-buttermilk-note. Now edit, and commit as in chapter 3:
git commit -am "Say how many the recipe really serves"
[fix-serving-size 6179e08] Say how many the recipe really serves
1 file changed, 1 insertion(+), 1 deletion(-)
(-a stages every already-tracked file that changed; it is safe here because nothing new was created. New files still need git add, which is the point.)
Push the branch, telling git once which remote branch it tracks:
git push -u origin fix-serving-size
To ~/remote/recipes.git
* [new branch] fix-serving-size -> fix-serving-size
branch 'fix-serving-size' set up to track 'origin/fix-serving-size'.
On GitHub, the repository page now shows a yellow banner offering Compare & pull request for that branch. Click it, write a title (your commit’s first line is a good default) and a description that says why, and Create pull request. From the terminal, GitHub’s command-line tool does the same in one line; this is the first pull request on the book’s repository, #1, being opened exactly that way:
gh pr create --title "Say how many the recipe really serves" --body "Four is true but unhelpful; two hungry people is what actually happens. Tested by eating them."
https://github.com/t0ul/git-and-github-the-safe-way/pull/1
Nothing has changed on main. The change is visible, reviewable and runnable, and it is waiting.
Review your own diff first
Before asking anyone else, open the pull request’s Files changed tab and read it as if a stranger wrote it. You will find the debug line you forgot, the file you did not mean to include, the comment that no longer matches the code. Fix them with more commits on the same branch and push again; the pull request updates itself.
Keep pull requests small. One change, a few files, a description that fits on a screen. A reviewer can hold that in their head; they cannot hold forty files, and a forty-file pull request gets a glance and an approval, which is worse than no review. If a change is genuinely large, split it, or open it as a draft so people can watch it grow without being asked to approve it yet.
When main moves under you
While Ada’s branch waited, Mo changed the same line on main and pushed. Ada’s branch is now behind, and the two changes disagree. Fetch to see it:
git fetch origin
git log --oneline --all --graph
* fc7902c Say how many pancakes you get
| * 6179e08 Say how many the recipe really serves
|/
* 5927ab6 Add the pancake recipe
Two lines of history from the same starting point. Bring main into the branch:
git merge origin/main
Auto-merging pancakes.md
CONFLICT (content): Merge conflict in pancakes.md
Automatic merge failed; fix conflicts and then commit the result.
A conflict is not an error. It is git saying “two people changed the same lines and I will not guess.” It leaves both versions in the file, marked:
cat pancakes.md
# Pancakes
<<<<<<< HEAD
Serves 4, or 2 hungry people.
=======
Serves 4 (about 12 pancakes).
>>>>>>> origin/main
- 200 g flour
- 2 eggs
- 300 ml milk
Everything between <<<<<<< HEAD and ======= is yours; everything between ======= and >>>>>>> origin/main is theirs. Edit the file until it says what it should, with no markers left, then tell git the file is resolved and finish the merge:
git add pancakes.md
git commit -m "Merge main into fix-serving-size, keeping both facts"
git log --oneline --graph --all
* 87f0898 Merge main into fix-serving-size, keeping both facts
|\
| * fc7902c Say how many pancakes you get
* | 6179e08 Say how many the recipe really serves
|/
* 5927ab6 Add the pancake recipe
Here Ada kept both facts: “Serves 4 (about 12 pancakes), or 2 hungry people.” That is usually the answer; a conflict is two people being right about different things. If a merge goes badly, git merge --abort puts everything back the way it was before you started, and you can try again.
Push, and the pull request now shows it can be merged cleanly. Keeping a branch current this way, early and often, turns one large conflict at the end into several small ones you barely notice.
Merging the pull request
The green Merge pull request button offers three ways to land the change, and the choice is about what the history should look like afterwards:
- Create a merge commit keeps every commit from the branch and adds one that joins them to
main. History shows exactly what happened. Mo’s merge of Ada’s branch looks like this:
git log --oneline --graph
* ff4da82 Merge pull request #1 from ada/fix-serving-size
|\
| * 87f0898 Merge main into fix-serving-size, keeping both facts
| |\
| |/
|/|
* | fc7902c Say how many pancakes you get
| * 6179e08 Say how many the recipe really serves
|/
* 5927ab6 Add the pancake recipe
- Squash and merge folds the branch into a single commit on
main. Clean history, one line per pull request, and the branch’s messy intermediate commits (“fix typo”, “oops”) disappear. This is what most teams choose, and it pairs with chapter 6’s linear-history rule. - Rebase and merge replays the branch’s commits onto
mainone by one, no merge commit. Linear too, but it rewrites the commits, which matters in chapter 8.
Whichever you pick, delete the branch afterwards (GitHub offers a button; git push origin --delete fix-serving-size from the terminal). Then on your machine:
git switch main
git pull
git branch -d fix-serving-size
Switched to branch 'main'
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
...
Deleted branch fix-serving-size (was 87f0898).
git branch -d (small d) refuses to delete a branch whose commits are not merged anywhere; that refusal is a safety check, and -D overrides it, which is a chapter 8 matter. One last tidy: git branch -a will keep listing remotes/origin/fix-serving-size after GitHub has deleted it, because your machine remembers what it last saw. git fetch --prune forgets the remote branches that no longer exist.
Conclusion
A branch for every change, a pull request for every branch, your own review before anyone else’s, git fetch and git merge origin/main to stay current, conflicts resolved by reading rather than guessing, squash or merge, delete the branch. Even alone, this is the shape to work in, because next chapter you tell GitHub to insist on it.