Skip to content

Instantly share code, notes, and snippets.

@Ricky-G
Last active October 26, 2023 21:46
Show Gist options
  • Save Ricky-G/6dc5cfc0de0eb1eb6cd25eb453cc5022 to your computer and use it in GitHub Desktop.
Save Ricky-G/6dc5cfc0de0eb1eb6cd25eb453cc5022 to your computer and use it in GitHub Desktop.
PowerShell script to Get a list of unique committers from Azure Devops && ADO Server
$tfsUrl = "http://your-tfs-server:8080/tfs" # Replace with your TFS server URL
$collection = "DefaultCollection" # Replace with your TFS collection name
$accessToken = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":your-personal-access-token")) #Replace with your Personal Access Token (NB : the colon is required at the beginning of the token before the pat itself)
$Headers = @{
"Authorization" = $accessToken
"Content-Type" = "application/json"
}
# Get all projects
$pageSize = 100
$skipCount = 0
$projects = @()
do {
$pagedApiUrl = "$tfsUrl/$collection/_apis/projects?`$top=$pageSize&`$skip=$skipCount"
try {
$projectsResponse = Invoke-RestMethod -Uri $pagedApiUrl -Headers $Headers -ContentType 'application/json' -ErrorAction Stop
$fetchedProjects = $projectsResponse.value
} catch {
Write-Host "Error: The API call was not successful. Response:`n$($_.Exception.Response)"
exit
}
# Check if the response has the expected properties
if ($null -eq $fetchedProjects -or $fetchedProjects.Count -eq 0 -or (-not ($fetchedProjects[0].PSObject.Properties.Name -contains 'id'))) {
Write-Host "Error: The response for the first API call to get a list of all projects is not valid. Please check your Personal Access Token, remember the PAT token needs to have a : colon in front of it, please check it has that."
exit
}
$projects += $fetchedProjects
$skipCount += $pageSize
} while ($fetchedProjects.Count -eq $pageSize)
$uniqueCommitters = @{}
foreach ($project in $projects) {
$projectName = $project.name
$repoApiUrl = "$tfsUrl/$collection/$projectName/_apis/git/repositories"
$reposResponse = Invoke-RestMethod -Uri $repoApiUrl -Headers $Headers
$repos = $reposResponse.value
foreach ($repo in $repos) {
$repoId = $repo.id
$fromDate = (Get-Date).AddDays(-90).ToString("yyyy-MM-dd")
$commitsApiUrl = "$tfsUrl/$collection/$projectName/_apis/git/repositories/$repoId/commits?searchCriteria.fromDate=$fromDate"
$commitsResponse = Invoke-RestMethod -Uri $commitsApiUrl -Headers $Headers
$commits = $commitsResponse.value
foreach ($commit in $commits) {
$committer = $commit.committer.name
$uniqueCommitters[$committer] = $true
}
}
}
# Check if the uniqueCommitters hashtable is empty
if ($uniqueCommitters.Count -eq 0) {
Write-Host "No committers found for the given time frame"
} else {
# Output unique committers
$uniqueCommitters.Keys | Sort-Object | ForEach-Object {
Write-Host "Name: $_ | Email: $($uniqueCommitters[$_].Email) | Last Commit Date: $($uniqueCommitters[$_].LastCommitDate)"
}
}
$organization = "" #Your organization name here
$accessToken = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":your-pat-token")) #Replace with your Personal Access Token NB: the colon is required at the beginning of the token
$Headers = @{
"Authorization" = $accessToken
"Content-Type" = "application/json"
}
# Get all projects
$projectApiUrl = "https://dev.azure.com/$organization/_apis/projects"
try {
$projectsResponse = Invoke-RestMethod -Uri $projectApiUrl -Headers $Headers -ContentType 'application/json' -ErrorAction Stop
$projects = $projectsResponse.value
} catch {
Write-Host "Error: The API call was not successful. Response:`n$($_.Exception.Response)"
exit
}
# Check if the response has the expected properties
if ($null -eq $projects -or $projects.Count -eq 0 -or (-not ($projects[0].PSObject.Properties.Name -contains 'id'))) {
Write-Host "Error: The response for the first API call to get a list of all projects is not valid. Please check your Personal Access Token."
exit
}
$uniqueCommitters = @{}
#Loop through each project
foreach ($project in $projects) {
$projectId = $project.id
$repoApiUrl = "https://dev.azure.com/$organization/$projectId/_apis/git/repositories"
$reposResponse = Invoke-RestMethod -Uri $repoApiUrl -Headers $Headers
$repos = $reposResponse.value
#Loop through each repo
foreach ($repo in $repos) {
$repoId = $repo.id
$fromDate = (Get-Date).AddDays(-90).ToString("yyyy-MM-dd") #Commiter history for the last 90 days
$commitsApiUrl = "https://dev.azure.com/$organization/$projectId/_apis/git/repositories/$repoId/commits?searchCriteria.fromDate=$fromDate"
$commitsResponse = Invoke-RestMethod -Uri $commitsApiUrl -Headers $Headers
$commits = $commitsResponse.value
foreach ($commit in $commits) {
$committer = $commit.committer.name
$committerEmail = $commit.committer.email
$lastCommitDate = $commit.committer.date
# For each committer, store their email and the date of their last commit in the hashtable
$uniqueCommitters[$committer] = @{
Email = $committerEmail
LastCommitDate = $lastCommitDate
}
}
}
}
# Check if the uniqueCommitters hashtable is empty
if ($uniqueCommitters.Count -eq 0) {
Write-Host "No committers found for the given time frame"
} else {
# Output unique committers
$uniqueCommitters.Keys | Sort-Object | ForEach-Object {
Write-Host "Name: $_ | Email: $($uniqueCommitters[$_].Email) | Last Commit Date: $($uniqueCommitters[$_].LastCommitDate)"
}
}
@Ricky-G
Copy link
Author

Ricky-G commented May 10, 2023

When using these scripts, please make sure to have a colon : in front the PAT Token, this is because

In PowerShell, when using Basic Authentication, the Authorization header must be set as "Basic " followed by the Base64-encoded : or : string. In the case of Azure DevOps or ADO Server, the username part is not required, but the colon (:) is still needed to separate the username and password/token fields.

By including the colon in the encoded string, you effectively tell the system that the username is empty, and the Personal Access Token (PAT) is being used as the password.

When you encode the string without the colon, it is interpreted as if the entire token is the username, and the password field is left empty. This causes authentication to fail, as the system expects a token in the password field.

So, adding the colon in front of the PAT ensures that the PAT is interpreted correctly as the password, allowing authentication to proceed as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment