Aller au contenu

Branches and branch protection

Branches work on GitHub exactly as in Intermediate Git: they let several pieces of work progress in parallel, without the risk of mixing unfinished versions of the same files. GitHub adds a way to manage them from the browser, and protection rules to decide what may happen to important branches such as main.

%%{init: {"gitGraph": {"showBranches": true, "rotateCommitLabel": false}, "themeVariables": {"git0": "#43a047", "git1": "#1e88e5", "git2": "#fb8c00", "gitBranchLabel0": "#ffffff", "gitBranchLabel1": "#ffffff", "gitBranchLabel2": "#ffffff", "gitInv0": "#ffffff", "commitLabelFontSize": "13px"}}}%%
gitGraph TB:
    commit id: "Create inventory"
    commit id: "Add services list"
    branch monitoring
    commit id: "Add monitoring server"
    checkout main
    branch backups
    commit id: "Add NAS backup script"
    checkout monitoring
    commit id: "Document Grafana"
    checkout main
    merge monitoring id: "Merge monitoring"
    checkout backups
    commit id: "Schedule backups"
    checkout main
    merge backups id: "Merge backups"

The default branch

Every repository has a default branch, usually main. It is the branch GitHub displays on the repository page, the one git clone checks out, and the default target of pull requests. Treat it as the stable version of the project: work happens on other branches, and reaches main once it is ready.

The branch selector, at the top left of the file list, shows which branch you are viewing. Next to it, the N branches link counts the branches of the repository.

Creating a branch

There are two ways to create a branch on GitHub:

  • From the branch selector: open it, type the new name in Find or create a branch..., and click Create branch monitoring from main. The new branch starts from the branch currently selected.
  • From the branches page (N branches link): click New branch, enter the Branch name, choose the Branch source, and click Create branch.

The new branch is a pointer to the same commit as its source, exactly like git branch monitoring. It exists only on GitHub: a local clone receives it at the next git fetch, as origin/monitoring.

The branches page sorts the branches into tabs: Overview, Yours (branches you pushed to), Active (recent commits), Stale (no commits for three months), and All. Each branch shows how many commits it is behind and ahead of the default branch, and whether it has a pull request.

Switching and working on a branch

Selecting a branch in the branch selector changes the view: the file list, the README, and the latest commit are those of the selected branch. The URL shows it too: https://github.com/sam-rivera/homelab/tree/monitoring.

Every change made in the browser is committed to the branch currently selected. Sam selects monitoring, then creates monitoring.md with Add file > Create new file: the commit goes to monitoring, and main does not change.

Once the branch has new commits, the repository page shows:

  • a yellow banner, monitoring had recent pushes..., with a Compare & pull request button;
  • when viewing the branch, a line such as This branch is 1 commit ahead of main.

Browser vs local: no checkout on GitHub

On your computer, git switch changes the files of your working directory. On GitHub, selecting a branch only changes what you look at, and where your next browser commit goes. There is no working directory on GitHub.

Comparing branches

Open https://github.com/sam-rivera/homelab/compare, or click Compare & pull request, and choose two branches:

Field Meaning
base The branch you compare against, usually main
compare The branch with the changes, here monitoring

GitHub lists the commits of compare that base does not have, and the differences in the files, like git log main..monitoring and git diff main...monitoring. The URL can be typed directly: /compare/main...monitoring. The same page is the start of a pull request.

Branches created on GitHub, seen from Git

A branch created in the browser reaches the local repositories like any other remote branch:

$ git fetch
From https://github.com/sam-rivera/homelab
 * [new branch]      monitoring -> origin/monitoring
$ git switch monitoring
branch 'monitoring' set up to track 'origin/monitoring'.
Switched to a new branch 'monitoring'

And a branch created locally appears on GitHub once pushed with git push -u origin <branch>.

Deleting a branch

On the branches page, the trash icon of a branch deletes it. Right after, a Restore button appears in its place, in case of a mistake. The default branch cannot be deleted. Locally, the remote-tracking branch disappears with git fetch --prune.

Branch protection

On a shared repository, a single wrong command can damage main: a git push --force that erases other people's commits, a deleted branch, or an untested change committed directly. Branch protection lets the repository's administrators set rules for specific branches, for example:

  • Require a pull request before merging: nobody can push or commit directly to the branch; every change goes through a pull request.
  • Require approvals: the pull request must be approved by a given number of reviewers before it can be merged.
  • Restrict deletions: the branch cannot be deleted.
  • Block force pushes: the history of the branch cannot be rewritten.

GitHub offers two systems to define these rules, which can coexist:

