How to buildOctober 11, 20269 min read
Programming FundamentalsStep 11 of 15

GitHub: fork, pull request and code review

In short

A pull request is a proposal to merge a branch with your changes into the project's main branch: on GitHub it gathers the description, discussion, review and automated checks. The order is: create a branch, commit, push it with git push -u origin branch, open a pull request with a clear description, answer review comments with new commits and merge after approval. Without write access to the repository you fork it first — make your own copy of the project.

How-toLevel: BeginnerGit & GitHubPractice: ~30–60 min

After this you will be able to

  • Push a branch to a remote repository and open a pull request with a description, a linked issue and a checklist
  • Choose a workflow: a branch in a shared repository or a fork with an upstream remote
  • Answer review comments with new commits and update the branch via merge or rebase with --force-with-lease
  • Explain what protected branches, required checks, SSH keys and fine-grained tokens give you

Verified with

Git 2.54 (Alpine Linux 3.24)OpenSSH 10.3markdownlint-cli2 0.23

Checked against docs: October 11, 2026

Contents
  1. 01Remote repositories: remote, push, fetch, pull
  2. 02Fork or branch in a shared repository
  3. 03Branch and pull request: description, linked issue, checklist
  4. 04Code review: giving and receiving feedback
  5. 05Updating the branch: merge or rebase
  6. 06Merging the pull request and cleaning up
  7. 07Protected branches and required checks
  8. 08SSH key or token: secure access

Remote repositories: remote, push, fetch, pull

The local repository from the Git lesson lives only on your computer. To work in a team you need a remote repository — a copy on a server everyone syncs with. Remotes get short names: the main one is conventionally called origin. On GitHub the address looks like https://github.com/user/repo.git or, for SSH, git@github.com:user/repo.git.

The whole scenario below is reproduced locally: a bare repository with no working directory (git init --bare) plays the part of GitHub, and a path stands in for the URL. The Git commands are the same as with GitHub. First the project lead publishes the repository, then the developer, Aigerim, clones it:

terminal
$ git remote add origin ../../server/coffee-site.git
$ git remote -v
origin	../../server/coffee-site.git (fetch)
origin	../../server/coffee-site.git (push)
$ git push -u origin main
To ../../server/coffee-site.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
$ cd ../../aigerim
$ git clone ../server/coffee-site.git
Cloning into 'coffee-site'...
done.
$ cd coffee-site
$ git branch -vv
* main c390cc0 [origin/main] Initial landing page
Git 2.54, Alpine Linux (real output). -u links the local branch to the remote one, so plain git push and git pull work from then on.

git clone URL

What it does
Downloads the whole repository and sets up the origin remote

git push -u origin branch

What it does
Sends the branch's commits to the server and remembers the link

git fetch

What it does
Downloads new commits into origin/… without touching your branches

git pull

What it does
fetch plus a merge into the current branch

git remote -v

What it does
Shows where the remotes point

The server accepts a push only if your branch continues its history. If someone else pushed first, your push is rejected — fetch their changes first:

terminal
$ git switch main
Switched to branch 'main'
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ git commit -am "Mention contacts in README"
[main f307c0e] Mention contacts in README
 1 file changed, 1 insertion(+)
$ git push origin main
To /w2/aigerim/../server/coffee-site.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '/w2/aigerim/../server/coffee-site.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git 2.54 (real output): a direct push to a main that has moved on. In teams with a protected main such a push is not allowed at all — changes go through pull requests.

Fork or branch in a shared repository

GitHub's documentation describes two collaboration models. Which one you use depends on whether you have write access to the repository.

Shared repository

  • Everyone can push to one repository
  • Each task is a separate branch; the pull request is opened inside the repository
  • Typical for small teams and private projects

Fork and pull

  • You create a fork — your own copy of someone else's repository — and push there
  • The pull request goes from a branch in your fork to the original (upstream) repository
  • Typical for open source: no need to ask the owner for access

A fork is created with the Fork button on the repository page or with gh repo fork. Then you clone your fork and add the original repository as a second remote — by the convention in GitHub's docs it is called upstream. Syncing a fork means fetching from upstream, merging and pushing to your fork:

