Git in depth
Git in a team: protections and good practices
Knowing how to commit is not enough when five people touch the same code. This page explains, step by step, how to protect the main branch, how to review each other, how to avoid scares and what to do when you have one. It is not slide material: it is here for you to consult when you need it.
Git in depth
Protect the main branch
The main branch is what you hand in and what you deploy. One wrong push can break Wednesday's demo. Branch protection makes GitHub stop the mistakes instead of hoping nobody slips.
How to turn it on in GitHub
- Open the repository and go to Settings, then Rules and Rulesets (or Branches, in the classic "branch protection rules" view).
- Create a new rule for the default branch (
main) and, if you use Gitflow, another fordevelop. - Set it to Active and tick the options in the list below.
- Test it: try a direct
git pushtomain. It must be rejected.
It depends on the plan. The exact names can change a little in GitHub's interface, and on private repositories whether the rules are enforced depends on the organisation's plan (if the buttons are greyed out or the rule does not apply, write it into your team agreement and watch it by hand). Ask what your group's organisation has enabled.
What to tick and why
Require a pull request before merging
Nobody pushes code to main directly: everything goes through a PR. Ask for 1 approval at least.
Dismiss stale approvals
If commits are added after approval, the approval expires and the PR needs another review.
Require status checks to pass
The PR cannot be merged if continuous integration fails (see below).
Require conversation resolution
No merging with unresolved review comments.
Block force pushes
Nobody can rewrite main's history with --force.
Restrict deletions
Nobody can delete the branch by accident.
Do not allow bypassing
Not even the repository admin skips the rules. That way there are no "just this once" exceptions.
Pull requests that can be reviewed
A pull request (PR) is a proposed change with a conversation. If it is small and clear, the review is quick and useful; if it is huge, nobody really reads it and it gets approved by eye.
A good PR
- One task, one PR. If it changes more than 300-400 lines or does two things, split it.
- A meaningful title, preferably in Conventional Commits format:
feat(signup): validate the email. - Link the task in Taiga (or a
Closes #34if you use GitHub issues, which closes the issue on merge). - Two lines of context: what changes, why and how you tested it. Screenshots if it touches the UI.
- Review your own diff first before asking for a review: you will spot forgotten prints and files that do not belong.
- Draft pull request if you want feedback on work in progress.
When you review
- Someone other than the author does it; rotating reviewers spreads knowledge.
- Check first that it does what the story's acceptance criteria say, then edge cases, tests and naming.
- Comment on the code, not the person: ask ("why not use...?") before ordering.
- Separate what is essential from what is optional (nit) so the author knows what must be fixed.
PR template (optional but useful)
If you create the file .github/pull_request_template.md, GitHub uses it as the starting text of every new PR:
## What changes
(One or two sentences: what this PR does and why.)
## Ticket
Taiga: #34
## How it was tested
- [ ] Automated tests
- [ ] Tried by hand (explain how)
## Notes for the reviewer
(What should be looked at most carefully? Screenshots if the UI changes.)Merge, squash or rebase: how to merge
GitHub offers three ways to merge a PR and each leaves a different history. Choosing one is a team decision, not each person's.
| Option | What it does | Resulting history | Watch out for |
|---|---|---|---|
| Create a merge commit | Adds all the branch's commits plus a merge commit. | Faithful: every commit and who made it is visible. | Many merge commits if it is not kept tidy. |
| Squash and merge | Squashes the whole PR into one commit on main. | Clean: one commit per task (the PR title becomes the message). | The commit detail is lost and credited to whoever opens the PR. |
| Rebase and merge | Replays the branch's commits on top of main. | Linear, no merge commits, keeping every commit. | Rewrites hashes; do not use it on shared branches. |
Recommendation for PES: the course looks for everyone committing often and evenly. With squash, all of a PR's work ends up under one person, and if you pair-program it hides who did what. That is why Create a merge commit or Rebase and merge fits better. If in a pair only one account makes the commit, add the other person with a Co-authored-by: Name <email> footer so GitHub counts it for them.
Also turn on Automatically delete head branches (Settings, General): on merge, GitHub deletes the task's branch so they do not pile up.
Continuous integration: main's doorman
Continuous integration (CI) runs the tests, the linter and the build on every PR on a clean machine. If CI is a required status check of the protection, a PR that breaks the tests cannot be merged.
- Create the file
.github/workflows/ci.yml(example below, for Node: adapt it to Java, Python or Android). - Open a PR: in the Actions tab you will see it run.
- In main's protection rule, under Require status checks, add the
testjob. - Keep it fast (under 5 minutes) and, if it fails on main, fix that before anything else.
CI does not replace tests: it runs them automatically and identically for everyone. Later you can deploy to virtech only from main and only if CI passed.
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm testKeep your branch up to date
The longer a branch lives, the harder it is to merge. Bring in main's changes every day and keep branches short (two or three days, not two weeks).
git switch main
git pull
git switch feature/34-sign-up
git rebase main # or: git merge main
git push --force-with-lease # only after a rebase and only on your own branch- Merge is the safest: it rewrites nothing and works on branches other people also use.
- Rebase leaves a linear history but rewrites commits: never do it on a branch someone else has already pulled.
--force-with-leasefails if someone pushed something you do not have; far safer than--force.- If two people touched the same line, Git marks the conflict. Open the file, keep what is right, delete the markers, run
git addand continue (git rebase --continueor make the merge commit). If you get lost,git rebase --abortorgit merge --aborttake you back to the start.
This is what a conflict looks like
<<<<<<< HEAD
your change
=======
the other person's change
>>>>>>> mainSecrets and files that must not be pushed
A GitHub repository is easy to clone, copy and remember forever. What goes up, stays up.
- A
.gitignorefrom the first commit:node_modules/,build/,.env,*.log, IDE folders,.apkfiles and signing keys. - Never push passwords, API tokens, private keys or the
.env. Add a.env.examplewith empty variables so teammates know which ones are needed. - For CI, store values in Settings, Secrets and variables, Actions, not in the code.
- If one leaks, revoke it and generate a new one right now. Deleting the commit is not enough: history, forks and caches keep it.
- Do not push big binaries (APKs, videos). For executables use GitHub Releases.
Tags and versions
A tag is a permanent name for a commit. Tag every delivery and you can always go back to exactly what you handed in.
git tag -a fag-3 -m "Fag-3 delivery"
git push origin fag-3- On GitHub, Releases lets you attach notes and the executable to a tag.
- For versions users see, use Semantic Versioning:
MAJOR.MINOR.PATCH(1.4.2). A fix bumps PATCH, a feature MINOR and a breaking change MAJOR. - Conventional Commits types (
fix,feat,!) tell you which number is next: see Conventional Commits.
When something goes wrong
Everyone makes mistakes with Git. The difference is knowing how to undo them without making things worse. Golden rule: if you already pushed it to a shared branch, do not rewrite history: make a new commit that fixes it.
| What happened | What to do |
|---|---|
| I committed on main by mistake (not pushed) | git switch -c feature/34-x (the commits are now on the new branch), then git switch main and git reset --hard origin/main. Careful: reset --hard discards unsaved changes. |
| The last commit message has a mistake (not pushed) | git commit --amend |
| I forgot a file in the last commit (not pushed) | git add file and git commit --amend --no-edit |
| I staged a file by mistake | git restore --staged file |
| I want to discard a file's changes | git restore file (cannot be undone) |
| I pushed a bad commit to a shared branch | git revert <hash>: creates a commit that undoes it. Do not reset or --force. |
| The push is rejected ("non-fast-forward") | Someone pushed first: git pull --rebase (or git pull) and push again. Do not force. |
| I think I lost commits | git reflog lists where HEAD has been; git switch -c rescue <hash> brings them back. |
Team agreement about Git
Write down in the report what you decided and review it at every retrospective. This list is a guide; it is saved in your browser.