The contoso-reservas project already exists, but it is empty. The bookings website code is still where it was: on Diego Salas' laptop, in a folder under local version control that nobody else can see, and in a succession of ZIPs in Marta's inbox. This lesson eliminates that situation at the root.

Azure Repos is Azure DevOps' private Git repository service. Hosting code in a remote repository is the easy part — anybody can do that. What really transforms the way a team works is what you build on top: a branching strategy everybody understands, pull requests that turn review into a recorded conversation, and branch policies that stop the team's rules from depending on somebody remembering them. By the end, Contoso's main branch will be, by construction, always deployable.

Contents

  1. The Git essentials: commit, branch, merge and rebase
  2. Creating the contoso-reservas repository and cloning it
  3. Authentication: Credential Manager, tokens and SSH
  4. Branching strategies and the one Contoso chooses
  5. Pull requests
  6. Branch policies on main
  7. Automatic reviewers by path: Azure Repos' CODEOWNERS
  8. Tags and semantic versioning
  9. .gitignore and the mistake of pushing a secret
  10. Migrating the local repository while preserving history
  11. Git LFS for heavy graphic assets
  12. Comparison with GitHub Repos
  13. Common Mistakes and Tips
  14. Exercises
  15. Conclusion

  1. The Git essentials: commit, branch, merge and rebase

This is not a Git lesson, but there are four concepts without which the rest makes no sense. A commit is a complete snapshot of the project, identified by a cryptographic hash and pointing at the previous commit: a repository's history is a chain of snapshots, not a list of diffs. A branch is just a movable pointer to a commit — creating one means writing a 41-byte file — which is why modern workflows create and destroy branches constantly.

Merge and rebase are two ways of bringing one branch's work into another, and the difference matters:

Merge Rebase
What it does Creates a new commit with two parents Rewrites the commits onto another base
Resulting history Branched, reflecting what actually happened Linear, easier to read
Risk None Never on already-shared branches
Typical use at Contoso Never by hand: the pull request does it Updating your branch with main before asking for review
git switch -c feature/1842-cabin-map   # Create a branch from main and switch to it
git commit -am "Add the cabin map with seat selection. AB#1842"
git fetch origin && git rebase origin/main   # Rewrite my commits on top of main
git add . && git rebase --continue           # After resolving a conflict: mark it and carry on
git push -u origin feature/1842-cabin-map    # Publish the branch in Azure Repos

The golden rule of rebase: only on your personal branch and before anyone else works on it. Rebasing main rewrites everybody's history and is the fastest way to ruin your team's day.

  1. Creating the contoso-reservas repository and cloning it

Contoso creates four repositories in the project, following the rule "one deployable artifact, one repository":

Repository Contents Deployed to
contoso-reservas The public website app-contoso-reservas-pro/dev
contoso-api-disponibilidad The Availability API app-contoso-api-disponibilidad-pro/dev
contoso-modelos Shared library of booking models A package in the feed (05-05)
contoso-infra Bicep templates for the whole platform Azure resources (05-06)
# Create the four repositories with the azure-devops CLI extension
for r in contoso-reservas contoso-api-disponibilidad contoso-modelos contoso-infra; do
  az repos create --name "$r"
done
az repos list --query "[].{name:name, url:webUrl}" -o table

git clone https://[email protected]/contoso-airlines/contoso-reservas/_git/contoso-reservas

  1. Authentication: Credential Manager, tokens and SSH

There are three ways for your local Git to authenticate against Azure Repos, and they are not equivalent:

Method How it works Recommendation
Git Credential Manager Opens the browser, authenticates with Entra ID (and MFA) and stores a short-lived token in the system credential store The default option on people's laptops
Personal access token (PAT) A manually generated string, with scopes and an expiry date, that acts as a password Only for automation that accepts nothing else
SSH keys A key pair; the public one is registered in your profile Good for Linux teams or anyone who prefers not to depend on a browser