terminal
$ git clone ../server/aigerim-fork.git coffee-site
Cloning into 'coffee-site'...
done.
$ cd coffee-site
$ git remote add upstream ../../server/coffee-site.git
$ git remote -v
origin	/w/forkwork/../server/aigerim-fork.git (fetch)
origin	/w/forkwork/../server/aigerim-fork.git (push)
upstream	../../server/coffee-site.git (fetch)
upstream	../../server/coffee-site.git (push)
$ git fetch upstream
From ../../server/coffee-site
 * [new branch]      main       -> upstream/main
$ git merge upstream/main
Updating 6cb64ae..ee2dd24
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
$ git push origin main
To /w/forkwork/../server/aigerim-fork.git
   6cb64ae..ee2dd24  main -> main
Git 2.54 (real output). The fork is modelled as a copy of the “server” repository (git clone --bare); on GitHub the Fork button creates it.

Branch and pull request: description, linked issue, checklist

Work starts with a branch off a fresh main. One branch, one task: that keeps the pull request small enough to actually review.

terminal
$ git switch -c feature/contact-form
Switched to a new branch 'feature/contact-form'
$ git add contact.html
$ git commit -m "Add contact form page"
[feature/contact-form 76e59a8] Add contact form page
 1 file changed, 7 insertions(+)
 create mode 100644 contact.html
$ git push -u origin feature/contact-form
To /w/aigerim/../server/coffee-site.git
 * [new branch]      feature/contact-form -> feature/contact-form
branch 'feature/contact-form' set up to track 'origin/feature/contact-form'.
$ git status
On branch feature/contact-form
Your branch is up to date with 'origin/feature/contact-form'.

nothing to commit, working tree clean
Git 2.54 (real output; contact.html was created in the editor before git add).
  1. Open the repository on GitHub

    After a push GitHub shows a yellow banner with a Compare & pull request button for the new branch.

  2. Pick the branches

    base is where to merge (usually main), compare is your branch. For a fork, click compare across forks and choose the head fork.

  3. Write a title and description

    The title says what the change does. The description follows the template below: what was done, how to test, screenshots.

  4. Create a pull request or a draft

    Create Pull Request sends it for review. Create Draft Pull Request makes a draft: it cannot be merged, and code owners are not automatically asked to review.

To give every pull request the same structure, add a template to the repository — GitHub inserts it into the description automatically. A Closes # line with an issue number links the PR to the issue: per the documentation, merging into the default branch closes the linked issue automatically (the keywords work only when the PR targets the default branch).

.github/pull_request_template.md
## What changed

In short: which task this pull request solves and what changed for the user.

Closes #

## How to test

1. Check out the branch and run the project.
2. Open the page or run the scenario: …
3. Expected result: …

## Screenshots

For UI changes: before and after (desktop and mobile).

## Author checklist

- [ ] The branch is up to date with `main`, no conflicts
- [ ] Checks (build, tests, linter) pass
- [ ] No keys, passwords or personal data in the code
- [ ] Docs and `.env.example` are updated if needed
- [ ] The pull request solves one task
Pull request description template. Checked with markdownlint-cli2 0.23 (rule MD041 about a top-level heading is disabled: the PR title is set separately).

You can create the same pull request from the terminal with GitHub CLI — the flags come from the official gh pr create manual:

bash
gh pr create --base main --title "Add contact form page" \
  --body-file .github/pull_request_template.md --draft
A fragment based on the GitHub CLI manual; not run in this lesson so as not to create anything on GitHub.

Code review: giving and receiving feedback

The reviewer goes through the Files changed tab, comments on lines and finishes the review with one of three decisions. Until the review is submitted, its comments are visible only to the reviewer and can still be edited.

Comment

Meaning per GitHub's documentation
General feedback without explicitly approving or requesting changes

Approve

Meaning per GitHub's documentation
The changes are ready to merge

Request changes

