PSGitHubStats.psm1
Function CreateAuthHeader([string]$canonicalizedString,[string]$storageAccount,[string]$storageKey) { [string]$signature = [string]::Empty [byte[]]$bytes = [System.Convert]::FromBase64String($storageKey) [System.Security.Cryptography.HMACSHA256] $SHA256 = New-Object System.Security.Cryptography.HMACSHA256(,$bytes) [byte[]] $dataToSha256 = [System.Text.Encoding]::UTF8.GetBytes($canonicalizedString) $signature = [System.Convert]::ToBase64String($SHA256.ComputeHash($dataToSha256)) "SharedKey $($storageAccount):$signature" } Function Get-PSDownloadStats() { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] # Statistics is the proper noun here [CmdletBinding(DefaultParameterSetName="default")] param( [parameter(ParameterSetName="Default")] [parameter(ParameterSetName="AzureTable")] [string]$repo = "powershell/powershell", [parameter(ParameterSetName="Default")] [parameter(ParameterSetName="AzureTable")] [datetime]$publishedSinceDate = "2016-08-16", # this is when we went public [parameter(ParameterSetName="Default",Mandatory=$true)] [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$accessToken, [parameter(ParameterSetName="AzureTable")] [switch]$publishToAzure, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageUrl, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageTable, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageAccount, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageKey ) $query = "https://api.github.com/repos/$repo/releases" $headers = @{Authorization="token $accessToken"} $releases = Invoke-RestMethod -Uri $query -Headers $headers foreach ($release in $releases) { foreach ($asset in $release.assets) { if ($release.draft -eq $false -and [datetime]::Parse($release.published_at) -ge $publishedSinceDate) { [string]$os = "Unknown" switch ([System.IO.Path]::GetExtension($asset.name)) { ".pkg" { $os = "MacOS"} ".deb" { $os = "Linux"} ".rpm" { $os = "Linux"} ".msi" { $os = "Windows"} ".zip" { $os = "Windows"} } [string]$distro = "Unknown" if ($os -eq "MacOS") { $distro = "MacOS" } elseif ($asset.name -match "win10") { $distro = "Windows10" } elseif ($asset.name -match "win7") { $distro = "Windows7" } elseif ($asset.name -match "win81") { $distro = "Windows8" } elseif ($asset.name -match "centos") { $distro = "CentOS" } elseif ($asset.name -match "ubuntu.*14") { $distro = "Ubuntu14" } elseif ($asset.name -match "ubuntu.*16") { $distro = "Ubuntu16" } $pkg = [PSCustomObject]@{PartitionKey=$release.tag_name;RowKey=[System.Guid]::NewGuid().ToString();Tag=$release.tag_name;Name=$asset.name;Count=$asset.download_count;Published=$release.published_at;OS=$os;Distro=$distro;Date=(get-date -f "yyyy-MM-dd")} $pkg.PSTypeNames.Insert(0,"PSGitHubStats.Package") if ($publishToAzure) { $json = $pkg | ConvertTo-Json -Compress $date = [datetime]::UtcNow.ToString("R", [System.Globalization.CultureInfo]::InvariantCulture) [string] $canonicalizedResource = "/$storageAccount/$storageTable" $contentType = "application/json" [string] $stringToSign = "POST`n`n$contentType`n$date`n$canonicalizedResource" $headers = @{"Prefer"="return-no-content";"Authorization"=(CreateAuthHeader -canonicalizedString $stringToSign -storageAccount $storageAccount -storageKey $storageKey); "DataServiceVersion"="3.0;NetFx";"MaxDataServiceVersion"="3.0;NetFx";"Accept"="application/json;odata=nometadata"; "Accept-Charset"="UTF-8";"x-ms-version"="2013-08-15";"x-ms-date"=$date} $null = Invoke-RestMethod -Uri $storageUrl -Headers $headers -Body $json -Method Post -ContentType $contentType } else { $pkg } } } } } $script:MsftMembers = @() Function Get-PSMicrosoftMembers() { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [parameter(Mandatory=$true)] [string]$accessToken ) if ($script:MsftMembers.Count -eq 0) { $query = "https://api.github.com/teams/1513871/members" $headers = @{Authorization="token $accessToken"} while ($query -ne $null) { $output = Invoke-WebRequest $query -UseBasicParsing -Headers $headers $query = $null foreach ($member in ($output | ConvertFrom-Json)) { $script:MsftMembers += $member.login Write-Verbose $member.login } if ($null -ne $output.Headers.Link) { $links = $output.Headers.Link.Split(",").Trim() foreach ($link in $links) { if ($link -match "<(?<url>.*?)>;\srel=`"(?<rel>.*?)`"") { if ($matches.rel -eq 'next') { $query = $matches.url } } } } } } $script:MsftMembers } Function Get-PSGitHubStats() { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(defaultParameterSetName="daterange")] param( [parameter(ParameterSetName="daterange")] [datetime]$startDate = (Get-Date).AddDays(-1), [parameter(ParameterSetName="daterange")] [datetime]$endDate = (Get-Date), [parameter(ParameterSetName="lastmonth")] [switch]$lastMonth, [string]$repo = "PowerShell/PowerShell", [ValidateSet("Issue","PR")] [string]$type = "Issue", [switch]$commentsOnly, [switch]$includeOwn, # include comments from creator of PR or item [parameter(Mandatory=$true)] [string]$accessToken ) [hashtable]$users = @{} function IncrementUserCount ([string] $user) { if ($user -eq 'msftclas') { # skip the Microsoft CLA bot return } if ($users.ContainsKey($user)) { $users[$user]++ } else { $users.Add($user, [int]1) } } if ($lastMonth) { $startDate = Get-Date -Month ((Get-Date).Month-1) -Day 1 $endDate = (Get-Date -Day 1).AddDays(-1) } Write-Verbose "Start date: $startDate" Write-Verbose "End date: $endDate" $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Net $repo = [System.Net.WebUtility]::UrlEncode($repo) $query = "https://api.github.com/search/issues?q=is:$type+created:$($startDate.ToString("yyyy-MM-dd"))..$($endDate.ToString("yyyy-MM-dd"))+repo:$repo" $headers = @{Authorization="token $accessToken"} while ($query -ne $null) { Write-Verbose "Query: $query" $output = Invoke-WebRequest $query -UseBasicParsing -Headers $headers $items = $output | ConvertFrom-Json $query = $null foreach ($item in $items.items) { if ($commentsOnly -and $item.comments -gt 0) { $Headers = @{Authorization="token $accessToken"} $comments = Invoke-WebRequest $item.comments_url -Headers $Headers -UseBasicParsing | ConvertFrom-Json foreach ($comment in $comments) { if (!$includeOwn -and $comment.user.login -eq $item.user.login) { continue } IncrementUserCount $comment.user.login } if ($type -eq "PR") { $pullRequest = Invoke-WebRequest $item.pull_request.url -Headers $Headers -UseBasicParsing | ConvertFrom-Json $reviewComments = Invoke-WebRequest $pullRequest.review_comments_url -Headers $Headers -UseBasicParsing | ConvertFrom-Json foreach ($comment in $reviewComments) { if (!$includeOwn -and $comment.user.login -eq $item.user.login) { continue } IncrementUserCount $comment.user.login } } } else { IncrementUserCount $item.user.login } } if ($null -ne $output.Headers.Link) { $links = $output.Headers.Link.Split(",").Trim() foreach ($link in $links) { if ($link -match "<(?<url>.*?)>;\srel=`"(?<rel>.*?)`"") { if ($matches.rel -eq 'next') { $query = $matches.url } } } } } $MsftMembers = Get-PSMicrosoftMembers -accessToken $accessToken $users.GetEnumerator() | ForEach-Object {$user = [pscustomobject]@{Name=$_.Name;Count=$_.Value;Org="Community"}; if ($MsftMembers.Contains($user.Name)){$user.Org="Microsoft"};$user} | ForEach-Object {$_.pstypenames.insert(0,"PSGitHub.Statistic");$_} | Sort-Object -Property Org,Count -Descending } Function Get-PSGitHubReport() { [CmdletBinding(DefaultParameterSetName="default")] param( [parameter(ParameterSetName="Default")] [parameter(ParameterSetName="AzureTable")] [datetime]$startDate = (Get-Date).AddDays(-7), # default to last 7 days [parameter(ParameterSetName="Default")] [parameter(ParameterSetName="AzureTable")] [datetime]$endDate = (Get-Date), [parameter(ParameterSetName="Default")] [parameter(ParameterSetName="AzureTable")] [string[]]$repos = "PowerShell/PowerShell", [parameter(ParameterSetName="Default",Mandatory=$true)] [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$accessToken, [parameter(ParameterSetName="AzureTable")] [switch]$publishToAzure, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageUrl, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageTable, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageAccount, [parameter(ParameterSetName="AzureTable",Mandatory=$true)] [string]$storageKey ) # can't use classes since Azure Function only supports PSv4 currently function New-Contributor() { # private function [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] # only in memory param ( [string] $Name, [string] $endDate, [string] $Org, [string] $Repo ) $contributor = [PSCustomObject]@{Name=$Name;endDate=$endDate;Org=$Org;PrCount=0;IssueCount=0; PrCommentCount=0;IssueCommentCount=0;Total=0;Repo=$Repo;PartitionKey=$name;RowKey=([System.Guid]::NewGuid().ToString())} $contributor.PSTypeNames.Insert(0,"Get-PSGitHubReport.Contributor") $contributor } $ErrorActionPreference = "Stop" Write-Verbose "Start date: $startDate" Write-Verbose "End date: $endDate" foreach ($repo in $repos) { [hashtable] $Contributors = @{} $ContributionTypes = @{ PR=@{CommentsOnly=$false;Property="PrCount";Type="PR"}; Issue=@{CommentsOnly=$false;Property="IssueCount";Type="Issue"}; IssueComments=@{CommentsOnly=$true;Property="IssueCommentCount";Type="Issue"}; PrComments=@{CommentsOnly=$true;Property="PrCommentCount";Type="PR"} } [int]$repoPercentComplete = 0 foreach ($ContributionType in $ContributionTypes.Keys) { Write-Progress -Activity "Generating Report for $repo" -PercentComplete $repoPercentComplete -Id 1 Write-Progress -Activity "Getting $ContributionType" -Id 2 $Contribution = $ContributionTypes[$ContributionType] $results = Get-PSGitHubStats -startDate $startDate -endDate $endDate -accessToken $accessToken -repo $repo -type $Contribution.Type -commentsOnly:$Contribution.CommentsOnly foreach ($user in $results) { if (!$Contributors.Contains($user.Name)) { $Contributors.Add($user.Name, (New-Contributor -Name $user.Name -EndDate $endDate.ToString("u") -Org $user.Org -Repo $repo)) } $contributor = $Contributors[$user.Name] $contributor.($Contribution.Property) = $user.Count } Write-Progress -Activity "Getting $ContributionType" -Completed -Id 2 $repoPercentComplete += 25 Write-Progress -Activity "Getting PR Comments" -Completed -Id 1 } Write-Progress -Activity "Generating Report for $repo" -PercentComplete 100 -Completed $Contributors.GetEnumerator() | ForEach-Object { $_.Value.Total = $_.Value.PrCount + $_.Value.IssueCount + $_.Value.PrCommentCount + $_.Value.IssueCommentCount} if ($publishToAzure) { $date = [datetime]::UtcNow.ToString("R", [System.Globalization.CultureInfo]::InvariantCulture) [string] $canonicalizedResource = "/$storageAccount/$storageTable" $contentType = "application/json" [string] $stringToSign = "POST`n`n$contentType`n$date`n$canonicalizedResource" foreach ($contributor in $contributors.values) { $json = $contributor | ConvertTo-Json -Compress Write-Verbose "JSON: $json" $headers = @{"Prefer"="return-no-content";"Authorization"=(CreateAuthHeader -canonicalizedString $stringToSign -storageAccount $storageAccount -storageKey $storageKey); "DataServiceVersion"="3.0;NetFx";"MaxDataServiceVersion"="3.0;NetFx";"Accept"="application/json;odata=nometadata"; "Accept-Charset"="UTF-8";"x-ms-version"="2013-08-15";"x-ms-date"=$date} $null = Invoke-RestMethod -Uri $storageUrl -Headers $headers -Body $json -Method Post -ContentType $contentType } } else { $Contributors.Values | Sort-Object -Property Org,Total -Descending } # sleep if more repos to avoid going over GitHub rate limit if ($repo -ne $repos[$repos.Length-1]) { Start-Sleep -seconds 61 } } } |