Design and implement a source control strategy
Choosing a branching model that matches how the team releases, enforcing quality at the merge point, and keeping the repository fast, permissioned and free of things that should never have been committed.
- Branching strategies for source code — trunk-based, feature, release; PR workflow; merge restrictions
- Configure & manage repositories — large files, scaling, permissions, tags, recovery, data removal
Terminology alert: Azure Repos calls the enforcement mechanism branch policies; GitHub calls it branch protection rules (and, at scale, rulesets). Both appear in the skills measured by name.
2.1 Branching strategies for the source code
Choosing a branch strategy
| Strategy | Long-lived branches | Branch lifetime | Best for | Complexity |
|---|---|---|---|---|
| Trunk-based | main only | Hours to 1–2 days (or none) | Continuous delivery, microservices, mature CI | Low |
| Feature branch | main | Days | Most teams; natural PR review point | Low–medium |
| GitHub Flow | main | Days | Web/SaaS deployed continuously | Low |
| Release branch | main + release/x.y | Life of the release | Multiple supported versions in production | Medium |
| Git Flow | main + develop | Weeks | Boxed/versioned software with QA hardening | High |
Trunk-based development
Everyone integrates into main at least daily. Branches, if used at all, live for hours.
- Feature flags are mandatory — they are what makes it safe to merge incomplete work into a branch that ships. (See Domain 3.4.)
- Requires fast, reliable CI on every commit, plus strong automated test coverage.
- Eliminates merge hell: there is nothing to diverge from.
Release branching
Caveat — always merge the fix forward
A hotfix applied only to release/1.0 will regress the moment 2.0 ships. Every release-branch fix must also be merged or cherry-picked back into main. Expect a build-list question ordering: branch from the release tag → fix → PR → merge to release → tag → deploy → cherry-pick to main.
Naming conventions
feature/1234-user-authentication # new work, references the work item
bugfix/5678-null-reference # non-urgent defect
hotfix/payment-gateway-timeout # urgent production fix
release/2.4 # stabilisation branch for a version
users/marco/spike-caching # personal/experimental (Azure Repos convention)
Consistent prefixes matter operationally: branch policies, pipeline triggers and branch-control checks are all configured with wildcards such as release/* and feature/*.
Pull request workflow
draft → ready] C --> D[Automated checks
build validation · status checks] C --> E[Human review
required reviewers · CODEOWNERS] D --> F{All policies met?} E --> F F -->|No| B F -->|Yes| G[Complete / merge] G --> H[Delete source branch]
Azure Repos branch policies
| Policy | What it enforces |
|---|---|
| Require a minimum number of reviewers | N approvals; options to reset votes on new push, disallow the requester approving their own PR, and allow completion with pending votes |
| Check for linked work items | PR must link at least one work item — the traceability enforcement point |
| Check for comment resolution | All PR comments must be resolved before completion |
| Limit merge types | Restrict which of the four merge strategies are allowed |
| Build validation | A named pipeline must succeed; can be required or optional, with an expiry after N hours |
| Status checks | An external service (SonarQube, a scanner, a custom app) must post a successful status via the REST API |
| Automatically included reviewers | Add specific people/groups as required or optional reviewers, optionally scoped to file paths |
| Require merge from specific branches | Restrict which source branches may merge into the target |
GitHub branch protection rules & rulesets
| Protection | What it enforces |
|---|---|
| Require a pull request before merging | No direct pushes; set required approvals, dismiss stale approvals on new commits, require review from Code Owners |
| Require status checks to pass | Named checks must be green; optionally require branches to be up to date before merging |
| Require conversation resolution | All review threads resolved |
| Require signed commits | Only GPG/S-MIME/SSH-signed commits |
| Require linear history | Blocks merge commits — forces squash or rebase |
| Require deployments to succeed | The branch must have deployed successfully to named environments |
| Restrict who can push | Limit push access to named users, teams or apps |
| Do not allow bypassing / include administrators | Rules apply to admins too |
| Block force pushes & deletions | Protect history and the branch's existence |
Rulesets — the modern, scalable form
GitHub rulesets supersede classic branch protection for organisation-wide governance: they can be defined at the org level and applied across many repositories, they target branches and tags, they support explicit bypass lists, and they have an evaluate (dry-run) mode so you can see what would be blocked before enforcing. Multiple rulesets layer additively — the most restrictive outcome wins.
Caveat — the vocabulary is the question
Azure Repos → branch policies, applied per branch or by wildcard, configured under Repos → Branches → Branch policies. GitHub → branch protection rules / rulesets, under Settings → Rules. "Build validation" (Azure) and "required status checks" (GitHub) are the same idea with different names — and both are the answer to "prevent merging code that fails CI".
Merge strategies and restrictions
| Strategy | Resulting history | Choose when |
|---|---|---|
| Merge (no fast-forward) | Preserves every commit plus a merge commit; non-linear | You want the full development history of the branch |
| Squash merge | One new commit on the target; branch commits are discarded | Noisy WIP commits; you want one commit per PR/feature |
| Rebase and fast-forward | Branch commits replayed onto the target; fully linear, no merge commit | You want a clean linear log and keep individual commits |
| Semi-linear merge (rebase + merge commit) | Rebases, then adds a merge commit | Linear-ish history that still records where each PR landed |
Caveat — squash merge and traceability
Squash merge rewrites the branch into a single new commit, so the original commit SHAs disappear from the target branch. That is exactly what you want for a tidy main, but it means the source branch should be deleted afterwards (re-merging it later causes conflicts) and per-commit authorship is lost. If a scenario demands "preserve individual commit attribution", squash is the wrong answer.
CODEOWNERS
# Later matches win — the LAST matching pattern takes precedence.
* @org/platform-team
/docs/ @org/docs-team
*.ts @org/frontend-team @lead-developer
/infrastructure/ @org/devops-team
/src/auth/ @org/security-team
/.github/workflows/ @org/devops-team
- Owners are automatically requested for review when a PR touches a matching path.
- Combine with Require review from Code Owners to make that approval mandatory.
- Azure Repos achieves the same with automatically included reviewers scoped to a path.
2.2 Configure and manage repositories
Managing large files
Git stores a complete copy of every version of every file in every clone. Large binaries — which do not delta-compress — bloat the repository permanently, because history is immutable.
Git LFS (Large File Storage)
LFS replaces large files in the repo with small text pointer files; the real content lives on a separate LFS server and is fetched on checkout.
git lfs install # one-time, per machine
git lfs track "*.psd" # writes rules into .gitattributes
git lfs track "*.mp4"
git lfs track "assets/**/*.bin"
# .gitattributes now contains:
# *.psd filter=lfs diff=lfs merge=lfs -text
git add .gitattributes # MUST be committed — it is what makes LFS work for everyone
git add design.psd
git commit -m "Add design assets"
git push origin main
git lfs ls-files # list files currently managed by LFS
git lfs migrate import --include="*.psd" # convert EXISTING history to LFS (rewrites history)
Caveat — LFS gotchas
Tracking is not retroactive. git lfs track only affects files committed after the rule exists; converting existing history needs git lfs migrate import, which rewrites history. Also: .gitattributes must be committed and pushed or other clones will not use LFS. And LFS files are not de-duplicated across branches — quota is consumed by every distinct version.
| Azure Repos | GitHub | |
|---|---|---|
| LFS support | Supported; no extra charge for LFS storage beyond repo limits | Supported; free tier of 1 GB storage + 1 GB bandwidth/month, then paid data packs |
| Authentication | Same as the repo (PAT / Entra) | Same as the repo (token / SSH) |
git-fat
A lighter-weight, third-party alternative to LFS: large files are replaced by a placeholder containing a SHA-1, and the real payload is synced to an external store over rsync (or S3-style backends).
[rsync]
remote = storage-server:/srv/fat-store
Use it when you need a self-hosted binary store with no vendor dependency. In practice LFS is the default answer; git-fat appears in the skills measured mainly so you recognise the name.
Scaling and optimizing a Git repository
Scalar
Scalar ships with Git itself and applies the large-repo configuration that Microsoft developed for the Windows and Office repositories.
| Technique | What it avoids |
|---|---|
Partial clone (--filter=blob:none) | Downloading file contents you never open — blobs are fetched on demand |
| Sparse checkout (cone mode) | Materialising directories your team does not work in |
| Commit-graph | Walking history from scratch — speeds up log, blame, merge-base |
| Multi-pack index | Slow object lookup across many pack files |
| FSMonitor (file system watcher) | Scanning the whole working tree on every git status |
| Background maintenance | Foreground stalls — prefetch and repack run on a schedule |
scalar clone https://dev.azure.com/org/project/_git/large-repo
scalar register # opt an EXISTING clone into Scalar's maintenance + config
scalar run all # run maintenance tasks now
scalar unregister
# The underlying primitives, if asked about them directly:
git clone --filter=blob:none <url>
git sparse-checkout set --cone src/api src/shared
Cross-repository sharing
| Technique | How it works | Trade-off |
|---|---|---|
| Package feed (Azure Artifacts / GitHub Packages) | Publish the shared code as a versioned package | Preferred — proper versioning and dependency resolution |
| Git submodule | A pointer to another repo pinned at a specific commit | Explicit versions, but easy to forget --recurse-submodules |
| Git subtree | Copies the other repo's content (and optionally history) into a subdirectory | No special client commands for consumers; messier history |
| Sparse checkout of a monorepo | One repo, each team checks out only its folders | Single source of truth; needs monorepo tooling and CI path filters |
git submodule add https://github.com/org/shared-lib ./libs/shared
git clone --recurse-submodules https://github.com/org/project
git submodule update --init --recursive # after a plain clone
git submodule update --remote --merge # advance to the submodule's latest
Caveat — share code as a package, not a submodule
When the exam asks how to share a library across teams or repositories, the expected answer is a package feed (Azure Artifacts / GitHub Packages) — it gives you semantic versioning, upstream caching and independent release cadence. Submodules are the answer only when the scenario explicitly needs the source pinned at a commit (e.g. vendored tooling built from source).
Permissions in the source control repository
Azure DevOps
Access is the intersection of an access level (what you paid for), security group membership (what role you hold) and object-level permissions (Allow / Deny / Not set on a specific repo or branch).
| Repository permission | Grants |
|---|---|
| Read | Clone, fetch, browse |
| Contribute | Push to branches that have no blocking policy |
| Create branch | Create new branches |
| Contribute to pull requests | Comment on and vote in PRs |
| Force push (rewrite history, delete branches) | Destructive history rewriting — grant to almost nobody |
| Bypass policies when pushing / completing pull requests | Ignore branch policies — audit these grants |
| Manage permissions | Edit the repo's ACLs |
| Delete or disable repository | Remove the repo |
Caveat — Deny beats Allow
In Azure DevOps, an explicit Deny always wins over an Allow inherited from any group — with one exception: members of Project Collection Administrators can override. Permissions also inherit from project → repository → branch, so the cleanest model is to grant at the group level and only set object-level permissions where you genuinely need an exception.
GitHub
| Role | Can |
|---|---|
| Read | View, clone, open issues and PRs |
| Triage | Read + manage issues and PRs (label, assign, close) — no write access to code |
| Write | Triage + push to unprotected branches, manage releases |
| Maintain | Write + repo settings management, excluding destructive/sensitive actions |
| Admin | Full control, including deletion, visibility changes and protection rules |
- Grant access through teams, not individuals — teams support nesting and inherit parent permissions.
- Outside collaborators are non-org members granted access to specific repositories; they do not consume a team's permissions model and should be reviewed regularly.
- Where a user gets permissions from multiple sources, the highest permission wins (the opposite of Azure DevOps' Deny-wins model).
Tags
git tag v1.0.0 # lightweight: just a pointer, no metadata
git tag -a v2.1.0 -m "Adds payment gateway" # annotated: tagger, date, message — RECOMMENDED for releases
git tag -s v2.1.0 -m "Signed release" # signed: annotated + GPG signature
git tag -a v1.9.5 abc1234 -m "Hotfix" # tag a specific historical commit
git push origin v2.1.0 # tags are NOT pushed by default
git push origin --tags # push all tags
git tag --list "v2.*"
git tag -d v1.0.0 # delete locally
git push origin --delete v1.0.0 # delete on the remote
Caveat — lightweight vs annotated
Annotated tags are full Git objects carrying the tagger, date, message and optional signature; lightweight tags are just a named pointer. For releases — and anywhere auditability matters — the answer is annotated (-a) or signed (-s). Also remember tags are not pushed by git push alone, and git describe only sees annotated tags unless you pass --tags.
Recovering specific data with Git commands
| Situation | Command | Safe on a shared branch? |
|---|---|---|
| Undo last commit, keep changes staged | git reset --soft HEAD~1 | No — rewrites history |
| Undo last commit, keep changes in the working tree | git reset --mixed HEAD~1 (default) | No |
| Undo last commit and discard the changes | git reset --hard HEAD~1 | No |
| Undo a commit that is already pushed | git revert <sha> — creates a new inverse commit | Yes |
| Restore one file from the last commit | git restore --source=HEAD -- path/to/file | Yes |
| Recover a deleted branch | git reflog to find the SHA, then git branch <name> <sha> | Yes |
| Find any "lost" commit | git reflog / git fsck --lost-found | Yes |
| Copy one commit onto another branch | git cherry-pick <sha> | Yes |
| Shelve work temporarily | git stash push -m "wip" / git stash pop | Yes |
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: the work I just destroyed ← recover this
# ghi9012 HEAD@{2}: commit: earlier work
git reset --hard HEAD@{1} # put HEAD back
git branch recovered def5678 # or rescue it onto a new branch
Caveat — revert vs reset
git revert is the only safe undo for anything already pushed. It adds a new commit that reverses the change, so collaborators' history stays valid. git reset rewrites history and requires a force push, which breaks every other clone. The exam phrases this as "the change is already in production / already shared" → revert. Note that reflog is local only and entries expire (90 days for reachable, 30 for unreachable, by default).
Removing specific data from source control
These operations rewrite history
Every commit SHA after the rewrite point changes. Coordinate with the whole team, and expect every existing clone to need re-cloning.
| Tool | Use | Notes |
|---|---|---|
| git filter-repo | The recommended modern tool for rewriting history | Fast, safe defaults; replaced git filter-branch, which Git now actively discourages |
| BFG Repo-Cleaner | Simple, very fast removal of files or secret strings | Java tool; will not touch the commit currently at HEAD — clean that up first |
| git filter-branch | Legacy | Slow and error-prone — a wrong answer if a modern option is offered |
# Remove one file from ALL history
git filter-repo --path secrets.env --invert-paths
# Remove every file matching a glob
git filter-repo --path-glob '*.pem' --invert-paths
# Redact a literal string everywhere
git filter-repo --replace-text patterns.txt
git clone --mirror https://.../repo.git
java -jar bfg.jar --delete-files secrets.env repo.git
java -jar bfg.jar --replace-text passwords.txt repo.git
java -jar bfg.jar --strip-blobs-bigger-than 50M repo.git
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
The full incident response for a leaked secret
- Rotate the secret first. Assume it is compromised the moment it was pushed — history rewriting is cleanup, not containment.
- Rewrite history with
git filter-repoor BFG, then force push all branches and tags. - Tell every collaborator to re-clone. Old clones still contain the secret and can push it back.
- Purge cached copies. Forks, PR refs and the server-side cache can still serve the old blobs — on GitHub, open a support request to purge cached views; on Azure DevOps, contact support to clear server-side data after the force push.
- Prevent recurrence — enable secret scanning with push protection, add pre-commit hooks (
gitleaks,detect-secrets) and move the value into Key Vault. (See Domain 4.)
Caveat — rotation beats rewriting
If a question offers both "remove it from history" and "rotate the credential", the first action is always to rotate. A build-list question will usually want: rotate → rewrite history → force push → notify collaborators to re-clone → enable push protection.
Domain 2 rapid recap
| The requirement says… | Answer |
|---|---|
| Daily deploys, experienced team, minimal branching | Trunk-based development + feature flags |
| Support several versions in production at once | Release branching (fix on the branch, port forward) |
| Block merging when CI fails (Azure Repos) | Branch policy: Build validation |
| Block merging when CI fails (GitHub) | Required status checks in a protection rule / ruleset |
| Apply the same rules to 200 repos at once | GitHub organisation-level ruleset |
Enforce one commit per feature on main | Squash merge (limit merge types) |
| Forbid merge commits entirely | Require linear history / rebase + fast-forward |
| Reviewers auto-assigned by folder | CODEOWNERS (or automatically included reviewers) |
| Repo bloated by design files and videos | Git LFS (+ git lfs migrate import for existing history) |
| Clone of a huge monorepo takes an hour | Scalar — partial clone + sparse checkout |
| Share a library between five repos | Azure Artifacts / GitHub Packages feed |
| Undo a change already merged and deployed | git revert |
| Recover a branch someone deleted | git reflog → git branch <name> <sha> |
| A password was committed last month | Rotate it, then git filter-repo/BFG, force push, re-clone |
| Tag a release with author and message | Annotated tag (git tag -a) |