Skip to main content
PProfileMatch

Git Remotes, Pull Requests and a Team Workflow on GitHub

DevOpsLesson 3 of 47 min read

What you'll learn

  • Explain remotes, origin and remote-tracking branches
  • Tell git fetch, git pull and git push apart
  • Fix a rejected push using fetch and pull --rebase
  • Follow a branch, pull request and review workflow

On this page

  1. A remote is another copy of the repository
  2. First push, and a branch for Ben
  3. fetch, pull and push
  4. The rejected push, then the fix
  5. Pull requests: the GitHub part
  6. A workflow that holds up
  7. Common mistakes
  8. How this shows up in projects and interviews

Ben commits a coffee break to the conference schedule, runs git push, and gets ! [rejected] main -> main (fetch first). Nothing is broken. It means Asha pushed first, and the shared copy of main has a commit Ben does not have. This lesson recreates that exact collision with two clones of one repository, shows how to resolve it, and then moves to the part that depends on your host: pull requests.

A remote is another copy of the repository

A remote is a named link to another copy of the repository, usually on a server. By convention the one you cloned from is called origin. Hosting services such as GitHub, GitLab and Bitbucket store those copies and add review tools around them, but at the Git level they are just remotes. For each remote branch, Git keeps a remote-tracking branch such as origin/main: a bookmark of where that branch was the last time you talked to the server. It only moves when you fetch, pull or push, so it can be out of date.

