Skip to content

Commit d6d6914

Browse files
script sample to clone all repo from azure devops
1 parent d87c892 commit d6d6914

File tree

1 file changed

+275
-18
lines changed
  • scripts/azure-devops-clone-all-repositories

1 file changed

+275
-18
lines changed

scripts/azure-devops-clone-all-repositories/README.md

Lines changed: 275 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,297 @@
1-
# Clone all repositories from Azure DevOps
2-
3-
> [!Note]
4-
> This is a submission helper template please find the [contributor guidance](/docfx/contribute.md) to help you write this scenario.
1+
# # Azure DevOps Repo Cloner (PowerShell & AzureCli)
52

63
## Summary
74

8-
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.
5+
A **secure, user-friendly PowerShell script** to clone **all accessible Git repositories** from an Azure DevOps organization — **no admin rights required**.
96

10-
![Example Screenshot](assets/example.png)
7+
## Features
118

12-
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.Aenean nec lorem. In porttitor. Donec laoreet nonummy augue.
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
1319

14-
# [Azure CLI](#tab/azure-cli)
20+
## Requirements
1521

16-
```powershell
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)
1726

18-
<your script>
27+
---
1928

20-
```
21-
[!INCLUDE [More about Azure CLI](../../docfx/includes/MORE-AZURECLI.md)]
29+
## How to Use
30+
31+
1. **Save** the script as `Clone-All-DevOpsRepos.ps1`
32+
2. **Run in PowerShell**:
33+
34+
````powershell
35+
.\Clone-All-DevOpsRepos.ps1
2236
2337
# [Azure CLI](#tab/azure-cli)
2438
2539
```powershell
2640
27-
<your script>
41+
<#
42+
.SYNOPSIS
43+
Clone all Azure DevOps Git repositories from every project.
44+
45+
.DESCRIPTION
46+
Interactive or non-interactive (PAT). Organizes repos as:
47+
<LocalFolder>/<Project>/<Repo>
48+
Logs to both console and a user-specified log file (with .log auto-added).
49+
50+
.NOTES
51+
• Requires: Azure CLI + `azure-devops` extension
52+
• Use $env:AZURE_DEVOPS_EXT_PAT for CI/CD
53+
• PowerShell 7+ recommended
54+
55+
.EXAMPLE
56+
.\Clone-All-DevOpsRepos.ps1
57+
58+
# Prompts for:
59+
# Organization URL
60+
# Local folder path
61+
# Log file path (e.g. C:\Logs\clone.log)
62+
#>
63+
64+
[CmdletBinding()]
65+
param()
66+
67+
# Global log stream and path
68+
$LogStream = $null
69+
$LogFilePath = $null
70+
71+
# -------------------------------------------------
72+
# Helper: Colored, timestamped logging (console + file)
73+
# -------------------------------------------------
74+
function Write-Log {
75+
param(
76+
[string]$Message,
77+
[ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS')][string]$Level = 'INFO'
78+
)
79+
$timestamp = Get-Date -Format "HH:mm:ss"
80+
$logLine = "[$timestamp] [$Level] $Message"
81+
82+
# Console output with color
83+
$color = switch ($Level) {
84+
'INFO' { 'White' }
85+
'WARN' { 'Yellow' }
86+
'ERROR' { 'Red' }
87+
'SUCCESS' { 'Green' }
88+
}
89+
Write-Host $logLine -ForegroundColor $color
90+
91+
# Write to file
92+
if ($LogStream) {
93+
try { $LogStream.WriteLine($logLine) } catch { }
94+
}
95+
}
96+
97+
# -------------------------------------------------
98+
# 1. Get Organization URL
99+
# -------------------------------------------------
100+
do {
101+
$org = Read-Host "Enter Azure DevOps organization URL (e.g. https://dev.azure.com/contoso)"
102+
$org = $org.Trim()
103+
if (-not $org) { Write-Host "Organization URL cannot be empty." -ForegroundColor Red }
104+
} while (-not $org)
105+
106+
Write-Log "Using organization: $org" SUCCESS
107+
108+
# -------------------------------------------------
109+
# 2. Get Local Clone Folder
110+
# -------------------------------------------------
111+
do {
112+
$folder = Read-Host "Enter local folder to clone repos into (e.g. C:\Repos or ./backup)"
113+
$folder = $folder.Trim()
114+
if (-not $folder) { Write-Host "Folder path cannot be empty." -ForegroundColor Red; continue }
115+
116+
try {
117+
$resolved = Resolve-Path -Path $folder -ErrorAction Stop
118+
$LocalFolder = $resolved.Path
119+
break
120+
}
121+
catch {
122+
try {
123+
New-Item -ItemType Directory -Path $folder -Force | Out-Null
124+
$LocalFolder = (Resolve-Path -Path $folder).Path
125+
break
126+
}
127+
catch {
128+
Write-Host "Invalid or inaccessible path: $folder" -ForegroundColor Red
129+
}
130+
}
131+
} while ($true)
132+
133+
Write-Log "Cloning into: $LocalFolder" SUCCESS
134+
135+
# -------------------------------------------------
136+
# 3. Get Log FILE Path (with .log auto-add)
137+
# -------------------------------------------------
138+
do {
139+
Write-Host "`nEnter FULL PATH for LOG FILE (e.g. C:\Logs\clone.log or ./clone.log)" -ForegroundColor Cyan
140+
$logInput = Read-Host "Log file path"
141+
$logInput = $logInput.Trim()
142+
if (-not $logInput) {
143+
Write-Host "Log file path is required." -ForegroundColor Red
144+
continue
145+
}
146+
147+
# Auto-add .log if missing
148+
if (-not $logInput.EndsWith('.log', [System.StringComparison]::OrdinalIgnoreCase)) {
149+
$logInput = "$logInput.log"
150+
Write-Host "Auto-added .log → $logInput" -ForegroundColor DarkGray
151+
}
152+
153+
$logDir = Split-Path $logInput -Parent
154+
if (-not $logDir) { $logDir = "." }
155+
156+
try {
157+
# Ensure directory exists
158+
if (-not (Test-Path $logDir)) {
159+
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
160+
Write-Log "Created log directory: $logDir" INFO
161+
}
162+
163+
# Test write access
164+
$testFile = Join-Path $logDir "log_$(Get-Random).tmp"
165+
"log" | Out-File $testFile -Force -Encoding utf8
166+
Remove-Item $testFile -Force
167+
168+
# Resolve full path
169+
$LogFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($logInput)
170+
171+
# Open stream for appending
172+
$LogStream = [System.IO.StreamWriter]::new($LogFilePath, $true, [System.Text.Encoding]::UTF8)
173+
$LogStream.AutoFlush = $true
174+
175+
Write-Log "Logging enabled to: $LogFilePath" SUCCESS
176+
break
177+
}
178+
catch {
179+
Write-Host "Cannot write to: $logInput" -ForegroundColor Red
180+
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
181+
if ($_.Exception.Message -like "*Access*denied*") {
182+
Write-Host "Tip: Run as Administrator, or use a user-writable folder (e.g. Documents)." -ForegroundColor Yellow
183+
}
184+
}
185+
} while ($true)
186+
187+
# -------------------------------------------------
188+
# 4. Ensure Azure CLI + extension
189+
# -------------------------------------------------
190+
Write-Log "Installing azure-devops extension..." INFO
191+
az extension add --name azure-devops --yes | Out-Null
192+
if ($LASTEXITCODE) { Write-Log "Failed to install extension" ERROR; exit 1 }
193+
194+
# -------------------------------------------------
195+
# 5. Authenticate
196+
# -------------------------------------------------
197+
Write-Log "Authenticating to Azure DevOps..." INFO
198+
if (-not $env:AZURE_DEVOPS_EXT_PAT) {
199+
Write-Host "Opening browser for login (close when done)..." -ForegroundColor Cyan
200+
az login --allow-no-subscriptions | Out-Null
201+
if ($LASTEXITCODE) { Write-Log "Login failed" ERROR; exit 1 }
202+
}
203+
az devops configure --defaults organization=$org | Out-Null
204+
if ($LASTEXITCODE) { Write-Log "Failed to set default org" ERROR; exit 1 }
205+
206+
# -------------------------------------------------
207+
# 6. Get all projects
208+
# -------------------------------------------------
209+
Write-Log "Fetching projects..." INFO
210+
$projectsJson = az devops project list --organization $org -o json
211+
if ($LASTEXITCODE) { Write-Log "Failed to list projects" ERROR; exit 1 }
212+
213+
$projects = ($projectsJson | ConvertFrom-Json).value | Select-Object -ExpandProperty name
214+
if (-not $projects) {
215+
Write-Log "No projects found. Check URL and permissions." ERROR
216+
if ($LogStream) { $LogStream.Close(); $LogStream.Dispose() }
217+
exit 1
218+
}
219+
220+
Write-Log "Found $($projects.Count) project(s): $($projects -join ', ')" SUCCESS
221+
222+
# -------------------------------------------------
223+
# 7. Function: Clone a single repo
224+
# -------------------------------------------------
225+
function Clone-Repo {
226+
param($Project, $RepoName, $RepoUrl, $Destination)
227+
228+
if (Test-Path $Destination) {
229+
Write-Log " [SKIP] $RepoName (already exists)" WARN
230+
return
231+
}
232+
233+
Write-Log " [CLONE] $RepoName ..." INFO
234+
$output = git clone $RepoUrl $Destination 2>&1
235+
if ($LASTEXITCODE -eq 0) {
236+
Write-Log " [DONE] $RepoName" SUCCESS
237+
}
238+
else {
239+
$errorMsg = ($output -join "`n").Trim()
240+
Write-Log " [FAIL] $RepoName`n$errorMsg" ERROR
241+
}
242+
}
243+
244+
# -------------------------------------------------
245+
# 8. Main: Process each project (Sequential)
246+
# -------------------------------------------------
247+
foreach ($proj in $projects) {
248+
Write-Log "`n=== Project: $proj ===" INFO
249+
250+
$reposJson = az repos list --org $org --project $proj `
251+
--query "[].{Name:name, Url:remoteUrl}" -o json
252+
253+
if ($LASTEXITCODE) {
254+
Write-Log " Failed to list repos in $proj" WARN
255+
continue
256+
}
257+
258+
$repos = $reposJson | ConvertFrom-Json
259+
if (-not $repos) {
260+
Write-Log " No repositories in $proj" WARN
261+
continue
262+
}
263+
264+
foreach ($repo in $repos) {
265+
$destPath = Join-Path $LocalFolder "$proj/$($repo.Name)"
266+
$projFolder = Split-Path $destPath -Parent
267+
New-Item -ItemType Directory -Force -Path $projFolder | Out-Null
268+
269+
Clone-Repo -Project $proj -RepoName $repo.Name -RepoUrl $repo.Url -Destination $destPath
270+
}
271+
}
272+
273+
# -------------------------------------------------
274+
# 9. Finalize
275+
# -------------------------------------------------
276+
Write-Log "`nCloning complete! All repositories are in:" SUCCESS
277+
Write-Host " $LocalFolder" -ForegroundColor Cyan
278+
Write-Log "Log file: $LogFilePath" INFO
279+
280+
# Close log stream
281+
if ($LogStream) {
282+
$LogStream.Close()
283+
$LogStream.Dispose()
284+
}
285+
28286
29-
```
30-
[!INCLUDE [More about Azure CLI](../../docfx/includes/MORE-AZURECLI.md)]
31-
***
287+
````
32288

289+
---
33290

34291
## Contributors
35292

36-
| Author(s) |
37-
|-----------|
293+
| Author(s) |
294+
| --------------- |
38295
| Harminder Singh |
39296

40297
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]

0 commit comments

Comments
 (0)