Meaning per GitHub's documentation
Feedback the author should address before merging
  • The change solves the task in the description and nothing more
  • Checks (build, tests, linter) are green
  • Names are clear; no dead or commented-out code
  • Errors and empty states are handled, not just the happy path
  • No secrets, passwords or personal data; user input is validated
  • For UI: the mobile layout and keyboard accessibility are checked
  • Every comment explains the “why” and suggests an option

Authors should remember that a comment is about the code, not the person. Fixes go in as new commits on the same branch — the pull request updates itself, and the reviewer sees what changed since last time. A reviewer can send a small fix as a suggestion: the author applies it with Commit suggestion, and GitHub creates a commit on the PR branch.

terminal
$ git diff
diff --git a/contact.html b/contact.html
index d909d01..b60a3dc 100644
--- a/contact.html
+++ b/contact.html
@@ -2,6 +2,6 @@
 <title>Contacts</title>
 <form method="post" action="/api/lead">
   <label>Name <input name="name"></label>
-  <label>Phone <input name="phone" type="tel"></label>
+  <label>Phone <input name="phone" type="tel" required></label>
   <button>Send</button>
 </form>
$ git commit -am "Make phone field required after review"
[feature/contact-form f9d0d45] Make phone field required after review
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
To /w/aigerim/../server/coffee-site.git
   76e59a8..f9d0d45  feature/contact-form -> feature/contact-form
Git 2.54 (real output): answering the comment “the phone field must be required” — edit, commit and push to the same branch.

Updating the branch: merge or rebase

While the review was going on, other people's commits landed in main. Update your branch before merging, so conflicts and test failures show up early. GitHub can do it with the Update branch button (as a merge or via Update with rebase); locally there are two ways.

terminal
$ git fetch origin
From /w/aigerim/../server/coffee-site
   c390cc0..08e9c80  main       -> origin/main
$ git log --oneline --graph --all
* f9d0d45 Make phone field required after review
* 76e59a8 Add contact form page
| * 08e9c80 Update opening hours
|/  
* c390cc0 Initial landing page
$ git merge origin/main -m "Merge origin/main into feature/contact-form"
Merge made by the 'ort' strategy.
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
To /w/aigerim/../server/coffee-site.git
   f9d0d45..4b8ff1d  feature/contact-form -> feature/contact-form
Option 1 — merge main into the branch. Git 2.54 (real output).
terminal
$ git fetch origin
From /w2/aigerim/../server/coffee-site
   c390cc0..08e9c80  main       -> origin/main
$ git rebase origin/main
Rebasing (1/1)Successfully rebased and updated refs/heads/feature/contact-form.
$ git push
To /w2/aigerim/../server/coffee-site.git
 ! [rejected]        feature/contact-form -> feature/contact-form (non-fast-forward)
error: failed to push some refs to '/w2/aigerim/../server/coffee-site.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To /w2/aigerim/../server/coffee-site.git
 + aea2fe5...0d33d81 feature/contact-form -> feature/contact-form (forced update)
$ git log --oneline --graph --all
* 0d33d81 Add contact form page
* 08e9c80 Update opening hours
* c390cc0 Initial landing page
Option 2 — rebase the branch onto the fresh main (a separate run of the same scenario). Git 2.54 (real output).

merge

  • Does not rewrite history; a normal git push works
  • Adds a merge commit
  • Safe when several people work on the branch

rebase

  • Linear history without a merge commit
  • Commits are recreated — you need git push --force-with-lease
  • Only for your own branch that nobody builds on

Merging the pull request and cleaning up

Once the review is approved and the checks pass, someone with write access clicks Merge. The merge method is configured per repository:

Create a merge commit

What lands in main
All the branch's commits plus a merge commit; per the docs, a merge with the --no-ff option

Squash and merge

What lands in main
One commit with all the changes; intermediate commits do not appear in main

Rebase and merge

What lands in main
The branch's commits one by one on top of main, no merge commit; GitHub creates new hashes
terminal
$ git fetch origin
From ../../server/coffee-site
 * [new branch]      feature/contact-form -> origin/feature/contact-form
