Course outline
Course / The terminal and the language

Git: Working With Other People

Reading

The idea

Picture the save-game system again, but now a whole team shares one game. The shared main save is the one everyone relies on, so nobody plays straight into it. Each person works in their own save slot (a branch) and only moves progress into the shared save once the team has looked at it.

This lesson covers the habits that make that work: asking for review, reviewing and contributing to other people's projects, setting work aside, fixing a rejected push, settling clashes, finding what broke the game, and labeling saves clearly.

Setup: use the Ubuntu (WSL2) app on Windows, or Terminal on macOS and Linux. Start inside the gonomicon-git repo from the last lesson (cd ~/gonomicon-git if you are not already there) with a clean tree on main. Check with git status. You also need the second copy gonomicon-git-copy from step 8 of that lesson; it plays your teammate in this lesson.

Pull requests

A pull request (PR) is a note on GitHub saying "my slot is ready, please review it before it joins main." Reviewers read the changes, comment, and approve. Make a branch, change something, and push it:

git switch main
git pull
git switch -c add-support-note
echo "Support: help@example.com" >> README.md
git add README.md
git commit -m "Add support contact"
git push -u origin add-support-note

git switch -c creates a branch and moves onto it in one step. git push -u uploads it and pairs it with origin/add-support-note.

On GitHub, open the repo. A yellow banner offers Compare & pull request. Click it, check that the base branch is main, write a short title, and click Create pull request. Open the Files changed tab: green lines were added and red lines were removed. That tab is the review. When you are satisfied, click Merge pull request, then Confirm merge. (The green button has a small arrow offering two other ways: Squash and merge squeezes all the branch's saves into one, and Rebase and merge replays them on top of main. Teams pick one style; the plain merge is fine for you.) A PR also has a Conversation tab for comments, and sometimes automatic checks that must pass first. Back in the terminal:

git switch main
git pull
git log --oneline -3
git branch -d add-support-note

git log --oneline -3 shows the three newest saves, including your merge. git branch -d removes the finished branch locally. One side note: git fetch downloads new commits without touching your files, while git pull is a fetch followed by a merge.

Reviewing a pull request, and forks

Reviewing is half of the job. When a teammate asks you to review, open their PR and go to Files changed. Hover over a line and click the blue + to attach a comment to exactly that line. When you have read everything, click Review changes and choose one of three: Comment (just notes), Approve (this is good to merge), or Request changes (this must be fixed first). Good reviews ask questions, point at the exact line, and say why. Review the change, not the person.

You cannot push to a project you do not own. To contribute to someone else's repo you make a fork: your own copy on your account. The workflow has two remotes, and the names are a convention everyone follows: origin is your fork, and upstream is the original project.

git clone git@github.com:YOUR-USERNAME/Spoon-Knife.git
cd Spoon-Knife
git remote add upstream https://github.com/octocat/Spoon-Knife.git
git remote -v
git fetch upstream
git switch -c fix-typo

After the clone, git remote -v shows origin (your fork); the remote add line registers the original as upstream, and git fetch upstream downloads its latest saves without touching your files. You work on a branch, push it to origin with git push -u origin fix-typo, then open a pull request from your fork's branch to the original project's main. To bring your fork's main up to date later: git switch main, git fetch upstream, git merge upstream/main, git push. (octocat/Spoon-Knife is GitHub's own practice project: forking it is safe, and no one will merge your pull request unless you ask.)

git stash

A stash is a quick-save slot kept off to the side. When you are halfway through something and need to switch tasks, you tuck your unfinished work into it and come back later. Say you are mid-edit and a teammate asks for something urgent:

echo "Draft: confirm support hours." >> README.md
git status --short
git stash push -m "unfinished support note"
git status --short
git stash list
git stash pop
git diff
git restore README.md

The first git status --short shows M README.md (modified). After git stash push your files are clean again and the second status prints nothing. git stash list shows stash@{0}: On main: unfinished support note. git stash pop puts the work back and removes it from the list, and git diff proves it is there. The final git restore throws the draft away so the next exercise starts clean. If you want a stash gone without applying it, use git stash drop.

A plain stash skips brand-new files that Git has never saved. The -u option includes them:

echo "idea" > scratch.txt
git stash push -m "plain"
git stash push -u -m "with new files"
git stash list
git stash pop
rm scratch.txt

The first push prints No local changes to save, because scratch.txt is untracked. The second one stashes it, and git stash pop brings it back before rm deletes it.

When your push is rejected

You commit, run git push, and Git refuses:

To github.com:you/gonomicon-git.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'github.com:you/gonomicon-git.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. ...

