Pull requests keep code quality high, but the mechanics can feel clunky if you’re bouncing between browser tabs and terminal windows. Here’s a practical, story‑style walkthrough of how I go from “I need to change something” to “it’s merged,” using just git and the GitHub CLI (gh).
We’ll move step by step and explain why each step matters, not just what to type.
Start on a clean base: sync your default branch
Think of your default branch (main in most repos) as home base. Before you branch off to do work, make sure home base reflects the latest truth on GitHub. This avoids drift, surprise conflicts, and flaky CI later.
|
1 2 3 4 5 6 7 8 9 10 11 |
# Clone or enter the repo gh repo clone <owner>/<repo> cd <repo> # Confirm your default branch name (usually "main") MAIN=main # or: MAIN=master git fetch origin git checkout $MAIN git pull --ff-only origin $MAIN |
Why --ff-only? It guarantees your local branch moves forward cleanly without creating merge commits you didn’t intend.
Create a feature branch that tells a story
A good branch name communicates intent at a glance—use short, descriptive slugs like feat/…, fix/…, or docs/…. This keeps your local history tidy and makes the eventual PR easy to understand.
|
1 2 3 |
BR=feat/<short-descriptor> # e.g., feat/renewal-threshold-flag git switch -c "$BR" |
Now you’ve created an isolated workspace. Anything you do here can be reviewed independently and merged without touching main until it’s ready.
Make the change and prove it works
Edit the files, keep saves small, and run whatever tests or linters the repo expects. Small, incremental changes are easier to review and to debug when CI turns red.
|
1 2 3 4 5 6 7 8 9 |
# edit files git status git diff # run tests/linters your repo uses, for example: # make test # npm test # pytest |
If your repo uses a formatter (e.g., pre-commit, eslint, black), run it now so CI doesn’t flag style nits later.
Commit with intent (and sign if required)
A strong commit message explains what changed and hints at why. If your org uses Conventional Commits, follow that shape.
|
1 2 3 4 5 |
git add -A git commit -m "feat: <what> [why]" # If DCO is enforced: # git commit -s -m "feat: <what> [why]" |
Examples:
feat: add renewal threshold flag for Plesk LE automationfix: correct domain autodiscovery when DB fallback is useddocs: add cron example with flock
Publish your branch
Push your story to GitHub so others can see it and CI can test it.
|
1 2 |
git push -u origin "$BR" |
The -u sets upstream tracking—future git push/git pull will “just work.”
Open the PR with helpful context
PRs are for humans. Spend a minute to write a clear title and a body that answers three questions: What changed? Why? How did you test it?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
gh pr create \ --title "feat: <concise title>" \ --body "Summary: - <what changed> Context: - <why> Testing: - <how tested>" \ --base "$MAIN" \ --head "$BR" \ --reviewer <user1>,<user2> # optional # --label "area:infra","type:feature" # optional |
A quick sanity check:
|
1 2 3 4 5 6 |
# Open the PR in your browser to skim it visually gh pr view --web # See status/checks gh pr status |
Navigate reviews and required checks
Most orgs require at least one approval and passing CI. When CI fails, read the logs, reproduce locally, commit a fix, and push—CI will re‑run automatically.
If the repo says your branch is out of date with the base branch, bring it up to date. Rebasing keeps history linear and is preferred in many teams:
|
1 2 3 4 5 6 7 8 9 |
git fetch origin git rebase origin/$MAIN # resolve any conflicts: # edit files, keep changes # git add -A # git rebase --continue git push --force-with-lease # required after rebase |
--force-with-lease is the safe variant—it refuses to clobber remote work you don’t have locally.
If your team prefers merge updates instead of rebase:
|
1 2 3 4 |
git fetch origin git merge origin/$MAIN # resolve, commit, and push |
You can also approve from the CLI (when appropriate):
|
1 2 |
gh pr review --approve |
Merge cleanly, the way your repo expects
Once the green lights are on and approvals are in, merge using the method your repo allows. Squash is common because it keeps main readable.
|
1 2 3 4 5 6 7 8 9 |
# Squash merge (common default) gh pr merge --squash --delete-branch # Or rebase merge # gh pr merge --rebase --delete-branch # Or a merge commit # gh pr merge --merge --delete-branch |
If you’d rather set it and forget it, enable auto‑merge—GitHub will merge as soon as checks pass:
|
1 2 |
gh pr merge --squash --auto --delete-branch |
Land the change locally and tidy up
After the PR merges, bring your local main up to date and remove the working branch.
|
1 2 3 4 |
git checkout $MAIN git pull --ff-only origin $MAIN git branch -d "$BR" |
Working from a fork (no direct push to upstream)
If you can’t push branches to the upstream repo, fork it and send your PR from there.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# Fork and clone your copy gh repo fork <owner>/<repo> --clone=true cd <repo> # Ensure remotes: # origin -> your fork # upstream -> original repo (add if missing) # git remote add upstream https://github.com/<owner>/<repo>.git # Sync default branch from upstream git checkout $MAIN git pull --ff-only upstream $MAIN git push origin $MAIN # Create your feature branch, commit, and push to your fork BR=feat/<short-descriptor> git switch -c "$BR" # ...work... git push -u origin "$BR" # Open a PR *from your fork to upstream* gh pr create \ --title "feat: ..." \ --body "..." \ --base "$MAIN" \ --head "<youruser>:$BR" |
Quick troubleshooting
- “Updates were rejected because the remote contains work…” → You’re behind.
git pull --ff-onlyor rebase ontoorigin/$MAINbefore pushing. - CI fails on formatting/linting → Run the repo’s formatter/linter, commit, push.
- DCO / sign‑off required →
git commit --amend -s --no-edit && git push --force-with-lease. - Protected branch / PR required → You can’t push to
$MAIN. Always branch and open a PR. - “This branch is out‑of‑date with the base branch” →
git rebase origin/$MAIN(or merge), resolve, push.
That’s the whole journey: start clean, branch with intent, prove your change, communicate clearly, and merge in a way that keeps history readable. With these habits, PR‑required repos feel fast—not fussy.