$ git merge --no-ff origin/feature/contact-form -m "Merge feature/contact-form: contact form page"
Merge made by the 'ort' strategy.
 contact.html | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 contact.html
$ git push
To ../../server/coffee-site.git
   08e9c80..6cb64ae  main -> main
Git 2.54 (real output): the local equivalent of the Create a merge commit button, run by the project lead. On GitHub the Merge button in the pull request does this.

After the merge the author updates their main and deletes the branch — locally and on the server (on GitHub the pull request also has a Delete branch button for the remote branch):

terminal
$ git switch main
Switched to branch 'main'
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ git pull
From /w/aigerim/../server/coffee-site
   08e9c80..6cb64ae  main       -> origin/main
Updating c390cc0..6cb64ae
Fast-forward
 contact.html | 7 +++++++
 index.html   | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)
 create mode 100644 contact.html
$ git branch -d feature/contact-form
Deleted branch feature/contact-form (was 4b8ff1d).
$ git push origin --delete feature/contact-form
To /w/aigerim/../server/coffee-site.git
 - [deleted]         feature/contact-form
$ git log --oneline --graph -7
*   6cb64ae Merge feature/contact-form: contact form page
|\  
| *   4b8ff1d Merge origin/main into feature/contact-form
| |\  
| |/  
|/|   
* | 08e9c80 Update opening hours
| * f9d0d45 Make phone field required after review
| * 76e59a8 Add contact form page
|/  
* c390cc0 Initial landing page
Git 2.54 (real output). The graph shows the whole story: two task commits, the branch update and the merge commit.

Protected branches and required checks

To make sure nothing reaches main without review, the repository owner enables a branch protection rule (Settings → Branches) or a ruleset; unlike a classic rule, several rulesets can apply to the same branch at once. Per GitHub's documentation, every rule by default blocks force pushes to the protected branch and prevents its deletion. The main settings:

  • Require pull request reviews before merging — a number of approvals; optionally dismiss approvals after new commits and require review from code owners (CODEOWNERS).
  • Require status checks before merging — merge only with green CI checks; in strict mode the branch must also be up to date with the base.
  • Require conversation resolution before merging — every discussion in the PR must be resolved.
  • Require linear history — no merge commits: squash or rebase only.
  • Do not allow bypassing the above settings — by default admins can bypass the rules; this setting applies them to admins too.

If you try to merge a pull request with pending or rejected reviews into a protected branch, the documentation shows this server response: remote: error: GH006: Protected branch update failed for refs/heads/main. Availability depends on the plan: protection is free for public repositories, and for private ones it starts with GitHub Pro and Team.

SSH key or token: secure access

When you push over HTTPS, GitHub does not accept your account password: the documentation states that password-based authentication for Git has been removed. Instead you enter a personal access token or use a credential manager such as Git Credential Manager. The other route is an SSH key: a pair of files where the private key stays on your computer and the public key is added to your GitHub settings.

bash
ssh-keygen -t ed25519 -C "your_email@example.com"
The command from GitHub's documentation. It will ask for a file path and a passphrase — set one. Checked with a non-interactive equivalent on OpenSSH 10.3.
terminal
$ git remote -v
origin	https://github.com/YOUR-USERNAME/coffee-site.git (fetch)
origin	https://github.com/YOUR-USERNAME/coffee-site.git (push)
$ git remote set-url origin git@github.com:YOUR-USERNAME/coffee-site.git
$ git remote -v
origin	git@github.com:YOUR-USERNAME/coffee-site.git (fetch)
origin	git@github.com:YOUR-USERNAME/coffee-site.git (push)
Git 2.54 (real output): switching an already cloned repository from HTTPS to SSH.
  • GitHub recommends fine-grained tokens over classic ones: access only to the repositories and permissions you need, with an expiration date.
  • Treat a token like a password: never put it in code, in the repository URL or in messages.
  • Never share the private SSH key (id_ed25519 without .pub); upload only the .pub file to GitHub.
  • Revoke a leaked token or key in your GitHub settings right away and issue a new one.
How this was verified4 · passed1 · partial2 · not runchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.