├── README.md ├── ProjectRules.json ├── appveyor.yml ├── .gitignore ├── LICENSE ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── Tests ├── Unit │ ├── Find-GitHubUser.Tests.ps1 │ ├── Find-GitHubCode.Tests.ps1 │ ├── Find-GitHubRepository.Tests.ps1 │ └── Find-GitHubIssue.Tests.ps1 └── TestData │ └── MockObjects.json ├── PSGithubSearch.BuildSettings.ps1 ├── PSGithubSearch ├── PSGithubSearch.psd1 ├── PSGithubSearch.format.ps1xml └── PSGithubSearch.psm1 └── PSGithubSearch.build.ps1 /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuBuisson/PSGithubSearch/HEAD/README.md -------------------------------------------------------------------------------- /ProjectRules.json: -------------------------------------------------------------------------------- 1 | { 2 | "PerFunctionMetrics": [], 3 | "OverallMetrics": [ 4 | { 5 | "LinesOfCodeAverage": { 6 | "WarningThreshold": 115, 7 | "FailThreshold": 165, 8 | "HigherIsBetter": false 9 | } 10 | }, 11 | { 12 | "TestCoverage": { 13 | "WarningThreshold": 75, 14 | "FailThreshold": 65, 15 | "HigherIsBetter": true 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | image: Visual Studio 2017 4 | 5 | environment: 6 | Coveralls_Key: 7 | secure: 8k+ZP0tz89b1tE7yKah3uwxdbfkvgwWHlRwxQwPTQxLgkuJ8NWy4dd6BXsQ6/0aW 8 | GitHub_Key: 9 | secure: wtrwAJK+i7Ar5L8TXeXOUtsxmVD+2wXu9u9bOf6GRfPP0Xn2V4yqTatLwaT7VWA6 10 | PSGallery_Key: 11 | secure: 1OaraGK9SJwOoGVdOHdM1DH5rvfL2AcDfvUFvrMxKYIkqJ0LIw3rwe4nUhxMgBfE 12 | 13 | before_build: 14 | - ps: Write-Host "Build version :` $env:APPVEYOR_BUILD_VERSION" 15 | - ps: Write-Host "Branch :` $env:APPVEYOR_REPO_BRANCH" 16 | - ps: Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 17 | - ps: Install-Module InvokeBuild -Scope CurrentUser -AllowClobber -Force 18 | - ps: Import-Module InvokeBuild 19 | 20 | build_script: 21 | - ps: Invoke-Build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | # Build results 9 | [Dd]ebug/ 10 | [Dd]ebugPublic/ 11 | [Rr]elease/ 12 | 13 | # Visual Studio 2015 cache/options directory 14 | .vs/ 15 | 16 | #NUNIT 17 | *.VisualState.xml 18 | TestResult.xml 19 | 20 | # Visual Studio profiler 21 | *.psess 22 | *.vsp 23 | *.vspx 24 | # TFS 2012 Local Workspace 25 | $tf/ 26 | 27 | # ReSharper is a .NET coding add-in 28 | _ReSharper*/ 29 | 30 | # TeamCity is a build add-in 31 | _TeamCity* 32 | 33 | # Folders created by Visual Studio Code 34 | *.vscode/ 35 | 36 | # Backup files created by Notepad++ and other applications 37 | *.orig 38 | 39 | # Windows image file caches 40 | Thumbs.db 41 | ehthumbs.db 42 | 43 | # Build results 44 | [Bb]uildOutput/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mathieu BUISSON 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Module version used: 30 | * Operating System and PowerShell version: -------------------------------------------------------------------------------- /Tests/Unit/Find-GitHubUser.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ModuleName = 'PSGithubSearch' 2 | Import-Module "$PSScriptRoot\..\..\$ModuleName\$ModuleName.psd1" -Force 3 | 4 | Describe 'Find-GitHubUser' { 5 | InModuleScope $ModuleName { 6 | 7 | $Mocks = ConvertFrom-Json (Get-Content -Path "$PSScriptRoot\..\TestData\MockObjects.json" -Raw ) 8 | 9 | Context 'Keywords' { 10 | 11 | It 'All results have the specified keywords' { 12 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.RamblingInLogin } 13 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.RamblingInLogin.Content } 14 | $KeywordTest = Find-GithubUser -Type 'user' -Keywords 'Rambling' -In 'login' -Repos '>12' 15 | 16 | Foreach ( $Result in $KeywordTest ) { 17 | $Result.login | Should Match 'Rambling' 18 | } 19 | } 20 | } 21 | Context 'Sorting of search results' { 22 | 23 | It 'When the $SortBy value is "followers", any result has more followers than the next one' { 24 | $SortByFollowers = Find-GithubUser -Type 'user' -Keywords 'Rambling' -In 'login' -Repos '>18' -SortBy 'followers' 25 | $SortByFollowers[0].Followers | Should BeGreaterThan $SortByFollowers[1].Followers 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /PSGithubSearch.BuildSettings.ps1: -------------------------------------------------------------------------------- 1 | # This file stores variables which are used by the build script 2 | 3 | # Storing all values in a single $Settings variable to make it obvious that the values are coming from this BuildSettings file when accessing them. 4 | $Settings = @{ 5 | 6 | Dependency = @('Coveralls','Pester','PsScriptAnalyzer','PSCodeHealth') 7 | CoverallsKey = $env:Coveralls_Key 8 | Branch = $env:APPVEYOR_REPO_BRANCH 9 | TestUploadUrl = "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)" 10 | Version = $env:APPVEYOR_BUILD_VERSION 11 | ManifestPath = '{0}\{1}\{1}.psd1' -f $PSScriptRoot, $env:APPVEYOR_PROJECT_NAME 12 | VersionRegex = "ModuleVersion\s=\s'(?\S+)'" -as [regex] 13 | 14 | UnitTestParams = @{ 15 | Script = '.\Tests\Unit' 16 | CodeCoverage = '.\PSGithubSearch\PSGithubSearch.psm1' 17 | OutputFile = "$PSScriptRoot\BuildOutput\UnitTestsResult.xml" 18 | PassThru = $True 19 | } 20 | 21 | CodeHealthParams = @{ 22 | Path = '.\PSGithubSearch\' 23 | } 24 | 25 | QualityGateParams = @{ 26 | CustomSettingsPath = '.\ProjectRules.json' 27 | SettingsGroup = 'OverallMetrics' 28 | MetricName = @('LinesOfCodeAverage', 29 | 'TestCoverage', 30 | 'ScriptAnalyzerFindingsTotal' 31 | 'ComplexityAverage' 32 | ) 33 | } 34 | 35 | GitHubKey = $env:GitHub_Key 36 | Email = 'MathieuBuisson@users.noreply.github.com' 37 | Name = 'Mathieu Buisson' 38 | BuildOutput = "$PSScriptRoot\BuildOutput" 39 | SourceFolder = "$PSScriptRoot\$($env:APPVEYOR_PROJECT_NAME)" 40 | OutputModulePath = "$PSScriptRoot\BuildOutput\$($env:APPVEYOR_PROJECT_NAME)" 41 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ## Checklist: 29 | 30 | 31 | - [ ] My code follows the code style of this project. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] I have read the **CONTRIBUTING** document. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] All new and existing tests passed. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | Contributions to PSGithubSearch are highly encouraged and desired. Below are some guidelines that will help make the process as smooth as possible. 3 | 4 | # Getting Started 5 | * Make sure you have a [GitHub account](https://github.com/signup/free) 6 | * Submit a new issue, assuming one does not already exist. 7 | * Clearly describe the issue including steps to reproduce when it is a bug. 8 | * Make sure you fill in the earliest version that you know has the issue. 9 | * Fork the repository on GitHub 10 | 11 | # Suggesting Enhancements 12 | I want to know what you think is missing from PSGithubSearch and how it can be made better. 13 | * When submitting an issue for an enhancement, please be as clear as possible about why you think the enhancement is needed and what the benefit of 14 | it would be. 15 | 16 | # Making Changes 17 | * From your fork of the repository, create a topic/feature branch where work on your change will take place. 18 | * To quickly create a topic/feature branch based on master; `git checkout -b my_topic_branch master`. Please avoid working directly on the `master` branch. 19 | * Make commits of logical units. 20 | * Check for unnecessary whitespace with `git diff --check` before committing. 21 | * Please follow the prevailing code conventions in the repository. Differences in style make the code harder to understand for everyone. 22 | * Make sure your commit messages are in the proper format. 23 | ```` 24 | Add more cowbell to Get-Something.ps1 25 | 26 | The functionaly of Get-Something would be greatly improved if there was a little 27 | more 'pizzazz' added to it. I propose a cowbell. Adding more cowbell has been 28 | shown in studies to both increase one's mojo, and cement one's status 29 | as a rock legend. 30 | ```` 31 | 32 | * Make sure you have added all the necessary Pester tests for your changes. 33 | * Run the Pester tests in the module and make sure they _all_ pass, to verify that your changes have not broken anything. 34 | 35 | # Documentation 36 | I am infallible and as such my documenation needs no corectoin. In the highly 37 | unlikely event that it is _not_ the case, contributions to update or add documentation 38 | are highly appreciated. 39 | 40 | # Submitting Changes 41 | * Push your changes to a topic/feature branch in your fork of the repository. 42 | * Submit a pull request to the main repository. 43 | * Once the pull request has been reviewed and accepted, it will be merged with the master branch in the main repository. 44 | * Be proud of your awesomeness and celebrate. 45 | 46 | # Additional Resources 47 | * [General GitHub documentation](https://help.github.com/) 48 | * [GitHub forking documentation](https://guides.github.com/activities/forking/) 49 | * [GitHub pull request documentation](https://help.github.com/send-pull-requests/) 50 | * [GitHub Flow guide](https://guides.github.com/introduction/flow/) 51 | * [GitHub's guide to contributing to open source projects](https://guides.github.com/activities/contributing-to-open-source/) -------------------------------------------------------------------------------- /Tests/Unit/Find-GitHubCode.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ModuleName = 'PSGithubSearch' 2 | Import-Module "$PSScriptRoot\..\..\$ModuleName\$ModuleName.psd1" -Force 3 | 4 | Describe 'Find-GitHubCode' { 5 | InModuleScope $ModuleName { 6 | 7 | $Mocks = ConvertFrom-Json (Get-Content -Path "$PSScriptRoot\..\TestData\MockObjects.json" -Raw ) 8 | 9 | Context 'Defaut parameter set using positional parameters' { 10 | 11 | It 'Should use the defaut parameter set and bind the second argument to the User parameter' { 12 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.ParameterSet } 13 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.ParameterSet.Content } 14 | $ParamSetTest = Find-GitHubCode 'SupportsShouldProcess' 'MathieuBuisson' 15 | 16 | Foreach ( $Result in $ParamSetTest ) { 17 | $Result.repository.owner.login | Should Be 'MathieuBuisson' 18 | } 19 | } 20 | } 21 | Context 'Keywords' { 22 | 23 | It 'All file results have the specified keyword in their path' { 24 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.DeploymentInPath } 25 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.DeploymentInPath.Content } 26 | $KeywordTest = Find-GitHubCode -Keywords 'Deployment' -User 'MathieuBuisson' -In path 27 | 28 | Foreach ( $Result in $KeywordTest ) { 29 | $Result.path | Should Match 'Deployment' 30 | } 31 | } 32 | It 'All results have the specified keywords when multiple keywords are specified' { 33 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MultiKeywords } 34 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.MultiKeywords.Content } 35 | $MultiKeyword = Find-GitHubCode -Keywords 'Deployment', 'Validation' -In path -Language 'PowerShell' 36 | 37 | Foreach ( $Result in $MultiKeyword ) { 38 | $Result.path | Should Match 'Deployment' 39 | } 40 | Foreach ( $Result in $MultiKeyword ) { 41 | $Result.path | Should Match 'Validation' 42 | } 43 | } 44 | } 45 | Context 'Search qualifiers behaviour' { 46 | 47 | It 'All results are from the repository specified via the Repo parameter' { 48 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MultiKeywords } 49 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.MultiKeywords.Content } 50 | $RepoTest = Find-GitHubCode -Keywords 'Any' -Repo 'MathieuBuisson/DeploymentReadinessChecker' 51 | 52 | Foreach ( $Result in $RepoTest ) { 53 | $Result.repository.full_name | Should Be 'MathieuBuisson/DeploymentReadinessChecker' 54 | } 55 | 56 | } 57 | It 'All file results have the string specified via the FileName parameter in their name' { 58 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MultiKeywords } 59 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.MultiKeywords.Content } 60 | $FileNameTest = Find-GitHubCode -Keywords 'Any' -User 'MathieuBuisson' -FileName 'Tests' 61 | 62 | Foreach ( $Result in $FileNameTest ) { 63 | $Result.name | Should Match 'Tests' 64 | } 65 | } 66 | It 'All file results have the extension specified via the Extension parameter' { 67 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MultiKeywords } 68 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.MultiKeywords.Content } 69 | $ExtensionTest = Find-GitHubCode -Keywords 'Any' -User 'MathieuBuisson' -Extension 'ps1' 70 | 71 | Foreach ( $Result in $ExtensionTest ) { 72 | $Result.name | Should Match '\.ps1$' 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /PSGithubSearch/PSGithubSearch.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PSGithubSearch' 3 | # 4 | # Generated by: Mathieu Buisson 5 | # 6 | # Generated on: 10/2/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = '.\PSGithubSearch.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.35' 16 | 17 | # ID used to uniquely identify this module 18 | GUID = '7fbef890-14aa-4313-a90b-aedf28b13810' 19 | 20 | # Author of this module 21 | Author = 'Mathieu Buisson' 22 | 23 | # Company or vendor of this module 24 | CompanyName = 'Unknown' 25 | 26 | # Copyright statement for this module 27 | Copyright = '(c) 2016 Mathieu Buisson. All rights reserved.' 28 | 29 | # Description of the functionality provided by this module 30 | Description = ' PowerShell module to search for the following on GitHub : 31 | - Repositories 32 | - Code 33 | - Issues 34 | - Pull requests 35 | - Users 36 | 37 | It uses the GitHub search API and implements most of its features in user-friendly cmdlets and parameters. 38 | The API documentation is available here : https://developer.github.com/v3/search/' 39 | 40 | # Minimum version of the Windows PowerShell engine required by this module 41 | PowerShellVersion = '4.0' 42 | 43 | # Name of the Windows PowerShell host required by this module 44 | # PowerShellHostName = '' 45 | 46 | # Minimum version of the Windows PowerShell host required by this module 47 | # PowerShellHostVersion = '' 48 | 49 | # Minimum version of Microsoft .NET Framework required by this module 50 | # DotNetFrameworkVersion = '' 51 | 52 | # Minimum version of the common language runtime (CLR) required by this module 53 | # CLRVersion = '' 54 | 55 | # Processor architecture (None, X86, Amd64) required by this module 56 | # ProcessorArchitecture = '' 57 | 58 | # Modules that must be imported into the global environment prior to importing this module 59 | # RequiredModules = @() 60 | 61 | # Assemblies that must be loaded prior to importing this module 62 | # RequiredAssemblies = @() 63 | 64 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 65 | # ScriptsToProcess = @() 66 | 67 | # Type files (.ps1xml) to be loaded when importing this module 68 | # TypesToProcess = @() 69 | 70 | # Format files (.ps1xml) to be loaded when importing this module 71 | FormatsToProcess = 'PSGithubSearch.format.ps1xml' 72 | 73 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 74 | # NestedModules = @() 75 | 76 | # Functions to export from this module 77 | FunctionsToExport = 'Find-GitHubCode', 'Find-GitHubIssue', 'Find-GitHubRepository', 'Find-GithubUser' 78 | 79 | # Cmdlets to export from this module 80 | #CmdletsToExport = '*' 81 | 82 | # Variables to export from this module 83 | #VariablesToExport = '*' 84 | 85 | # Aliases to export from this module 86 | #AliasesToExport = '*' 87 | 88 | # DSC resources to export from this module 89 | # DscResourcesToExport = @() 90 | 91 | # List of all modules packaged with this module 92 | # ModuleList = @() 93 | 94 | # List of all files packaged with this module 95 | # FileList = @() 96 | 97 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 98 | PrivateData = @{ 99 | 100 | PSData = @{ 101 | 102 | # Tags applied to this module. These help with module discovery in online galleries. 103 | Tags = 'GitHub' 104 | 105 | # A URL to the license for this module. 106 | LicenseUri = 'https://github.com/MathieuBuisson/PSGithubSearch/blob/master/LICENSE' 107 | 108 | # A URL to the main website for this project. 109 | ProjectUri = 'https://github.com/MathieuBuisson/PSGithubSearch' 110 | 111 | # A URL to an icon representing this module. 112 | # IconUri = '' 113 | 114 | # ReleaseNotes of this module 115 | # ReleaseNotes = '' 116 | 117 | } # End of PSData hashtable 118 | 119 | } # End of PrivateData hashtable 120 | 121 | # HelpInfo URI of this module 122 | # HelpInfoURI = '' 123 | 124 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 125 | # DefaultCommandPrefix = '' 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Tests/Unit/Find-GitHubRepository.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ModuleName = 'PSGithubSearch' 2 | Import-Module "$PSScriptRoot\..\..\$ModuleName\$ModuleName.psd1" -Force 3 | 4 | # Helper stuff 5 | Add-Type @" 6 | using System.Net; 7 | using System.Security.Cryptography.X509Certificates; 8 | public class TrustAllCertsPolicy : ICertificatePolicy { 9 | public bool CheckValidationResult( 10 | ServicePoint srvPoint, X509Certificate certificate, 11 | WebRequest request, int certificateProblem) { 12 | return true; 13 | } 14 | } 15 | "@ 16 | [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy 17 | 18 | Describe 'Find-GitHubRepository' { 19 | InModuleScope $ModuleName { 20 | 21 | $Mocks = ConvertFrom-Json (Get-Content -Path "$PSScriptRoot\..\TestData\MockObjects.json" -Raw ) 22 | Mock ConvertFrom-Json { return $InputObject } 23 | 24 | Context 'Keywords' { 25 | $MockedResponse = $Mocks.'Invoke-WebRequest'.MathieuBuissonPowerShell 26 | Mock Invoke-WebRequest { $MockedResponse } 27 | 28 | It 'All results have the specified keyword' { 29 | 30 | $KeywordTest = Find-GitHubRepository -Keywords 'PowerShell' -User 'MathieuBuisson' -In description 31 | 32 | Foreach ( $Result in $KeywordTest ) { 33 | $Result.description | Should Match 'PowerShell' 34 | } 35 | } 36 | } 37 | Context 'Search qualifiers behaviour' { 38 | 39 | It 'All results have the specified language' { 40 | 41 | $LanguageTest = Find-GitHubRepository -Keywords 'script' -Language 'PowerShell' -User 'MathieuBuisson' 42 | 43 | Foreach ( $Result in $LanguageTest ) { 44 | $Result.language | Should Be 'PowerShell' 45 | } 46 | } 47 | It 'All results have the specified keyword in the field specified via the In parameter' { 48 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.PowerShellInName } 49 | $InTest = Find-GitHubRepository -Keywords 'PowerShell-' -In 'name' -User 'MathieuBuisson' 50 | 51 | Foreach ( $Result in $InTest ) { 52 | $Result.name | Should Match 'PowerShell-' 53 | } 54 | } 55 | It 'All results match the size filter (greater than) specified via the SizeKB parameter' { 56 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.SizeGreaterThan100 } 57 | $SizeKBTest = Find-GitHubRepository -Keywords 'PowerShell' -User 'MathieuBuisson' -SizeKB '>100' 58 | 59 | Foreach ( $Result in $SizeKBTest ) { 60 | $Result.size | Should BeGreaterThan 100 61 | } 62 | } 63 | It 'All results are NOT forks if the Fork parameter is not used' { 64 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.NoFork } 65 | $NoFork = Find-GitHubRepository -Keywords 'Any' -In name 66 | 67 | Foreach ( $Result in $NoFork ) { 68 | $Result.fork | Should Be $False 69 | } 70 | } 71 | It 'All results are forks if the Fork parameter has the value "only"' { 72 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.ForksOnly } 73 | $OnlyFork = Find-GitHubRepository -Keywords 'Any' -In name -Fork 'only' 74 | 75 | Foreach ( $Result in $OnlyFork ) { 76 | $Result.fork | Should Be $True 77 | } 78 | } 79 | It 'All results have the owner specified via the User parameter' { 80 | 81 | $UserTest = Find-GitHubRepository -Keywords 'PS' -User 'MathieuBuisson' 82 | 83 | Foreach ( $Result in $UserTest ) { 84 | $Result.owner.login | Should match 'MathieuBuisson' 85 | } 86 | } 87 | It 'All results match the stars filter specified via the Stars parameter' { 88 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MoreThan20Stars } 89 | $StarsTest = Find-GitHubRepository -Keywords 'script' -User 'MathieuBuisson' -Stars '>20' 90 | 91 | Foreach ( $Result in $StarsTest ) { 92 | $Result.stargazers_count | Should BeGreaterThan 20 93 | } 94 | } 95 | } 96 | Context 'Sorting of search results' { 97 | 98 | It 'When the $SortBy value is "stars", any result has more stars than the next one' { 99 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.MoreThan20Stars } 100 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.MoreThan20Stars.Content } 101 | 102 | $SortByTest = Find-GitHubRepository -Keywords 'Any' -SortBy 'stars' 103 | $SortByTest[0].stargazers_count | Should BeGreaterThan $SortByTest[1].stargazers_count 104 | 105 | } 106 | It "When the $SortBy value is 'forks', any result has more forks than the next one" { 107 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.SortByForks } 108 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.SortByForks.Content } 109 | 110 | $SortbyForks = Find-GitHubRepository -Keywords 'Any' -SortBy forks 111 | $SortbyForks[0].forks | Should BeGreaterThan $SortbyForks[1].forks 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Tests/Unit/Find-GitHubIssue.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ModuleName = 'PSGithubSearch' 2 | Import-Module "$PSScriptRoot\..\..\$ModuleName\$ModuleName.psd1" -Force 3 | 4 | Describe 'Find-GitHubIssue' { 5 | InModuleScope $ModuleName { 6 | 7 | $Mocks = ConvertFrom-Json (Get-Content -Path "$PSScriptRoot\..\TestData\MockObjects.json" -Raw ) 8 | 9 | Context 'Keywords' { 10 | 11 | It 'All results have the specified keywords when multiple keywords are specified' { 12 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.IssueKeywords } 13 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.IssueKeywords.Content } 14 | $KeywordTest = Find-GitHubIssue -Type issue -Keywords 'case', 'sensitive' -In title 15 | 16 | Foreach ( $Result in $KeywordTest ) { 17 | $Result.title | Should Match 'case' 18 | } 19 | Foreach ( $Result in $KeywordTest ) { 20 | $Result.title | Should Match 'sensitive' 21 | } 22 | } 23 | } 24 | Context 'Search qualifiers behaviour' { 25 | 26 | It 'All results have the specified keyword in the field specified via the In parameter' { 27 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.IssueKeywords } 28 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.IssueKeywords.Content } 29 | $InTest = Find-GitHubIssue -Type issue -Keywords 'error', 'cannot' -In body 30 | 31 | Foreach ( $Result in $InTest ) { 32 | $Result.body | Should Match 'error' 33 | } 34 | Foreach ( $Result in $InTest ) { 35 | $Result.body | Should Match 'cannot' 36 | } 37 | } 38 | It 'All issues were opened by the user specified via the Author parameter' { 39 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.AuthorSnover } 40 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.AuthorSnover.Content } 41 | $AuthorTest = Find-GitHubIssue -Author 'jpsnover' -Type issue -Mentions 'jpsnover' 42 | 43 | Foreach ( $Result in $AuthorTest ) { 44 | $Result.user.login | Should Be 'jpsnover' 45 | } 46 | } 47 | It 'All results have the type specified via the Type parameter' { 48 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.TypePR } 49 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.TypePR.Content } 50 | $TypeTest = Find-GitHubIssue -Type pr -Author 'mwrock' -Commenter 'lzybkr' 51 | 52 | Foreach ( $Result in $TypeTest ) { 53 | $Result.pull_request | Should Not BeNullOrEmpty 54 | } 55 | } 56 | It 'All results have the assignee specified via the Assignee parameter' { 57 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.AuthorSnover } 58 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.AuthorSnover.Content } 59 | $AssigneeTest = Find-GitHubIssue -Type issue -Assignee 'lzybkr' 60 | 61 | Foreach ( $Result in $AssigneeTest ) { 62 | $Result.assignees.login -join ' ' | Should Match 'lzybkr' 63 | } 64 | } 65 | It 'All results have the state specified via the State parameter' { 66 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.AuthorSnover } 67 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.AuthorSnover.Content } 68 | $StateTest = Find-GitHubIssue -Type issue -Assignee 'lzybkr' 69 | 70 | Foreach ( $Result in $StateTest ) { 71 | $Result.state | Should Be 'closed' 72 | } 73 | } 74 | It 'All results have the label specified via the Labels parameter' { 75 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.AuthorSnover } 76 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.AuthorSnover.Content } 77 | $LabelTest = Find-GitHubIssue -Type issue -Labels 'Area-Engine' 78 | 79 | Foreach ( $Result in $LabelTest ) { 80 | $Result.labels.name -join ' ' | Should Match 'Area-Engine' 81 | } 82 | } 83 | It 'All results have the metadata field specified via the No parameter empty' { 84 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.NoAssignee } 85 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.NoAssignee.Content } 86 | $NoTest = Find-GitHubIssue -Type issue -Repo 'powershell/powershell' -Labels 'Area-Test' -No assignee -State closed 87 | 88 | Foreach ( $Result in $NoTest ) { 89 | $Result.assignees | Should BeNullOrEmpty 90 | } 91 | } 92 | } 93 | Context 'Sorting of search results' { 94 | It 'When the SortBy value is "comments", any result has more comments than the next one' { 95 | Mock Invoke-WebRequest { $Mocks.'Invoke-WebRequest'.NoAssignee } 96 | Mock ConvertFrom-Json { $Mocks.'Invoke-WebRequest'.NoAssignee.Content } 97 | $SortByTest = Find-GitHubIssue -Type 'issue' -Labels 'Area-Test' -SortBy comments 98 | 99 | $SortByTest[0].comments | Should BeGreaterThan $SortByTest[1].comments 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PSGithubSearch.build.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules 'InvokeBuild' 2 | 3 | # Importing all build settings into the current scope 4 | . '.\PSGithubSearch.BuildSettings.ps1' 5 | 6 | Function Write-TaskBanner ( [string]$TaskName ) { 7 | "`n" + ('-' * 79) + "`n" + "`t`t`t $($TaskName.ToUpper()) `n" + ('-' * 79) + "`n" 8 | } 9 | 10 | task Clean { 11 | Write-TaskBanner -TaskName $Task.Name 12 | 13 | If (Test-Path -Path $Settings.BuildOutput) { 14 | "Removing existing files and folders in $($Settings.BuildOutput)\" 15 | Get-ChildItem $Settings.BuildOutput | Remove-Item -Force -Recurse 16 | } 17 | Else { 18 | "$($Settings.BuildOutput) is not present, nothing to clean up." 19 | $Null = New-Item -ItemType Directory -Path $Settings.BuildOutput 20 | } 21 | } 22 | 23 | task Install_Dependencies { 24 | Write-TaskBanner -TaskName $Task.Name 25 | 26 | Foreach ( $Depend in $Settings.Dependency ) { 27 | "Installing build dependency : $Depend" 28 | If ( $Depend -eq 'Selenium.WebDriver' ) { 29 | Install-Package $Depend -Source nuget.org -Force 30 | } 31 | Else { 32 | Install-Module $Depend -Scope CurrentUser -Force -SkipPublisherCheck 33 | Import-Module $Depend -Force 34 | } 35 | } 36 | } 37 | 38 | task Unit_Tests { 39 | Write-TaskBanner -TaskName $Task.Name 40 | 41 | $UnitTestSettings = $Settings.UnitTestParams 42 | $Script:UnitTestsResult = Invoke-Pester @UnitTestSettings 43 | } 44 | 45 | task Fail_If_Failed_Unit_Test { 46 | Write-TaskBanner -TaskName $Task.Name 47 | 48 | $FailureMessage = '{0} Unit test(s) failed. Aborting build' -f $UnitTestsResult.FailedCount 49 | assert ($UnitTestsResult.FailedCount -eq 0) $FailureMessage 50 | } 51 | 52 | task Publish_Unit_Tests_Coverage { 53 | Write-TaskBanner -TaskName $Task.Name 54 | 55 | $Coverage = Format-Coverage -PesterResults $UnitTestsResult -CoverallsApiToken $Settings.CoverallsKey -BranchName $Settings.Branch 56 | Publish-Coverage -Coverage $Coverage 57 | } 58 | 59 | task Upload_Test_Results_To_AppVeyor { 60 | Write-TaskBanner -TaskName $Task.Name 61 | 62 | $TestResultFiles = (Get-ChildItem -Path $Settings.BuildOutput -Filter '*TestsResult.xml').FullName 63 | Foreach ( $TestResultFile in $TestResultFiles ) { 64 | "Uploading test result file : $TestResultFile" 65 | (New-Object 'System.Net.WebClient').UploadFile($Settings.TestUploadUrl, $TestResultFile) 66 | } 67 | } 68 | 69 | task Test Unit_Tests, 70 | Fail_If_Failed_Unit_Test, 71 | Publish_Unit_Tests_Coverage, 72 | Upload_Test_Results_To_AppVeyor 73 | 74 | task Generate_Quality_Report { 75 | Write-TaskBanner -TaskName $Task.Name 76 | 77 | $CodeHealthSettings = $Settings.CodeHealthParams 78 | $Script:HealthReport = Invoke-PSCodeHealth @CodeHealthSettings -TestsResult $UnitTestsResult 79 | } 80 | 81 | task Fail_If_Quality_Goal_Not_Met { 82 | Write-TaskBanner -TaskName $Task.Name 83 | 84 | Add-AppveyorTest -Name 'Quality Gate' -Outcome Running 85 | $QualityGateSettings = $Settings.QualityGateParams 86 | $Compliance = Test-PSCodeHealthCompliance @QualityGateSettings -HealthReport $HealthReport 87 | $Compliance | Select-Object 'MetricName','Value','Result' 88 | $FailedRules = $Compliance | Where-Object Result -eq 'Fail' 89 | 90 | If ( $FailedRules ) { 91 | $FailedString = $FailedRules | Select-Object 'MetricName','Value','Result' | Out-String 92 | $WarningMessage = "Failed compliance rules : `n" + $FailedString 93 | Write-Warning $WarningMessage 94 | $ErrorMessage = "Project's code didn't pass the quality gate. Aborting build" 95 | Update-AppveyorTest -Name 'Quality Gate' -Outcome Failed -ErrorMessage $ErrorMessage 96 | Throw $ErrorMessage 97 | } 98 | Else { 99 | Update-AppveyorTest -Name 'Quality Gate' -Outcome Passed 100 | } 101 | } 102 | 103 | task Set_Module_Version { 104 | Write-TaskBanner -TaskName $Task.Name 105 | 106 | $ManifestContent = Get-Content -Path $Settings.ManifestPath 107 | $CurrentVersion = $Settings.VersionRegex.Match($ManifestContent).Groups['ModuleVersion'].Value 108 | "Current module version in the manifest : $CurrentVersion" 109 | 110 | $ManifestContent -replace $CurrentVersion,$Settings.Version | Set-Content -Path $Settings.ManifestPath -Force 111 | $NewManifestContent = Get-Content -Path $Settings.ManifestPath 112 | $NewVersion = $Settings.VersionRegex.Match($NewManifestContent).Groups['ModuleVersion'].Value 113 | "Updated module version in the manifest : $NewVersion" 114 | 115 | If ( $NewVersion -ne $Settings.Version ) { 116 | Throw "Module version was not updated correctly to $($Settings.Version) in the manifest." 117 | } 118 | } 119 | 120 | task Push_Build_Changes_To_Repo { 121 | Write-TaskBanner -TaskName $Task.Name 122 | 123 | cmd /c "git config --global credential.helper store 2>&1" 124 | Add-Content "$env:USERPROFILE\.git-credentials" "https://$($Settings.GitHubKey):x-oauth-basic@github.com`n" 125 | cmd /c "git config --global user.email ""$($Settings.Email)"" 2>&1" 126 | cmd /c "git config --global user.name ""$($Settings.Name)"" 2>&1" 127 | cmd /c "git config --global core.autocrlf true 2>&1" 128 | cmd /c "git checkout $($Settings.Branch) 2>&1" 129 | cmd /c "git add -A 2>&1" 130 | cmd /c "git commit -m ""Commit build changes [ci skip]"" 2>&1" 131 | cmd /c "git status 2>&1" 132 | cmd /c "git push origin $($Settings.Branch) 2>&1" 133 | } 134 | 135 | task Copy_Source_To_Build_Output { 136 | Write-TaskBanner -TaskName $Task.Name 137 | 138 | "Copying the source folder [$($Settings.SourceFolder)] into the build output folder : [$($Settings.BuildOutput)]" 139 | Copy-Item -Path $Settings.SourceFolder -Destination $Settings.BuildOutput -Recurse 140 | } 141 | 142 | # Default task : 143 | task . Clean, 144 | Install_Dependencies, 145 | Test, 146 | Generate_Quality_Report, 147 | Fail_If_Quality_Goal_Not_Met, 148 | Set_Module_Version, 149 | Push_Build_Changes_To_Repo, 150 | Copy_Source_To_Build_Output -------------------------------------------------------------------------------- /PSGithubSearch/PSGithubSearch.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PSGithubSearch.Repository 6 | 7 | PSGithubSearch.Repository 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | name 16 | 17 | 18 | 19 | full_name 20 | 21 | 22 | 23 | $_.owner.login 24 | 25 | 26 | 27 | html_url 28 | 29 | 30 | 31 | description 32 | 33 | 34 | 35 | fork 36 | 37 | 38 | 39 | updated_at 40 | 41 | 42 | 43 | pushed_at 44 | 45 | 46 | 47 | clone_url 48 | 49 | 50 | 51 | size 52 | 53 | 54 | 55 | stargazers_count 56 | 57 | 58 | 59 | language 60 | 61 | 62 | 63 | forks 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | PSGithubSearch.Code 72 | 73 | PSGithubSearch.Code 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | name 82 | 83 | 84 | 85 | path 86 | 87 | 88 | 89 | html_url 90 | 91 | 92 | 93 | url 94 | 95 | 96 | 97 | $_.repository.full_name 98 | 99 | 100 | 101 | $_.repository.description 102 | 103 | 104 | 105 | $_.repository.html_url 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | PSGithubSearch.IssueOrPR 114 | 115 | PSGithubSearch.IssueOrPR 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | title 124 | 125 | 126 | 127 | number 128 | 129 | 130 | 131 | id 132 | 133 | 134 | 135 | html_url 136 | 137 | 138 | 139 | $_.user.login 140 | 141 | 142 | 143 | $_.labels.name 144 | 145 | 146 | 147 | state 148 | 149 | 150 | 151 | $_.assignees.login 152 | 153 | 154 | 155 | comment 156 | 157 | 158 | 159 | created_at 160 | 161 | 162 | 163 | updated_at 164 | 165 | 166 | 167 | closed_at 168 | 169 | 170 | 171 | body 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | PSGithubSearch.User 180 | 181 | PSGithubSearch.User 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | login 190 | 191 | 192 | 193 | Full Name 194 | 195 | 196 | 197 | type 198 | 199 | 200 | 201 | html_url 202 | 203 | 204 | 205 | Company 206 | 207 | 208 | 209 | Blog 210 | 211 | 212 | 213 | Location 214 | 215 | 216 | 217 | Email Address 218 | 219 | 220 | 221 | Hireable 222 | 223 | 224 | 225 | Bio 226 | 227 | 228 | 229 | Repos 230 | 231 | 232 | 233 | Gists 234 | 235 | 236 | 237 | Followers 238 | 239 | 240 | 241 | Following 242 | 243 | 244 | 245 | Joined 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /Tests/TestData/MockObjects.json: -------------------------------------------------------------------------------- 1 | { 2 | "Invoke-WebRequest": [ 3 | { 4 | "MathieuBuissonPowerShell": { 5 | "StatusCode": 200, 6 | "StatusDescription": "OK", 7 | "Headers": { 8 | "Link": false, 9 | "X-RateLimit-Remaining": "9" 10 | }, 11 | "Content": { 12 | "items": [ 13 | { 14 | "name": "Powershell-VMware", 15 | "owner": { 16 | "login": "MathieuBuisson" 17 | }, 18 | "description": "Powershell or PowerCLI modules for VMware administration/troubleshooting tasks", 19 | "stargazers_count": 36, 20 | "language": "PowerShell", 21 | "forks_count": 13, 22 | "forks": 13 23 | }, 24 | { 25 | "name": "PSCodeHealth", 26 | "owner": { 27 | "login": "MathieuBuisson" 28 | }, 29 | "description": "PowerShell module gathering PowerShell code quality and maintainability metrics", 30 | "stargazers_count": 6, 31 | "language": "PowerShell", 32 | "forks_count": 0, 33 | "forks": 0 34 | }, 35 | { 36 | "name": "PSGithubSearch", 37 | "owner": { 38 | "login": "MathieuBuisson" 39 | }, 40 | "description": "Powershell or PowerCLI modules for VMware administration/troubleshooting tasks", 41 | "stargazers_count": 4, 42 | "language": "PowerShell", 43 | "forks_count": 3, 44 | "forks": 3 45 | } 46 | ] 47 | } 48 | } 49 | }, 50 | { 51 | "PowerShellInName": { 52 | "StatusCode": 200, 53 | "StatusDescription": "OK", 54 | "Headers": { 55 | "Link": false, 56 | "X-RateLimit-Remaining": "9" 57 | }, 58 | "Content": { 59 | "items": [ 60 | { 61 | "name": "Powershell-VMware", 62 | "owner": { 63 | "login": "MathieuBuisson" 64 | }, 65 | "description": "Powershell or PowerCLI modules for VMware administration/troubleshooting tasks", 66 | "stargazers_count": 36, 67 | "language": "PowerShell", 68 | "forks_count": 13, 69 | "forks": 13 70 | }, 71 | { 72 | "name": "Powershell-Utility", 73 | "owner": { 74 | "login": "MathieuBuisson" 75 | }, 76 | "description": "Various generic tools (scripts or modules) which can be reused from other scripts or modules", 77 | "stargazers_count": 7, 78 | "language": "PowerShell", 79 | "forks_count": 3, 80 | "forks": 3 81 | }, 82 | { 83 | "name": "PowerShell-DevOps", 84 | "owner": { 85 | "login": "MathieuBuisson" 86 | }, 87 | "description": "PowerShell scripts or modules for purposes related to DevOps practices", 88 | "stargazers_count": 5, 89 | "language": "PowerShell", 90 | "forks_count": 1, 91 | "forks": 1 92 | } 93 | ] 94 | } 95 | } 96 | }, 97 | { 98 | "SizeGreaterThan100": { 99 | "StatusCode": 200, 100 | "StatusDescription": "OK", 101 | "Headers": { 102 | "Link": false, 103 | "X-RateLimit-Remaining": "9" 104 | }, 105 | "Content": { 106 | "items": [ 107 | { 108 | "name": "Powershell-VMware", 109 | "owner": { 110 | "login": "MathieuBuisson" 111 | }, 112 | "size": 102 113 | }, 114 | { 115 | "name": "PSCodeHealth", 116 | "owner": { 117 | "login": "MathieuBuisson" 118 | }, 119 | "size": 788 120 | } 121 | ] 122 | } 123 | } 124 | }, 125 | { 126 | "NoFork": { 127 | "StatusCode": 200, 128 | "StatusDescription": "OK", 129 | "Headers": { 130 | "Link": false, 131 | "X-RateLimit-Remaining": "9" 132 | }, 133 | "Content": { 134 | "items": [ 135 | { 136 | "name": "Powershell-VMware", 137 | "owner": { 138 | "login": "MathieuBuisson" 139 | }, 140 | "fork": false 141 | }, 142 | { 143 | "name": "PSCodeHealth", 144 | "owner": { 145 | "login": "MathieuBuisson" 146 | }, 147 | "fork": false 148 | } 149 | ] 150 | } 151 | } 152 | }, 153 | { 154 | "ForksOnly": { 155 | "StatusCode": 200, 156 | "StatusDescription": "OK", 157 | "Headers": { 158 | "Link": false, 159 | "X-RateLimit-Remaining": "9" 160 | }, 161 | "Content": { 162 | "items": [ 163 | { 164 | "name": "Powershell-VMware", 165 | "owner": { 166 | "login": "MathieuBuisson" 167 | }, 168 | "fork": true 169 | }, 170 | { 171 | "name": "PSCodeHealth", 172 | "owner": { 173 | "login": "MathieuBuisson" 174 | }, 175 | "fork": true 176 | } 177 | ] 178 | } 179 | } 180 | }, 181 | { 182 | "MoreThan20Stars": { 183 | "StatusCode": 200, 184 | "StatusDescription": "OK", 185 | "Headers": { 186 | "Link": false, 187 | "X-RateLimit-Remaining": "9" 188 | }, 189 | "Content": { 190 | "items": [ 191 | { 192 | "name": "Powershell-VMware", 193 | "owner": { 194 | "login": "MathieuBuisson" 195 | }, 196 | "stargazers_count": 36 197 | }, 198 | { 199 | "name": "PSCodeHealth", 200 | "owner": { 201 | "login": "MathieuBuisson" 202 | }, 203 | "stargazers_count": 22 204 | } 205 | ] 206 | } 207 | } 208 | }, 209 | { 210 | "SortByForks": { 211 | "StatusCode": 200, 212 | "StatusDescription": "OK", 213 | "Headers": { 214 | "Link": false, 215 | "X-RateLimit-Remaining": "9" 216 | }, 217 | "Content": { 218 | "items": [ 219 | { 220 | "name": "Powershell-VMware", 221 | "owner": { 222 | "login": "MathieuBuisson" 223 | }, 224 | "forks": 7 225 | }, 226 | { 227 | "name": "PSCodeHealth", 228 | "owner": { 229 | "login": "MathieuBuisson" 230 | }, 231 | "forks": 5 232 | } 233 | ] 234 | } 235 | } 236 | }, 237 | { 238 | "ParameterSet": { 239 | "StatusCode": 200, 240 | "StatusDescription": "OK", 241 | "Headers": { 242 | "Link": false, 243 | "X-RateLimit-Remaining": "9" 244 | }, 245 | "Content": { 246 | "items": [ 247 | { 248 | "name": "Update-ChocolateyPackage.psm1", 249 | "repository": { 250 | "name": "Powershell-Utility", 251 | "owner": { 252 | "login": "MathieuBuisson" 253 | }, 254 | "description": "Various generic tools (scripts or modules) which can be reused from other scripts or modules" 255 | } 256 | }, 257 | { 258 | "name": "Restore-DfsrSysvol.psm1", 259 | "repository": { 260 | "name": "Powershell-Administration", 261 | "owner": { 262 | "login": "MathieuBuisson" 263 | }, 264 | "description": "Tools (scripts or modules) for Windows or Windows Server administration tasks" 265 | } 266 | } 267 | ] 268 | } 269 | } 270 | }, 271 | { 272 | "DeploymentInPath": { 273 | "StatusCode": 200, 274 | "StatusDescription": "OK", 275 | "Headers": { 276 | "Link": false, 277 | "X-RateLimit-Remaining": "9" 278 | }, 279 | "Content": { 280 | "items": [ 281 | { 282 | "name": "DeploymentReadinessChecker.psd1", 283 | "path": "DeploymentReadinessChecker.psd1", 284 | "repository": { 285 | "name": "DeploymentReadinessChecker", 286 | "owner": { 287 | "login": "MathieuBuisson" 288 | }, 289 | "description": "Pester-based tool to validate that a (potentially large) number of machines meet the prerequisites for a software deployment/upgrade." 290 | } 291 | }, 292 | { 293 | "name": "DeploymentReadinessChecker.psm1", 294 | "path": "DeploymentReadinessChecker.psm1", 295 | "repository": { 296 | "name": "DeploymentReadinessChecker", 297 | "owner": { 298 | "login": "MathieuBuisson" 299 | }, 300 | "description": "Pester-based tool to validate that a (potentially large) number of machines meet the prerequisites for a software deployment/upgrade." 301 | } 302 | } 303 | ] 304 | } 305 | } 306 | }, 307 | { 308 | "MultiKeywords": { 309 | "StatusCode": 200, 310 | "StatusDescription": "OK", 311 | "Headers": { 312 | "Link": false, 313 | "X-RateLimit-Remaining": "9" 314 | }, 315 | "Content": { 316 | "items": [ 317 | { 318 | "name": "ServerDeployment.Tests.ps1", 319 | "path": "ValidationScripts/ServerDeployment.Tests.ps1", 320 | "repository": { 321 | "name": "DeploymentReadinessChecker", 322 | "full_name": "MathieuBuisson/DeploymentReadinessChecker", 323 | "owner": { 324 | "login": "MathieuBuisson" 325 | } 326 | } 327 | }, 328 | { 329 | "name": "ClientDeployment.Tests.ps1", 330 | "path": "ValidationScripts/ClientDeployment.Tests.ps1", 331 | "repository": { 332 | "name": "DeploymentReadinessChecker", 333 | "full_name": "MathieuBuisson/DeploymentReadinessChecker", 334 | "owner": { 335 | "login": "MathieuBuisson" 336 | } 337 | } 338 | } 339 | ] 340 | } 341 | } 342 | }, 343 | { 344 | "IssueKeywords": { 345 | "StatusCode": 200, 346 | "StatusDescription": "OK", 347 | "Headers": { 348 | "Link": false, 349 | "X-RateLimit-Remaining": "9" 350 | }, 351 | "Content": { 352 | "items": [ 353 | { 354 | "title": "Importing modules by name is case sensitive", 355 | "body": "Expect to be able to re-import PSReadline but Error : reporting module cannot be found", 356 | "user": { 357 | "login": "jdhitsolutions" 358 | } 359 | }, 360 | { 361 | "title": "ConvertFrom-Json does not handles same keys with different case (but JSON is case sensitive)", 362 | "body": "Error : Cannot convert the JSON string because a dictionary that was converted from the string contains the duplicated keys 'firstName' and 'Firstname'", 363 | "user": { 364 | "login": "uxone" 365 | } 366 | } 367 | ] 368 | } 369 | } 370 | }, 371 | { 372 | "AuthorSnover": { 373 | "StatusCode": 200, 374 | "StatusDescription": "OK", 375 | "Headers": { 376 | "Link": false, 377 | "X-RateLimit-Remaining": "9" 378 | }, 379 | "Content": { 380 | "items": [ 381 | { 382 | "title": "$ENV:PSMODULEPATH is not set correctly for remoting", 383 | "body": "And that is why I can't access the bulk of the WIndows cmdlets", 384 | "user": { 385 | "login": "jpsnover" 386 | }, 387 | "assignees": { 388 | "login": "lzybkr" 389 | }, 390 | "state": "closed", 391 | "labels": [ 392 | { 393 | "name": "Area-Cmdlets-Management" 394 | }, 395 | { 396 | "name": "Area-Engine" 397 | } 398 | ] 399 | }, 400 | { 401 | "title": "Invoke-Item -Path does not work", 402 | "body": "Access is denied", 403 | "user": { 404 | "login": "jpsnover" 405 | }, 406 | "assignees": { 407 | "login": "lzybkr" 408 | }, 409 | "state": "closed", 410 | "labels": [ 411 | { 412 | "name": "Issue-Bug" 413 | }, 414 | { 415 | "name": "Area-Engine" 416 | } 417 | ] 418 | } 419 | ] 420 | } 421 | } 422 | }, 423 | { 424 | "TypePR": { 425 | "StatusCode": 200, 426 | "StatusDescription": "OK", 427 | "Headers": { 428 | "Link": false, 429 | "X-RateLimit-Remaining": "9" 430 | }, 431 | "Content": { 432 | "items": [ 433 | { 434 | "title": "improved suport for pipeline processing with begin block", 435 | "pull_request": "422", 436 | "number": 422, 437 | "body": "This fixes scenarios where a mocked function is an advanced function and initializes state in a `Begin` block", 438 | "user": { 439 | "login": "mwrock" 440 | } 441 | }, 442 | { 443 | "title": "fix dynamic parameter mocks", 444 | "pull_request": "419", 445 | "number": 419, 446 | "body": "This PR filters any dynamic parameters up front out of the initial metadata which should keep compatibility with both post 10565 and builds prior", 447 | "user": { 448 | "login": "mwrock" 449 | } 450 | } 451 | ] 452 | } 453 | } 454 | }, 455 | { 456 | "NoAssignee": { 457 | "StatusCode": 200, 458 | "StatusDescription": "OK", 459 | "Headers": { 460 | "Link": false, 461 | "X-RateLimit-Remaining": "9" 462 | }, 463 | "Content": { 464 | "items": [ 465 | { 466 | "title": "WindowsInstaller.Tests.ps1 failed on debian", 467 | "body": "Warning: This site requires the use of scripts, which your browser does not currently allow.See how to enable scripts", 468 | "user": { 469 | "login": "JamesWTruher" 470 | }, 471 | "assignees": null, 472 | "comments": 3 473 | }, 474 | { 475 | "title": "Building on macOS fails as of 22 Jul 2017", 476 | "body": "Error : The term 'Register-PackageSource' is not recognized as the name of a cmdlet, function, script file, or operable program.", 477 | "user": { 478 | "login": "mklement0" 479 | }, 480 | "assignees": null, 481 | "comments": 2 482 | } 483 | ] 484 | } 485 | } 486 | }, 487 | { 488 | "RamblingInLogin": { 489 | "StatusCode": 200, 490 | "StatusDescription": "OK", 491 | "Headers": { 492 | "Link": false, 493 | "X-RateLimit-Remaining": "9" 494 | }, 495 | "Content": { 496 | "items": [ 497 | { 498 | "login": "RamblingCookieMonster", 499 | "type": "user", 500 | "url": "https://api.github.com/users/RamblingCookieMonster" 501 | }, 502 | { 503 | "login": "rambling", 504 | "type": "user", 505 | "url": "https://api.github.com/users/rambling" 506 | } 507 | ] 508 | } 509 | } 510 | } 511 | ] 512 | } -------------------------------------------------------------------------------- /PSGithubSearch/PSGithubSearch.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 4 2 | 3 | Function Find-GitHubRepository { 4 | <# 5 | .SYNOPSIS 6 | Searches repositories on GitHub.com based on the specified search keyword(s) and parameters. 7 | 8 | .DESCRIPTION 9 | Uses the GitHub search API to find repositories on GitHub.com based on the specified search keyword(s) and parameters. 10 | 11 | This API's documentation is available here : https://developer.github.com/v3/search/ 12 | 13 | NOTE : The GitHub Search API limits each search to 1,000 results and the number of requests to 10 per minute. 14 | 15 | .PARAMETER Keywords 16 | One or more keywords to search. 17 | 18 | .PARAMETER Language 19 | To search repositories based on the language they are written in. 20 | 21 | .PARAMETER In 22 | To qualify which field is searched. With this qualifier you can restrict the search to just the repository name, description, or readme. 23 | If not specified, the default behaviour is to search in both the name, description and readme. 24 | 25 | .PARAMETER SizeKB 26 | To search repositories that match a certain size (in kilobytes). 27 | 28 | .PARAMETER Fork 29 | Filters whether forked repositories should be included ( if the value is 'true' ) or only forked repositories should be returned ( if the value is 'only' ). 30 | If this parameter is not specified, the default behaviour is to exclude forks. 31 | 32 | .PARAMETER User 33 | To Limit searches to repositories owned by a specific user. 34 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 35 | 36 | .PARAMETER Stars 37 | To filter repositories based on the number of stars. 38 | 39 | .PARAMETER SortBy 40 | To specify on which field the results should be sorted on : number of stars, forks or last updated date. 41 | By default, the results are sorted by best match. 42 | 43 | .EXAMPLE 44 | Find-GitHubRepository -Keywords 'TCP' -Language 'Python' 45 | 46 | Searches GitHub repositories which have the word "TCP" in their name or description or readme and are written in Python. 47 | 48 | .EXAMPLE 49 | Find-GitHubRepository -Keywords 'TCP' -Language 'Python' -In name 50 | 51 | Searches GitHub repositories which have the word "TCP" in their name and are written in Python. 52 | 53 | .EXAMPLE 54 | Find-GitHubRepository -Keywords 'UDP' -In description -SizeKB '>200' 55 | 56 | Searches GitHub repositories which have the word "UDP" in their description and are larger than 200 kilobytes. 57 | 58 | .EXAMPLE 59 | Find-GitHubRepository -Keywords 'PowerShell-Docs' -In name -Fork only 60 | 61 | Searches forks which have the word "PowerShell-Docs" in their name. 62 | 63 | .EXAMPLE 64 | Find-GitHubRepository -Keywords 'script' -User 'MathieuBuisson' 65 | 66 | Searches GitHub repositories which have the word "script" in their name or description or readme and are owned by the user MathieuBuisson. 67 | 68 | .EXAMPLE 69 | Find-GitHubRepository -Keywords 'disk','space' -In readme -Stars '>=10' 70 | 71 | Searches GitHub repositories which have both words "disk" and "space" in their readme and have 10 or more stars. 72 | 73 | .EXAMPLE 74 | Find-GitHubRepository -SortBy stars -In name -Language 'PowerShell' -Keywords 'Pester' 75 | 76 | Searches GitHub repositories written in PowerShell which have the word "Pester" in their name and sorts them by the number of stars (descending order). 77 | 78 | .NOTES 79 | Author : Mathieu Buisson 80 | 81 | .LINK 82 | https://github.com/MathieuBuisson/PSGithubSearch 83 | #> 84 | [CmdletBinding()] 85 | 86 | Param( 87 | [Parameter(Mandatory=$True,Position=0)] 88 | [string[]]$Keywords, 89 | 90 | [Parameter(Position=1)] 91 | [string]$Language, 92 | 93 | [Parameter(Position=2)] 94 | [ValidateSet('name','description','readme')] 95 | [string]$In, 96 | 97 | [Parameter(Position=3)] 98 | [ValidatePattern('^[\d<>][=\d]\d*$')] 99 | [string]$SizeKB, 100 | 101 | [Parameter(Position=4)] 102 | [ValidateSet('true','only')] 103 | [string]$Fork, 104 | 105 | [Parameter(Position=5)] 106 | [string]$User, 107 | 108 | [Parameter(Position=6)] 109 | [ValidatePattern('^[\d<>][=\d]\d*$')] 110 | [string]$Stars, 111 | 112 | [Parameter(Position=7)] 113 | [ValidateSet('stars','forks','updated')] 114 | [string]$SortBy 115 | ) 116 | 117 | [string]$KeywordsString = $Keywords -join '+' 118 | [string]$QueryString = 'q=' + $KeywordsString 119 | 120 | If ( $Language ) { 121 | $QueryString += '+language:' + $Language 122 | } 123 | If ( $In ) { 124 | $QueryString += '+in:' + $In 125 | } 126 | If ( $SizeKB ) { 127 | $QueryString += '+size:' + $SizeKB 128 | } 129 | If ( $Fork ) { 130 | $QueryString += '+fork:' + $Fork 131 | } 132 | If ( $User ) { 133 | $QueryString += '+user:' + $User 134 | } 135 | If ( $Stars ) { 136 | $QueryString += '+stars:' + $Stars 137 | } 138 | If ( $SortBy ) { 139 | $QueryString += "&sort=$SortBy" 140 | } 141 | 142 | # Using the maximum number of results per page to limit the number of requests 143 | $QueryString += '&per_page=100' 144 | 145 | $UriBuilder = New-Object System.UriBuilder -ArgumentList 'https://api.github.com' 146 | $UriBuilder.Path = 'search/repositories' -as [uri] 147 | $UriBuilder.Query = $QueryString 148 | 149 | $BaseUri = $UriBuilder.Uri 150 | Write-Verbose "Constructed base URI : $($BaseUri.AbsoluteUri)" 151 | 152 | $Response = Invoke-WebRequest -Uri $BaseUri 153 | If ( $Response.StatusCode -ne 200 ) { 154 | 155 | Write-Warning "The status code was $($Response.StatusCode) : $($Response.StatusDescription)" 156 | } 157 | $NumberOfPages = Get-NumberofPage -SearchResult $Response 158 | Write-Verbose "Number of pages for this search result : $($NumberOfPages)" 159 | 160 | Foreach ( $PageNumber in 1..$NumberOfPages ) { 161 | 162 | $ResultPageUri = $BaseUri.AbsoluteUri + "&page=$($PageNumber.ToString())" 163 | 164 | Try { 165 | $PageResponse = Invoke-WebRequest -Uri $ResultPageUri -ErrorAction Stop 166 | } 167 | Catch { 168 | Throw $_.Exception.Message 169 | } 170 | 171 | # The search API limits the number of requests to 10 requests per minute and per IP address (for unauthenticated requests) 172 | # We might be subject to the limit on the number of requests if we run function multiple times in the last minute 173 | $RemainingRequestsNumber = $PageResponse.Headers.'X-RateLimit-Remaining' -as [int] 174 | Write-Verbose "Number of remaining API requests : $($RemainingRequestsNumber)." 175 | 176 | If ( $RemainingRequestsNumber -le 1 ) { 177 | Write-Warning "The search API limits the number of requests to 10 requests per minute" 178 | Write-Warning "Waiting 60 seconds before processing the remaining result pages because we have exceeded this limit." 179 | Start-Sleep -Seconds 60 180 | } 181 | $PageResponseContent = $PageResponse.Content | ConvertFrom-Json 182 | 183 | Foreach ( $PageResult in $PageResponseContent.items ) { 184 | 185 | $PageResult.psobject.TypeNames.Insert(0,'PSGithubSearch.Repository') 186 | $PageResult 187 | } 188 | } 189 | } 190 | 191 | Function Find-GitHubCode { 192 | <# 193 | .SYNOPSIS 194 | Searches file contents on GitHub.com based on the specified search keyword(s) and parameters. 195 | 196 | .DESCRIPTION 197 | Uses the GitHub search API to find code in files on GitHub.com based on the specified search keyword(s) and parameters. 198 | 199 | This API's documentation is available here : https://developer.github.com/v3/search/ 200 | 201 | NOTE : Due to the complexity of searching code, the GitHub Search API has a few restrictions on how searches are performed : 202 | - Only the default branch is considered. In most cases, this will be the master branch. 203 | - Only files smaller than 384 KB are searchable. 204 | - Only repositories with fewer than 500,000 files are searchable. 205 | 206 | .PARAMETER Keywords 207 | One or more keywords or code snippet to search. 208 | 209 | .PARAMETER User 210 | To Limit searches to code owned by a specific user. 211 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 212 | 213 | .PARAMETER Repo 214 | To Limit searches to code in a specific repository. 215 | The value of this parameter must match exactly with the full name of an existing GitHub repository, formatted as : Username/RepoName. 216 | 217 | .PARAMETER Language 218 | To search code based on the language it is written in. 219 | 220 | .PARAMETER In 221 | To qualify which field is searched. With this qualifier you can restrict the search to files contents or to files paths. 222 | If not specified, the default behaviour is to search in both files contents and paths. 223 | 224 | .PARAMETER SizeBytes 225 | To search only files that match a certain size (in bytes). 226 | 227 | .PARAMETER Fork 228 | Filters whether forked repositories should be included in the search. 229 | By default, forks are not searched unless the fork has more stars than the parent repository. 230 | 231 | .PARAMETER FileName 232 | Filters the search to files with a name containing the string specified via this parameter. 233 | 234 | .PARAMETER Extension 235 | Filters the search to files with the specified extension. 236 | 237 | .PARAMETER SortByLastIndexed 238 | Sorts the results by how recently a file has been indexed by the GitHub search infrastructure. 239 | By default, the results are sorted by best match. 240 | 241 | .EXAMPLE 242 | Find-GitHubCode -User 'MathieuBuisson' -Keywords 'SupportsShouldProcess' -Extension 'psm1' 243 | 244 | Finds the .psm1 files which contain the code 'SupportsShouldProcess' in repositories from the user 'MathieuBuisson'. 245 | 246 | .NOTES 247 | Author : Mathieu Buisson 248 | 249 | .LINK 250 | https://github.com/MathieuBuisson/PSGithubSearch 251 | #> 252 | [CmdletBinding(DefaultParameterSetName='User')] 253 | 254 | Param( 255 | [Parameter(Mandatory=$True,Position=0)] 256 | [string[]]$Keywords, 257 | 258 | [Parameter(Position=1,ParameterSetName='User')] 259 | [string]$User, 260 | 261 | [Parameter(Position=1,ParameterSetName='Repo')] 262 | [ValidatePattern('^[a-zA-Z]+/[a-zA-Z]+')] 263 | [string]$Repo, 264 | 265 | [Parameter(Position=2)] 266 | [string]$Language, 267 | 268 | [Parameter(Position=3)] 269 | [ValidateSet('file','path')] 270 | [string]$In, 271 | 272 | [Parameter(Position=4)] 273 | [ValidatePattern('^[\d<>][=\d]\d*$')] 274 | [string]$SizeBytes, 275 | 276 | [Parameter(Position=5)] 277 | [switch]$Fork, 278 | 279 | [Parameter(Position=6)] 280 | [string]$FileName, 281 | 282 | [Parameter(Position=7)] 283 | [string]$Extension, 284 | 285 | [Parameter(Position=8)] 286 | [switch]$SortByLastIndexed 287 | ) 288 | 289 | # Cleaning up the value of $Extension if the user puts a "dot" at the beginning 290 | If ( $Extension -match '^\.' ) { 291 | $Extension = $Extension.TrimStart('.') 292 | } 293 | 294 | [string]$KeywordsString = $Keywords -join '+' 295 | [string]$QueryString = 'q=' + $KeywordsString 296 | 297 | If ( $Language ) { 298 | $QueryString += '+language:' + $Language 299 | } 300 | If ( $In ) { 301 | $QueryString += '+in:' + $In 302 | } 303 | If ( $SizeBytes ) { 304 | $QueryString += '+size:' + $SizeBytes 305 | } 306 | If ( $Fork ) { 307 | $QueryString += '+fork:true' 308 | } 309 | If ( $User ) { 310 | $QueryString += '+user:' + $User 311 | } 312 | If ( $Repo ) { 313 | $QueryString += '+repo:' + $Repo 314 | } 315 | If ( $FileName ) { 316 | $QueryString += '+filename:' + $FileName 317 | } 318 | If ( $Extension ) { 319 | $QueryString += '+extension:' + $Extension 320 | } 321 | If ( $SortByLastIndexed ) { 322 | $QueryString += "&sort=indexed" 323 | } 324 | 325 | # Using the maximum number of results per page to limit the number of requests 326 | $QueryString += '&per_page=100' 327 | 328 | $UriBuilder = New-Object System.UriBuilder -ArgumentList 'https://api.github.com' 329 | $UriBuilder.Path = 'search/code' -as [uri] 330 | $UriBuilder.Query = $QueryString 331 | 332 | $BaseUri = $UriBuilder.Uri 333 | Write-Verbose "Constructed base URI : $($BaseUri.AbsoluteUri)" 334 | 335 | $Response = Invoke-WebRequest -Uri $BaseUri 336 | If ( $Response.StatusCode -ne 200 ) { 337 | 338 | Write-Warning "The status code was $($Response.StatusCode) : $($Response.StatusDescription)" 339 | } 340 | $NumberOfPages = Get-NumberofPage -SearchResult $Response 341 | Write-Verbose "Number of pages for this search result : $($NumberOfPages)" 342 | 343 | Foreach ( $PageNumber in 1..$NumberOfPages ) { 344 | 345 | $ResultPageUri = $BaseUri.AbsoluteUri + "&page=$($PageNumber.ToString())" 346 | 347 | Try { 348 | $PageResponse = Invoke-WebRequest -Uri $ResultPageUri -ErrorAction Stop 349 | } 350 | Catch { 351 | Throw $_.Exception.Message 352 | } 353 | 354 | # The search API limits the number of requests to 10 requests per minute and per IP address (for unauthenticated requests) 355 | # We might be subject to the limit on the number of requests if we run function multiple times in the last minute 356 | $RemainingRequestsNumber = $PageResponse.Headers.'X-RateLimit-Remaining' -as [int] 357 | Write-Verbose "Number of remaining API requests : $($RemainingRequestsNumber)." 358 | 359 | If ( $RemainingRequestsNumber -le 1 ) { 360 | Write-Warning "The search API limits the number of requests to 10 requests per minute" 361 | Write-Warning "Waiting 60 seconds before processing the remaining result pages because we have exceeded this limit." 362 | Start-Sleep -Seconds 60 363 | } 364 | $PageResponseContent = $PageResponse.Content | ConvertFrom-Json 365 | 366 | Foreach ( $PageResult in $PageResponseContent.items ) { 367 | 368 | $PageResult.psobject.TypeNames.Insert(0,'PSGithubSearch.Code') 369 | $PageResult 370 | } 371 | } 372 | } 373 | 374 | Function Find-GitHubIssue { 375 | <# 376 | .SYNOPSIS 377 | Searches issues and pull requests on GitHub.com based on the specified search keyword(s) and parameters. 378 | 379 | .DESCRIPTION 380 | Uses the GitHub search API to find issues and/or pull requests on GitHub.com based on the specified search keyword(s) and parameters. 381 | 382 | This API's documentation is available here : https://developer.github.com/v3/search/ 383 | 384 | .PARAMETER Keywords 385 | To search issues and/or pull requests containing the specified keyword(s) in their title, body or comments. 386 | 387 | .PARAMETER Type 388 | To restrict the search to issues only or pull requests only. 389 | By default, both are searched. 390 | 391 | .PARAMETER In 392 | To qualify which field is searched for the specified keyword(s). With this qualifier you can restrict the search to the title, body or comments of issues and pull requests. 393 | By default, all these fields are searched for the specified keyword(s). 394 | 395 | .PARAMETER Author 396 | To Limit searches to issues or pull requests created by a specific user. 397 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 398 | 399 | .PARAMETER Assignee 400 | To Limit searches to issues or pull requests assigned to a specific user. 401 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 402 | 403 | .PARAMETER Mentions 404 | To Limit searches to issues or pull requests in which a specific user is mentioned. 405 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 406 | 407 | .PARAMETER Commenter 408 | To Limit searches to issues or pull requests in which a specific user commented. 409 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 410 | 411 | .PARAMETER Involves 412 | To Limit searches to issues or pull requests which were either created by a specific user, assigned to that user, mention that user, or were commented on by that user. 413 | If the value of this parameter doesn't match exactly with an existing GitHub user name, it throws an error. 414 | 415 | .PARAMETER State 416 | Filter issues and/or pull requests based on whether they are open or closed. 417 | 418 | .PARAMETER Labels 419 | Filters issues and/or pull requests based on their labels. 420 | Limitation : this doesn't retrieve labels containing spaces or forward slashes. 421 | 422 | If multiple labels are specified, only issues which have all the specified labels are returned. 423 | 424 | .PARAMETER No 425 | To Limit searches to issues or pull requests which are missing certain metadata : label, milestone or assignee. 426 | 427 | .PARAMETER Language 428 | Searches for issues and/or pull requests within repositories that match a certain language. 429 | 430 | .PARAMETER Repo 431 | To Limit searches to issues and/or pull requests within a specific repository. 432 | 433 | .PARAMETER SortBy 434 | To specify on which field the results should be sorted on : number of comments, creation date or last updated date. 435 | By default, the results are sorted by best match. 436 | 437 | .EXAMPLE 438 | $PowershellPR = Find-GitHubIssue -Type pr -Repo 'powershell/powershell' 439 | $PowershellPR | Group-Object -Property { $_.User.login } | Sort-Object -Property Count -Descending | Select-Object -First 10 440 | 441 | Gets the username of the 10 largest contributors to the PowerShell repository, in number of pull requests. 442 | 443 | .NOTES 444 | Author : Mathieu Buisson 445 | 446 | .LINK 447 | https://github.com/MathieuBuisson/PSGithubSearch 448 | #> 449 | [CmdletBinding()] 450 | 451 | Param( 452 | [Parameter(Position=0)] 453 | [string[]]$Keywords, 454 | 455 | [Parameter(Position=1)] 456 | [ValidateSet('issue','pr')] 457 | [string]$Type, 458 | 459 | [Parameter(Position=2)] 460 | [ValidateSet('title','body','comments')] 461 | [string]$In, 462 | 463 | [Parameter(Position=3)] 464 | [string]$Author, 465 | 466 | [Parameter(Position=4)] 467 | [string]$Assignee, 468 | 469 | [Parameter(Position=5)] 470 | [string]$Mentions, 471 | 472 | [Parameter(Position=6)] 473 | [string]$Commenter, 474 | 475 | [Parameter(Position=7)] 476 | [string]$Involves, 477 | 478 | [Parameter(Position=8)] 479 | [ValidateSet('open','closed')] 480 | [string]$State, 481 | 482 | [Parameter(Position=9)] 483 | [string[]]$Labels, 484 | 485 | [Parameter(Position=10)] 486 | [ValidateSet('label','milestone','assignee')] 487 | [string]$No, 488 | 489 | [Parameter(Position=11)] 490 | [string]$Language, 491 | 492 | [Parameter(Position=12)] 493 | [string]$Repo, 494 | 495 | [Parameter(Position=13)] 496 | [ValidateSet('comments','created','updated')] 497 | [string]$SortBy 498 | ) 499 | 500 | If ( $Keywords ) { 501 | $KeywordFilter = $Keywords -join '+' 502 | } 503 | If ( $Labels ) { 504 | $LabelsFilterArray = Foreach ( $Label in $Labels ) { 'label:{0}' -f $Label } 505 | [string]$LabelsFilter = If ( $LabelsFilterArray.Count -gt 0 ) { $LabelsFilterArray -join '+' } 506 | } 507 | 508 | # The remaining filters can be used the same way to build their respective filter strings 509 | If ( $PSBoundParameters.ContainsKey('Keywords') ) { 510 | $Null = $PSBoundParameters.Remove('Keywords') 511 | } 512 | If ( $PSBoundParameters.ContainsKey('Labels') ) { 513 | $Null = $PSBoundParameters.Remove('Labels') 514 | } 515 | If ( $PSBoundParameters.ContainsKey('SortBy') ) { 516 | $Null = $PSBoundParameters.Remove('SortBy') 517 | } 518 | [System.Collections.ArrayList]$JoinableFilterStrings = @() 519 | Foreach ( $ParamName in $PSBoundParameters.Keys ) { 520 | 521 | $JoinableFilterString = '{0}:{1}' -f $ParamName.ToLower(), $PSBoundParameters[$ParamName] 522 | $Null = $JoinableFilterStrings.Add($JoinableFilterString) 523 | } 524 | $JoinedFilterStrings = If ($JoinableFilterStrings.Count -gt 0) {$JoinableFilterStrings -join '+'} 525 | $JoinedFilter = ($KeywordFilter, $JoinedFilterStrings, $LabelsFilter -join '+').Trim('+') 526 | $QueryString = 'q={0}' -f $JoinedFilter 527 | 528 | If ( $SortBy ) { 529 | $QueryString += '&sort={0}' -f $SortBy 530 | } 531 | $QueryString += '&per_page=100' 532 | 533 | $UriBuilder = New-Object System.UriBuilder -ArgumentList 'https://api.github.com' 534 | $UriBuilder.Path = 'search/issues' -as [uri] 535 | $UriBuilder.Query = $QueryString 536 | 537 | $BaseUri = $UriBuilder.Uri 538 | Write-Verbose "Constructed base URI : $($BaseUri.AbsoluteUri)" 539 | 540 | $Response = Invoke-WebRequest -Uri $BaseUri 541 | If ( $Response.StatusCode -ne 200 ) { 542 | 543 | Write-Warning "The status code was $($Response.StatusCode) : $($Response.StatusDescription)" 544 | } 545 | $NumberOfPages = Get-NumberofPage -SearchResult $Response 546 | Write-Verbose "Number of pages for this search result : $($NumberOfPages)" 547 | 548 | Foreach ( $PageNumber in 1..$NumberOfPages ) { 549 | 550 | $ResultPageUri = $BaseUri.AbsoluteUri + "&page=$($PageNumber.ToString())" 551 | 552 | Try { 553 | $PageResponse = Invoke-WebRequest -Uri $ResultPageUri -ErrorAction Stop 554 | } 555 | Catch { 556 | Throw $_.Exception.Message 557 | } 558 | 559 | # The search API limits the number of requests to 10 requests per minute and per IP address (for unauthenticated requests) 560 | # We might be subject to the limit on the number of requests if we run function multiple times in the last minute 561 | $RemainingRequestsNumber = $PageResponse.Headers.'X-RateLimit-Remaining' -as [int] 562 | Write-Verbose "Number of remaining API requests : $($RemainingRequestsNumber)." 563 | 564 | If ( $RemainingRequestsNumber -le 1 ) { 565 | Write-Warning "The search API limits the number of requests to 10 requests per minute" 566 | Write-Warning "Waiting 60 seconds before processing the remaining result pages because we have exceeded this limit." 567 | Start-Sleep -Seconds 60 568 | } 569 | $PageResponseContent = $PageResponse.Content | ConvertFrom-Json 570 | 571 | Foreach ( $PageResult in $PageResponseContent.items ) { 572 | 573 | $PageResult.psobject.TypeNames.Insert(0,'PSGithubSearch.IssueOrPR') 574 | $PageResult 575 | } 576 | } 577 | } 578 | 579 | Function Find-GithubUser { 580 | <# 581 | .SYNOPSIS 582 | Searches users and/or organisations on GitHub.com based on the specified search keyword(s) and parameters. 583 | 584 | .DESCRIPTION 585 | Uses the GitHub search API to find users or organisations on GitHub.com based on the specified search keyword(s) and parameters. 586 | 587 | This API's documentation is available here : https://developer.github.com/v3/search/ 588 | 589 | NOTE : The GitHub Search API limits each search to 1,000 results and the number of requests to 10 per minute. 590 | 591 | .PARAMETER Keywords 592 | One or more keywords to search. 593 | 594 | .PARAMETER Language 595 | To search users who have repositories written in the specified language. 596 | 597 | .PARAMETER In 598 | To qualify which field is searched. With this qualifier you can restrict the search to just the username (login), public email address (email) or full name (fullname). 599 | If not specified, the default behaviour is to search in both the username, full name and public email address.. 600 | 601 | .PARAMETER Type 602 | To restrict the search to personal accounts or organization accounts. 603 | By default, both are searched. 604 | 605 | .PARAMETER Repos 606 | To filter the users or organization based on the number of repositories they have. 607 | 608 | .PARAMETER Location 609 | To filter users or organizations based on the location indicated on their profile. 610 | 611 | .PARAMETER Followers 612 | To filter users or organizations based on the number of followers they have. 613 | 614 | .PARAMETER SortBy 615 | To specify on which field the results should be sorted on : number of followers, number of repositories, or when they joined GitHub. 616 | By default, the results are sorted by best match. 617 | 618 | .EXAMPLE 619 | Find-GithubUser -Type user -Language 'PowerShell' -Location 'Ireland' | Where-Object { $_.Hireable } 620 | 621 | Gets information on GitHub users located in Ireland, who have at least one PowerShell repository and who have indicated on their profile that they are available for hire. 622 | 623 | .NOTES 624 | Author : Mathieu Buisson 625 | 626 | .LINK 627 | https://github.com/MathieuBuisson/PSGithubSearch 628 | #> 629 | [CmdletBinding()] 630 | 631 | Param( 632 | [Parameter(Position=0)] 633 | [string[]]$Keywords, 634 | 635 | [Parameter(Position=1)] 636 | [string]$Language, 637 | 638 | [Parameter(Position=2)] 639 | [ValidateSet('login','email','fullname')] 640 | [string]$In, 641 | 642 | [Parameter(Position=3)] 643 | [ValidateSet('user','org')] 644 | [string]$Type, 645 | 646 | [Parameter(Position=4)] 647 | [ValidatePattern('^[\d<>][=\d]\d*$')] 648 | [string]$Repos, 649 | 650 | [Parameter(Position=5)] 651 | [string]$Location, 652 | 653 | [Parameter(Position=6)] 654 | [ValidatePattern('^[\d<>][=\d]\d*$')] 655 | [string]$Followers, 656 | 657 | [Parameter(Position=7)] 658 | [ValidateSet('followers','repositories','joined')] 659 | [string]$SortBy 660 | ) 661 | 662 | [string]$QueryString = 'q=' 663 | $EmptyQueryString = $True 664 | 665 | If ( $Keywords ) { 666 | $QueryString += ($Keywords -join '+') 667 | $EmptyQueryString = $False 668 | } 669 | If ( $Language ) { 670 | If ( $EmptyQueryString ) { 671 | $QueryString += 'language:' + $Language 672 | $EmptyQueryString = $False 673 | } 674 | Else { 675 | $QueryString += '+language:' + $Language 676 | } 677 | } 678 | If ( $In ) { 679 | If ( $EmptyQueryString ) { 680 | $QueryString += 'in:' + $In 681 | $EmptyQueryString = $False 682 | } 683 | Else { 684 | $QueryString += '+in:' + $In 685 | } 686 | } 687 | If ( $Type ) { 688 | If ( $EmptyQueryString ) { 689 | $QueryString += 'type:' + $Type 690 | $EmptyQueryString = $False 691 | } 692 | Else { 693 | $QueryString += '+type:' + $Type 694 | } 695 | } 696 | If ( $Repos ) { 697 | If ( $EmptyQueryString ) { 698 | $QueryString += 'repos:' + $Repos 699 | $EmptyQueryString = $False 700 | } 701 | Else { 702 | $QueryString += '+repos:' + $Repos 703 | } 704 | } 705 | If ( $Location ) { 706 | If ( $EmptyQueryString ) { 707 | $QueryString += 'location:' + $Location 708 | $EmptyQueryString = $False 709 | } 710 | Else { 711 | $QueryString += '+location:' + $Location 712 | } 713 | } 714 | If ( $Followers ) { 715 | If ( $EmptyQueryString ) { 716 | $QueryString += 'followers:' + $Followers 717 | $EmptyQueryString = $False 718 | } 719 | Else { 720 | $QueryString += '+followers:' + $Followers 721 | } 722 | } 723 | If ( $SortBy ) { 724 | $QueryString += "&sort=$SortBy" 725 | } 726 | 727 | # Using the maximum number of results per page to limit the number of requests 728 | $QueryString += '&per_page=100' 729 | 730 | $UriBuilder = New-Object System.UriBuilder -ArgumentList 'https://api.github.com' 731 | $UriBuilder.Path = 'search/users' -as [uri] 732 | $UriBuilder.Query = $QueryString 733 | 734 | $BaseUri = $UriBuilder.Uri 735 | Write-Verbose "Constructed base URI : $($BaseUri.AbsoluteUri)" 736 | 737 | $Response = Invoke-WebRequest -Uri $BaseUri 738 | If ( $Response.StatusCode -ne 200 ) { 739 | 740 | Write-Warning "The status code was $($Response.StatusCode) : $($Response.StatusDescription)" 741 | } 742 | $NumberOfPages = Get-NumberofPage -SearchResult $Response 743 | Write-Verbose "Number of pages for this search result : $($NumberOfPages)" 744 | 745 | Foreach ( $PageNumber in 1..$NumberOfPages ) { 746 | 747 | $ResultPageUri = $BaseUri.AbsoluteUri + "&page=$($PageNumber.ToString())" 748 | 749 | Try { 750 | $PageResponse = Invoke-WebRequest -Uri $ResultPageUri -ErrorAction Stop 751 | } 752 | Catch { 753 | Throw $_.Exception.Message 754 | } 755 | 756 | # The search API limits the number of requests to 10 requests per minute and per IP address (for unauthenticated requests) 757 | # We might be subject to the limit on the number of requests if we run function multiple times in the last minute 758 | $RemainingRequestsNumber = $PageResponse.Headers.'X-RateLimit-Remaining' -as [int] 759 | Write-Verbose "Number of remaining API requests : $($RemainingRequestsNumber)." 760 | 761 | If ( $RemainingRequestsNumber -le 1 ) { 762 | Write-Warning "The search API limits the number of requests to 10 requests per minute" 763 | Write-Warning "Waiting 60 seconds before processing the remaining result pages because we have exceeded this limit." 764 | Start-Sleep -Seconds 60 765 | } 766 | $PageResponseContent = $PageResponse.Content | ConvertFrom-Json 767 | 768 | Foreach ( $PageResult in $PageResponseContent.items ) { 769 | 770 | $UserDetailsResponse = Invoke-WebRequest -Uri $PageResult.url 771 | 772 | If ( ($UserDetailsResponse.Headers.'X-RateLimit-Remaining' -as [int]) -le 1 ) { 773 | Write-Warning "The search API limits the number of requests to 10 requests per minute" 774 | Write-Warning "Waiting 60 seconds before processing the remaining result pages because we have exceeded this limit." 775 | Start-Sleep -Seconds 60 776 | } 777 | $UserDetails = $UserDetailsResponse.Content | ConvertFrom-Json 778 | 779 | $UserProperties = [ordered]@{ 780 | 'Full Name' = $UserDetails.name 781 | 'Company' = $UserDetails.company 782 | 'Blog' = $UserDetails.blog 783 | 'Location' = $UserDetails.location 784 | 'Email Address' = $UserDetails.email 785 | 'Hireable' = $UserDetails.hireable 786 | 'Bio' = $UserDetails.bio 787 | 'Repos' = $UserDetails.public_repos 788 | 'Gists' = $UserDetails.public_gists 789 | 'Followers' = $UserDetails.followers 790 | 'Following' = $UserDetails.following 791 | 'Joined' = $UserDetails.created_at 792 | } 793 | 794 | Add-Member -InputObject $PageResult -NotePropertyMembers $UserProperties -Force 795 | 796 | $PageResult.psobject.TypeNames.Insert(0,'PSGithubSearch.User') 797 | $PageResult 798 | } 799 | } 800 | } 801 | 802 | Function Get-NumberofPage { 803 | <# 804 | .SYNOPSIS 805 | Helper function to get the number of pages from a search result response. 806 | #> 807 | [CmdletBinding()] 808 | [OutputType([System.Int32])] 809 | Param( 810 | [Parameter(Mandatory=$True,Position=0)] 811 | [PSObject]$SearchResult 812 | ) 813 | 814 | $PaginationInfo = $SearchResult.Headers.Link 815 | 816 | If ( -not($PaginationInfo) ) { 817 | $NumberOfPages = 1 818 | } 819 | Else { 820 | $SplitPaginationInfo = $PaginationInfo -split ', ' 821 | $LastPage = $SplitPaginationInfo | Where-Object { $_ -like '*"last"*' } 822 | $NumberOfPages = (($LastPage -split '&page=')[1] -split '>')[0] -as [int] 823 | } 824 | return $NumberOfPages 825 | } --------------------------------------------------------------------------------