loading...
Cover image for Migrating repos from Atlassian Stash to Azure DevOps
Mega Therion AS

Migrating repos from Atlassian Stash to Azure DevOps

alexanderviken profile image Alexander Viken ・5 min read

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
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 )
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 )
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ***********************************"
Enter fullscreen mode Exit fullscreen mode

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 ***********************************"
Enter fullscreen mode Exit fullscreen mode

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.

Discussion

pic
Editor guide