Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Team workflow: feature branches, pull requests and agreements

Module 3: Collaboration · 12 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • create a feature branch, publish it with git push -u, and explain why that does not change main apply
  • tell the steps Git performs from what the hosting service adds: pull requests, branch protection and CI understand
  • review a published branch with git log main..origin/greeting and git diff main...origin/greeting, then merge it and publish main apply

Understand: From branch to main, via review

In a team, main is the shared version everyone builds on. It should always work. So each change gets its own feature branch. You create the branch greeting from main and commit there. Main and origin are untouched.

git push -u origin greeting publishes the branch. Origin gets a branch called greeting, and your greeting gets origin/greeting as its upstream. Main in origin is the same as before. Publishing is not integrating.

Now you open a pull request on the hosting service. It says: please review greeting and merge it into main. GitLab calls it a merge request. Git itself knows nothing about it. The service wraps the diff, comments, approval and test results around the branch.

The reviewer fetches first and asks two questions. Which commits are new? git log main..origin/greeting lists the commits on the branch that main does not have. What is the change? git diff main...origin/greeting shows the change since the branch parted from main.

Once the change is approved, the branch is merged into main, often with a merge commit. The merge button on the hosting service does this right inside origin. Now main in origin contains your commit. Your own main gets it the next time you pull.

Keep branches short-lived: one focused change, merged within a day or two. The longer a branch lives, the more main moves underneath it, and the bigger the conflicts get. After the merge you delete the branch in origin with git push origin --delete greeting.

The team also agrees on rules: nobody pushes straight to main, every change needs an approved review, and CI, the tests the service runs on every pull request, must be green. Git itself enforces none of this. The hosting service does, with branch protection on main.

Watch: You make a branch, Ada reviews and merges

You and Ada each have a clone, and everyone agrees on main. You start a new change on a branch of its own called greeting.

$ git switch -c greeting

We make a small, focused change and commit it. Only greeting moves.

$ echo "Hello, team!" > greeting.txt && git add greeting.txt && git commit -m "Add team greeting"

Now we publish the branch. Look at the graph: greeting appears inside origin, but main in origin stays put.

$ git push -u origin greeting

On a hosting service you would now open a pull request. Here we pretend Ada has received it. She is the reviewer, and first she fetches.

$ git fetch

Which commits would the branch add to main? Two dots means: reachable from origin/greeting, but not from main. Here it is one commit.

$ git log --oneline main..origin/greeting

What is the actual change? Three dots compare with the merge base, the point where the branch parted from main. Ada sees only what was done on the branch.

$ git diff main...origin/greeting

Ada approves and merges. Here she does locally what the merge button on the hosting service would otherwise do in origin. --no-ff makes a merge commit even when a fast-forward would do, and --no-edit keeps the suggested message.

$ git merge --no-ff --no-edit origin/greeting

Finally Ada pushes main. Now the change is integrated in origin. Your main is still where it was, until you pull.

$ git push

Tasks

  1. Start a new branch called greeting and move onto it.
  2. Publish the branch to origin, and make it follow origin/greeting.
  3. As Ada: download the new branch from origin.
  4. As Ada: list the commits the branch would add to main.
  5. As Ada: show the change the branch makes since it parted from main.
  6. As Ada: merge the reviewed branch into main with a merge commit.
  7. As Ada: publish the merged main.

Practise: Transfer: you review Ada's branch

The roles are swapped. Ada has published the branch docs and asks you for a review. Meanwhile main has received a new commit. Because the lines have split, the merge makes a merge commit even without --no-ff.

Tasks

  1. Get Ada's branch onto your machine, and see which commits it would add to main and what it changes.
  2. Approve: merge docs into main with a merge commit, and publish main.

Check yourself: Recall it

What is a pull request?

  • A special kind of Git commit
  • Another name for git pull
  • A request on the hosting service to review a branch and merge it

You are reviewing Ada's branch docs. After git fetch, git log --oneline main..origin/docs prints nothing. What does that mean?

  • The branch docs has no commits
  • Every commit on origin/docs is already in main, so there is nothing new to review
  • That main has not received new commits since docs was created

CI is green on a pull request. What does that prove?

  • That the change is correct and can be merged without review
  • That the tests that ran passed on exactly that version
  • That a colleague has approved the change

Recall from module 2: main had ten commits when you created greeting. Since then both branches have new commits. What is the merge base of main and greeting?

  • The very first commit in the repository
  • The commit main pointed to when greeting was created, the newest one both share
  • The commit main points to right now