Git Credential Manager ships with recent Git installations and respects the conditional access and MFA from 04-01: the session on Diego's laptop expires when the Entra ID policy says so, not when somebody happens to think of it. Personal access tokens are the most common source of security incidents in Azure DevOps. If you have to use one: grant it only the scopes it needs (Code (read) for a tool that only reads) and never Full access; give it the shortest viable expiry; store it in kv-contoso-pro and not in a configuration file; and bear in mind that a PAT inherits all the permissions of whoever created it, so an administrator's PAT is a master key.

ssh-keygen -t ed25519 -C "[email protected]"   # Generate the key pair
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub    # The public key is pasted into your Azure DevOps profile
# From then on you clone over SSH, with no passwords and no tokens
git clone [email protected]:v3/contoso-airlines/contoso-reservas/contoso-reservas

For pipelines, none of these three: they will use the workload identity federation of the service connections from 05-01. The consistency is the same as in module 4: if there is a way to authenticate without a secret, that is the one you use.

  1. Branching strategies and the one Contoso chooses

Strategy How it works Advantages Drawbacks Fits
GitFlow develop, release/*, hotfix/*, feature/* and main branches Supports several live versions at once Complex, long merges, slows delivery down Installable software with maintained versions
GitHub Flow main + feature branches; deploy on merge Simple, ideal for continuous deployment Requires solid automated tests Web services with a single live version
Trunk-based development with feature branches main always deployable + very short-lived branches (1-2 days) Trivial merges, fast feedback, better DORA metrics Demands discipline and feature flags Teams deploying daily
Pure trunk-based Everyone commits straight to main Maximum integration speed No prior review; unworkable with PCI DSS auditing Very mature teams with exhaustive tests

Contoso chooses trunk-based development with feature branches for three specific reasons: there is only one live version — the one on the internet — so GitFlow would solve a problem Contoso does not have while charging its complexity anyway; the PCI DSS audit requires review of changes, which rules out pure trunk-based with no pull requests; and short branches attack its worst DORA metric head on, the lead time for changes. The agreed naming convention: feature/<id>-<description>, bugfix/<id>-<description> and hotfix/<id>-<description>, always with the work item identifier up front.

gitGraph
    commit id: "v2.4.0"
    branch feature/1842-cabin-map
    commit id: "map API"
    checkout main
    branch bugfix/1855-seats
    commit id: "fix seat count"
    checkout main
    merge bugfix/1855-seats id: "PR 58"
    checkout feature/1842-cabin-map
    commit id: "rebase onto main"
    checkout main
    merge feature/1842-cabin-map id: "PR 61" tag: "v2.5.0"

The rule that makes this work: a branch open for more than two days is a warning sign. If the work is bigger than that, it is split into increments that can be merged into main without being switched on yet, using the feature flags from 05-04.

  1. Pull requests

A pull request proposes merging one branch into another and opens a conversation about it. It is the central quality mechanism and, incidentally, the documentary record the auditor was asking for in 05-01.

# Create the pull request from the CLI, linking the work item
az repos pr create --repository contoso-reservas \
  --source-branch feature/1842-cabin-map --target-branch main \
  --title "Cabin map with seat selection" \
  --description "Implements AB#1842. Includes tests for the seat count." \
  --work-items 1842 --reviewers [email protected] \
  --delete-source-branch true --squash true

--delete-source-branch true deletes the branch on completion: without it, in six months there will be two hundred dead branches and nobody will know which ones are still alive. --squash true is explained in the next section.

Contoso defines a description template in .azuredevops/pull_request_template.md, which Azure Repos preloads into every new pull request:

## What changes and why
<!-- Context for the reviewer: the problem, not just the solution -->
## Work item
AB#
## How it was tested
- [ ] New or updated unit tests
- [ ] Tested on `app-contoso-reservas-dev`
## Risks and rollback
<!-- What can break and how to go back -->
## Checklist
- [ ] No secrets or connection strings in the code
- [ ] Backward-compatible database migrations

Four practices separate a useful review from a formality. Small pull requests: above 400 lines, review quality falls off a cliff and things get approved out of fatigue, whereas a 60-line one gets real comments. Suggested changes: Azure Repos lets you propose the exact text from within a comment, and the recipient applies it with one click. Comment on the code, not the person: "this query makes one call per seat" instead of "you have written an N+1". And above all, the conversation is documentation: when Marta asks "why 90 seconds of cache and not 300?" and Diego answers with the figure from the fares provider, that answer stays attached to the change forever. It is the best architecture documentation that exists, because nobody has to remember to write it separately.

  1. Branch policies on main

This is where the verbal agreement becomes an enforced rule. A branch policy is a condition that must be met before a pull request can be completed. Contoso protects main with six:

Policy Contoso's configuration What it prevents
Minimum number of reviewers 2, and the author cannot approve A change getting in with no outside eyes
Check for linked work items Required Changes with no traceable justification
Check for comment resolution All resolved Objections quietly ignored
Build validation CI pipeline (05-03), required Merging code that does not build or fails tests
Limit merge types Squash merge only An unreadable main history
Automatic reviewers By path (section 7) Nobody with the necessary context finding out
COMMON="--repository-id $REPO_ID --branch main --blocking true --enabled true"

# 1. Minimum of 2 reviewers, no self-approval, resetting the
#    approvals if new commits arrive after approval
az repos policy approver-count create $COMMON \
  --minimum-approver-count 2 --creator-vote-counts false \
  --reset-on-source-push true --allow-downvotes false

# 2. Linked work item required (traceability)
az repos policy work-item-linking create $COMMON

# 3. All comments resolved before completing
az repos policy comment-required create $COMMON

# 4. Build validation: the CI pipeline must pass,
#    and its result expires after 12 hours (720 minutes)
az repos policy build create $COMMON \
  --build-definition-id $CI_PIPELINE_ID \
  --display-name "Contoso Bookings CI" \
  --valid-duration 720 --queue-on-source-update-only true

# 5. Squash merge is the only type allowed
az repos policy merge-strategy create $COMMON --allow-squash true \
  --allow-no-fast-forward false --allow-rebase false --allow-rebase-merge false

Three details people overlook. --reset-on-source-push true: if new commits arrive after approval, the approvals are voided — without this, someone can approve a two-line change and then slip five hundred lines in behind it. --valid-duration 720: the build expires after 12 hours, which stops you merging something validated against a main that has since become something else. And squash merge, which makes each pull request appear in main as a single clean commit: the history becomes the list of functional changes delivered, exactly what somebody investigating an incident wants to read.

The policies apply to administrators too, and that is the whole point. There is an option to grant "bypass policies", and the right answer is almost always not to grant it: if you need to skip the process because of an emergency, the process is badly calibrated. For genuine emergencies, Contoso keeps hotfix/* branches with their own policy of a single reviewer, going through the same pipeline.

  1. Automatic reviewers by path: Azure Repos' CODEOWNERS

In GitHub, a CODEOWNERS file assigns owners per folder. Azure Repos does the same with the automatically included reviewers policy, configured per path — not in a file in the repository, but in the policy itself:

# Infrastructure and pipelines: mandatory review by Contoso-Infraestructura
az repos policy required-reviewer create $COMMON \
  --message "Infrastructure changes: review by Contoso-Infraestructura" \
  --required-reviewer-ids "Contoso-Infraestructura" \
  --path-filter "/infra/*;/*.bicep;/azure-pipelines*.yml"

# Same for the data schema, with Contoso-DBA-Reservas and the filter
# --path-filter "/src/Contoso.Reservas.Datos/Migraciones/*"

The groups are the same ones from module 4, so people management stays in a single place. The practical effect: Diego can no longer change the db-reservas schema without a DBA finding out, and not because he has been forbidden to, but because the merge will not complete.

  1. Tags and semantic versioning

A tag marks a specific commit with a permanent name. It is what lets you answer "which exact code is version 2.5.0, the one in production?".

Contoso uses semantic versioning MAJOR.MINOR.PATCH:

Part When it goes up Example at Contoso
MAJOR Breaking change The Availability API stops accepting the old date format
MINOR New backward-compatible functionality The cabin map is added
PATCH Backward-compatible fix The seat count is fixed
# Annotated tag (with author, date and message) on the current commit
git tag -a v2.5.0 -m "Cabin map and seat count fix"
git push origin v2.5.0   # Tags do NOT travel with a normal git push
git show v2.5.0 --stat   # Exactly which commit the deployed version is

In 05-05 the package version number will be generated automatically from the pipeline, and in 05-04 the tag will be created by itself on deployment to production, so that the correspondence between tag and environment does not depend on anybody typing it by hand.

  1. .gitignore and the mistake of pushing a secret

The .gitignore file states what must never enter the repository:

bin/
obj/
node_modules/
appsettings.Development.local.json   # Local configuration with secrets
.env
*.pfx                                # Private keys: never in the repository
*.pem
.vs/

Now the serious mistake. Diego accidentally commits an appsettings.json with the connection string for sql-contoso-reservas-dev and the payment gateway key. He notices ten minutes later and deletes the file in the next commit. The secret is still compromised, because Git history is immutable and every commit keeps the full content: anybody who has cloned the repository — or any backup, or any build log — has the value. Deleting it afterwards does not remove it, it only hides it from a surface glance.

The correct procedure, in strict order:

  1. Rotate the secret immediately. That is the only thing that genuinely closes the hole: generate a new payment gateway key, update it in kv-contoso-pro and leave the previous one worthless. Everything else is cleanup, not remedy.
  2. Check for usage. Review the resource's access logs in case the credential was used from outside; this is what the Microsoft Defender for Cloud alerts from 04-05 are for.
  3. Clean the history with git filter-repo, coordinating a re-clone with the whole team. It is destructive and disruptive, and on large repositories it often is not worth it.
  4. Prevent a repeat: a correct .gitignore, review in the pull request and automatic detection.

Azure DevOps includes secret scanning that analyzes commits and warns you — and with GitHub Advanced Security for Azure DevOps, which is paid, it can block the push before the secret gets in. A local hook or an analysis step in the pipeline stops most of the remaining cases.

The underlying solution is one you already know from module 4: if there are no secrets, they cannot leak. Contoso's application authenticates against SQL and against storage with a managed identity, and the little that genuinely remains secret — pasarela-pago-clave, token-meteo — lives in kv-contoso-pro and is referenced with @Microsoft.KeyVault(...). The configuration file in the repository contains resource names, never credentials.

  1. Migrating the local repository while preserving history

Diego's repository holds three years of history that cannot be lost: it is what lets you know why each line was written. The migration preserves all of it:

cd ~/projects/contoso-reservas   # Diego's existing local repository
git remote add azure https://[email protected]/contoso-airlines/contoso-reservas/_git/contoso-reservas
git push azure --all       # ALL branches, with their history intact
git push azure --tags      # ALL tags
git log azure/main --oneline | wc -l   # Check that the history arrived complete

# If the origin was another Git server, the clean way is a mirror clone,
# which includes every ref and not just the local branches:
git clone --mirror https://servidor-antiguo.contosoairlines.example/reservas.git
cd reservas.git && git push --mirror $AZURE_REPOS_URL

Before calling the migration done, two things: set main as the default branch and make the old repository read-only. If it carries on accepting pushes, two divergent histories will appear and somebody will lose work.

  1. Git LFS for heavy graphic assets

The website includes destination images, high-resolution cabin maps and header videos: binaries tens of megabytes in size. Git stores every full version of every binary forever, so a 30 MB map modified twenty times is 600 MB permanently that everybody downloads when they clone. Git LFS (Large File Storage) replaces the binary in history with a text pointer and stores the content separately, downloading only the version you need:

git lfs install                              # Once per repository and machine
git lfs track "*.psd" "*.mp4" "recursos/imagenes/**/*.png"
git add .gitattributes                       # The tracking config is stored there
git commit -m "Configure Git LFS for heavy graphic assets"