Nothing is broken. Somebody (a teammate, or you from another computer) pushed saves to main that you do not have. Git will not let you overwrite them, so first you must bring them in. You can look before you touch anything with git fetch, then git status -sb, which prints ## main...origin/main [ahead 1, behind 1]: you have one save they lack, and they have one you lack. Then run git pull.

Often it just works. But when both sides have new saves, newer Git versions stop and say:

hint: You have divergent branches and need to specify how to reconcile them.
fatal: Need to specify how to reconcile divergent branches.

Choose how to combine them, once. git pull --no-rebase merges (you get a merge commit joining the two lines of work, and your editor opens for the message: in nano, Ctrl+O, Enter, Ctrl+X). git config --global pull.rebase false makes merging the default so you are not asked again. After the pull succeeds, git push works. If the pull hits a conflict, resolve it as in the next section.

The temptation is git push --force, which overwrites the remote with your version and permanently deletes your teammate's saves for everyone. Do not use it on a shared branch. --force-with-lease is a safer variant that refuses if the remote changed since your last fetch, but it is still destructive; leave it to the day you can explain exactly what it will delete. (Force-pushing your own private branch after you amended a commit is the one normal use.)

Merge conflicts

Two players edit the same line of the same file, each in their own slot. When the slots are merged, Git cannot know which version wins, so it stops and asks you. That is a merge conflict. It writes both versions into the file between marker lines:

<<<<<<< HEAD
your version
=======
their version
>>>>>>> other-branch

Everything between the <<<<<<< line and the ======= line is yours (the branch you are on). Everything between the ======= line and the >>>>>>> line is theirs. To resolve a conflict you decide the final text, delete all three marker lines, then run git add on the file and git commit. If you ever want to back out, git merge --abort returns you to where you were before the merge. A conflict can also appear during git pull (a pull is a merge) and when you merge a branch from a pull request; the markers and the fix are identical. You will create and resolve one in the steps below.

git blame and git bisect

Git blame is the label on each line saying which checkpoint last changed it. git blame FILE prints every line with the short id of that commit, the author, and the date. It answers "when and why did this line change?", because the commit id leads you to the commit message. It does not tell you whether the change was right. A leading ^ before an id just marks the very first save.

Git bisect hunts for the checkpoint that broke the game. You tell Git one save that worked and one that didn't, and it keeps halving the range until one commit is left. With 100 commits between them, it needs about seven checks instead of a hundred. You can check each commit by hand, or let git bisect run do it with a command: an exit status of 0 means good and anything else means bad. You will try both below.

Writing good commit messages

A commit message is the label on a checkpoint. Teammates, and future you, read these labels to understand the history. A good subject line is short (about 50 characters), starts with a verb, and says what changed: Document weekday support hours, not updates. Write the subject in the imperative, as if completing the sentence "This commit will...": Add support contact, not Added or adding. Keep it under about 50 characters, with no full stop. When the reason is not obvious, add a body with a second -m, or leave off -m entirely and your editor opens: type the subject on line 1, leave line 2 blank, then explain why in the lines below, and save and close. The body answers "why did you do this?" because the diff already shows "what". A git commit -m "updates" tells the next reader nothing.

Common mistakes

  • Merging a pull request without reading the Files changed tab.
  • Forgetting that a plain git stash leaves new, never-saved files behind. Use git stash push -u.
  • Leaving conflict markers (<<<<<<<) in a file and committing it. Always search for them before you commit.
  • Treating git blame as proof of fault. It says who last touched a line, not why or whether they were right.
  • Writing vague commit messages like stuff or updates.
  • Rewriting a commit (--amend) after you have pushed it. Teammates may already have the old version. To undo something pushed, use git revert.
  • Forcing a push (--force) to get past a rejection. Pull and merge first.
  • Working on a stale main. Run git pull on main before you create a branch.
  • Pushing straight to the original project instead of working through a fork and pull request.

Try it

  1. Open a pull request. Run the pull request commands above, open and merge the PR on GitHub, then run the second block. You should see your "Add support contact" save in git log --oneline -3, and cat README.md ends with the support line. Try the three merge-button styles on later practice PRs if you like.
  2. Review and fork. On GitHub, open any pull request (your own from step 1 works) and leave one comment on a line in Files changed; choose Review changes, Comment, and submit. Then fork https://github.com/octocat/Spoon-Knife with the Fork button, clone your fork somewhere outside your practice repos (cd ~ first), add the upstream remote as shown above, and run git remote -v: you should see both origin and upstream.
  3. Stash. Run both stash blocks. After git stash push the status is empty; after git stash pop your line is back; the plain stash prints No local changes to save.
  4. Get a push rejected, and fix it. Your second copy plays the teammate. First the teammate catches up (git pull brings in the pull request you merged) and pushes a new file:
