Created
July 25, 2021 17:31
-
-
Save Ba4bes/29afffc1a8708169e9f40b326b162ff3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Invoke-AzDoRepoMigration { | |
<# | |
.SYNOPSIS | |
Migrates git repo(s) from one Azure DevOps project to another. | |
.DESCRIPTION | |
This function migrates git repo(s) from one Azure DevOps project to another. | |
If a repo is already in the target project, it will be skipped. | |
If no repo-parameter is given, all repos will be migrated. | |
The source repo will not be deleted. | |
.PARAMETER UserName | |
The Azure DevOps user name. | |
.PARAMETER Token | |
The Azure DevOps PAT token. | |
Requires full permission to: | |
- Code | |
- Service Connections | |
.PARAMETER OrganizationName | |
The Azure DevOps organization name as it is visible in the URL. | |
.PARAMETER SourceProjectName | |
The Azure DevOps project name of the source repos. | |
.PARAMETER SourceRepoName | |
The name of the source repo. If not given, all repos will be migrated. | |
.PARAMETER DestinationProjectName | |
The Azure DevOps project name where the repos will be moved to. | |
.PARAMETER DestinationRepoName | |
The name of the destination repo. If not given, the name of the source repo will be used. | |
Can only be used if a source repo is given. | |
.EXAMPLE | |
Invoke-AzDoRepoMigration -UserName [email protected] -Token $token -OrganizationName "exampleOrg" -SourceProjectName "SourceProject" -SourceRepoName "srcRepo" -DestinationProjectName "DestinationProject" | |
=== | |
Will move the repo srcRepo from SourceProject to DestinationProject. The repo name will remain the same. | |
.EXAMPLE | |
Invoke-AzDoRepoMigration -UserName [email protected] -Token $token -OrganizationName "exampleOrg" -SourceProjectName "SourceProject" -DestinationProjectName "DestinationProject" | |
==== | |
Will move all repos from SourceProject to DestinationProject. | |
.NOTES | |
Created by: Barbara Forbes | |
@Ba4bes | |
with help from https://stackoverflow.com/questions/56916593/azure-devops-import-git-repositories-requiring-authorization-via-api | |
.LINK | |
https://4bes.nl/2021/07/25/migrate-azure-devops-repos-with-powershell/ | |
#> | |
param( | |
[Parameter(Mandatory = $true)] | |
[String]$UserName , | |
[Parameter(Mandatory = $true)] | |
[String]$Token , | |
[Parameter(Mandatory = $true)] | |
[String]$OrganizationName , | |
[Parameter(Mandatory = $true)] | |
[String]$SourceProjectName , | |
[Parameter(Mandatory = $false)] | |
[String]$SourceRepoName, | |
[Parameter(Mandatory = $true)] | |
[String]$DestinationProjectName , | |
[Parameter(Mandatory = $false)] | |
[String]$DestinationRepoName | |
) | |
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $Token))) | |
$Header = @{ | |
Authorization = ("Basic {0}" -f $base64AuthInfo) | |
} | |
$URLBase = "https://dev.azure.com/$OrganizationName" | |
Write-Verbose "INFO: Starting with project $SourceProjectName in organization $OrganizationName" | |
$RepositoryURL = "$URLBase/$SourceProjectName/_apis/git/repositories?api-version=6.0" | |
# Collect all Repo's by default. | |
Try { | |
$AllSourceRepos = (Invoke-RestMethod $RepositoryURL -Headers $Header).value | |
} | |
Catch { | |
if ($_ -match "Access Denied") { | |
Throw "Access has been denied, please check your token" | |
} | |
else { | |
Throw $_ | |
} | |
} | |
# if a specific repo is specified, only that repo will be collected. | |
if ($SourceRepoName) { | |
Write-Verbose "Found SourceRepoName: $SourceRepoName" | |
$AllSourceRepos = $AllSourceRepos | Where-Object { $_.name -eq $SourceRepoName } | |
} | |
Write-Verbose "Found $($AllSourceRepos.Count) repos" | |
$LoopCount = 0 | |
foreach ($SourceRepo in $AllSourceRepos) { | |
$LoopCount++ | |
Write-Verbose "INFO: Starting with repo $($SourceRepo.name)" | |
$SourceRepoName = $SourceRepo.name | |
# Check if the repo is not empty. | |
try { | |
$null = Invoke-RestMethod "$($SourceRepo.url)/items?recursionLevel=Full&api-version=6.0" -Headers $Header -Erroraction stop | |
$Errormessage = "none" | |
} | |
catch { | |
$Errormessage = $_.ErrorDetails.Message | |
} | |
if ($Errormessage -like "*Cannot find any branches*" ) { | |
Write-Verbose "INFO: Repo is empty" | |
} | |
else { | |
# Start the import process. | |
$EndpointURL = "$URLBase/$DestinationProjectName/_apis/serviceendpoint/endpoints?api-version=5.1-preview.2" | |
# create endpoint | |
$ServiceConnection = Invoke-RestMethod $EndpointURL -Headers $Header | |
if ( ($ServiceConnection).value.name -contains "Git Import") { | |
Write-Verbose "INFO: ServiceConnection already exists" | |
$Endpoint = $ServiceConnection.value | |
} | |
else { | |
Write-Verbose "INFO: Creating ServiceConnection" | |
$Body = @{ | |
"name" = "Git Import" | |
"type" = "git" | |
"url" = "https://$OrganizationName@dev.azure.com/$OrganizationName/$SourceProjectName/_git/$SourceRepoName" | |
"authorization" = @{ | |
"parameters" = @{ | |
"username" = "$UserName" | |
"password" = "$Token" | |
} | |
"scheme" = "UsernamePassword" | |
} | |
} | |
$Parameters = @{ | |
Uri = $EndpointURL | |
Method = "POST" | |
ContentType = "application/json" | |
Headers = $Header | |
Body = ( $Body | ConvertTo-Json ) | |
} | |
Try { | |
$Endpoint = Invoke-RestMethod @Parameters | |
} | |
Catch { | |
Throw "Could not create Endpoint: $_" | |
} | |
} | |
# Check if the repo already exists | |
if (!$DestinationRepoName) { | |
$DestinationRepoName = $SourceRepoName | |
} | |
$RepoURL = "$URLBase/$DestinationProjectName/_apis/git/repositories?api-version=6.0" | |
$DestinationRepos = (Invoke-RestMethod $RepoURL -Headers $Header).value | |
if ($DestinationRepos.name -contains $DestinationRepoName) { | |
Write-Verbose "Repo already exists, skipping" | |
} | |
else { | |
Write-Verbose "Creating Repo" | |
$Body = @{ | |
"name" = $DestinationRepoName | |
} | |
$Parameters = @{ | |
uri = "$URLBase/$DestinationProjectName/_apis/git/repositories/?api-version=5.0" | |
Method = 'POST' | |
ContentType = "application/json" | |
Headers = $Header | |
Body = ($Body | ConvertTo-Json) | |
} | |
Try { | |
Invoke-RestMethod @Parameters | |
} | |
Catch { | |
Throw "Could not create Repo: $_" | |
} | |
# import repository | |
if ($LoopCount -ge $AllSourceRepos.count) { | |
$deleteServiceEndpointAfterImport = $true | |
Write-Verbose "more repo's coming up, keeping ServiceEndpoint" | |
} | |
else { | |
$deleteServiceEndpointAfterImport = $false | |
Write-Verbose "Last repo, deleting ServiceEndpoint after import" | |
} | |
$Body = @{ | |
"parameters" = @{ | |
"deleteServiceEndpointAfterImportIsDone" = $deleteServiceEndpointAfterImport | |
"gitSource" = @{ | |
"url" = "https://$OrganizationName@dev.azure.com/$OrganizationName/$sourceProjectName/_git/$SourceRepoName" | |
"overwrite" = $false | |
} | |
"tfvcSource" = $null | |
"serviceEndpointId" = $Endpoint.id | |
} | |
} | |
$Parameters = @{ | |
uri = "$URLBase/$DestinationProjectName/_apis/git/repositories/$DestinationRepoName/importRequests?api-version=5.0-preview" | |
Method = 'Post' | |
ContentType = "application/json" | |
Headers = $Header | |
Body = ($Body | ConvertTo-Json) | |
} | |
Try { | |
Invoke-RestMethod @Parameters | |
} | |
Catch { | |
Throw "Could not import Repo: $_" | |
} | |
Write-Verbose "INFO: Done with Repo $DestinationReponame" | |
} | |
$DestinationRepoName = $null | |
} | |
} | |
Write-Verbose "INFO: done with project" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment