Migrating repos from Atlassian Stash to Azure DevOps
Alexander Viken
Posted on October 23, 2020
Here is a guide on how I helped a cliend move from an internal, on-prem installation of Atlassian Stash to Azure DevOps using PowerShell and Azure CLI. Not sure if this is helpful for others, but it might in light of Atlassians lates announcment to discontinue their Server products by early 2024. I am not sure if the Stash system is part of this, but here is a guide anyhow.
To use this you'll need powershell, git and Azure CLI installed. in addition you need to add the azure devops extension to Azure CLI
d:\repos>az extension add --name azure-devops
The next you will need to do is to login to your Azure DevOps organizaton and generate a Personal Access Token (PAT). You can find it here: https://dev.azure.com/{your_organization}/_usersSettings/tokens
d:\repos>az devops login
Token:
paste in the PAT at the "Token" prompt and you are logged in to DevOps via the CLI.
The organization I was working with had a lot of projects in Stash, with several repositories in each project. I only needed to do this for a single project so there was no need to do it at a higher level, but extending this to add a feature for looping through all projects in stash, then add a feature to create new projects in Azure DevOps to match the stash projects should be a minor change. Also, I haven't added any error handling so this is all happy path coding so if you are uncertain about your environment you should probably add some error handling and checks in the code.
My first function is for retrieving a list of all repos for a project in Stash using the built in Stash API. I am not sure how this Works on different versions of Stash so it might need changes if you have newer/older versions. Also, if you have more than 50 repos in the project increase the number with the limit parameter in the url.
function GetStashReposForProject($user, $pass, $stashHost, $projectKey)
{
$pair = "$($user):$($pass)"
$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$basicAuthValue = "Basic $encodedCreds"
$Headers = @{
Authorization = $basicAuthValue
}
$uri = $stashHost + "/rest/api/1.0/projects/"+ $projectKey + "/repos?limit=50"
$request = Invoke-WebRequest -Uri $uri -Headers $Headers
$repos = $request.Content | ConvertFrom-Json
return $repos.values
}
With this method we get a full payload with all the repos listed as part of the @repos.values return value.
The next function clones the stash repo onto the workstation and sets the stash $clone_uri as remotes/origin by default. The function creates a folder for the repo. It is based on where you execute the code, so make sure you are on a disk with enough space and have a clean "root" folder to work from.
function CloneStashRepository($clone_url, $repo_name)
{
mkdir $repo_name
cd $repo_name
$gitclone = "/c git clone " + $clone_url + " ."
Start-Process cmd -Argument $gitclone -Wait
}
Next we need a function that checks out all the branches in the repository so that it can later be pushed to DevOps.
function CheckoutAllBranches()
{
$branches = git branch --all
foreach($branch in $branches)
{
if ($branch -like "*remotes/*")
{
$branchName = $branch -replace "remotes/origin/" -replace ""
$gitCheckout = "/c git checkout " + $branchName
Start-Process cmd -Argument $gitCheckout -Wait
Write-Host('checkedout branch: '+ $branchName )
}
}
}
Now that I have all the branches for the repo locally on my machine I need to choose what project in Azure DevOps they should be pushed to.
function SelectAzureDevOpsProject($devOpsOrg)
{
$projects = $(az devops project list --org $devOpsOrg -o json) | ConvertFrom-Json
$count = 1
foreach($p in $projects.value)
{
Write-Host $count ") " $p.name
$count++
}
$index = Read-Host "Chooose project (enter number)"
#Write-Host "Selected inde is: " $index
$sp = $projects.value[$index-1]
$arr = @($sp.id, $sp.name)
Write-Host "Selected Project name: " $arr[1]
return $arr
}
This function returns an array that has the guid and the name of the project we want to use.
A small function handles creating the new repo in Azure DevOps
function CreateRepoInDevOpsProject($slug, $projectId, $devOpsOrg)
{
az repos create --name $slug --project $projectId --organization $devOpsOrg
}
The final worker function adds the devops repo as remote named devops in the local repository. Then loops through all the local branches and pushes them to the remote devops repo.
function PushToDevops($orgPath, $project_name, $repo_name)
{
$projectName = [uri]::EscapeDataString($project_name)
$slug = [uri]::EscapeDataString($repo_name)
$remoteUrl = "https://$orgPath@dev.azure.com/$orgPath/$projectName/_git/$slug"
$makeRemote = git remote add devops $remoteUrl
$branches = git branch
foreach($branch in $branches)
{
$gitCheckout = "/c git push devops " + $branch
Start-Process cmd -Argument $gitCheckout -Wait
Write-Host('pushed branch: '+ $branch )
}
}
Finally, the code block that is run when the ps1 file is executed.
$user = Read-Host "Enter Stash Username"
$pass = Read-Host -AsSecureString "Enter Stash Password "
$stashHost = Read-Host "http://domain:port"
$projectKey = Read-Host "Enter stash project key"
$devOpsOrg = Read-Host "https://dev.azure.com/"
$orgUri = "https://dev.azure.com/" + $devOpsOrg
$selectedDevOpsProject = SelectAzureDevOpsProject $orgUri
$selectedDevOpsProjectId = $selectedDevOpsProject[0]
$selectedDevOpsProjectName = $selectedDevOpsProject[1]
foreach($repos in GetStashReposForProject $user $pass $stashHost $projectKey -Wait)
{
$slug = $repos.slug
CloneStashRepository $repos.cloneUrl $slug -Wait
CheckoutAllBranches -Wait
CreateRepoInDevOpsProject $slug $selectedDevOpsProjectId $orgUri -Wait
PushToDevops $devOpsOrg $selectedDevOpsProjectName $slug -Wait
#Jump out of working folder and back to root so the loop know where to start.
cd ..
}
Write-Host "*********************************** IMPORT COMPLETE ***********************************"
The full script should look something like this. You can copy the code and save it to a ps1 file. To run it you probably need to change your execution policy, or sign the script.
function CheckoutAllBranches()
{
$branches = git branch --all
foreach($branch in $branches)
{
if ($branch -like "*remotes/*")
{
$branchName = $branch -replace "remotes/origin/" -replace ""
$gitCheckout = "/c git checkout " + $branchName
Start-Process cmd -Argument $gitCheckout -Wait
Write-Host('checkedout branch: '+ $branchName )
}
}
}
function CloneStashRepository($clone_url, $repo_name)
{
mkdir $repo_name
cd $repo_name
$gitclone = "/c git clone " + $clone_url + " ."
Start-Process cmd -Argument $gitclone -Wait
}
function PushToDevops($orgPath, $project_name, $repo_name)
{
$projectName = [uri]::EscapeDataString($project_name)
$slug = [uri]::EscapeDataString($repo_name)
$remoteUrl = "https://$orgPath@dev.azure.com/$orgPath/$projectName/_git/$slug"
$makeRemote = git remote add devops $remoteUrl
$branches = git branch
foreach($branch in $branches)
{
$gitCheckout = "/c git push devops " + $branch
Start-Process cmd -Argument $gitCheckout -Wait
Write-Host('pushed branch: '+ $branch )
}
}
function GetStashReposForProject($user, $pass, $stashHost, $projectKey)
{
$pair = "$($user):$($pass)"
$encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
$basicAuthValue = "Basic $encodedCreds"
$Headers = @{
Authorization = $basicAuthValue
}
$uri = $stashHost + "/rest/api/1.0/projects/"+ $projectKey + "/repos?limit=50"
$request = Invoke-WebRequest -Uri $uri -Headers $Headers
$repos = $request.Content | ConvertFrom-Json
return $repos.values
}
function SelectAzureDevOpsProject($devOpsOrg)
{
$projects = $(az devops project list --org $devOpsOrg -o json) | ConvertFrom-Json
$count = 1
foreach($p in $projects.value)
{
Write-Host $count ") " $p.name
$count++
}
$index = Read-Host "Chooose project (enter number)"
#Write-Host "Selected inde is: " $index
$sp = $projects.value[$index-1]
$arr = @($sp.id, $sp.name)
Write-Host "Selected Project name: " $arr[1]
return $arr
}
function CreateRepoInDevOpsProject($slug, $projectId, $devOpsOrg)
{
az repos create --name $slug --project $projectId --organization $devOpsOrg
}
$user = Read-Host "Enter Stash Username: "
$pass = Read-Host "Enter Stash Password: "
$stashHost = Read-Host "htp://domain:port"
$projectKey = Read-Host "Enter stash project key"
$devOpsOrg = Read-Host "https://dev.azure.com/"
$orgUri = "https://dev.azure.com/" + $devOpsOrg
$selectedDevOpsProject = SelectAzureDevOpsProject $orgUri
$selectedDevOpsProjectId = $selectedDevOpsProject[0]
$selectedDevOpsProjectName = $selectedDevOpsProject[1]
foreach($repos in GetStashReposForProject $user $pass $stashHost $projectKey -Wait)
{
$slug = $repos.slug
CloneStashRepository $repos.cloneUrl $slug -Wait
CheckoutAllBranches -Wait
CreateRepoInDevOpsProject $slug $selectedDevOpsProjectId $orgUri -Wait
PushToDevops $devOpsOrg $selectedDevOpsProjectName $slug -Wait
#Jump out of working folder and back to root so the loop know where to start.
cd ..
}
Write-Host "*********************************** IMPORT COMPLETE ***********************************"
Disclaimer
All code here is provided as is. I take no responsibility for any damage that might occur, economic and technical if you choose to use it. Add validation and error handling if you plan to use this in automation.
Posted on October 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.