Branch protection rules (classic) Rulesets (recommended)
Where Settings > Branches > Add branch protection rule (or Add classic branch protection rule) Settings > Rules > Rulesets > New ruleset > New branch ruleset
Targets One Branch name pattern per rule: main, release/* Any number of branch patterns, or simply Include default branch
Several rules on one branch Only one rule applies to a given branch Rulesets stack: every matching ruleset applies
On or off Active as soon as it exists Enforcement status: Active, Disabled, or Evaluate (report only, on some plans)
Who can bypass Administrators, unless Do not allow bypassing the above settings is ticked Only the roles, teams, or apps added to the Bypass list

Settings of a classic branch protection rule

Setting Effect
Require a pull request before merging No direct commits; option Require approvals with a number of required approving reviews
Require status checks to pass before merging Automated checks (for example, GitHub Actions tests) must succeed
Require conversation resolution before merging Every review comment must be marked as resolved
Require signed commits Commits must carry a verified signature
Require linear history No merge commits: only squash or rebase merges
Lock branch The branch becomes read-only
Do not allow bypassing the above settings The rules also apply to administrators
Restrict who can push to matching branches Only listed people or teams can push (organizations only)
Allow force pushes Unticked by default: force pushes are blocked
Allow deletions Unticked by default: the branch cannot be deleted

A ruleset offers the same rules, with slightly different names: Restrict deletions and Block force pushes are ticked by default, and Require a pull request before merging has the same approval options.

Availability

Branch protection rules and rulesets are available for public repositories with GitHub Free, and for private repositories with GitHub Pro, Team, or Enterprise. On a private repository of a free personal account, the settings page explains that the rules are not enforced.

Sam protects main

Once Alex joins the project, Sam adds a classic rule on main:

Setting Value
Branch name pattern main
Require a pull request before merging Ticked, with Require approvals: 1
Do not allow bypassing the above settings Ticked, so that the rule applies to Sam too
Allow force pushes, Allow deletions Unticked

Then Create. From now on:

  • In the web editor, on main, Commit directly to the main branch is no longer available: only the new branch option remains.
  • A direct push to main is refused by GitHub:
$ git push origin main
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Changes must be made through a pull request.
To https://github.com/sam-rivera/homelab.git
 ! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'https://github.com/sam-rivera/homelab.git'

With a ruleset, the message reads GH013: Repository rule violations found for refs/heads/main., followed by the list of violated rules. In both cases, the fix is the same: push the commits to a new branch, and open a pull request.

git switch -c add-printer        # the commits come along to the new branch
git push -u origin add-printer

Local main is then one commit ahead of origin/main: once the pull request is merged, git pull brings main back in line (see Reviewing and merging pull requests).

Summary

Action Where
See or change the branch you view Branch selector, top left of the file list
Create a branch Branch selector > type a name > Create branch, or N branches > New branch
Compare two branches /compare/<base>...<compare>, or Compare & pull request
Delete or restore a branch N branches > trash icon > Restore
Protect a branch Settings > Branches (classic rule) or Settings > Rules > Rulesets
Get a branch created on GitHub git fetch, then git switch <branch>

Common mistakes

  • Committing in the browser without checking the branch selector. The commit goes to the branch currently displayed.
  • Expecting a classic rule to stop administrators. Tick Do not allow bypassing the above settings.
  • Trying to fix a rejected push to a protected branch with --force. Force pushes are blocked too: push to a new branch and open a pull request.
  • Protecting a private repository on a free personal plan. The rules are not enforced.

Hands-on labs

Three labs, from guided to more autonomous. Labs 1 and 3 need your GitHub account; Lab 2 runs entirely in the terminal. Replace <username> with your GitHub username.

Lab 1: a branch made in the browser

Objective: create a branch on GitHub, commit to it, compare it with main, then retrieve it locally.

Prerequisites and initial state: a GitHub account, Git installed.

Setup: on GitHub, create a public repository homelab-practice-5 with a README. Then:

mkdir -p ~/git-practice/web-branches && cd ~/git-practice/web-branches
git clone https://github.com/<username>/homelab-practice-5.git

Tasks:

  1. On GitHub, create a branch monitoring from main with the branch selector.
  2. With monitoring selected, create monitoring.md containing # Monitoring and Grafana on mon01, and commit to monitoring with the message Document monitoring.
  3. Look at the repository page with monitoring selected, then with main selected. Note what differs.
  4. Open the comparison between main and monitoring (/compare/main...monitoring). Do not create a pull request.
  5. On the branches page, check how many commits monitoring is ahead of and behind main.
  6. In the clone, retrieve the new branch, switch to it, and display monitoring.md.

Expected result and verification:

  • With main selected, monitoring.md is absent; with monitoring selected, it is present, and the page says the branch is 1 commit ahead of main.
  • The comparison shows 1 commit and 1 file changed.
  • git fetch prints * [new branch] monitoring -> origin/monitoring; git switch monitoring sets up tracking of origin/monitoring.
Solution
cd ~/git-practice/web-branches/homelab-practice-5
git fetch                           # * [new branch] monitoring -> origin/monitoring
git branch -a
git switch monitoring               # branch 'monitoring' set up to track 'origin/monitoring'.
cat monitoring.md
  • The branch created on GitHub is an ordinary remote branch for Git.
  • git switch monitoring creates the local branch automatically, because origin/monitoring exists and no other remote has a branch with that name.

Keep homelab-practice-5 for Lab 3. Clean up the clone: rm -rf ~/git-practice/web-branches.

Lab 2: a protected branch, rehearsal

Objective: experience a rejected push to a protected branch, and the right way out, using a local bare repository that imitates GitHub's protection with a server-side hook.

Prerequisites and initial state: Git installed. The setup creates a "server" whose main refuses direct pushes (a pre-receive hook plays the protection rule), and an up-to-date clone sam.

Setup:

mkdir -p ~/git-practice/protected && cd ~/git-practice/protected
git init -q --bare -b main server.git
git clone -q server.git sam 2>/dev/null
cd sam
git config user.name "Sam Rivera"
git config user.email "[email protected]"
printf '# homelab\n' > README.md
git add . && git commit -q -m "Create README" && git push -q origin main
cd ..
cat > server.git/hooks/pre-receive <<'EOF'
#!/bin/sh
# Simulated protection rule: refuse direct pushes to main
while read old new ref; do
  if [ "$ref" = "refs/heads/main" ]; then
    echo "error: Protected branch update failed for refs/heads/main."
    echo "error: Changes must be made through a pull request."
    exit 1
  fi
done
EOF
chmod +x server.git/hooks/pre-receive

Tasks:

  1. In sam, add a line - [ ] Add a printer to README.md, commit it on main with the message Add printer to-do, and push main. Read the error.
  2. Try again with --force. Read the error.
  3. Without losing the commit, publish it on a new branch printer-todo, with an upstream.
  4. Check where your commit is: on printer-todo, on local main, on origin/main?

Expected result and verification:

  • Tasks 1 and 2 print remote: error: Protected branch update failed for refs/heads/main. and ! [remote rejected] main -> main (pre-receive hook declined).
  • Task 3 prints * [new branch] printer-todo -> printer-todo and sets up tracking.
  • git log --oneline --all --graph shows the commit on printer-todo and local main, one commit ahead of origin/main.
Solution
cd ~/git-practice/protected/sam

# 1. Direct push to main: refused
echo "- [ ] Add a printer" >> README.md
git commit -am "Add printer to-do"
git push origin main                # ! [remote rejected] main -> main (pre-receive hook declined)

# 2. Forcing changes nothing: the server decides
git push --force origin main        # same refusal

# 3. The way out: a branch, then a pull request
git switch -c printer-todo
git push -u origin printer-todo

# 4. Where is the commit?
git log --oneline --all --graph
git status -sb
  • On GitHub, the reason reads (protected branch hook declined): GitHub implements protection with the same kind of server-side check.
  • --force only asks the server to accept a non-fast-forward update; it cannot override the server's rules.
  • Local main stays one commit ahead. After the pull request is merged on GitHub, git switch main then git pull realigns it (with a merge commit, main simply moves forward; with a squash, see the cleanup section of the review page).

Clean up when you are done: rm -rf ~/git-practice/protected.

Lab 3: protect main for real

Objective: add a protection rule to a public repository, and observe its effect on the web editor and the branches page.

Prerequisites and initial state: homelab-practice-5 from Lab 1, public (protection is not enforced on private repositories of free personal accounts).

Tasks:

  1. In Settings > Branches, add a classic branch protection rule for main: require a pull request before merging, with 1 approval, and make the rule apply to administrators too. Leave force pushes and deletions blocked.
  2. On the repository page, with main selected, edit README.md and open the commit dialog. Note which options are available. Commit with the available option, using the branch name readme-update, and close the pull request form without creating the pull request.
  3. On the branches page, try to delete main, then delete readme-update and restore it.
  4. Find where the rule is listed, and how you would disable it temporarily without deleting it. (Hint: compare with a ruleset.)

Expected result and verification:

  • Task 2: the dialog no longer offers to commit directly to main; only Create a new branch for this commit and start a pull request remains.
  • Task 3: main has no trash icon (it is the default branch, and protected); readme-update can be deleted and restored.
  • Task 4: classic rules are listed in Settings > Branches and can only be edited or deleted; a ruleset can be switched to Disabled with its Enforcement status.
Solution
  1. Settings > Branches > Add classic branch protection rule: Branch name pattern main; tick Require a pull request before merging (with Require approvals: 1) and Do not allow bypassing the above settings; leave Allow force pushes and Allow deletions unticked; click Create.
  2. The commit goes to the new branch readme-update. GitHub opens the pull request form: leave the page without clicking Create pull request.
  3. N branches: the trash icon of readme-update, then Restore.

  4. Without Do not allow bypassing the above settings, you are an administrator of your own repository, so the rule would not apply to you: the web editor would still let you commit to main, with a warning.

  5. You cannot approve your own pull request: on a personal repository with a single person, a rule requiring 1 approval that applies to administrators means nobody can merge. On a solo project, untick Do not allow bypassing the above settings, or remove the rule, after the lab.

Clean up when you are done: delete homelab-practice-5 on GitHub (Settings > General > Danger Zone).