2

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

torek
  • 448,244
  • 59
  • 642
  • 775
Nessi
  • 139
  • 7
  • Presumably the "thing" that is making the API calls has a copy of the repo too? If yes, once you know the commit IDs in question, it may be easier to just do what you want with Git commands in that local repo rather than through the AzDev API. – TTT Nov 29 '21 at 17:11
  • [This may help.](https://stackoverflow.com/q/54137998/184546) – TTT Nov 30 '21 at 05:28
  • @TTT It's a simple PS script, that should download the "before" and "after" versions of changes files in a PR to then invoke a comparison application for a reviewer. This external step is required becuase there is no "proper"way to review/compare changes in an Excel sheet don't ask, why it's Excel ;) ) So I want to simplify a reviewer's life by automatically downloading the files into a "before"and "aster" folder structure for easier comparison. And no, there's no possibility to invoke git in any way – Nessi Nov 30 '21 at 09:34
  • OK- I think the link in the second comment may work for plucking out file versions at specific commits. Note "after" may actually need to be the would-be merge commit rather than the branch's commits. And, the merge commit could change anytime the target branch is updated. – TTT Nov 30 '21 at 15:33
  • Side Note- you're using AzDev for PR's but doing the code review externally? Is that actually better than using the built-in review functionality in AzDev for the code reviews? – TTT Nov 30 '21 at 15:37
  • @TTT: I use an external tool, because you cannot vire Excel files using the built-in AzureDev review functionality. It simply gives you the "The two versions of the file are different" message, and two buttons to download the files - and that's what I want to do with a script: download these files ;) – Nessi Dec 02 '21 at 09:45
  • @TT: you wrote: "And, the merge commit could change anytime the target branch is updated". And that is OK because I always want to download the 'latest' version of the PR. But thanks for the hints (esp. the 'would-be merge commit'). Any hint which property that is? – Nessi Dec 02 '21 at 09:47
  • Ah OK- I misunderstand your previous statement about the excel file. Apparently you had to say the same thing twice for me to get it. :D (Sorry about that.) – TTT Dec 02 '21 at 14:37
  • It looks like the commit ID you need to see the merged file is `lastMergeCommit` taken from [here](https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-request?view=azure-devops-rest-6.0). Ideally for a binary(ish) file the source branch would already be up to date with the target branch (at least for that file) so that Git doesn't have to actually merge it. If it is up to date I think the file at `lastMergeCommit` would be the same as at `lastMergeTargetCommit`. – TTT Dec 02 '21 at 14:42

1 Answers1

2

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
}
Nessi
  • 139
  • 7