Common misconceptions

  • “A pull request is part of Git.” It is a feature of the hosting service. Git sees only branches and commits.
  • “When I push my branch, the work is in main.” Publishing a branch is not integrating it. Main changes only when someone merges.
  • “Green CI means the change is correct.” It shows only that the tests that ran passed on that version.

Read more

One change, start to finish

A team workflow is a set of agreements made before anyone has a conflict: how big a change may be, who reviews it, what the tests must show, and how it reaches main. A typical path for one change looks like this:

git switch main
git pull --ff-only
git switch -c feature/usage
# edit USAGE.md, stage it, then look at exactly what you are about to record
git add USAGE.md
git diff --staged
git commit -m "Document local usage"
git push -u origin feature/usage

Then you open a pull request on the hosting service. Git never sees the pull request. It is a page around your branch with a description, the diff, comments, approvals and test results. GitLab calls the same thing a merge request.

Writing a pull request people can review

Keep it small: one purpose, ideally a few hundred changed lines at most, reviewed within a day or two. A short description saves the reviewer guesswork:

Problem:   New students do not know how to run the example.
Change:    Adds USAGE.md with the one command that starts it.
Behaviour: Before, README had no instructions. After, USAGE.md explains it.
Checked:   ran ./run-example.sh; it printed "OK" (exit status 0).

Write the test you actually ran and its actual result, not "should work".

Reviewing: fetch, then ask two questions

The reviewer starts by fetching, brings main up to date with git pull --ff-only, then compares the branch with main:

$ git log --oneline main..origin/greeting
f785ce9 Greet Ada by name
035b6f9 Add team greeting
$ git diff --stat main...origin/greeting
 greeting.txt | 2 ++
 1 file changed, 2 insertions(+)
Syntax In git log In git diff
main..origin/greeting commits on the branch that main lacks the difference between the two tips, including what changed on main
main...origin/greeting commits on either side but not both only the branch's own change, measured from the merge base where it parted from main

So for review, use two dots with log and three dots with diff. To run the code, check the branch out locally with git switch greeting.

A useful review asks: does the change do what the description promises? Are tests missing for the new behaviour? Is anything risky, such as deleted files, secrets or changed configuration? Will the next person understand it? Comment on the code, not the person, and say which comments must be fixed and which are suggestions. The author answers with new commits on the same branch and a plain push. If the author rebases the branch onto a newer main instead, the branch history is rewritten and the update needs git push --force-with-lease, which is fine on your own pull-request branch and never on main. Rebase, and when --force-with-lease fits, are covered in the lesson “Rebase” in module 4.

Three ways to integrate

The merge button usually offers three strategies. They give the same files on main and different history:

Strategy Local equivalent Commits on main afterwards
Merge commit git merge --no-ff feature every branch commit plus a merge commit with two parents
Squash git merge --squash feature then git commit one new commit with one parent; the branch commits are not ancestors
Rebase and merge git rebase main on the branch, then fast-forward copies of every branch commit, with new IDs, in a straight line

git merge --squash prepares the combined change but does not commit:

$ git merge --squash origin/greeting
Updating 5dd6192..f785ce9
Fast-forward
Squash commit -- not updating HEAD
 greeting.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 greeting.txt
$ git commit -m "Add team greeting (#12)"

Agree on one strategy. Merge commits keep the true story; squash keeps main short but loses the individual steps; rebase keeps the steps but changes their IDs. After a squash, or a rebase done by the merge button, the branch's own commits are not ancestors of main. Git then does not see the branch as merged, so delete it with care.

Branch protection and CI

Git itself enforces no team rules. The hosting service does, through branch protection on main: no direct pushes, no force pushes, no deletion, a required number of approvals and required status checks. A local hook cannot enforce a rule for everybody, because hooks are not cloned and anyone can skip them.

CI (continuous integration) runs the agreed checks on every push to a pull request, often on the result of merging it into the newest main. Green CI proves only that those checks passed on that exact version. It does not prove that the tests are good enough, or that the change is what users need. That is why teams require both green CI and an approved review.

Forks

In open source you often push to a fork and open the pull request from there. The setup with origin and upstream is in the lesson “Remotes and clone”.

Cleaning up after the merge

Delete the branch in the remote. The push also removes your own origin/greeting:

$ git push origin --delete greeting
 - [deleted]         greeting
$ git branch -vv
* greeting f785ce9 [origin/greeting: gone] Greet Ada by name

In other clones, such as Ada's, origin/greeting stays until they run git fetch --prune. gone means the upstream no longer exists. Switch away and remove the local branch with git branch -d greeting. Git refuses if it cannot see the commits as merged, which is what happens after a squash; check with git log main..greeting before you consider the uppercase -D.

Official documentation