Skip to content

Commit 28621f2

Browse files
authored
Merge pull request #883 from HarminderSethi/main
Script sample to clone all repo from azure devops
2 parents 063d33f + be21cff commit 28621f2

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Azure DevOps Repo Cloner (PowerShell & AzureCli)
2+
3+
## Summary
4+
5+
A **secure, user-friendly PowerShell script** to clone **all accessible Git repositories** from an Azure DevOps organization — **no admin rights required**.
6+
7+
## Features
8+
9+
- **Interactive prompts** for:
10+
- Organization URL
11+
- Local clone folder
12+
- **Log file path** (auto-creates `.log`, validates write access)
13+
- **Logs to both console and file** with timestamps and color-coding
14+
- **Only clones repos the user has access to** — respects Azure DevOps permissions
15+
- **Skips already cloned repos** — safe to re-run
16+
- **No local admin rights needed** — works for developers, contributors, externals
17+
- **Supports PAT or browser login** (`$env:AZURE_DEVOPS_EXT_PAT`)
18+
- **Robust error handling** — never crashes on permission issues
19+
20+
## Requirements
21+
22+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) (`az`)
23+
- `azure-devops` extension (`az extension add --name azure-devops`)
24+
- Git (`git`)
25+
- PowerShell 5.1+ (7+ recommended)
26+
27+
---
28+
29+
## How to Use
30+
31+
1. **Save** the script as `Clone-All-DevOpsRepos.ps1`
32+
2. **Run in PowerShell**:
33+
34+
35+
```powershell
36+
.\Clone-All-DevOpsRepos.ps1
37+
```
38+
39+
# [Azure CLI](#tab/azure-cli)
40+
41+
```powershell
42+
43+
<#
44+
.SYNOPSIS
45+
Clone all Azure DevOps Git repositories from every project.
46+
47+
.DESCRIPTION
48+
Interactive or non-interactive (PAT). Organizes repos as:
49+
<LocalFolder>/<Project>/<Repo>
50+
Logs to both console and a user-specified log file (with .log auto-added).
51+
52+
.NOTES
53+
• Requires: Azure CLI + `azure-devops` extension
54+
• Use $env:AZURE_DEVOPS_EXT_PAT for CI/CD
55+
• PowerShell 7+ recommended
56+
57+
.EXAMPLE
58+
.\Clone-All-DevOpsRepos.ps1
59+
60+
# Prompts for:
61+
# Organization URL
62+
# Local folder path
63+
# Log file path (e.g. C:\Logs\clone.log)
64+
#>
65+
66+
[CmdletBinding()]
67+
param()
68+
69+
# Global log stream and path
70+
$LogStream = $null
71+
$LogFilePath = $null
72+
73+
# -------------------------------------------------
74+
# Helper: Colored, timestamped logging (console + file)
75+
# -------------------------------------------------
76+
function Write-Log {
77+
param(
78+
[string]$Message,
79+
[ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS')][string]$Level = 'INFO'
80+
)
81+
$timestamp = Get-Date -Format "HH:mm:ss"
82+
$logLine = "[$timestamp] [$Level] $Message"
83+
84+
# Console output with color
85+
$color = switch ($Level) {
86+
'INFO' { 'White' }
87+
'WARN' { 'Yellow' }
88+
'ERROR' { 'Red' }
89+
'SUCCESS' { 'Green' }
90+
}
91+
Write-Host $logLine -ForegroundColor $color
92+
93+
# Write to file
94+
if ($LogStream) {
95+
try { $LogStream.WriteLine($logLine) } catch { }
96+
}
97+
}
98+
99+
# -------------------------------------------------
100+
# 1. Get Organization URL
101+
# -------------------------------------------------
102+
do {
103+
$org = Read-Host "Enter Azure DevOps organization URL (e.g. https://dev.azure.com/contoso)"
104+
$org = $org.Trim()
105+
if (-not $org) { Write-Host "Organization URL cannot be empty." -ForegroundColor Red }
106+
} while (-not $org)
107+
108+
Write-Log "Using organization: $org" SUCCESS
109+
110+
# -------------------------------------------------
111+
# 2. Get Local Clone Folder
112+
# -------------------------------------------------
113+
do {
114+
$folder = Read-Host "Enter local folder to clone repos into (e.g. C:\Repos or ./backup)"
115+
$folder = $folder.Trim()
116+
if (-not $folder) { Write-Host "Folder path cannot be empty." -ForegroundColor Red; continue }
117+
118+
try {
119+
$resolved = Resolve-Path -Path $folder -ErrorAction Stop
120+
$LocalFolder = $resolved.Path
121+
break
122+
}
123+
catch {
124+
try {
125+
New-Item -ItemType Directory -Path $folder -Force | Out-Null
126+
$LocalFolder = (Resolve-Path -Path $folder).Path
127+
break
128+
}
129+
catch {
130+
Write-Host "Invalid or inaccessible path: $folder" -ForegroundColor Red
131+
}
132+
}
133+
} while ($true)
134+
135+
Write-Log "Cloning into: $LocalFolder" SUCCESS
136+
137+
# -------------------------------------------------
138+
# 3. Get Log FILE Path (with .log auto-add)
139+
# -------------------------------------------------
140+
do {
141+
Write-Host "`nEnter FULL PATH for LOG FILE (e.g. C:\Logs\clone.log or ./clone.log)" -ForegroundColor Cyan
142+
$logInput = Read-Host "Log file path"
143+
$logInput = $logInput.Trim()
144+
if (-not $logInput) {
145+
Write-Host "Log file path is required." -ForegroundColor Red
146+
continue
147+
}
148+
149+
# Auto-add .log if missing
150+
if (-not $logInput.EndsWith('.log', [System.StringComparison]::OrdinalIgnoreCase)) {
151+
$logInput = "$logInput.log"
152+
Write-Host "Auto-added .log → $logInput" -ForegroundColor DarkGray
153+
}
154+
155+
$logDir = Split-Path $logInput -Parent
156+
if (-not $logDir) { $logDir = "." }
157+
158+
try {
159+
# Ensure directory exists
160+
if (-not (Test-Path $logDir)) {
161+
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
162+
Write-Log "Created log directory: $logDir" INFO
163+
}
164+
165+
# Test write access
166+
$testFile = Join-Path $logDir "log_$(Get-Random).tmp"
167+
"log" | Out-File $testFile -Force -Encoding utf8
168+
Remove-Item $testFile -Force
169+
170+
# Resolve full path
171+
$LogFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($logInput)
172+
173+
# Open stream for appending
174+
$LogStream = [System.IO.StreamWriter]::new($LogFilePath, $true, [System.Text.Encoding]::UTF8)
175+
$LogStream.AutoFlush = $true
176+
177+
Write-Log "Logging enabled to: $LogFilePath" SUCCESS
178+
break
179+
}
180+
catch {
181+
Write-Host "Cannot write to: $logInput" -ForegroundColor Red
182+
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
183+
if ($_.Exception.Message -like "*Access*denied*") {
184+
Write-Host "Tip: Run as Administrator, or use a user-writable folder (e.g. Documents)." -ForegroundColor Yellow
185+
}
186+
}
187+
} while ($true)
188+
189+
# -------------------------------------------------
190+
# 4. Ensure Azure CLI + extension
191+
# -------------------------------------------------
192+
Write-Log "Installing azure-devops extension..." INFO
193+
az extension add --name azure-devops --yes | Out-Null
194+
if ($LASTEXITCODE) { Write-Log "Failed to install extension" ERROR; exit 1 }
195+
196+
# -------------------------------------------------
197+
# 5. Authenticate
198+
# -------------------------------------------------
199+
Write-Log "Authenticating to Azure DevOps..." INFO
200+
if (-not $env:AZURE_DEVOPS_EXT_PAT) {
201+
Write-Host "Opening browser for login (close when done)..." -ForegroundColor Cyan
202+
az login --allow-no-subscriptions | Out-Null
203+
if ($LASTEXITCODE) { Write-Log "Login failed" ERROR; exit 1 }
204+
}
205+
az devops configure --defaults organization=$org | Out-Null
206+
if ($LASTEXITCODE) { Write-Log "Failed to set default org" ERROR; exit 1 }
207+
208+
# -------------------------------------------------
209+
# 6. Get all projects
210+
# -------------------------------------------------
211+
Write-Log "Fetching projects..." INFO
212+
$projectsJson = az devops project list --organization $org -o json
213+
if ($LASTEXITCODE) { Write-Log "Failed to list projects" ERROR; exit 1 }
214+
215+
$projects = ($projectsJson | ConvertFrom-Json).value | Select-Object -ExpandProperty name
216+
if (-not $projects) {
217+
Write-Log "No projects found. Check URL and permissions." ERROR
218+
if ($LogStream) { $LogStream.Close(); $LogStream.Dispose() }
219+
exit 1
220+
}
221+
222+
Write-Log "Found $($projects.Count) project(s): $($projects -join ', ')" SUCCESS
223+
224+
# -------------------------------------------------
225+
# 7. Function: Clone a single repo
226+
# -------------------------------------------------
227+
function Clone-Repo {
228+
param($Project, $RepoName, $RepoUrl, $Destination)
229+
230+
if (Test-Path $Destination) {
231+
Write-Log " [SKIP] $RepoName (already exists)" WARN
232+
return
233+
}
234+
235+
Write-Log " [CLONE] $RepoName ..." INFO
236+
$output = git clone $RepoUrl $Destination 2>&1
237+
if ($LASTEXITCODE -eq 0) {
238+
Write-Log " [DONE] $RepoName" SUCCESS
239+
}
240+
else {
241+
$errorMsg = ($output -join "`n").Trim()
242+
Write-Log " [FAIL] $RepoName`n$errorMsg" ERROR
243+
}
244+
}
245+
246+
# -------------------------------------------------
247+
# 8. Main: Process each project (Sequential)
248+
# -------------------------------------------------
249+
foreach ($proj in $projects) {
250+
Write-Log "`n=== Project: $proj ===" INFO
251+
252+
$reposJson = az repos list --org $org --project $proj `
253+
--query "[].{Name:name, Url:remoteUrl}" -o json
254+
255+
if ($LASTEXITCODE) {
256+
Write-Log " Failed to list repos in $proj" WARN
257+
continue
258+
}
259+
260+
$repos = $reposJson | ConvertFrom-Json
261+
if (-not $repos) {
262+
Write-Log " No repositories in $proj" WARN
263+
continue
264+
}
265+
266+
foreach ($repo in $repos) {
267+
$destPath = Join-Path $LocalFolder "$proj/$($repo.Name)"
268+
$projFolder = Split-Path $destPath -Parent
269+
New-Item -ItemType Directory -Force -Path $projFolder | Out-Null
270+
271+
Clone-Repo -Project $proj -RepoName $repo.Name -RepoUrl $repo.Url -Destination $destPath
272+
}
273+
}
274+
275+
# -------------------------------------------------
276+
# 9. Finalize
277+
# -------------------------------------------------
278+
Write-Log "`nCloning complete! All repositories are in:" SUCCESS
279+
Write-Host " $LocalFolder" -ForegroundColor Cyan
280+
Write-Log "Log file: $LogFilePath" INFO
281+
282+
# Close log stream
283+
if ($LogStream) {
284+
$LogStream.Close()
285+
$LogStream.Dispose()
286+
}
287+
288+
```
289+
[!INCLUDE [More about Azure CLI](../../docfx/includes/MORE-AZURECLI.md)]
290+
***
291+
292+
## Contributors
293+
294+
| Author(s) |
295+
| --------------- |
296+
| Harminder Singh |
297+
298+
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
299+
<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/azure-devops-clone-all-repositories" aria-hidden="true" />
328 KB
Loading
58.7 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[
2+
{
3+
"name": "azure-devops-clone-all-repositories",
4+
"source": "pnp",
5+
"title": "Clone all repositories from Azure DevOps",
6+
"shortDescription": "A secure, user-friendly PowerShell script to clone all accessible Git repositories from an Azure DevOps organization",
7+
"url": "https://pnp.github.io/script-samples/azure-devops-clone-all-repositories/README.html",
8+
"longDescription": [""],
9+
"creationDateTime": "2025-10-29",
10+
"updateDateTime": "2025-10-29",
11+
"products": ["Azure"],
12+
"metadata": [
13+
{
14+
"key": "AZURE-CLI",
15+
"value": "2.27.0"
16+
}
17+
],
18+
"categories": ["Configure"],
19+
"tags": ["az extension add --name azure-devops", "az login", "az devops configure", "az devops project", " Clone-Repo"],
20+
"thumbnails": [
21+
{
22+
"type": "image",
23+
"order": 100,
24+
"url": "https://raw.githubusercontent.com/pnp/script-samples/main/scripts/azure-devops-clone-all-repositories/assets/preview.png",
25+
"alt": "Preview of the sample Clone all repositories from Azure DevOps"
26+
}
27+
],
28+
"authors": [
29+
{
30+
"gitHubAccount": "HarminderSethi",
31+
"company": "",
32+
"pictureUrl": "https://github.com/HarminderSethi.png",
33+
"name": "Harminder Singh"
34+
}
35+
],
36+
"references": [
37+
{
38+
"name": "Want to learn more about Azure CLI and the commands",
39+
"description": "Check out the Azure CLI documentation site to get started and for the reference to the commands.",
40+
"url": "https://learn.microsoft.com/cli/azure/"
41+
},
42+
{
43+
"name": "Want to learn more about Azure CLI and the commands",
44+
"description": "Check out the Azure CLI documentation site to get started and for the reference to the commands.",
45+
"url": "https://learn.microsoft.com/cli/azure/"
46+
}
47+
]
48+
}
49+
]

0 commit comments

Comments
 (0)