To practise without an account, you can make a bare repository (one with no working folder, like a server's copy) and treat it as the remote. Everything below works the same against a hosted repository; only the address changes.

A practice remote on your own disk
git init --bare origin.git
git clone origin.git asha
git clone origin.git ben

First push, and a branch for Ben

Asha clones the empty repository, commits a draft schedule and pushes with -u, which links her local main to origin/main so later git push and git pull need no arguments. Ben clones, creates a branch for his lunch slot and pushes that branch. Commands are tagged with who runs them, and the transcript paths are shortened.

Terminal
[asha] $ git clone origin.git asha
Cloning into 'asha'...
warning: You appear to have cloned an empty repository.
done.

[asha] $ printf 'Day 1 09:00 Registration\nDay 1 10:00 Opening talk\nDay 1 16:00 Workshop\n' > schedule.txt

[asha] $ git add schedule.txt

[asha] $ git commit -m "Draft day 1 schedule"
[main (root-commit) c146bd6] Draft day 1 schedule
 1 file changed, 3 insertions(+)
 create mode 100644 schedule.txt

[asha] $ git push -u origin main
branch 'main' set up to track 'origin/main'.
To /srv/git/conf/origin.git
 * [new branch]      main -> main

[asha] $ git remote -v
origin	/srv/git/conf/origin.git (fetch)
origin	/srv/git/conf/origin.git (push)

[asha] $ git branch -vv
* main c146bd6 [origin/main] Draft day 1 schedule

[ben] $ git clone origin.git ben
Cloning into 'ben'...
done.

[ben] $ git switch -c add-lunch
Switched to a new branch 'add-lunch'

[ben] $ printf 'Day 1 12:30 Lunch\n' >> schedule.txt

[ben] $ git commit -am "Add lunch slot"
[add-lunch 0d2e941] Add lunch slot
 1 file changed, 1 insertion(+)

[ben] $ git push -u origin add-lunch
branch 'add-lunch' set up to track 'origin/add-lunch'.
To /srv/git/conf/origin.git
 * [new branch]      add-lunch -> add-lunch

git branch -vv shows the link: [origin/main] after the commit message means main tracks that remote branch. Without -u, a plain git push on a new branch fails with fatal: The current branch topic has no upstream branch, and git pull complains that there is no tracking information; both messages tell you the exact fix.

fetch, pull and push

CommandWhat it doesChanges your files?
git fetchDownloads new commits and updates origin/* bookmarksNo
git pullgit fetch, then merge the remote branch into your current branchYes
git pull --rebasegit fetch, then replay your local commits on top of the remote branchYes
git pushUploads your commits and moves the remote branch forwardNo

The rejected push, then the fix

Asha pushes a day-2 panel to main first. Ben then inserts a coffee break near the top of the same file and pushes. Watch the order: Ben's push is rejected, fetch reveals the fork in the graph, and pull --rebase replays his commit on top of Asha's.

Terminal
[asha] $ printf 'Day 2 09:30 Panel discussion\n' >> schedule.txt

[asha] $ git commit -am "Start day 2 with a panel"
[main 3d658e6] Start day 2 with a panel
 1 file changed, 1 insertion(+)

[asha] $ git push
To /srv/git/conf/origin.git
   c146bd6..3d658e6  main -> main

[ben] $ git switch main
Your branch is up to date with 'origin/main'.
Switched to branch 'main'

[ben] $ sed -i '2a Day 1 11:00 Coffee break' schedule.txt

[ben] $ git commit -am "Add coffee break"
[main aea1f92] Add coffee break
 1 file changed, 1 insertion(+)

[ben] $ git push
To /srv/git/conf/origin.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '/srv/git/conf/origin.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

[ben] $ git fetch
From /srv/git/conf/origin
   c146bd6..3d658e6  main       -> origin/main

[ben] $ git log --oneline --graph --all
* aea1f92 Add coffee break
| * 3d658e6 Start day 2 with a panel
|/
| * 0d2e941 Add lunch slot
|/
* c146bd6 Draft day 1 schedule

[ben] $ git pull --rebase
Rebasing (1/1)
Successfully rebased and updated refs/heads/main.

[ben] $ git push
To /srv/git/conf/origin.git
   3d658e6..8e10a8c  main -> main

[ben] $ git log --oneline --graph
* 8e10a8c Add coffee break
* 3d658e6 Start day 2 with a panel
* c146bd6 Draft day 1 schedule

[ben] $ git push origin --delete add-lunch
To /srv/git/conf/origin.git
 - [deleted]         add-lunch

The graph made the problem visible: two commits grew from the same parent. A rebase took Ben's commit, applied it on top of Asha's, and gave it a new hash (aea1f92 became 8e10a8c), because a commit's hash depends on its parent. The result is a straight line that is safe to push, since it only adds to the remote. You can instead run git pull (a merge) and get a merge commit; both are valid, and teams pick one. To make rebase the default, run git config pull.rebase true. The edits touched different lines, so nothing conflicted; if they had, you would resolve them as in the previous lesson and continue with git rebase --continue.

Pull requests: the GitHub part

Everything above is Git. A pull request (PR) is a feature of the hosting service. GitLab calls it a merge request. It is a page that says: please merge the branch add-lunch into main, here is why, and here is the diff for discussion. Reviewers comment on lines, automated checks such as tests run against the branch, and when it is approved someone merges it with a button. The usual flow on GitHub:

  1. Branch from an up-to-date main and commit your work.
  2. Push the branch: git push -u origin add-lunch.
  3. Open the PR in the web page, or with the GitHub CLI: gh pr create --title "Add lunch slot" --body "Adds the 12:30 lunch slot to day 1.". Use --fill to take the title and body from your commits.
  4. Respond to review by pushing more commits to the same branch; the PR updates itself.
  5. Merge when approved and green. GitHub offers a merge commit, squash and merge, or rebase and merge.
  6. Clean up: delete the remote branch (git push origin --delete add-lunch or the button) and update your main.

The gh commands are GitHub CLI syntax that I checked against its help text but did not run, since they need a real GitHub repository and login. If you cannot push to a project (for example, open source), you fork it first: a fork is your own server-side copy, and the PR goes from your fork's branch to the original repository.

A PR description template
Title: Add 12:30 lunch slot to day 1

What changed
- Adds "Day 1 12:30 Lunch" to schedule.txt.

Why
- Attendees asked where lunch fits; the venue confirmed 12:30.

How I checked
- Opened schedule.txt in the preview; times are in order.

Notes for reviewers
- Day 2 is untouched; Asha owns that part.

A workflow that holds up

  • Treat main as always shippable. Many teams turn on branch protection in the host's settings so nobody pushes to it directly and checks must pass first.
  • One branch per change, named for the purpose (fix-login-redirect, add-lunch).
  • Keep PRs small enough to review in one sitting. A reviewer faced with 2,000 changed lines will skim.
  • Pull or rebase before you push, and never rewrite a branch other people have based work on.

Common mistakes

  • Force-pushing a shared branch. Symptom: teammates run git pull and get a wall of conflicts or their commits vanish from the remote. If you must force-push your own branch, use --force-with-lease, covered next lesson.
  • Working directly on `main`. Symptom: your half-finished commits block a hotfix, or git push is rejected because protection is on. Branch first.
  • Forgetting `-u` on a new branch. Symptom: fatal: The current branch ... has no upstream branch; the message prints the git push --set-upstream origin <branch> fix.
  • Committing a secret and pushing. Symptom: an API key in the history. Removing the file in a later commit does not remove it from earlier ones. Revoke and replace the key first; cleaning history is secondary.

How this shows up in projects and interviews

Your day-to-day on a team is this loop: branch, commit, push, PR, review, merge. Interviewers ask about fetch versus pull, merge versus rebase, and what to do when a push is rejected; the transcript above is a ready answer. For process-heavy questions, they may ask how you review a PR. A good answer covers reading the description, running it, checking tests and commenting on behaviour rather than style. Link your own PRs when you describe your experience in a resume, and see CI/CD for how the automatic checks on a PR are produced.

Try it yourself

Make a bare repository and two clones in a temp folder, exactly like the practice remote in this lesson. Have both clones commit to a notes.txt on main, make the second push get rejected, and fix it with git pull --rebase. Then repeat but make both commits edit the same line, and resolve the rebase conflict with git rebase --continue.

Show a hint

After a conflicted pull --rebase, edit the file, run git add, then git rebase --continue. If you panic, git rebase --abort returns to where you started.

Key takeaways

  • A remote is another copy of the repository; origin/main is your bookmark of where it was last seen.
  • git fetch updates the bookmarks only; git pull is fetch plus merge or rebase.
  • A rejected push means someone pushed first: fetch, integrate with pull --rebase or merge, then push again.
  • A pull request is a host feature for reviewing a branch before it is merged into main.
  • Use small branches, protect main and never force-push a branch others use.

Test yourself

A trick from this lesson's ride: origin/main is last week's postcard of the server, not a live camera. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is the difference between git fetch and git pull?

git fetch downloads new commits from the remote and updates your remote-tracking branches such as origin/main without touching your files. git pull runs a fetch and then merges (or with --rebase, rebases onto) the remote branch into your current branch. Fetch is the safe way to look before you integrate.

Why is my git push rejected?

Usually the remote branch has commits you do not have, so Git will not overwrite them; the message says (fetch first) or (non-fast-forward). Run git pull --rebase or git pull to integrate those commits and push again. If you rewrote history with amend or rebase, a force push with --force-with-lease may be what you actually need.

What is a pull request on GitHub?

A pull request is a GitHub page that proposes merging one branch into another and collects review comments, discussion and automated check results. It is a hosting feature, not a Git command; GitLab calls the same thing a merge request. After approval the branch is merged by merge commit, squash or rebase.