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
monitoringfrommain. 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 themainbranch is no longer available: only the new branch option remains. - A direct push to
mainis 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:
- On GitHub, create a branch
monitoringfrommainwith the branch selector. - With
monitoringselected, createmonitoring.mdcontaining# MonitoringandGrafana on mon01, and commit tomonitoringwith the messageDocument monitoring. - Look at the repository page with
monitoringselected, then withmainselected. Note what differs. - Open the comparison between
mainandmonitoring(/compare/main...monitoring). Do not create a pull request. - On the branches page, check how many commits
monitoringis ahead of and behindmain. - In the clone, retrieve the new branch, switch to it, and display
monitoring.md.
Expected result and verification:
- With
mainselected,monitoring.mdis absent; withmonitoringselected, it is present, and the page says the branch is 1 commit ahead ofmain. - The comparison shows 1 commit and 1 file changed.
git fetchprints* [new branch] monitoring -> origin/monitoring;git switch monitoringsets up tracking oforigin/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 monitoringcreates the local branch automatically, becauseorigin/monitoringexists 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:
- In
sam, add a line- [ ] Add a printertoREADME.md, commit it onmainwith the messageAdd printer to-do, and pushmain. Read the error. - Try again with
--force. Read the error. - Without losing the commit, publish it on a new branch
printer-todo, with an upstream. - Check where your commit is: on
printer-todo, on localmain, onorigin/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-todoand sets up tracking. git log --oneline --all --graphshows the commit onprinter-todoand localmain, one commit ahead oforigin/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. --forceonly asks the server to accept a non-fast-forward update; it cannot override the server's rules.- Local
mainstays one commit ahead. After the pull request is merged on GitHub,git switch mainthengit pullrealigns it (with a merge commit,mainsimply 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:
- 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. - On the repository page, with
mainselected, editREADME.mdand open the commit dialog. Note which options are available. Commit with the available option, using the branch namereadme-update, and close the pull request form without creating the pull request. - On the branches page, try to delete
main, then deletereadme-updateand restore it. - 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:
mainhas no trash icon (it is the default branch, and protected);readme-updatecan 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
- 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. - The commit goes to the new branch
readme-update. GitHub opens the pull request form: leave the page without clicking Create pull request. -
N branches: the trash icon of
readme-update, then Restore. -
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. - 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).