The Linux kernel — one of the largest open-source projects in history — manages over 30,000 files and contributions from thousands of developers worldwide. It was the project that motivated Linus Torvalds to create Git in 2005. Azure Repos uses that same Git engine under the hood.
Azure Repos & Branching Strategies
Master Git-based version control in Azure DevOps — repositories, branch policies, pull request workflows, merge strategies, and branching models that keep your codebase stable and your team productive.
🧒 Simple Explanation (ELI5)
Imagine a team of artists all working on the same giant painting. If everyone just paints on one canvas at the same time, it's chaos — someone paints a tree where someone else was painting a cloud, colors get mixed up, and nobody knows who changed what.
Version control is like giving each artist their own transparent overlay sheet. Everyone draws on their own sheet. When they're happy with their work, they hold it up and say "Hey team, I'd like to add this to the master painting." The team reviews it, checks it doesn't clash with other changes, and then carefully layers it onto the main canvas.
- Azure Repos = the art studio — it stores every version of the painting, every overlay sheet, and a full history of every brushstroke ever made
- A branch = a transparent overlay sheet — your personal workspace where you make changes without disturbing the main painting
- A pull request (PR) = holding up your sheet for review — teammates check your work, leave comments, and approve before it's merged
- Merge = carefully layering your approved changes onto the master painting
- Branch policies = the studio rules — "at least two artists must approve," "the manager must verify it doesn't break anything," "you can't glue your sheet on without a review"
Without branching? Developers push directly to the main codebase, bugs land in production instantly, and nobody reviews anything. It's the equivalent of all artists painting on the same canvas with no coordination — guaranteed collisions and messes that take hours to untangle.
🔧 Technical Explanation
Git vs. TFVC in Azure Repos
Azure Repos supports two version control systems. Git is the default and recommended choice for all new projects.
| Aspect | Git (Distributed) | TFVC (Centralized) |
|---|---|---|
| Architecture | Every developer has a full copy of the repo locally — commits, branches, and history all happen offline | Code lives on a central server. Developers check out files, edit, and check back in. No local history. |
| Branching | Lightweight — branches are just pointers. Creating a branch is instant, even on repos with millions of files. | Heavy — branches are server-side copies. Branch creation is slower and consumes server resources. |
| Offline Work | Full functionality offline — commit, branch, diff, log, blame all work without network. | Requires server connection for most operations. Offline edits cause conflicts on reconnect. |
| Merge Model | Three-way merge with advanced conflict resolution. PRs are native and expected. | Server-based merge. Conflicts resolved at check-in time. |
| Ecosystem | Universal — every CI/CD system, IDE, and code review tool supports Git natively. | Proprietary — only Visual Studio (classic) and Azure DevOps web UI support TFVC. |
| Recommendation | Always choose Git for new projects | Only for legacy codebases migrating from TFS that can't convert yet |
Microsoft has announced that TFVC will no longer receive new features. All investment goes into Git-based Azure Repos. If you're starting fresh, there is zero reason to pick TFVC. If you're on TFVC, plan a migration to Git using the built-in import tool or git-tfs.
Azure Repos Key Features
- Unlimited free private repositories — no per-repo pricing. Each project can have multiple repos (mono-repo or multi-repo).
- Web-based editor — edit files, create commits, and make PRs directly in the browser without cloning.
- Code Search — semantic search across all repos in your organization. Find classes, methods, and code patterns instantly.
- Rich diff viewer — inline and side-by-side diffs, syntax highlighting, and blame annotations in the web UI.
- Git LFS support — store large binary files (images, videos, ML models) in Git Large File Storage without bloating your repo.
- Forks — create server-side forks for open-source-style contribution models within your organization.
- GitHub Advanced Security for Azure DevOps (GHAzDO) — secret scanning, dependency scanning, and code scanning integrated directly into Azure Repos.
Branch Policies
Branch policies are rules that enforce code quality and team governance on protected branches (typically main, develop, and release/*). They're configured per branch (or branch pattern) in Project Settings → Repositories → Policies.
| Policy | What It Does | Typical Setting |
|---|---|---|
| Minimum number of reviewers | PRs must have N approving reviewers before merge is allowed | 2 reviewers for main, 1 for develop |
| Check for linked work items | Requires at least one Azure Boards work item linked to the PR | Required on all protected branches |
| Check for comment resolution | All PR comments must be resolved (marked as "resolved" or "won't fix") before merge | Required — prevents ignored feedback |
| Build validation | A CI pipeline must run and pass on the PR branch before merge | Required — catches build/test failures before merge |
| Limit merge types | Restrict which merge strategies are allowed (squash, rebase, merge commit, semi-linear) | Squash merge only for feature branches |
| Automatically include reviewers | Auto-add specific reviewers when certain file paths change (e.g., infra team for /terraform/**) | Path-based code owners |
| Reset votes on new pushes | If the author pushes new commits after approval, previous approvals are reset — reviewers must re-approve | Required for high-security branches |
Branch policies only apply to changes coming through pull requests. They do not prevent project administrators from bypassing or pushing directly (unless you also remove the "Bypass policies when pushing" permission from the admin group in repository security settings). Always audit these permissions for production-critical branches.
Pull Request Workflow
Pull requests are the core collaboration mechanism in Azure Repos. Here's the complete lifecycle:
- Developer creates a branch from
main(e.g.,feature/user-login) - Developer writes code, makes commits, and pushes the branch to the remote
- Developer opens a PR — sets a title, description, links work items, and requests reviewers
- Build validation runs automatically — CI pipeline triggers on the PR branch. If it fails, the PR is blocked.
- Reviewers review the code — leave inline comments, suggest changes, request modifications, or approve
- Developer addresses feedback — pushes additional commits. If "reset votes on push" is enabled, reviewers re-approve.
- All policies pass — required reviewers approved, build succeeded, comments resolved, work item linked
- PR is merged — using the allowed merge strategy (squash, rebase, or merge commit)
- Branch is deleted — the feature branch is cleaned up automatically (if configured)
Merge Strategies
Azure Repos supports four merge strategies. Understanding when to use each is critical for maintaining a clean Git history.
1. Merge Commit (No Fast-Forward)
Creates a merge commit that joins the feature branch into the target. All individual commits from the feature branch are preserved in history.
# History after merge commit: * M Merge PR #42: feature/login → main |\ | * C3 Add login validation | * C2 Add login form | * C1 Create auth module |/ * B Previous commit on main
Pros: Full history preserved, easy to see what belonged to a feature branch.
Cons: History can get noisy with many branches. Hard to read linear log.
Best for: Teams that value complete history and traceability.
2. Squash Merge
Combines all commits from the feature branch into a single commit on the target branch. The feature branch history is discarded.
# History after squash merge: * S Add user login feature (squashed from PR #42) * B Previous commit on main # The 3 individual commits (C1, C2, C3) are gone from main's history
Pros: Clean, linear history. Each PR = one commit. Easy to revert an entire feature.
Cons: Loses granular commit history. Can't see intermediate steps.
Best for: Most teams — the recommended default. Feature history lives in the PR; main stays clean.
3. Rebase
Replays each commit from the feature branch on top of the target branch, creating new commit hashes. No merge commit.
# History after rebase: * C3' Add login validation * C2' Add login form * C1' Create auth module * B Previous commit on main # Commits are replayed with new hashes (C1', C2', C3')
Pros: Linear history with all individual commits preserved.
Cons: Rewrites history (new hashes). Can cause issues if commits aren't well-structured.
Best for: Teams with disciplined commit hygiene who want linear history without squash.
4. Semi-Linear Merge (Rebase + Merge Commit)
First rebases the feature branch on the target, then creates a merge commit. Combines linear history with merge context.
# History after semi-linear merge: * M Merge PR #42: feature/login → main |\ | * C3' Add login validation | * C2' Add login form | * C1' Create auth module |/ * B Previous commit on main
Pros: Clean merge topology — every feature branch is based on the latest main.
Cons: More complex. Requires the branch to be up-to-date with main before merging.
Best for: Teams that want the structure of merge commits but the cleanliness of rebased branches.
Branching Strategies: Git Flow vs. Trunk-Based Development
Your branching strategy defines how your team organizes branches and manages the flow of code from development to production. The two dominant strategies are Git Flow and Trunk-Based Development.
Git Flow
A structured model with multiple long-lived branches for different stages of development:
main— always reflects production. Every commit is a release. Tagged with version numbers.develop— integration branch where features are merged. Represents the next release.feature/*— short-lived branches offdevelopfor new features. Merged back todevelopvia PR.release/*— branched fromdevelopwhen preparing a release. Bug fixes go here. Merged to bothmainanddevelop.hotfix/*— branched frommainfor critical production fixes. Merged to bothmainanddevelop.
Best for: Teams with scheduled releases, multiple versions in production, or long QA cycles.
Trunk-Based Development
A simpler model where all development happens on a single branch (main) with very short-lived feature branches:
main(trunk) — the single source of truth. Deployable at all times.feature/*— ultra-short-lived branches (hours to 1–2 days max). Merged via PR with squash.- No
develop,release, orhotfixbranches — everything goes throughmain. - Feature flags — incomplete features are hidden behind feature flags (LaunchDarkly, Azure App Configuration) so code can be merged to
mainwithout being exposed to users. - Continuous deployment — every merge to
maintriggers a deployment to production (or at least to staging).
Best for: Teams practicing continuous delivery/deployment, microservices, and modern cloud-native workflows.
| Aspect | Git Flow | Trunk-Based Development |
|---|---|---|
| Branches | 5 types (main, develop, feature, release, hotfix) | 2 types (main + short-lived features) |
| Branch lifetime | Features: days–weeks. Develop: permanent. | Features: hours to 1–2 days max. |
| Merge conflicts | Higher — long-lived branches diverge significantly | Lower — branches are short, frequent integration |
| Release process | Formal: cut a release branch, QA, merge, tag | Continuous: every merge to main is deployable |
| Complexity | Higher — more branches to manage, more merge paths | Lower — one branch, simple rules |
| CI/CD maturity | Moderate — CI on develop, CD on release branches | High — CI/CD on every commit to main |
| Feature flags | Not required (features isolated in branches) | Essential (incomplete features merged to main) |
| Best for | Scheduled releases, versioned products, mobile apps | Web services, microservices, continuous deployment |
Most modern teams (including Microsoft, Google, and Meta) use Trunk-Based Development. If you're building a web application or microservices deployed to Kubernetes, TBD with feature flags is the right choice. Git Flow is still valid for versioned desktop software, mobile apps, or products supporting multiple versions simultaneously.
📊 Branching Strategy Comparison
🛠️ Hands-on: Branches, Policies & Pull Requests
Follow these steps to set up a professional branching workflow in Azure DevOps from scratch.
Prerequisites
- An Azure DevOps project with a Git repository (created in Lesson 1)
- Git installed locally (
git --versionshould return 2.x+) - A Personal Access Token with
Code (Read & Write)scope
Step 1: Clone the Repository
# Clone via HTTPS (use your PAT as the password when prompted)
git clone https://dev.azure.com/{org}/{project}/_git/{repo}
cd {repo}
# Verify you're on the main branch
git branch
# * main
Step 2: Create a Feature Branch
# Create and switch to a new feature branch git checkout -b feature/user-authentication # Verify you're on the new branch git branch # main # * feature/user-authentication # Make some changes echo "# Authentication Module" > auth.md echo "def login(user, password):" > auth.py echo " return authenticate(user, password)" >> auth.py # Stage and commit git add . git commit -m "feat: add authentication module skeleton" # Push the branch to Azure Repos git push -u origin feature/user-authentication
Use a prefix to categorize branches: feature/ for new functionality, bugfix/ for bug fixes, hotfix/ for production emergencies, release/ for release preparation. Include the work item ID when possible: feature/1234-user-login. This makes it easy to link branches to Azure Boards work items.
Step 3: Set Branch Policies on main
Configure branch policies in the Azure DevOps web UI:
- Navigate to Project Settings → Repositories → your repo → Policies
- Under Branch Policies, select the
mainbranch - Configure the following policies:
✅ Require a minimum number of reviewers → Minimum reviewers: 2 → Allow requestors to approve their own changes: No → Reset code reviewer votes when there are new changes: Yes ✅ Check for linked work items → Required ✅ Check for comment resolution → Required ✅ Limit merge types → ✅ Squash merge → ❌ Basic merge (no fast-forward) → ❌ Rebase and fast-forward → ❌ Rebase with merge commit
Step 4: Add Build Validation
Still on the branch policies page for main:
- Scroll to Build Validation and click "+ Add build policy"
- Select your CI pipeline (e.g.,
MyApp-CI) - Set the trigger to "Automatic" — the build runs on every PR update
- Set "Policy requirement" to "Required" — PR cannot be merged unless the build passes
- Set a display name:
CI Build Validation - Set build expiration: "Immediately when main is updated" — if someone else merges to main before you, your validation re-runs
Build validation is the single most impactful branch policy. It guarantees that no PR can be merged with a broken build or failing tests. Configure this before anything else — it's the safety net that catches 90% of integration issues.
Step 5: Create a Pull Request
# Push your latest changes git push origin feature/user-authentication
Then in the Azure DevOps web UI:
- Navigate to Repos → Pull Requests → "New pull request"
- Set source:
feature/user-authentication→ target:main - Title:
feat: add user authentication module - Description: explain what the PR does, why, and any testing notes
- Link a work item: click "Link work items" and select the related user story
- Add reviewers: select team members from your project
- Click "Create"
You can also create PRs from the command line using the Azure CLI:
# Install Azure DevOps CLI extension
az extension add --name azure-devops
# Sign in
az devops configure --defaults organization=https://dev.azure.com/{org} project={project}
az login
# Create PR from command line
az repos pr create \
--repository {repo} \
--source-branch feature/user-authentication \
--target-branch main \
--title "feat: add user authentication module" \
--description "Adds the auth module skeleton with login function" \
--work-items 1234
Step 6: Review and Merge the PR
- Reviewers open the PR, review the diff, leave inline comments
- The author addresses comments and pushes fixes (votes reset if configured)
- Reviewers approve — the "Approve" button shows a green checkmark
- All policies pass: ✅ Reviewers, ✅ Build passed, ✅ Comments resolved, ✅ Work item linked
- Click "Complete" → set merge type to "Squash commit"
- Check "Delete feature/user-authentication after merging"
- Click "Complete merge"
Step 7: Clean Up Locally
# Switch back to main and pull the merged changes git checkout main git pull origin main # Delete the local feature branch (it's been merged and deleted on remote) git branch -d feature/user-authentication # Prune remote-tracking branches that no longer exist on the server git fetch --prune # Verify clean state git log --oneline -5 # abc1234 feat: add user authentication module (squashed)
Essential Git Commands Reference
# ===== BRANCHING ===== git branch # List local branches git branch -r # List remote branches git branch -a # List all branches (local + remote) git checkout -b feature/new-thing # Create and switch to new branch git switch -c feature/new-thing # Same as above (modern Git syntax) git branch -d feature/merged-branch # Delete a local branch (safe — only if merged) git branch -D feature/force-delete # Force-delete a local branch (use with caution) git push origin --delete feature/old # Delete a remote branch # ===== REBASING & UPDATING ===== git fetch origin # Download remote changes without merging git pull --rebase origin main # Pull main and rebase your branch on top git rebase main # Rebase current branch onto main git rebase --abort # Cancel an in-progress rebase # ===== STASHING ===== git stash # Save uncommitted changes temporarily git stash pop # Restore stashed changes git stash list # List all stashes # ===== LOG & HISTORY ===== git log --oneline --graph --all # Visual branch graph git log --author="name" --since="2 weeks ago" # Filter by author and date git blame path/to/file.py # See who changed each line and when git diff main..feature/branch # Compare two branches # ===== UNDOING ===== git reset --soft HEAD~1 # Undo last commit, keep changes staged git reset --hard HEAD~1 # Undo last commit AND discard changes (DANGEROUS) git revert abc1234 # Create a new commit that undoes a specific commit (safe)
🐛 Debugging Scenarios
Scenario 1: "PR Stuck in Waiting — Build Validation Failing"
Symptom: Your PR shows "1 check failed" or "Waiting for build validation." The Complete button is disabled.
Root Causes & Fixes:
- Build is genuinely failing: Click the failed check to open the build run. Check the logs — it could be a compilation error, failing test, or linting issue in your branch. Fix the code, commit, and push — the build re-triggers automatically.
- Build is queued but not running: Your organization may have hit the parallel job limit. Go to Organization Settings → Pipelines → Parallel Jobs to check. Free tier: 1 parallel job, 1,800 min/month. If at capacity, wait or purchase additional parallel jobs.
- Build pipeline was deleted or renamed: The branch policy still references an old build definition. Go to Project Settings → Repositories → Policies and update the build validation to point to the current pipeline.
- Build expires after main is updated: If someone else merged to main, your build validation may have expired (if "immediately when main is updated" is set). The build needs to re-run. Push a trivial commit or re-queue the build manually.
# Quick fix: update your branch with latest main to trigger a rebuild git fetch origin git rebase origin/main git push --force-with-lease origin feature/your-branch # --force-with-lease is safer than --force — it refuses to push # if someone else has pushed to your branch since your last fetch
Scenario 2: "Cannot Push to Main — Branch Policy Violation"
Symptom: Running git push origin main returns an error like: "TF402455: Pushes to this branch are not permitted; you must use a pull request to update this branch."
Root Causes & Fixes:
- This is expected behavior! Branch policies prevent direct pushes to
main. You must create a feature branch, push there, and open a PR. - If you genuinely need to bypass (emergency): A Project Collection Administrator can push directly — but only if they have the "Bypass policies when pushing" permission. This should be used only for true emergencies and should be audited.
- If you committed to main locally by mistake:
# You accidentally committed to main locally. Move the commit to a feature branch: # 1. Create a new branch from your current position (which has the accidental commit) git branch feature/my-accidental-work # 2. Reset main back to match the remote git reset --hard origin/main # 3. Switch to the feature branch and push git checkout feature/my-accidental-work git push -u origin feature/my-accidental-work # 4. Now create a PR in the web UI
Scenario 3: "Merge Conflicts in Pull Request"
Symptom: The PR shows "This pull request has merge conflicts that must be resolved before it can be completed." Files are listed with conflict indicators.
Root Causes & Fixes:
- Another PR was merged to main that changed the same files. Your branch has diverged from main. You need to resolve conflicts by updating your branch.
# Option A: Rebase your branch onto latest main (preferred — cleaner history) git fetch origin git checkout feature/your-branch git rebase origin/main # If conflicts appear, Git will pause at each conflicting commit: # 1. Open the conflicting files — look for conflict markers: # <<<<<<< HEAD # (your changes) # ======= # (changes from main) # >>>>>>> origin/main # 2. Edit the file to keep the correct code (remove all markers) # 3. Stage the resolved file git add path/to/resolved-file.py # 4. Continue the rebase git rebase --continue # 5. Push (force-with-lease because rebase rewrites history) git push --force-with-lease origin feature/your-branch # ==================== # Option B: Merge main into your branch (simpler but creates a merge commit) git fetch origin git checkout feature/your-branch git merge origin/main # Resolve conflicts the same way (steps 1–3 above) git add path/to/resolved-file.py git commit # Git auto-generates a merge commit message git push origin feature/your-branch
You can also resolve simple conflicts directly in the Azure DevOps web UI. Click the conflicting file in the PR, and use the inline conflict editor to choose "Take source" or "Take target" for each conflict block. This works for simple cases but is not practical for complex conflicts across many files.
Scenario 4: "Clone Authentication Failed"
Symptom: Running git clone returns fatal: Authentication failed for 'https://dev.azure.com/...'.
Root Causes & Fixes:
- Using your Microsoft password instead of a PAT: Azure DevOps HTTPS requires a Personal Access Token. Generate one from User Settings → Personal Access Tokens with
Code (Read & Write)scope. Use it as the password. - PAT expired or missing Code scope: Check your token in the Personal Access Tokens page. Create a new one if expired.
- Credential Manager caching stale credentials:
# Windows: Clear cached credentials
git credential-manager reject https://dev.azure.com
# Or use Credential Manager UI:
# Control Panel → Credential Manager → Windows Credentials
# Find and delete entries for "git:https://dev.azure.com"
# macOS: Clear from Keychain
git credential-osxkeychain erase <<EOF
host=dev.azure.com
protocol=https
EOF
# Linux: If using credential store
git config --global --unset credential.helper
# Then re-clone to be prompted for fresh credentials
# Alternative: Use SSH instead of HTTPS
# 1. Generate SSH key
ssh-keygen -t ed25519 -C "your-email@company.com"
# 2. Add public key to Azure DevOps:
# User Settings → SSH Public Keys → + New Key
# Paste contents of ~/.ssh/id_ed25519.pub
# 3. Clone with SSH URL
git clone git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
🎯 Interview Questions
Beginner
Azure Repos is the version control service within Azure DevOps that provides enterprise-grade Git repositories. It supports two version control systems: Git (distributed) — the recommended choice for all new projects, providing full local history, lightweight branching, and offline capability. TFVC (Team Foundation Version Control, centralized) — a legacy system carried over from TFS, where code lives on a central server and developers check out files. Each project can have multiple Git repositories. Azure Repos provides branch policies, pull request workflows, code search, web-based editing, Git LFS for large files, and integrates with Azure Boards and Azure Pipelines. Microsoft recommends Git for all new projects — TFVC receives no new features.
A branch policy is a rule configured on a branch (typically main) that enforces quality and process requirements before code can be merged via pull request. Key policies include: minimum reviewers (e.g., 2 approvals required), build validation (CI pipeline must pass), linked work items (traceability to user stories), comment resolution (all review comments must be resolved), and merge type restrictions (e.g., squash only). Branch policies are critical because they automate enforcement of team standards — nobody can skip code review, merge broken builds, or bypass traceability. Without them, quality depends entirely on developer discipline, which doesn't scale. They're configured per branch or branch pattern in Project Settings → Repositories → Policies.
Merge commit: Creates a new commit that joins two branches, preserving all individual commits from the feature branch in history. The history shows the branch topology. Squash merge: Combines all commits from the feature branch into a single commit on the target branch. The granular history is lost from the main branch (but preserved in the PR). Produces a clean, linear history. Rebase: Replays each commit from the feature branch on top of the target branch, creating new commit hashes. Produces a linear history while preserving individual commits. When to use each: Squash is the most popular for team workflows — each PR becomes one commit, easy to revert. Merge commit when you need full history. Rebase for teams with disciplined commit practices who want linear history without squashing.
You create a PR after pushing a feature branch to Azure Repos. Steps: 1. Push your branch: git push -u origin feature/my-feature. 2. In Azure DevOps web UI, go to Repos → Pull Requests → New Pull Request. 3. Select the source branch (feature/my-feature) and target branch (main). 4. Write a descriptive title and description explaining the changes. 5. Link related work items for traceability. 6. Add reviewers. 7. Click Create. Alternatively, use the Azure CLI: az repos pr create --source-branch feature/my-feature --target-branch main --title "My PR". After creation, build validation runs automatically (if configured), reviewers are notified, and the PR shows policy compliance status.
git pull and git fetch?git fetch downloads new commits, branches, and tags from the remote repository to your local machine, but does not modify your working directory or current branch. It updates your remote-tracking branches (e.g., origin/main) so you can see what's changed. git pull is essentially git fetch + git merge — it downloads remote changes AND immediately merges them into your current branch. Best practice: Use git fetch first to see what's changed, then decide how to integrate (merge or rebase). Or use git pull --rebase to fetch and rebase your local commits on top of remote changes, avoiding unnecessary merge commits.
Intermediate
Git Flow uses multiple long-lived branches: main (production), develop (integration), feature/*, release/*, and hotfix/*. It's structured with formal release processes — features merge to develop, release branches are cut for QA, then merged to main with version tags. Best for: scheduled releases, versioned products (mobile apps, desktop software), and teams supporting multiple versions. Trunk-Based Development (TBD) uses one long-lived branch (main) with very short-lived feature branches (hours to 1–2 days). Incomplete features are hidden behind feature flags. Every merge to main is deployable. Best for: web services, microservices, continuous deployment, and teams with mature CI/CD. Key trade-off: Git Flow provides isolation at the cost of complexity and merge pain. TBD provides simplicity and speed at the cost of requiring feature flags and robust CI/CD. Most modern teams (Google, Microsoft, Meta) use TBD.
Use the "Automatically included reviewers" branch policy. In Project Settings → Repositories → Policies → [branch], add entries under "Automatically include reviewers" with: 1. A path filter (e.g., /terraform/**, /src/security/**, *.sql). 2. The reviewers to auto-add when those paths are modified. 3. Whether the review is "Required" (blocks merge) or "Optional" (advisory). This implements a code-owners-like pattern: infrastructure changes auto-add the DevOps team, database changes auto-add the DBA, and security-sensitive code auto-adds the security team. You can also set a minimum number of approvals from the auto-included reviewers. This ensures domain experts always review changes in their area without relying on PR authors to remember to add them.
git rebase and when should you avoid it?git rebase moves (replays) your branch's commits on top of another branch's latest commit, rewriting commit history with new hashes. It creates a linear history without merge commits. When to use: Rebasing your local feature branch onto main before creating a PR, to ensure a clean merge and up-to-date base. Also useful for interactive rebase (git rebase -i) to squash, reorder, or edit commits before sharing. When to avoid: Never rebase shared branches — if other developers have based work on your branch, rewriting history will cause conflicts and confusion. Never rebase main, develop, or any branch others are pulling from. The rule is: rebase local branches before sharing; never rebase branches that have already been pushed and shared. Use git push --force-with-lease (not --force) after rebasing a branch you've previously pushed, as a safety check.
Use Git LFS (Large File Storage). Git is designed for text files — storing large binaries (images, videos, ML models, compiled assets) directly in Git bloats the repo, slows clones, and wastes storage. Git LFS replaces large files with pointer files in the repo while storing the actual content on a separate LFS server. Steps: 1. Install Git LFS: git lfs install. 2. Track file patterns: git lfs track "*.psd" "*.zip" "*.model". This creates a .gitattributes file. 3. Commit the .gitattributes and push. From now on, matching files are stored in LFS. Azure Repos supports Git LFS natively — no additional configuration needed. LFS storage counts against your Azure DevOps storage quota (free tier: included in the 2GB Artifacts limit). For very large datasets, consider Azure Blob Storage instead of LFS.
The "Reset code reviewer votes when there are new changes" policy invalidates all existing approvals on a PR whenever the author pushes new commits. Reviewers must re-review and re-approve. Why enable it: Without this policy, an author could get 2 approvals, then push completely different (potentially malicious or broken) code, and merge — the approvals from the old code would still satisfy the policy. This is a security and quality risk. With reset votes enabled, every code change after approval requires fresh review. Tradeoff: It adds review overhead — trivial typo fixes after approval require re-approval. Some teams use a lighter variant: "Reset votes on source branch changes" which only resets on commits (not on base branch updates). For security-critical branches, always enable full reset.
Scenario-Based
Step 1 (Immediate — minutes): Rotate the secret. Generate a new API key in the service that issued it and revoke the old one. This is the #1 priority — removing the commit from history does NOT protect you because clones, forks, caches, and CI logs may already have it. Step 2: Remove from Git history using git filter-repo or BFG Repo Cleaner to purge the file/string from all commits and branches. Force-push the cleaned history to Azure Repos. Step 3: Notify all developers to re-clone the repo (their local history still has the secret). Step 4 (Prevention): Enable GitHub Advanced Security for Azure DevOps (GHAzDO) — it provides real-time secret scanning on push. Add a CredScan pipeline task for CI-time detection. Set up pre-commit hooks (e.g., detect-secrets) for local prevention. Store secrets in Azure Key Vault and reference them via pipeline variables — never in code.
A multi-pronged approach: 1. Smaller PRs: Enforce a culture of small, focused PRs (under 400 lines changed). Large PRs are overwhelming and get deprioritized. Break features into incremental, mergeable chunks. 2. Auto-assign reviewers: Use the "Automatically include reviewers" branch policy with path-based rules so the right people are assigned instantly — no waiting for the author to tag someone. 3. Reduce required reviewers: Consider requiring 1 reviewer instead of 2 for non-critical branches. Use 2 reviewers only for main. 4. Review SLAs: Establish a team agreement — all PRs reviewed within 4 hours during business hours. Track review time in retrospectives. 5. PR notifications: Ensure Slack/Teams/email notifications are configured for PR assignments so reviewers are alerted immediately. 6. Pair programming: For complex changes, pair-program instead — the pair partner approves immediately after the session, since they co-authored the code.
Branch policies on main: Require 2 reviewers, build validation (CI runs all affected services), linked work items, comment resolution, squash merge only. Enable "reset votes on new push." Path-based code owners: Use automatically included reviewers with path filters — /services/auth/** requires Auth Team approval, /services/payments/** requires Payments Team + Security Team, /infrastructure/** requires DevOps Team. Set these as "Required." Build validation: Configure the CI pipeline with path-based triggers so only affected services are built and tested on PR (saves time — don't rebuild all 15 services for a 1-service change). Permissions: Create Azure AD security groups per team. Grant each team "Contribute" on the repo but use branch-level permissions to restrict who can create release/* branches (only release managers). Remove "Bypass policies when pushing" from all non-admin groups. Branch naming: Enforce naming conventions with a branch policy or a server-side hook: feature/*, bugfix/*, hotfix/* only.
git push --force to main and overwrote the last 5 commits. How do you recover?Immediate recovery: Azure Repos keeps a reflog of branch updates. Go to Repos → Pushes in the web UI — you'll see the force-push event and the previous commit SHA. Step 1: Find the last good commit SHA from the push history or from another developer's local clone who still has the original commits. Step 2: Force-push back to the correct state: git push --force origin <good-sha>:main. Step 3: Notify the team to git fetch and git reset --hard origin/main. Prevention: 1. Enable branch policies on main — with policies active, direct pushes (including force-push) are blocked. 2. Remove the "Force push (rewrite history, delete branches)" permission from all users and groups for protected branches. Go to Project Settings → Repositories → Security → [branch] and set "Force push" to "Deny" for Contributors. 3. Use --force-with-lease instead of --force as a team-wide Git alias — it refuses to overwrite others' work.
Phase 1 — Assessment: Inventory all GitHub repos, branch protection rules, GitHub Actions workflows, and integrations. Map GitHub concepts to Azure DevOps: branch protection → branch policies, CODEOWNERS → auto-included reviewers, Actions → Pipelines, Issues → Boards. Phase 2 — Import repos: Use Azure DevOps' built-in "Import repository" feature (Repos → Import) — it imports the full Git history, all branches, and tags from any Git URL. For large repos, use git clone --mirror then git push --mirror. Phase 3 — Re-create policies: Set up branch policies on main mirroring GitHub's branch protection. Configure auto-included reviewers to replace CODEOWNERS. Phase 4 — Developer training: The Git workflow is identical — developers still use git clone, git push, branches, and PRs. The biggest change is the web UI (PR reviews in Azure DevOps instead of GitHub). Run a 30-minute walkthrough session. Phase 5 — Parallel run (2 weeks): Keep both systems active, freeze GitHub repos as read-only, and have all new work go to Azure Repos. Cut over fully after the parallel period.
🌍 Real-World Use Case
A FinTech Startup Taming Branch Chaos
Consider a FinTech startup with 12 developers, 4 microservices, and a history of production outages caused by untested code reaching production.
The problem:
- Developers pushed directly to
main— no branch policies, no PR reviews - Two production outages in one month caused by broken code that was never tested before deploy
- No branching strategy — some developers used
develop, others pushed tomain, others had personal branches lingering for weeks - Merge conflicts were a daily nightmare — long-lived branches diverged significantly
- No traceability — impossible to answer "which user story caused this bug?"
What they implemented in Azure Repos:
- Trunk-Based Development — one
mainbranch as the single source of truth. All feature branches live less than 2 days. - Branch policies on
main: 2 reviewers required, build validation (CI pipeline with unit + integration tests must pass), linked work items mandatory, squash merge only, comment resolution required. - Path-based auto-reviewers: Changes to
/services/payments/**auto-add the security team. Changes to/infrastructure/**auto-add the DevOps lead. - Feature flags (Azure App Configuration): Incomplete features merged to
mainbehind flags — code is always deployable even when features aren't ready. - Naming convention:
feature/AB#1234-short-description— every branch linked to a Board work item.
Results after 3 months:
- Production outages from untested code: 0 (down from 2/month)
- Average PR review time: under 4 hours (team SLA)
- Merge conflicts: reduced 80% — short-lived branches rarely diverge
- Full traceability: every production commit traces back to a user story, PR, CI build, and deployment
- Developer satisfaction: significantly improved — fewer firefights, cleaner workflow, clear process
Branch policies are not bureaucracy — they're automated quality gates that protect the team from itself. The most common regret teams have is "we should have set up branch policies on day one." It takes 10 minutes to configure and saves hundreds of hours of debugging production issues caused by unreviewed code.
📝 Summary
| Concept | Key Takeaway |
|---|---|
| Azure Repos | Enterprise Git hosting in Azure DevOps with unlimited private repos, code search, PRs, and branch policies. Also supports TFVC (legacy). |
| Git vs. TFVC | Always choose Git for new projects. TFVC is legacy, centralized, and receives no new features. |
| Branch Policies | Rules on protected branches: minimum reviewers, build validation, linked work items, comment resolution, merge type restrictions. |
| Pull Requests | The core collaboration workflow: create branch → push → open PR → review → build validation → merge → delete branch. |
| Merge Strategies | Squash (recommended default, clean history), Merge Commit (full history), Rebase (linear, individual commits), Semi-Linear (rebase + merge). |
| Git Flow | Structured: main + develop + feature/release/hotfix branches. Best for scheduled releases and versioned products. |
| Trunk-Based Dev | Simple: main + short-lived features + feature flags. Best for continuous deployment, microservices, and web services. |
| Build Validation | The single most impactful policy — CI pipeline must pass on the PR before merge. Catches 90% of integration issues. |
| Authentication | Use PATs for HTTPS Git access. Use SSH keys as an alternative. Never use your Microsoft account password for Git. |
| Force Push Protection | Remove "Force push" permission from Contributors on protected branches. Use --force-with-lease instead of --force as a safety net. |