Two warnings: LFS has to be configured before you push the binaries (doing it afterwards requires rewriting history), and its storage counts against the organization's billable Artifacts quota. The assets the website serves in production live in a storage account behind the CDN from 02-06; only what is part of the source code goes into the repository.

  1. Comparison with GitHub Repos

Azure Repos GitHub Repos
Branch protection More granular policies, with path filters and expiry Branch rules, simpler
Code owners Reviewer policy by path Versioned CODEOWNERS file
Code review Good; suggested changes Excellent; the industry standard
Code security Secret scanning; paid Advanced Security More mature Advanced Security; Dependabot
Ecosystem and community Internal Enormous

In practice, GitHub's review experience tends to feel nicer, while Azure Repos' policies are more granular. Contoso stays on Azure Repos for the Boards integration and the traceability PCI DSS demands.

Common Mistakes and Tips

  • Long-lived branches. A three-week branch guarantees a painful merge. If the work is big, split it into mergeable increments and hide it behind feature flags.
  • Rebasing a shared branch. It rewrites everybody's history. Rebase only on your own branch and before anyone else works on it.
  • Enormous pull requests and unread approvals. Beyond 400 lines the review is a formality; and the two-reviewer policy is worth nothing if the second signature is automatic. Better five 80-line pull requests and one honest review.
  • Believing that deleting the commit deletes the secret. It does not. Always rotate, then clean up.
  • Granting "bypass policies" to administrators. It turns the protection into a suggestion. If there are emergencies, configure a separate, more relaxed policy on hotfix/*.
  • Pushing heavy binaries without LFS. The repository grows forever and cloning becomes unbearable; and remember that LFS consumes billable quota.
  • Tip: write commit messages with a short imperative title, a body explaining why — the what is already visible in the diff — and the AB#<id> reference.
  • Tip: enable automatic deletion of the source branch on pull request completion and periodically purge branches with no activity.

Exercises

Exercise 1: choosing and justifying the branching strategy

The Contoso Miles team (centro-coste=CC-2077) maintains a web portal and, on top of that, a mobile app whose version 3.2 is still installed on customers' phones and receives fixes while 4.0 is being developed.

  1. Which branching strategy suits each of the two products, and why are they different?
  2. For the portal, define the branch policies you would put on main along with your reasoning.
  3. How would you handle an urgent fix to the portal on a Saturday without skipping the process?

Exercise 2: a secret in the repository

Diego commits and pushes to main an appsettings.json containing the payment gateway key and the connection string for sql-contoso-reservas-pro. He realizes twenty minutes later. Two colleagues have already run git pull.

  1. List the actions in order, stating which one actually closes the risk.
  2. Why is a commit that deletes the file not enough, nor a revert?
  3. Which three technical measures would stop it happening again, and which module 4 architecture decision makes it almost impossible?

Exercise 3: policies and reviewers by path

Contoso wants: (a) nobody to merge into main without two approvals and without a work item; (b) every change under /src/Contoso.Reservas.Datos/Migraciones/ to be reviewed by Contoso-DBA-Reservas; (c) the main history to stay linear; (d) a build approved three days ago to be useless for merging today.

  1. State which specific policy solves each point and write the CLI command for (b).
  2. What happens if Marta, who is a project administrator, tries to merge without meeting them?

Solutions

Solution 1:

  1. For the portal, trunk-based development with feature branches: a single live version (the deployed one), frequent deployment and branches lasting a day or two. For the mobile app, a GitFlow-style flow or at least long-lived release/3.2 and release/4.0 branches, because there are two live versions simultaneously: the one installed on phones, which is still getting patches, and the one under development. The difference is not a matter of taste: on the web you control which version the customer runs; on mobile you do not.
  2. A minimum of 2 reviewers with no self-approval and reset on push; linked work item (traceability); comments resolved; build validation with a 12-hour expiry; squash merge only; and automatic reviewers by path for infrastructure and schema.
  3. A hotfix/<id>-<description> branch from main, with its own policy on the hotfix/* pattern requiring a single reviewer but keeping build validation and the work item. It goes through the same pipeline and the same deployment; the only thing relaxed is the number of eyes, not the automation. Never grant "bypass policies".

Solution 2:

  1. (a) Rotate the payment gateway key and change the database credential — this and only this closes the risk; update the new value in kv-contoso-pro. (b) Review the access logs and the Defender for Cloud alerts in case there was misuse. (c) Add the file to .gitignore and remove it from tracking. (d) Consider rewriting the history with git filter-repo and coordinating the re-clone, given that the repository is private and the team is small. (e) Report it to the security lead: this is an incident, not an oversight.
  2. Because Git history is immutable and every commit stores the full content: the value is still recoverable in any clone, backup or build log. A revert adds a new commit, it does not remove the previous one. As long as the credential is still valid, it is still compromised.
  3. Secret scanning enabled on the repository; a pre-commit hook or an analysis step in the CI pipeline; and a complete .gitignore with mandatory review of configuration files in the pull request. The architecture decision: authentication with a managed identity — no password-bearing connection string to leak — and the little that is irreducible in Key Vault, referenced with @Microsoft.KeyVault(...). If there is no secret in the code, there is no secret to push.

Solution 3:

  1. (a) approver-count with --minimum-approver-count 2 --creator-vote-counts false, plus work-item-linking. (b) required-reviewer with its path filter and the Entra ID group: az repos policy required-reviewer create --repository-id $REPO_ID --branch main --blocking true --enabled true --required-reviewer-ids "Contoso-DBA-Reservas" --path-filter "/src/Contoso.Reservas.Datos/Migraciones/*". (c) merge-strategy allowing only --allow-squash true. (d) policy build with --valid-duration 720, which invalidates builds older than 12 hours.
  2. She cannot. Policies apply equally to project administrators unless the bypass-policies permission is explicitly granted, which Contoso does not do. That is precisely what turns the team's agreement into a real rule and what satisfies the auditor: there are no exceptions by job title.

Conclusion

Contoso Airlines' code no longer lives on a laptop or travels by email. You have gone over the Git essentials — commit, branch, merge and rebase, with the rule of never rebasing shared branches — you have created the project's four repositories and you have migrated Diego's local repository preserving its three years of history, leaving the old origin read-only so that two truths do not appear. You know how to authenticate with Git Credential Manager as the default, with SSH where it suits, and with personal access tokens only when there is no alternative, knowing that a PAT inherits every permission of whoever created it.

You have consciously chosen the branching strategy: trunk-based development with feature branches, with very short-lived branches, because Contoso has a single live version, needs review for PCI DSS and its weakest DORA metric is lead time. And you have turned the quality process into something that does not depend on anybody's memory: pull requests with a template, reviewers and a recorded conversation — which is, incidentally, the best architecture documentation the team will ever have — and policies on main requiring two approvals with no self-approval, a linked work item, resolved comments, a valid build validation and squash merge, with automatic reviewers by path that take every schema change to Contoso-DBA-Reservas and every template to Contoso-Infraestructura. You know how to tag with semantic versioning, how to maintain a serious .gitignore and, above all, you know that a pushed secret has to be rotated: deleting the commit deletes nothing, and the underlying solution is still module 4's, having no secrets to leak.

One very visible loose end remains. The most important policy you have configured — build validation — points at a pipeline that does not exist yet: today nobody checks that the code builds, that the tests pass or that no obvious bug has slipped in before merging. In the next lesson, Azure Pipelines: Continuous Integration, you will build that pipeline: hosted and self-hosted agents with their real costs, the anatomy of an azure-pipelines.yml explained line by line, restore, build, tests with published results and coverage, static analysis, caching, reusable templates so you do not repeat the same YAML in every service, and secrets coming in from Key Vault without ever passing through the repository. By the end of it, the rule "main always builds" will stop being an intention and become a fact verified on every change.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved