Nessi
Nessi

Reputation: 149

Azure DevOps API: Get all files (source and target) of Pull Request

I searched quite a bit, found several threads here (e.g. this, that, and more), but I could not find the answer to the following task: Use the Azure DevOps API to retrieve the content changes (basically the file before and after) of all the files of a specific PR.

I can find a PR, can loop through changes, iterations, commits (in various combinations), but I have not been able to download both the first and the last version of each or the files (and there should be a way as I can view the before and after in a PR in DevOps).

Any hints where/how I can retrieve both versions of a file of a certain commit/change/iteration?

Many thanks in advance!

Cheers, Udo

Upvotes: 2

Views: 4039

Answers (1)

Nessi
Nessi

Reputation: 149

Thanks for all the hints. Looks like I managed to find a wat to pull it. Please feel free to correct my approach.

Here's the complete PowerShell file:

[CmdletBinding()]
param (
    [int]
    $pullRequestId,
    [string]
    $repoName
)

# --- your own values ---
$pat = 'your-personal access token for Azure DevOps'
$urlOrganization = 'your Azure DevOps organization'
$urlProject = 'your Azure DevOps project'
$basePath = "$($env:TEMP)/pullRequest/" # a location to store all the data
# --- your own values ---

if (!$repoName)
{
    $userInput = Read-Host "Please enter the repository name"
    if (!$userInput)
    {
        Write-Error "No repository name given."
        exit
    }
    $repoName = $userInput
}

if (!$pullRequestId)
{
    $userInput = Read-Host "Please enter the PullRequest ID for $($repoName)"
    if (!$userInput)
    {
        Write-Error "No PullRequest ID given."
        exit
    }
    $pullRequestId = $userInput
}

$prPath = "$($basePath)$($pullRequestId)"
$sourcePath = "$($basePath)$($pullRequestId)/before"
$targetPath = "$($basePath)$($pullRequestId)/after"

# --- helper methods ---
function CleanLocation([string]$toBeCreated)
{
    RemoveLocation $toBeCreated
    CreateLocation $toBeCreated
    if (!(Test-Path $toBeCreated))
    {
        Write-Error "Path '$toBeCreated' could not be created"
    }
}

function CreateLocation([string]$toBeCreated)
{
    if (!(Test-Path $toBeCreated)) { New-Item -ErrorAction Ignore -ItemType directory -Path $toBeCreated > $null }
}

function RemoveLocation([string]$toBeDeleted)
{
    if (Test-Path $toBeDeleted) { Remove-Item -Path $toBeDeleted -Recurse -Force }
}

function DeleteFile([string]$toBeDeleted)
{
    if (Test-Path $toBeDeleted) { Remove-Item -Path $toBeDeleted -Force }
}
# --- helper methods ---

# --- Azure DevOps helper methods ---
function GetFromDevOps($url)
{
    $patToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)"))
    $repoHeader = @{ "Authorization" = "Basic $patToken" }
    $response = $(Invoke-WebRequest $url -Headers $repoHeader)
    if ($response.StatusCode -ne 200)
    {
        Write-Error "FAILED: $($response.StatusDescription)"
    }
    return $response
}

function JsonFromDevOps($url)
{
    $response = GetFromDevOps $url
    return ConvertFrom-Json -InputObject $response.Content
}

function DownloadFromDevOps($url, $filename)
{
    $patToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)"))
    $repoHeader = @{ "Authorization" = "Basic $patToken" }
    $ProgressPreference = 'SilentlyContinue'
    Invoke-WebRequest $url -Headers $repoHeader -OutFile $filename
    $ProgressPreference = 'Continue'
}

function DownloadAndSaveFile($itemUrl, $outputPath, $path, $overwrite)
{
    $outFile = "$outputPath$($path)"
    $fileExists = Test-Path $outFile
    if (!$fileExists -or $overwrite)
    {
        $outPath = [System.IO.Path]::GetDirectoryName($outFile)
        CreateLocation $outPath
        if ($fileExists)
        {
            Write-Host "    overwriting to $outputPath"
        }
        else
        {
            Write-Host "    downloading to $outputPath"
        }
        DownloadFromDevOps $itemUrl $outFile
    }
}

function DownloadFiles($changes, $beforePath, $beforeCommitId, $afterPath, $afterCommitId)
{
    foreach ($change in $changes)
    {
        $item = $change.item

        if ($item.isFolder)
        {
            continue;
        }
        $path = $item.path
        $originalPath = $change.originalPath
        if (!$path)
        {
            $path = $change.originalPath
        }
        $displayPath = $path ?? $originalPath
        $changeType = $change.changeType
        Write-Host "[$($changeType)] $($displayPath)"
        if (($changeType -eq "edit, rename"))
        {
            $itemUrl = "$($urlRepository)/items?path=$($originalPath)&versionDescriptor.version=$($beforeCommitId)&versionDescriptor.versionOptions=0&versionDescriptor.versionType=2&download=true"
            DownloadAndSaveFile $itemUrl $beforePath $originalPath
        }
        # just get the source/before version
        if ($changeType -eq "delete")
        {
            $itemUrl = "$($urlRepository)/items?path=$($originalPath)&versionDescriptor.version=$($beforeCommitId)&versionDescriptor.versionOptions=0&versionDescriptor.versionType=2&download=true"
            DownloadAndSaveFile $itemUrl $beforePath $originalPath
            DeleteFile "$($afterPath)$($originalPath)"
        }
        if ($changeType -eq "edit")
        {
            $itemUrl = "$($urlRepository)/items?path=$($path)&versionDescriptor.version=$($beforeCommitId)&versionDescriptor.versionOptions=0&versionDescriptor.versionType=2&download=true"
            DownloadAndSaveFile $itemUrl $beforePath $path
        }
        if (($changeType -eq "add") -or ($changeType -eq "edit") -or ($changeType -eq "edit, rename"))
        {
            $itemUrl = "$($urlRepository)/items?path=$($path)&versionDescriptor.version=$($afterCommitId)&versionDescriptor.versionOptions=0&versionDescriptor.versionType=2&download=true"
            DownloadAndSaveFile $itemUrl $afterPath $path $true
        }
        $validChangeTypes = @('add', 'delete', 'edit', 'edit, rename')
        if (!($validChangeTypes.Contains($changeType)))
        {
            Write-Warning "Unknown change type $($changeType)"
        }
    }
}
# --- Azure DevOps helper methods ---

$urlBase = "https://dev.azure.com/$($urlOrganization)/$($urlProject)/_apis"
$urlRepository = "$($urlBase)/git/repositories/$($repoName)"
$urlPullRequests = "$($urlRepository)/pullRequests"
$urlPullRequest = "$($urlPullRequests)/$($pullRequestId)"
$urlIterations = "$($urlPullRequest)/iterations"

CleanLocation $prPath

$iterations = JsonFromDevOps $urlIterations
foreach ($iteration in $iterations.value)
{
    # the modified file
    $srcId = $iteration.sourceRefCommit.commitId
    # the original file
    $comId = $iteration.commonRefCommit.commitId
    $comId = $iteration.targetRefCommit.commitId

    $urlIterationChanges = "$($urlIterations)/$($iteration.id)/changes"
    $iterationChanges = JsonFromDevOps $urlIterationChanges

    DownloadFiles $iterationChanges.changeEntries $sourcePath $comId $targetPath $srcId
}

Upvotes: 3

Related Questions