cd ~/gonomicon-git-copy
git pull
echo "Team note" > team.txt
git add team.txt
git commit -m "Add team note"
git push

Now, in your own repo, make a change and try to push without having pulled:

cd ~/gonomicon-git
echo "My note" > my.txt
git add my.txt
git commit -m "Add my note"
git push

Git prints ! [rejected] main -> main (fetch first). Fix it:

git fetch
git status -sb
git pull

git status -sb shows [ahead 1, behind 1]. If git pull complains about divergent branches, run git config --global pull.rebase false and git pull again; nano opens with a merge message, so press Ctrl+O, Enter, Ctrl+X. Then git push succeeds and git log --oneline --graph shows the two lines of work joined. Run git pull in gonomicon-git-copy to catch the teammate copy up too.

  1. Create a merge conflict. Two slots change the same line differently.
echo "Greeting: hello" > greeting.txt
git add greeting.txt
git commit -m "Add greeting"
git switch -c greeting-welcome
echo "Greeting: welcome" > greeting.txt
git add greeting.txt
git commit -m "Welcome readers"
git switch main
echo "Greeting: hi there" > greeting.txt
git add greeting.txt
git commit -m "Change greeting"
git merge greeting-welcome

Git prints CONFLICT (content): Merge conflict in greeting.txt and Automatic merge failed. The command ends with a failure status, which is expected here.

  1. Resolve it.
git status
cat greeting.txt

git status says both modified: greeting.txt. You see the markers around "Greeting: hi there" (yours) and "Greeting: welcome" (theirs). In a real project you would fix the file in an editor. Here we simply overwrite it:

echo "Greeting: welcome, everyone!" > greeting.txt
grep -n "<<<<<<<" greeting.txt
git add greeting.txt
git commit -m "Resolve greeting conflict"
git log --oneline --graph

The grep prints nothing, which means no markers are left. git add marks the conflict resolved, and git commit -m finishes the merge. (A plain git commit here would open an editor with a prefilled message.) --graph draws the two branches joining back together.

  1. Trace a line's history.
git blame greeting.txt
git log --oneline -- greeting.txt
git show HEAD

git blame prints each line with the short id of the commit that last changed it, the author, the date, and the line. Your greeting belongs to the merge commit, because that is where you wrote it. git log -- greeting.txt lists only the commits that touched that file (-- ends the options so what follows is a file name). git show HEAD shows the newest commit's message and its changes. Use this to understand what happened, not to find someone to blame.

  1. Find a broken checkpoint by hand. First create four commits, one of which breaks status.txt:
echo "status=ok" > status.txt
git add status.txt
git commit -m "Add healthy status"
echo "Note one" >> README.md
git commit -am "Add note one"
echo "status=broken" > status.txt
git commit -am "Update status"
echo "Note two" >> README.md
git commit -am "Add note two"

-a stages changes to files Git already tracks, so you can skip git add. Now tell Git where the good and bad saves are:

git bisect start
git bisect bad
git bisect good HEAD~3
cat status.txt

git bisect bad marks your current commit as broken. HEAD~3 means "three commits before the current one", which is the healthy save. Git moves you to a commit in the middle. If status.txt says status=ok, run git bisect good; if it says status=broken, run git bisect bad. Repeat until Git prints is the first bad commit and names "Update status". Then run git bisect reset to return to main.

  1. Let Git do the checking. git bisect run runs a command at every step. An exit status of 0 means good, and anything else (except 125) means bad. grep -q from the shell scripting lesson stays silent and only hands back that exit status, and sh -c '...' runs the quoted text as a shell command:
git bisect start
git bisect bad
git bisect good HEAD~3
git bisect run sh -c 'grep -q "status=ok" status.txt'
git bisect reset

Git tests the commits automatically and reports the same first bad commit. Always run git bisect reset when you finish.

  1. Write a good commit message. A good subject line is short, starts with a verb, and says what changed. If the reason is not obvious, add a body with a second -m:
echo "Support hours: weekdays." >> README.md
git add README.md
git commit -m "Document support hours" -m "Customers kept asking when we answer, so the README now says weekdays only."
git log -1
git commit --amend -m "Document weekday support hours"
git log --oneline -3

git log -1 shows the full message with its body. git commit --amend -m replaces the whole message of the latest commit, which is fine for a commit you have not pushed yet. Compare Document weekday support hours with updates: only the first tells a teammate what happened. Finally, undo something safely: git revert HEAD adds a new save that cancels the newest one, which is how you undo something you have already pushed.

Something wrong with this lesson? Report a problem