diff --git a/.gitignore b/.gitignore index edb33db4..8e0d3cf4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -ApiTokens.psm1 \ No newline at end of file +Tests/Config/Settings.ps1 diff --git a/ApiTokensTemplate.psm1 b/ApiTokensTemplate.psm1 deleted file mode 100644 index 9a501d17..00000000 --- a/ApiTokensTemplate.psm1 +++ /dev/null @@ -1 +0,0 @@ -$global:gitHubApiToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c0d47afa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ +# PowerShellForGitHub PowerShell Module +## Changelog + +## [0.0.2](https://github.com/PowerShell/PowerShellForGitHub/tree/0.2.0) - (2018/11/08) +### Features: ++ Significant restructing and refactoring of entire module to make future expansion easier. ++ Significant documentation updates ([CHANGELOG](./CHANGELOG.md), [CONTRIBUTING.md](./CONTRIBUTING.md), + [GOVERNANCE.md](./GOVERNANCE.md), [README.md](./README.md), [USAGE.md](./USAGE.md)) ++ Added `Set-GitHubAuthentication` (and related methods) for securely caching the Access Token ++ Added `Set-GitHubConfiguration` (and related methods) to enable short and long-term configuration + of the module. ++ Added ability to asynchronously see status update of REST requests. ++ Added logging and telemetry to the module (each can be disabled if desired). ++ Tests now auto-configure themselves across whatever account information is supplied in + [Tests/Config/Settings.ps1](./Tests/Config/Settings.ps1) ++ Added support for a number of additional GitHub API's: + + All [Miscellaneous API's](https://developer.github.com/v3/misc/) + + Ability to fully query, update, remove, lock, and unlock Issues. + + Enhanced pull request querying support + + Ability tofully query, create, and remove Repositories, as well as transfer ownership, + get tags, get/set topic and current used programming languages. + + Enhanced user query support as well as being able update information for the current user. + +### Fixes: +* Made parameter ordering consistent across all functions (OwnerName is now first, then RepositoryName) +* Normalized all parameters to use SentenceCase +* All functions that can take a Uri or OwnerName/RepositoryName now support both options. +* Made all parameter names consistent across functions: + * `GitHubAccessToken` -> `AccessToken` + * `RepositoryUrl` -> `Uri` + * `Organization` -> `OrganizationName` + * `Repository` -> `RepositoryName` + * `Owner` -> `OwnerName` +* Normalized usage of Verbose, Info and Error streams + +### Functionality Modified from 0.1.0: +- `New-GitHubLabels` was renamed to `Set-GitHubLabel` and can now optionally take in the labels + to apply to the Repository. +- `Get-GitHubIssueForRepository` has been removed and replaced with `Get-GitHubIssue`. + The key difference between these two is that it no longer accepts multiple repositories as single + input, and filtering on creation/closed date can be done after the fact piping the results into + `Where-Object` now that the returned objects from `Get-GitHubIssue` have actual `[DateTime]` values + for the date properties. For an updated example of doing this, refer to [example usage](USAGE.md#querying-issues). +- `Get-GitHubWeeklyIssueForRepository` has been removed and functionally replaced by `Group-GitHubIssue`. + For an updated example of using it, refer to [example usage](USAGE.md#querying-issues) +- `Get-GitHubTopIssueRepository` has been removed. We have [updated examples](USAGE.md#querying-issues) + for how to accomplish the same scenario. +- `Get-GitHubPullRequestForRepository` has been removed and replaced with `Get-GitHubPullRequest`. + The key difference between these two is that it no longer accepts multiple repositories as single + input, and filtering on creation/merged date can be done after the fact piping the results into + `Where-Object` now that the returned objects from `Get-GitHubPullRequest` have actual `[DateTime]` values + for the date properties. For an updated example of doing this, refer to [example usage](USAGE.md#querying-pull-requests). +- `Get-GitHubWeeklyPullRequestForRepository` has been removed and functionally replaced by `Group-GitHubPullRequest`. + For an updated example of using it, refer to [example usage](USAGE.md#querying-pull-requests) +- `Get-GitHubTopPullRequestRepository` has been removed. We have [updated examples](USAGE.md#querying-pull-requests) + for how to accomplish the same scenario. +- `Get-GitHubRepositoryNameFromUrl` and `GitHubRepositoryOwnerFromUrl` have been removed and + functionally replaced by `Split-GitHubUri` +- `Get-GitHubRepositoryUniqueContributor` has been removed. We have an + [updated example](USAGE.md#querying-contributors) for how to accomplish the same scenario. +- `GitHubOrganizationRepository` has been removed. You can now retrieve repositories for an + organization via `Get-GitHubRepository -OrganizationName `. +- `Get-GitHubAuthenticatedUser` has been replaced with `Get-GitHubUser -Current`. + +More Info: [[pr]](https://github.com/PowerShell/PowerShellForGitHub/pull/TODO) | [[cl]](https://github.com/PowerShell/PowerHellForGitHub/commit/TODO) + +Author: [**@HowardWolosky**](https://github.com/HowardWolosky) + +------ + +## [0.1.0](https://github.com/PowerShell/PowerShellForGitHub/tree/0.1.0) - (2016/11/29) +### Features: ++ Initial public release + +More Info: [[cl]](https://github.com/PowerShell/PowerShellForGitHub/commit/6a3b400019d6a97ccc2f08a951fd4b2d09282eb5) + +Author: [**@KarolKaczmarek**](https://github.com/KarolKaczmarek) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..48b72f1d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,8 @@ +# PowerShellForGitHub PowerShell Module + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions +or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1ea43beb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,449 @@ +# PowerShellForGitHub PowerShell Module +## Contributing + +Looking to help out? You've come to the right place. We'd love your help in making this the best +way to automate GitHub repos. + +Looking for information on how to use this module? Head on over to [README.md](README.md). + +---------- +#### Table of Contents + +* [Overview](#overview) +* [Maintainers](#maintainers) +* [Feedback](#feedback) + * [Bugs](#bugs) + * [Suggestions](#suggestions) + * [Questions](#questions) +* [Static Analysis](#static-analysis) +* [Module Manifest](#module-manifest) +* [Logging](#logging) +* [PowerShell Version](#powershell-version) +* [Coding Guidelines](#coding-guidelines) +* [Adding New Configuration Properties](#adding-new-configuration-properties) +* [Code Comments](#code-comments) +* [Testing](#testing) + * [Installing Pester](#installing-pester) + * [Configuring Your Environment](#configuring-your-environment) + * [Running the Tests](#running-the-tests) + * [Automated Tests](#automated-tests) + * [New Test Guidelines](#new-test-guidelines) +* [Releasing](#releasing) + * [Updating the CHANGELOG](#updating-the-changelog) + * [Adding a New Tag](#adding-a-new-tag) + * [Publish a Signed Update to PowerShell Gallery](#publish-a-signed-update-to-PowerShellGallery) +* [Contributors](#contributors) +* [Legal and Licensing](#legal-and-licensing) + +---------- + +## Overview + +We're excited that _you're_ excited about this project, and would welcome your contributions to help +it grow. There are many different ways that you can contribute: + + 1. Submit a [bug report](#bugs). + 2. Verify existing fixes for bugs. + 3. Submit your own fixes for a bug. Before submitting, please make sure you have: + * Performed code reviews of your own + * Updated the [test cases](#testing) if needed + * Run the [test cases](#testing) to ensure no feature breaks or test breaks + * Added the [test cases](#testing) for new code + * Ensured that the code is free of [static analysis](#static-analysis) issues + 4. Submit a feature request. + 5. Help answer [questions](https://github.com/PowerShell/PowerShellForGitHub/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Aquestion). + 6. Write new [test cases](#testing). + 7. Tell others about the project. + 8. Tell the developers how much you appreciate the product! + +You might also read these two blog posts about contributing code: + * [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza + * [Don't "Push" Your Pull Requests](http://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. + +Before submitting a feature or substantial code contribution, please discuss it with the +PowerShellForGitHub team via [Issues](https://github.com/PowerShell/PowerShellForGitHub/issues), and ensure it +follows the product roadmap. Note that all code submissions will be rigorously reviewed by the +PowerShellForGitHub Team. Only those that meet a high bar for both quality and roadmap fit will be merged +into the source. + +## Maintainers + +PowerShellForGitHub is maintained by: + +- **[@HowardWolosky](http://github.com/HowardWolosky)** + +As this module is a production dependency for Microsoft, we have a couple workflow restrictions: + +- Anyone with commit rights can merge Pull Requests provided that there is a :+1: from one of + the members above. +- Releases are performed by a member above so that we can ensure Microsoft internal processes + remain up to date with the latest and that there are no regressions. + +## Feedback + +All issue types are tracked on the project's [Issues]( https://github.com/PowerShell/PowerShellForGitHub/issues) +page. + +In all cases, make sure to search the list of issues before opening a new one. +Duplicate issues will be closed. + +### Bugs + +For a great primer on how to submit a great bug report, we recommend that you read: +[Painless Bug Tracking](http://www.joelonsoftware.com/articles/fog0000000029.html). + +To report a bug, please include as much information as possible, namely: + +* The version of the module (located in `PowerShellForGitHub.psd1`) +* Your OS version +* Your version of PowerShell (`$PSVersionTable.PSVersion`) +* As much information as possible to reproduce the problem. +* If possible, logs from your execution of the task that exhibit the erroneous behavior +* The behavior you expect to see + +Please also mark your issue with the 'bug' label. + +### Suggestions + +We welcome your suggestions for enhancements to the extension. +To ensure that we can integrate your suggestions effectively, try to be as detailed as possible +and include: + +* What you want to achieve / what is the problem that you want to address. +* What is your approach for solving the problem. +* If applicable, a user scenario of the feature / enhancement in action. + +Please also mark your issue with the 'suggestion' label. + +### Questions + +If you've read through all of the documentation, checked the Wiki, and the PowerShell help for +the command you're using still isn't enough, then please open an issue with the `question` +label and include: + +* What you want to achieve / what is the problem that you want to address. +* What have you tried so far. + +---------- + +## Static Analysis + +This project leverages the [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer/) +PowerShell module for static analysis. + +It is expected that this module shall remain "clean" from the perspective of that module. + +If you have never installed PSScriptAnalyzer, do this from an Administrator PowerShell console window: + +```powershell +Install-Module -Name PSScriptAnalyzer +``` + +In the future, before running it, make sure it's up-to-date (run this from an Administrator +PowerShell console window): + +```powershell +Update-Module -Name PSScriptAnalyzer +``` + +Once it's installed (or updated), from the root of your enlistment simply call + +```powershell +Invoke-ScriptAnalyzer -Path .\ -Recurse +``` + +That should return with no output. If you see any output when calling that command, +either fix the issues that it calls out, or add a `[Diagnostics.CodeAnalysis.SuppressMessageAttribute()]` +with a justification explaining why it's ok to suppress that rule within that part of the script. +Refer to the [PSScriptAnalyzer documentation](https://github.com/PowerShell/PSScriptAnalyzer/) for +more information on how to use that attribute, or look at other existing examples within this module. + +---------- + +### Module Manifest + +This is a manifested PowerShell module, and the manifest can be found here: + + PowerShellForGitHub.psd1 + +If you add any new modules/files to this module, be sure to update the manifest as well. +New modules should be added to `NestedModules`, and any new functions or aliases that +should be exported need to be added to the corresponding `FunctionsToExport` or +`AliasesToExport` section. Please keep all entries to those sections in **alphabetical order**. + +---------- + +### Logging + +Instead of using the built-in `Write-*` methods (`Write-Host`, `Write-Warning`, etc...), +please use + +```powershell +Write-Log +``` + +which is implemented in Helpers.ps1. It will take care of formatting your content in a +consistent manner, as well ensure that the content is logged to a file (if configured to do so +by the user). + +---------- + +### PowerShell Version + +This module must be able to run on PowerShell version 4. It is permitted to add functionality +that requires a higher version of PowerShell, but only if there is a fallback implementation +that accomplishes the same thing in a PowerShell version 4 compatible way, and the path choice +is controlled by a PowerShell version check. + +For an example of this, see `Write-Log` in `Helpers.ps1` which uses `Write-Information` +for `Informational` messages on v5+ and falls back to `Write-Host` for earlier versions: + +```powershell +if ($PSVersionTable.PSVersion.Major -ge 5) +{ + Write-Information $ConsoleMessage -InformationAction Continue +} +else +{ + Write-Host $ConsoleMessage +} +``` + +---------- + +### Coding Guidelines + +As a general rule, our coding convention is to follow the style of the surrounding code. +Avoid reformatting any code when submitting a PR as it obscures the functional changes of your change. + +A basic rule of formatting is to use "Visual Studio defaults". +Here are some general guidelines + +* No tabs, indent 4 spaces. +* Braces usually go on their own line, + with the exception of single line statements that are properly indented. +* Use `camelCase` for instance fields, `PascalCase` for function and parameter names +* Avoid the creation of `script` scoped variables unless absolutely necessary. + If referencing one, be sure to explicitly reference it by scope. +* Don't use globals. If you want to add module configuration, [add a new property instead](#adding-new-configuration-properties). +* Avoid more than one blank empty line. +* Always use a blank line following a closing bracket `}` unless the next line itself is a closing bracket. +* Add full [Comment Based Help](https://technet.microsoft.com/en-us/library/hh847834.aspx) for all + methods added, whether internal-only or external. The act of writing this documentation may help + you better design your function. +* File encoding should be ASCII (preferred) or UTF8 (with BOM) if absolutely necessary. +* We try to adhere to the [PoshCode Best Practices](https://github.com/PoshCode/PowerShellPracticeAndStyle/tree/master/Best%20Practices) + and [DSCResources Style Guidelines](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md) + and think that you should too. +* We try to limit lines to 100 characters to limit the amount of horizontal scrolling needed when + reviewing/maintaining code. There are of course exceptions, but this is generally an enforced + preference. The [Visual Studio Productivity Power Tools](https://visualstudiogallery.msdn.microsoft.com/34ebc6a2-2777-421d-8914-e29c1dfa7f5d) + extension has a "Column Guides" feature that makes it easy to add a Guideline in column 100 + to make it really obvious when coding. + +---------- + +## Adding New Configuration Properties + +If you want to add a new configuration value to the module, you must modify the following: + * In `Restore-GitHubConfiguration`, update `$config` to declare the new property along with + it's default value, being sure that PowerShell will understand what its type is. + * Update `Get-GitHubConfiguration` and add the new property name to the `ValidateSet` list + so that tab-completion and documentation gets auto-updated. You shouldn't have to add anything + to the body of the method. + * Add a new explicit parameter to `Set-GitHubConfiguration` to receive the property, along with + updating the CBH (Comment Based Help) by adding a new `.PAREMETER` entry. You shouldn't + have to add anything to the body of the method. + +---------- + +### Code comments + +It's strongly encouraged to add comments when you are making changes to the code and tests, +especially when the changes are not trivial or may raise confusion. +Make sure the added comments are accurate and easy to understand. +Good code comments should improve readability of the code, and make it much more maintainable. + +That being said, some of the best code you can write is self-commenting. By refactoring your code +into small, well-named functions that concisely describe their purpose, it's possible to write +code that reads clearly while requiring minimal comments to understand what it's doing. + +---------- + +### Testing +[![Build status](https://ci.appveyor.com/api/projects/status/vsfq8kxo2et2dn7i?svg=true +)](https://ci.appveyor.com/project/HowardWolosky/powershellforgithub) + +#### Installing Pester +This module supports testing using the [Pester UT framework](https://github.com/pester/Pester). + +To install it: + +```powershell +Install-Module -Name Pester +``` + +#### Configuring Your Environment +The tests intentionally do not mock out interaction with the real GitHub API, as we want to know +when our interaction with the API has been broken. That means that to execute the tests, you will +need Administrator privelege for an account. For our purposes, we have a "test" account that our +team uses for having the tests [run automated](#automated-tests). For you to run the tests locally, +you must make a couple changes: + + 1. Choose if you'll be executing the tests on your own pesonal account or your own test account + (the tests should be non-destructive, but ... hey ... we are developing code here, mistakes happen.) + 2. Update your local copy of [tests/config/Settings.ps1](./tests/config/Settings.ps1) to note + the `OwerName` and `OrganizationName` that the tests will be running under. + > While you can certainly check-in this file to your own fork, please DO NOT include your + > changes as part of any pull request that you may make. The `.gitignore` file tries + > to help prevent that. + 3. Run `Set-GitHubAuthentication` to ensure that it is configured with an administrator-level + Access Token for the specified owner/organization. + > Unfortunately, you cannot use `-SessionOnly` with `Set-GitHubAuthentication` when testing, + > as Pester works by making new sessions for every test. That means that it must be "globally" + > configured with that access token for the duration of the Pester test execution. + +#### Running the Tests +Tests can be run either from the project root directory or from the `Tests` subfolder. +Navigate to the correct folder and simply run: + +```powershell +Invoke-Pester +``` + +Make sure you have previously configured your Access Token via `Set-GitHubAuthentication`. +Please keep in mind some tests may fail on your machine, as they test private items (e.g. secret teams) which your key won't have access to. + +Pester can also be used to test code-coverage, like so: + +```powershell +Invoke-Pester -CodeCoverage "$root\GitHubLabels.ps1" -TestName "*" +``` + +This command tells Pester to check the `GitHubLabels.ps1` file for code-coverage. +The `-TestName` parameter tells Pester to run any `Describe` blocks with a `Name` like +`"*"` (which in this case, is every test, but can be made more specific). + +The code-coverage object can be captured and interacted with, like so: + +```powershell +$cc = (Invoke-Pester -CodeCoverage "$root\GitHubLabels.ps1" -TestName "*" -PassThru -Quiet).CodeCoverage +``` + +There are many more nuances to code-coverage, see +[its documentation](https://github.com/pester/Pester/wiki/Code-Coverage) for more details. + +#### Automated Tests +[![Build status](https://ci.appveyor.com/api/projects/status/vsfq8kxo2et2dn7i?svg=true +)](https://ci.appveyor.com/project/HowardWolosky/powershellforgithub) + +These test are configured to automatically execute upon any update to the `master` branch +on the `HowardWolosky` fork of `PowerShellForGitHub`. (At this time, we don't have the proper +permissions to have AppVeyor execute the tests on the main instance.) This setup thus depends +on the `HowardWolosky` fork always being kept up-to-date with the `PowerShell` master. + +The [AppVeyor project](https://ci.appveyor.com/project/HowardWolosky/powershellforgithub) has +been [configured](./appveyor.yml) to execute the tests against a test GitHub account (for the user +`PowerShellForGitHubTeam`, and the org `PowerShellForGitHubTeamTestOrg`). + +Additionally, the Access Token with administrator access for that account has been encrypted on +AppVeyor and mentioned in the [config file](./appveyor.yml). That Access Token is not accessible +by anyone else. To run the tests locally with your own account, see +[configuring-your-environment](#configuring-your-environment). + +#### New Test Guidelines +Your tests should have NO dependencies on an account being set up in a specific way. They should +get the configured account set up in the appropriate state that it can then test/verify. In this +way, anyone should be able to run the tests from their own machine/account. + +Use a new GUID for any object that you have to create (repository, label, team name, etc...) to avoid +any possible name collisions with existing objects on the executing user's accounts. + +---------- + +### Releasing + +If you are a maintainer: + +Ensure that the version number of the module is updated with every pull request that is being +accepted. + +This project follows [semantic versioning](http://semver.org/) in the following way: + + .. + +Where: +* `` - Changes only with _significant_ updates. +* `` - If this is a feature update, increment by one and be sure to reset `` to 0. +* `` - If this is a bug fix, leave `` alone and increment this by one. + +When new code changes are checked in to the repo, the module must be signed and published by +Microsoft to [PowerShell Gallery](https://www.powershellgallery.com/packages/PowerShellForGitHub). + +Once the new version has been pulled into master, there are additional tasks to perform: + + * Ensure that [all tests pass](#testing) and that the module is still [static analysis clean](#static-analysis) + * Update [CHANGELOG.md](./CHANGELOG.md) + * Add a tag for that version to the repo + * Publish a _signed_ update to [PowerShellGallery](https://www.powershellgallery.com/packages/PowerShellForGitHub/). + +#### Updating the CHANGELOG +To update [CHANGELOG.md](./CHANGELOG.md), just duplicate the previous section and update it to be +relevant for the new release. Be sure to update all of the sections: + * The version number + * The hard path to the change (we'll get that path working in a moment) + * The release date + * A brief list of all the changes (use a `-` for the bullet point if it's fixing a bug, or a `+` for a feature) + * The link to the pull request (pr) (so that the discussion on the change can be easily reviewed) and the changelist (cl) + * The author (and a link to their profile) + * If it's a new contributor, also add them to the [Contributors](#contributors) list below. + +Then get a new pull request out for that change to CHANGELOG. + +#### Adding a New Tag +To add a new tag: + 1. Make sure that you're in a clone of the actual repo and not your own private fork. + 2. Make sure that you have checked out `master` and that it's fully up-to-date + 3. Run `git tag -a '' + 4. In the pop-up editor, give a one-line summary of the change (that you possibly already wrote for the CHANGELOG) + 5. Save and close the editor + 6. Run `git push --tags` to upload the new tag you just created + +If you want to make sure you get these tags on any other forks/clients, you can run +`git fetch origin --tags` or `git fetch upstream --tags`, or whatever you've named the source to be. + +#### Publish a Signed Update to PowerShellGallery +The final steps is getting the module updated on +[PowerShellGallery](https://www.powershellgallery.com/packages/PowerShellForGitHub/) +so that it is easy for users to adopt the changes. + +The module files should be signed by Microsoft prior to publishing. +Instructions for signing the files and for then publishing the update to PowerShellGallery +can be found in the [internal Microsoft repo for this project](https://microsoft.visualstudio.com/Apps/_git/eng.powershellforgithub). + +---------- + +### Contributors + +Thank you to all of our contributors, no matter how big or small the contribution: + +- **[Howard Wolosky (@HowardWolosky)](http://github.com/HowardWolosky)** +- **[Karol Kaczmarek (@KarolKaczmarek)](https://github.com/KarolKaczmarek)** +- **[Josh Rolstad (@jrolstad)](https://github.com/jrolstad)** +- **[Zachary Alexander (@zjalexander)](http://github.com/zjalexander)** +- **[Andrew Dahl (@aedahl)](http://github.com/aedahl)** + +---------- + +### Legal and Licensing + +PowerShellForGitHub is licensed under the [MIT license](..\LICENSE). + +You will need to complete a Contributor License Agreement (CLA) for any code submissions. +Briefly, this agreement testifies that you are granting us permission to use the submitted change +according to the terms of the project's license, and that the work being submitted is under +appropriate copyright. You only need to do this once. + +When you submit a pull request, [@msftclas](https://github.com/msftclas) will automatically +determine whether you need to sign a CLA, comment on the PR and label it appropriately. +If you do need to sign a CLA, please visit https://cla.microsoft.com and follow the steps. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000..6f5c9082 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,121 @@ +# PowerShellForGitHub PowerShell Module +## Governance + +## Terms + +* [**PowerShellForGitHub Committee**](#powershellforgithub-committee): A committee of project owners who are + responsible for design decisions, and approving new maintainers/committee members. + +* **Project Leads**: Project Leads support the PowerShellForGitHub Committee, engineering teams, and + community by working across Microsoft teams and leadership, and working through issues with + other companies. The initial Project Leads for PowerShellForGitHub are: + + * Howard Wolosky ([HowardWolosky](https://github.com/HowardWolosky)) + +* [**Repository maintainer**](#repository-maintainers): An individual responsible for merging + pull requests (PRs) into `master` when all requirements are met (code review, tests, docs, + as applicable). Repository Maintainers are the only people with write permissions into `master`. + +* [**Area experts**](#area-experts): People who are experts for specific components or + technologies (e.g. security, performance). Area experts are responsible for code reviews, + issue triage, and providing their expertise to others. + +* **Corporation**: The Corporation owns the PowerShellForGitHub repository and, under extreme circumstances, + reserves the right to dissolve or reform the PowerShellForGitHub Committee, the Project Leads, and the + Corporate Maintainer. The Corporation for PowerShellForGitHub is Microsoft. + +* **Corporate Maintainer**: The Corporate Maintainer is an entity, person or set of persons, with + the ability to veto decisions made by the PowerShellForGitHub Committee or any other collaborators on the + PowerShellForGitHub project. This veto power will be used with restraint since it is intended that the + community drive the project. The Corporate Maintainer is determined by the Corporation both + initially and in continuation. The initial Corporate Maintainer for PowerShellForGitHub is + Howard Wolosky ([HowardWolosky-MSFT](https://github.com/HowardWolosky)). + +## PowerShellForGitHub Committee + +The PowerShellForGitHub Committee and its members (aka Committee Members) are the primary caretakers of +PowerShellForGitHub. + +### Current Committee Members + + * Howard Wolosky ([@HowardWolosky](https://github.com/HowardWolosky)) + +### Committee Member Responsibilities + +Committee Members are responsible for reviewing and approving proposed new features or design changes. + +#### Committee Member DOs and DON'Ts + +As a PowerShellForGitHub Committee Member: + +1. **DO** reply to issues and pull requests with design opinions + (this could include offering support for good work or exciting new features) +2. **DO** encourage healthy discussion about the direction of PowerShellForGitHub +3. **DO** raise "red flags" on PRs that require a larger design discussion +4. **DO** contribute to documentation and best practices +5. **DO** maintain a presence in the PowerShellForGitHub community outside of GitHub + (Twitter, blogs, StackOverflow, Reddit, Hacker News, etc.) +6. **DO** heavily incorporate community feedback into the weight of your decisions +7. **DO** be polite and respectful to a wide variety of opinions and perspectives +8. **DO** make sure contributors are following the [contributor guidelines](../CONTRIBUTING.md) +9. **DON'T** constantly raise "red flags" for unimportant or minor problems to the point that the + progress of the project is being slowed +10. **DON'T** offer up your opinions as the absolute opinion of the PowerShellForGitHub Committee. + Members are encouraged to share their opinions, but they should be presented as such. + +### PowerShellForGitHub Committee Membership + +The initial PowerShellForGitHub Committee consists of Microsoft employees. +It is expected that over time, PowerShellForGitHub experts in the community will be made Committee Members. +Membership is heavily dependent on the level of contribution and expertise: +individuals who contribute in meaningful ways to the project will be recognized accordingly. + +At any point in time, a Committee Member can nominate a strong community member to join the Committee. +Nominations should be submitted in the form of an `Issue` with the `committee nomination` label +detailing why that individual is qualified and how they will contribute. After the `Issue` has +been discussed, a unanimous vote will be required for the new Committee Member to be confirmed. + +## Repository Maintainers + +Repository Maintainers are trusted stewards of the PowerShellForGitHub repository responsible for +maintaining consistency and quality of the code. + +One of their primary responsibilities is merging pull requests after all requirements have been +fulfilled. + +## Area Experts + +Area Experts are people with knowledge of specific components or technologies in the PowerShellForGitHub +domain. They are responsible for code reviews, issue triage, and providing their expertise to others. + +They have [write access](https://help.github.com/articles/repository-permission-levels-for-an-organization/) +to the PowerShellForGitHub repository which gives them the power to: + +1. `git push` to all branches *except* `master`. +2. Merge pull requests to all branches *except* `master` (though this should not be common + given that `master` is the only long-living branch. +3. Assign labels, milestones, and people to [issues](https://guides.github.com/features/issues/). + +### Area Expert Responsibilities + +If you are an Area Expert, you are expected to be actively involved in any development, design, or contributions in your area of expertise. + +If you are an Area Expert: + +1. **DO** assign the correct labels +2. **DO** assign yourself to issues labeled with your area of expertise +3. **DO** code reviews for issues where you're assigned or in your areas of expertise. +4. **DO** reply to new issues and pull requests that are related to your area of expertise + (while reviewing PRs, leave your comment even if everything looks good - a simple + "Looks good to me" or "LGTM" will suffice, so that we know someone has already taken a look at it). +5. **DO** make sure contributors are following the [contributor guidelines](CONTRIBUTING.md). +6. **DO** ask people to resend a pull request, if it doesn't target `master`. +7. **DO** ensure that contributors [write Pester tests](CONTRIBUTING.md#testing) for all + new/changed functionality when possible. +8. **DO** ensure that contributors write documentation for all new-/changed functionality +9. **DO** encourage contributors to refer to issues in their pull request description + (e.g. `Resolves issue #123`). +10. **DO** encourage contributors to create meaningful titles for all PRs. Edit title if necessary. +11. **DO** verify that all contributors are following the [Coding Guidelines](CONTRIBUTING.md#coding-guidelines). +12. **DON'T** create new features, new designs, or change behaviors without having a public + discussion on the design within its Issue. diff --git a/GitHubAnalytics.ps1 b/GitHubAnalytics.ps1 new file mode 100644 index 00000000..f71dadd6 --- /dev/null +++ b/GitHubAnalytics.ps1 @@ -0,0 +1,231 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Group-GitHubIssue +{ +<# + .SYNOPSIS + Groups the provided issues based on the specified grouping criteria. + + .DESCRIPTION + Groups the provided issues based on the specified grouping criteria. + + Currently able to group Issues by week. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Issue + The Issue(s) to be grouped. + + .PARAMETER Weeks + The number of weeks to group the Issues by. + + .PARAMETER DateType + The date property that should be inspected when determining which week grouping the issue + if part of. + + .OUTPUTS + [PSCustomObject[]] Collection of issues and counts, by week, along with the total count of issues. + + .EXAMPLE + $issues = @() + $issues += Get-GitHubIssue -Uri 'https://github.com/powershell/xpsdesiredstateconfiguration' + $issues += Get-GitHubIssue -Uri 'https://github.com/powershell/xactivedirectory' + $issues | Group-GitHubIssue -Weeks 12 -DateType closed +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Weekly')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param + ( + [Parameter( + Mandatory, + ValueFromPipeline)] + [PSCustomObject[]] $Issue, + + [Parameter( + Mandatory, + ParameterSetName='Weekly')] + [ValidateRange(0, 10000)] + [int] $Weeks, + + [Parameter(ParameterSetName='Weekly')] + [ValidateSet('created', 'closed')] + [string] $DateType = 'created' + ) + + Write-InvocationLog -Invocation $MyInvocation + + if ($PSCmdlet.ParameterSetName -eq 'Weekly') + { + $totalIssues = 0 + $weekDates = Get-WeekDate -Weeks $Weeks + $endOfWeek = Get-Date + + foreach ($week in $weekDates) + { + $filteredIssues = @($Issue | Where-Object { + (($DateType -eq 'created') -and ($_.created_at -ge $week) -and ($_.created_at -le $endOfWeek)) -or + (($DateType -eq 'closed') -and ($_.closed_at -ge $week) -and ($_.closed_at -le $endOfWeek)) + }) + + $endOfWeek = $week + $count = $filteredIssues.Count + $totalIssues += $count + + Write-Output -InputObject ([PSCustomObject]([ordered]@{ + 'WeekStart' = $week + 'Count' = $count + 'Issues' = $filteredIssues + })) + } + + Write-Output -InputObject ([PSCustomObject]([ordered]@{ + 'WeekStart' = 'total' + 'Count' = $totalIssues + 'Issues' = $Issue + })) + } + else + { + Write-Output -InputObject $Issue + } +} + +function Group-GitHubPullRequest +{ +<# + .SYNOPSIS + Groups the provided pull requests based on the specified grouping criteria. + + .DESCRIPTION + Groups the provided pull requests based on the specified grouping criteria. + + Currently able to group Pull Requests by week. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER PullRequest + The Pull Requests(s) to be grouped. + + .PARAMETER Weeks + The number of weeks to group the Pull Requests by. + + .PARAMETER DateType + The date property that should be inspected when determining which week grouping the + pull request if part of. + + .OUTPUTS + [PSCustomObject[]] Collection of pull requests and counts, by week, along with the + total count of pull requests. + + .EXAMPLE + $pullRequests = @() + $pullRequests += Get-GitHubPullRequest -Uri 'https://github.com/powershell/xpsdesiredstateconfiguration' + $pullRequests += Get-GitHubPullRequest -Uri 'https://github.com/powershell/xactivedirectory' + $pullRequests | Group-GitHubPullRequest -Weeks 12 -DateType closed +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Weekly')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param + ( + [Parameter( + Mandatory, + ValueFromPipeline)] + [PSCustomObject[]] $PullRequest, + + [Parameter( + Mandatory, + ParameterSetName='Weekly')] + [ValidateRange(0, 10000)] + [int] $Weeks, + + [Parameter(ParameterSetName='Weekly')] + [ValidateSet('created', 'merged')] + [string] $DateType = 'created' + ) + + Write-InvocationLog -Invocation $MyInvocation + + if ($PSCmdlet.ParameterSetName -eq 'Weekly') + { + $totalPullRequests = 0 + $weekDates = Get-WeekDate -Weeks $Weeks + $endOfWeek = Get-Date + + foreach ($week in $weekDates) + { + $filteredPullRequests = @($PullRequest | Where-Object { + (($DateType -eq 'created') -and ($_.created_at -ge $week) -and ($_.created_at -le $endOfWeek)) -or + (($DateType -eq 'merged') -and ($_.merged_at -ge $week) -and ($_.merged_at -le $endOfWeek)) + }) + + $endOfWeek = $week + $count = $filteredPullRequests.Count + $totalPullRequests += $count + + Write-Output -InputObject ([PSCustomObject]([ordered]@{ + 'WeekStart' = $week + 'Count' = $count + 'PullRequests' = $filteredPullRequests + })) + } + + Write-Output -InputObject ([PSCustomObject]([ordered]@{ + 'WeekStart' = 'total' + 'Count' = $totalPullRequests + 'PullRequests' = $PullRequest + })) + } + else + { + Write-Output -InputObject $PullRequest + } +} + +function Get-WeekDate +{ +<# + .SYNOPSIS + Retrieves an array of dates with starts of $Weeks previous weeks. + Dates are sorted in reverse chronological order + + .DESCRIPTION + Retrieves an array of dates with starts of $Weeks previous weeks. + Dates are sorted in reverse chronological order + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Weeks + The number of weeks prior to today that should be included in the final result. + + .OUTPUTS + [DateTime[]] List of DateTimes representing the first day of each requested week. + + .EXAMPLE + Get-WeekDate -Weeks 10 +#> + [CmdletBinding()] + param( + [ValidateRange(0, 10000)] + [int] $Weeks = 12 + ) + + $dates = @() + + $midnightToday = Get-Date -Hour 0 -Minute 0 -Second 0 -Millisecond 0 + $startOfWeek = $midnightToday.AddDays(- ($midnightToday.DayOfWeek.value__ - 1)) + + $i = 0 + while ($i -lt $Weeks) + { + $dates += $startOfWeek + $startOfWeek = $startOfWeek.AddDays(-7) + $i++ + } + + return $dates +} diff --git a/GitHubAnalytics.psm1 b/GitHubAnalytics.psm1 deleted file mode 100644 index 7d6000fe..00000000 --- a/GitHubAnalytics.psm1 +++ /dev/null @@ -1,1206 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -<# - .SYNOPSIS PowerShell module for GitHub analytics -#> - -# Import module which defines $global:gitHubApiToken with GitHub API access token. Create this file it if it doesn't exist. -$apiTokensFilePath = "$PSScriptRoot\ApiTokens.psm1" -if (Test-Path $apiTokensFilePath) -{ - Write-Host "Importing $apiTokensFilePath" - Import-Module -force $apiTokensFilePath -} -else -{ - Write-Warning "$apiTokensFilePath does not exist, skipping import" - Write-Warning @' -This module should define $global:gitHubApiToken with your GitHub API access token in ApiTokens.psm1. Create this file if it doesn't exist. -You can simply rename ApiTokensTemplate.psm1 to ApiTokens.psm1 and update value of $global:gitHubApiToken, then reimport this module with -Force switch. -You can get GitHub token from https://github.com/settings/tokens -If you don't provide it, you can still use this module, but you will be limited to 60 queries per hour. -'@ -} - -$script:gitHubToken = $global:gitHubApiToken -$script:gitHubApiUrl = "https://api.github.com" -$script:gitHubApiReposUrl = "https://api.github.com/repos" -$script:gitHubApiOrgsUrl = "https://api.github.com/orgs" - -<# - .SYNOPSIS Function which gets list of issues for given repository - .PARAM - RepositoryUrl Array of repository urls which we want to get issues from - .PARAM - State Whether we want to get open, closed or all issues - .PARAM - CreatedOnOrAfter Filter to only get issues created on or after specific date - .PARAM - CreatedOnOrBefore Filter to only get issues created on or before specific date - .PARAM - ClosedOnOrAfter Filter to only get issues closed on or after specific date - .PARAM - ClosedOnOrBefore Filter to only get issues closed on or before specific date - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - $issues = Get-GitHubIssueForRepository -RepositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration') - .EXAMPLE - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration', "https://github.com/PowerShell/xWindowsUpdate" ) ` - -CreatedOnOrAfter '2015-04-20' -#> -function Get-GitHubIssueForRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [ValidateSet("open", "closed", "all")] - [String] $State = "open", - [DateTime] $CreatedOnOrAfter, - [DateTime] $CreatedOnOrBefore, - [DateTime] $ClosedOnOrAfter, - [DateTime] $ClosedOnOrBefore, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - $index = 0 - - foreach ($repository in $RepositoryUrl) - { - Write-Host "Getting issues for repository $repository" -ForegroundColor Yellow - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryOwner = Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl $repository - - # Create query for issues - $query = "$script:gitHubApiReposUrl/$repositoryOwner/$repositoryName/issues?state=$State" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "&access_token=$GitHubAccessToken" - } - - # Obtain issues - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $issues = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($issue in $issues) - { - # GitHub considers pull request to be an issue, so let's skip pull requests. - if ($issue.pull_request -ne $null) - { - continue - } - - # Filter according to CreatedOnOrAfter - $createdDate = Get-Date -Date $issue.created_at - if (($CreatedOnOrAfter -ne $null) -and ($createdDate -lt $CreatedOnOrAfter)) - { - continue - } - - # Filter according to CreatedOnOrBefore - if (($CreatedOnOrBefore -ne $null) -and ($createdDate -gt $CreatedOnOrBefore)) - { - continue - } - - if ($issue.closed_at -ne $null) - { - # Filter according to ClosedOnOrAfter - $closedDate = Get-Date -Date $issue.closed_at - if (($ClosedOnOrAfter -ne $null) -and ($closedDate -lt $ClosedOnOrAfter)) - { - continue - } - - # Filter according to ClosedOnOrBefore - if (($ClosedOnOrBefore -ne $null) -and ($closedDate -gt $ClosedOnOrBefore)) - { - continue - } - } - else - { - # If issue isn't closed, but we specified filtering on closedOn, skip it - if (($ClosedOnOrAfter -ne $null) -or ($ClosedOnOrBefore -ne $null)) - { - continue - } - } - - Write-Verbose "$index. $($issue.html_url) ## Created: $($issue.created_at) ## Closed: $($issue.closed_at)" - $index++ - - $resultToReturn += $issue - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - } - - return $resultToReturn -} - -<# - .SYNOPSIS Function which returns number of issues created/merged in every week in specific repositories - .PARAM - RepositoryUrl Array of repository urls which we want to get pull requests from - .PARAM - NumberOfWeeks How many weeks we want to obtain data for - .PARAM - DataType Whether we want to get information about created or merged issues in specific weeks - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Get-GitHubWeeklyIssueForRepository -RepositoryUrl @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') -Datatype closed - -#> -function Get-GitHubWeeklyIssueForRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [int] $NumberOfWeeks = 12, - [Parameter(Mandatory=$true)] - [ValidateSet("created","closed")] - [string] $DataType, - $GitHubAccessToken = $script:gitHubToken - ) - - $weekDates = Get-WeekDate -NumberOfWeeks $NumberOfWeeks - $endOfWeek = Get-Date - $results = @() - $totalIssues = 0 - - foreach ($week in $weekDates) - { - Write-Host "Getting issues from week of $week" - - $issues = $null - - if ($DataType -eq "closed") - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $RepositoryUrl -State 'all' -ClosedOnOrAfter $week -ClosedOnOrBefore $endOfWeek - } - elseif ($DataType -eq "created") - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $RepositoryUrl -State 'all' -CreatedOnOrAfter $week -CreatedOnOrBefore $endOfWeek - } - - $endOfWeek = $week - - if (($issues -ne $null) -and ($issues.Count -eq $null)) - { - $count = 1 - } - else - { - $count = $issues.Count - } - - $totalIssues += $count - - $results += @{"BeginningOfWeek"=$week; "Issues"=$count} - } - - $results += @{"BeginningOfWeek"="total"; "Issues"=$totalIssues} - return $results -} - -<# - .SYNOPSIS Function which returns repositories with biggest number of issues meeting specified criteria - .PARAM - RepositoryUrl Array of repository urls which we want to get issues from - .PARAM - State Whether we want to get information about open issues, closed or both - .PARAM - CreatedOnOrAfter Get information about issues created after specific date - .PARAM - ClosedOnOrAfter Get information about issues closed after specific date - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Get-GitHubTopIssueRepository -RepositoryUrl @('https://github.com/powershell/xsharepoint', 'https://github.com/powershell/xCertificate', 'https://github.com/powershell/xwebadministration') -State open - -#> -function Get-GitHubTopIssueRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [ValidateSet("open", "closed", "all")] - [String] $State = "open", - [DateTime] $CreatedOnOrAfter, - [DateTime] $ClosedOnOrAfter, - $GitHubAccessToken = $script:gitHubToken - ) - - if (($State -eq "open") -and ($ClosedOnOrAfter -ne $null)) - { - Throw "ClosedOnOrAfter cannot be specified if State is open" - } - - $repositoryIssues = @{} - - foreach ($repository in $RepositoryUrl) - { - if (($ClosedOnOrAfter -ne $null) -and ($CreatedOnOrAfter -ne $null)) - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $repository ` - -State $State -ClosedOnOrAfter $ClosedOnOrAfter -CreatedOnOrAfter $CreatedOnOrAfter - } - elseif (($ClosedOnOrAfter -ne $null) -and ($CreatedOnOrAfter -eq $null)) - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $repository ` - -State $State -ClosedOnOrAfter $ClosedOnOrAfter - } - elseif (($ClosedOnOrAfter -eq $null) -and ($CreatedOnOrAfter -ne $null)) - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $repository ` - -State $State -CreatedOnOrAfter $CreatedOnOrAfter - } - elseif (($ClosedOnOrAfter -eq $null) -and ($CreatedOnOrAfter -eq $null)) - { - $issues = Get-GitHubIssueForRepository ` - -RepositoryUrl $repository ` - -State $State - } - - if (($issues -ne $null) -and ($issues.Count -eq $null)) - { - $count = 1 - } - else - { - $count = $issues.Count - } - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryIssues.Add($repositoryName, $count) - } - - $repositoryIssues = $repositoryIssues.GetEnumerator() | Sort-Object Value -Descending - - return $repositoryIssues -} - -<# - .SYNOPSIS Function which gets list of pull requests for given repository - .PARAM - RepositoryUrl Array of repository urls which we want to get pull requests from - .PARAM - State Whether we want to get open, closed or all pull requests - .PARAM - CreatedOnOrAfter Filter to only get pull requests created on or after specific date - .PARAM - CreatedOnOrBefore Filter to only get pull requests created on or before specific date - .PARAM - MergedOnOrAfter Filter to only get issues merged on or after specific date - .PARAM - MergedOnOrBefore Filter to only get issues merged on or before specific date - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - $pullRequests = Get-GitHubPullRequestForRepository -RepositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration') - .EXAMPLE - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration', 'https://github.com/PowerShell/xWebAdministration') ` - -State closed -MergedOnOrAfter 2015-02-13 -MergedOnOrBefore 2015-06-17 - -#> -function Get-GitHubPullRequestForRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [ValidateSet("open", "closed", "all")] - [String] $State = "open", - [DateTime] $CreatedOnOrAfter, - [DateTime] $CreatedOnOrBefore, - [DateTime] $MergedOnOrAfter, - [DateTime] $MergedOnOrBefore, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - $index = 0 - - foreach ($repository in $RepositoryUrl) - { - Write-Host "Getting pull requests for repository $repository" -ForegroundColor Yellow - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryOwner = Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl $repository - - # Create query for pull requests - $query = "$script:gitHubApiReposUrl/$repositoryOwner/$repositoryName/pulls?state=$State" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "&access_token=$GitHubAccessToken" - } - - # Obtain pull requests - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $pullRequests = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($pullRequest in $pullRequests) - { - # Filter according to CreatedOnOrAfter - $createdDate = Get-Date -Date $pullRequest.created_at - if (($CreatedOnOrAfter -ne $null) -and ($createdDate -lt $CreatedOnOrAfter)) - { - continue - } - - # Filter according to CreatedOnOrBefore - if (($CreatedOnOrBefore -ne $null) -and ($createdDate -gt $CreatedOnOrBefore)) - { - continue - } - - if ($pullRequest.merged_at -ne $null) - { - # Filter according to MergedOnOrAfter - $mergedDate = Get-Date -Date $pullRequest.merged_at - if (($MergedOnOrAfter -ne $null) -and ($mergedDate -lt $MergedOnOrAfter)) - { - continue - } - - # Filter according to MergedOnOrBefore - if (($MergedOnOrBefore -ne $null) -and ($mergedDate -gt $MergedOnOrBefore)) - { - continue - } - } - else - { - # If issue isn't merged, but we specified filtering on mergedOn, skip it - if (($MergedOnOrAfter -ne $null) -or ($MergedOnOrBefore -ne $null)) - { - continue - } - } - - Write-Verbose "$index. $($pullRequest.html_url) ## Created: $($pullRequest.created_at) ## Merged: $($pullRequest.merged_at)" - $index++ - - $resultToReturn += $pullRequest - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - } - - return $resultToReturn -} - -<# - .SYNOPSIS Function which returns number of pull requests created/merged in every week in specific repositories - .PARAM - RepositoryUrl Array of repository urls which we want to get pull requests from - .PARAM - NumberOfWeeks How many weeks we want to obtain data for - .PARAM - DataType Whether we want to get information about created or merged pull requests in specific weeks - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Get-GitHubWeeklyPullRequestForRepository -RepositoryUrl @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xwebadministration') -Datatype merged - -#> -function Get-GitHubWeeklyPullRequestForRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [int] $NumberOfWeeks = 12, - [Parameter(Mandatory=$true)] - [ValidateSet("created","merged")] - [string] $DataType, - $GitHubAccessToken = $script:gitHubToken - ) - - $weekDates = Get-WeekDate -NumberOfWeeks $NumberOfWeeks - $endOfWeek = Get-Date - $results = @() - $totalPullRequests = 0 - - foreach ($week in $weekDates) - { - Write-Host "Getting Pull Requests from week of $week" - - $pullRequests = $null - - if ($DataType -eq "merged") - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $RepositoryUrl ` - -State 'all' -MergedOnOrAfter $week -MergedOnOrBefore $endOfWeek - } - elseif ($DataType -eq "created") - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $RepositoryUrl ` - -State 'all' -CreatedOnOrAfter $week -CreatedOnOrBefore $endOfWeek - } - - - $endOfWeek = $week - - - if (($pullRequests -ne $null) -and ($pullRequests.Count -eq $null)) - { - $count = 1 - } - else - { - $count = $pullRequests.Count - } - $totalPullRequests += $count - - $results += @{"BeginningOfWeek"=$week; "PullRequests"=$count} - } - - $results += @{"BeginningOfWeek"="total"; "PullRequests"=$totalPullRequests} - return $results -} - -<# - .SYNOPSIS Function which returns repositories with biggest number of pull requests meeting specified criteria - .PARAM - RepositoryUrl Array of repository urls which we want to get pull requests from - .PARAM - State Whether we want to get information about open pull requests, closed or both - .PARAM - CreatedOnOrAfter Get information about pull requests created after specific date - .PARAM - MergedOnOrAfter Get information about pull requests merged after specific date - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Get-GitHubTopPullRequestRepository -RepositoryUrl @('https://github.com/powershell/xsharepoint', 'https://github.com/powershell/xwebadministration') -State closed -MergedOnOrAfter 2015-04-20 - -#> -function Get-GitHubTopPullRequestRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - [ValidateSet("open", "closed", "all")] - [String] $State = "open", - [DateTime] $CreatedOnOrAfter, - [DateTime] $MergedOnOrAfter, - $GitHubAccessToken = $script:gitHubToken - ) - - if (($State -eq "open") -and ($MergedOnOrAfter -ne $null)) - { - Throw "MergedOnOrAfter cannot be specified if State is open" - } - - $repositoryPullRequests = @{} - - foreach ($repository in $RepositoryUrl) - { - if (($MergedOnOrAfter -ne $null) -and ($CreatedOnOrAfter -ne $null)) - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $repository ` - -State $State -MergedOnOrAfter $MergedOnOrAfter -CreatedOnOrAfter $CreatedOnOrAfter - } - elseif (($MergedOnOrAfter -ne $null) -and ($CreatedOnOrAfter -eq $null)) - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $repository ` - -State $State -MergedOnOrAfter $MergedOnOrAfter - } - elseif (($MergedOnOrAfter -eq $null) -and ($CreatedOnOrAfter -ne $null)) - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $repository ` - -State $State -CreatedOnOrAfter $CreatedOnOrAfter - } - elseif (($MergedOnOrAfter -eq $null) -and ($CreatedOnOrAfter -eq $null)) - { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl $repository ` - -State $State - } - - if (($pullRequests -ne $null) -and ($pullRequests.Count -eq $null)) - { - $count = 1 - } - else - { - $count = $pullRequests.Count - } - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryPullRequests.Add($repositoryName, $count) - } - - $repositoryPullRequests = $repositoryPullRequests.GetEnumerator() | Sort-Object Value -Descending - - return $repositoryPullRequests -} - -<# - .SYNOPSIS Obtain repository collaborators - - .EXAMPLE $collaborators = Get-GitHubRepositoryCollaborator -RepositoryUrl @('https://github.com/PowerShell/DscResources') -#> -function Get-GitHubRepositoryCollaborator -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - foreach ($repository in $RepositoryUrl) - { - $index = 0 - Write-Host "Getting repository collaborators for repository $repository" -ForegroundColor Yellow - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryOwner = Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl $repository - - $query = "$script:gitHubApiReposUrl/$repositoryOwner/$repositoryName/collaborators" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "?access_token=$GitHubAccessToken" - } - - # Obtain all collaborators - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $collaborators = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($collaborator in $collaborators) - { - Write-Verbose "$index. $($collaborator.login)" - $index++ - $resultToReturn += $collaborator - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - } - return $resultToReturn -} - -<# - .SYNOPSIS Obtain repository contributors - - .EXAMPLE $contributors = Get-GitHubRepositoryContributor -RepositoryUrl @('https://github.com/PowerShell/DscResources', 'https://github.com/PowerShell/xWebAdministration') -#> -function Get-GitHubRepositoryContributor -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String[]] $RepositoryUrl, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - foreach ($repository in $RepositoryUrl) - { - $index = 0 - Write-Host "Getting repository contributors for repository $repository" -ForegroundColor Yellow - - $repositoryName = Get-GitHubRepositoryNameFromUrl -RepositoryUrl $repository - $repositoryOwner = Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl $repository - - $query = "$script:gitHubApiReposUrl/$repositoryOwner/$repositoryName/stats/contributors" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "?access_token=$GitHubAccessToken" - } - - # Obtain all contributors - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $contributors = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($contributor in $contributors) - { - Write-Verbose "$index. $($contributor.author.login). Commits: $($contributor.total)" - $index++ - $resultToReturn += $contributor - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - - } - - return $resultToReturn -} - -<# - .SYNOPSIS Obtain organization members list - .PARAM - OrganizationName name of the organization - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - - .EXAMPLE $members = Get-GitHubOrganizationMember -OrganizationName PowerShell -#> -function Get-GitHubOrganizationMember -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $OrganizationName, - $GitHubAccessToken = $script:gitHubToken - ) - $resultToReturn = @() - $index = 0 - - $query = "$script:gitHubApiOrgsUrl/$OrganizationName/members" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "?access_token=$GitHubAccessToken" - } - - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $members = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($member in $members) - { - Write-Verbose "$index. $(($member).login)" - $index++ - $resultToReturn += $member - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - return $resultToReturn -} - -<# - .SYNOPSIS Obtain organization teams list - .PARAM - OrganizationName name of the organization - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE Get-GitHubTeam -OrganizationName PowerShell -#> -function Get-GitHubTeam -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [Alias('OrganizationName')] - [String] $OrganizationName, - $GitHubAccessToken = $script:gitHubToken - ) - $resultToReturn = @() - $index = 0 - - $query = "$script:gitHubApiUrl/orgs/$OrganizationName/teams" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "?access_token=$GitHubAccessToken" - } - - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $teams = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($team in $teams) - { - Write-Verbose "$index. $(($team).name)" - $index++ - $resultToReturn += $team - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - return $resultToReturn -} - -<# - .SYNOPSIS Obtain organization team members list - .PARAM - OrganizationName name of the organization - .PARAM - TeamName name of the team in the organization - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - - .EXAMPLE $members = Get-GitHubTeamMember -Organization PowerShell -TeamName Everybody -#> -function Get-GitHubTeamMember -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $OrganizationName, - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $TeamName, - $GitHubAccessToken = $script:gitHubToken - ) - $resultToReturn = @() - $index = 0 - - $teams = Get-GitHubTeam -OrganizationName $OrganizationName - $team = $teams | ? {$_.name -eq $TeamName} - if ($team) { - Write-Host "Found team $TeamName with id $($team.id)" - } else { - Write-Host "Cannot find team $TeamName" - return - } - - $query = "$script:gitHubApiUrl/teams/$($team.id)/members" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "?access_token=$GitHubAccessToken" - } - - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $members = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($member in $members) - { - Write-Verbose "$index. $($member.login)" - $index++ - $resultToReturn += $member - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - return $resultToReturn -} - -<# - .SYNOPSIS Function which gets list of repositories for a given organization - .PARAM - OrganizationName The name of the organization - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - $repositories = Get-GitHubOrganizationRepository -OrganizationName 'PowerShell' -#> -function Get-GitHubOrganizationRepository -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [Alias('Organization')] - [String] $OrganizationName, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - $query = "$script:gitHubApiUrl/orgs/$OrganizationName/repos?" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "&access_token=$GitHubAccessToken" - } - - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $repositories = (ConvertFrom-Json -InputObject $jsonResult.content) - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach($repository in $repositories) - { - $resultToReturn += $repository - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - return $resultToReturn -} - -<# - .SYNOPSIS Function which gets a list of branches for a given repository - .PARAM - OwnerName The name of the repository owner - .PARAM - Repository The name of the repository - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - $branches = Get-GitHubRepositoryBranch -Owner PowerShell -Repository PowerShellForGitHub -#> -function Get-GitHubRepositoryBranch -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [Alias('Owner')] - [String] $OwnerName, - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $Repository, - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - $query = "$script:gitHubApiUrl/repos/$OwnerName/$Repository/branches?" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "&access_token=$GitHubAccessToken" - } - - do - { - try - { - $jsonResult = Invoke-WebRequest $query - $branches = (ConvertFrom-Json -InputObject $jsonResult.content) - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach($branch in $branches) - { - $resultToReturn += $branch - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - - return $resultToReturn -} - -<# - .SYNOPSIS Function to get next page with results from query to GitHub API - - .PARAM - JsonResult Result from the query to GitHub API -#> -function Get-NextResultPage -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $JsonResult - ) - - if($JsonResult.Headers.Link -eq $null) - { - return $null - } - - $nextLinkString = $JsonResult.Headers.Link.Split(',')[0] - - # Get url query for the next page - $query = $nextLinkString.Split(';')[0].replace('<','').replace('>','') - if ($query -notmatch '&page=1') - { - return $query - } - else - { - return $null - } -} - -<# - .SYNOPSIS Function which gets the authenticated user - - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - $user = Get-GitHubAuthenticatedUser -#> -function Get-GitHubAuthenticatedUser -{ - param - ( - $GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - - $query = "$script:gitHubApiUrl/user?" - - if (![string]::IsNullOrEmpty($GitHubAccessToken)) - { - $query += "&access_token=$GitHubAccessToken" - } - - $jsonResult = Invoke-WebRequest $query - $user = ConvertFrom-Json -InputObject $jsonResult.content - - return $user -} - -<# - .SYNOPSIS Returns array of unique contributors which were contributing to given set of repositories. Accepts output of Get-GitHubRepositoryContributor - - .EXAMPLE $Contributors = Get-GitHubRepositoryContributor -RepositoryUrl @('https://github.com/PowerShell/DscResources', 'https://github.com/PowerShell/xWebAdministration') - $uniqueContributors = Get-GitHubRepositoryUniqueContributor -Contributors $contributors -#> -function Get-GitHubRepositoryUniqueContributor -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [object[]] $Contributors - ) - - $uniqueContributors = @() - - Write-Host "Getting unique repository contributors" -ForegroundColor Yellow - - foreach ($contributor in $Contributors) - { - if (-not $uniqueContributors.Contains($contributor.author.login)) - { - $uniqueContributors += $contributor.author.login - } - } - - return $uniqueContributors -} - -<# - .SYNOPSIS Obtain repository name from it's url - - .EXAMPLE Get-GitHubRepositoryNameFromUrl -RepositoryUrl "https://github.com/PowerShell/xRobocopy" -#> -function Get-GitHubRepositoryNameFromUrl -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $RepositoryUrl - ) - - $repositoryName = Split-Path $RepositoryUrl -Leaf - return $repositoryName -} - -<# - .SYNOPSIS Obtain repository owner from it's url - - .EXAMPLE Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl "https://github.com/PowerShell/xRobocopy" -#> -function Get-GitHubRepositoryOwnerFromUrl -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $RepositoryUrl - ) - - $repositoryOwner = Split-Path $RepositoryUrl -Parent - $repositoryOwner = Split-Path $repositoryOwner -Leaf - return $repositoryOwner -} - -<# - .SYNOPSIS Returns array with dates with starts of $NumberOfWeeks previous weeks. - Dates are sorted in reverse chronological order - - .EXAMPLE Get-WeekDate -NumberOfWeeks 10 -#> -function Get-WeekDate -{ - param - ( - [int] $NumberOfWeeks = 12 - ) - - $beginningsOfWeeks = @() - - $today = Get-Date - $midnightToday = Get-Date -Hour 0 -Minute 0 -Second 0 -Millisecond 0 - $startOfWeek = $midnightToday.AddDays(- ($midnightToday.DayOfWeek.value__ - 1)) - - if ($NumberOfWeeks -ge 1) - { - $beginningsOfWeeks += $startOfWeek - } - - for ($week = 2; $week -le $NumberOfWeeks; $week++) - { - # Get date of previous Monday - $startOfWeek = $startOfWeek.AddDays(-7) - $beginningsOfWeeks += $startOfWeek - } - - return $beginningsOfWeeks -} \ No newline at end of file diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 new file mode 100644 index 00000000..0c60f93e --- /dev/null +++ b/GitHubBranches.ps1 @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubRepositoryBranch +{ +<# + .SYNOPSIS + Retrieve branches for a given GitHub repository. + + .DESCRIPTION + Retrieve branches for a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Name of the specific branch to be retieved. If not supplied, all branches will be retrieved. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of branches within the given repository. + + .EXAMPLE + Get-GitHubRepositoryBranch -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + Gets all branches for the specified repository. + + .EXAMPLE + Get-GitHubRepositoryBranch -Uri 'https://github.com/PowerShell/PowerShellForGitHub' -Name master + + Gets information only on the master branch for the specified repository. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [Alias('Get-GitHubBranch')] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $Name, + + [switch] $ProtectedOnly, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $uriFragment = "repos/$OwnerName/$RepositoryName/branches`?" + if (-not [String]::IsNullOrEmpty($Name)) { $uriFragment = $uriFragment + "/$Name" } + + $getParams = @() + if ($ProtectedOnly) { $getParams += 'protected=true' } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = "Getting branches for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + diff --git a/GitHubConfiguration.ps1 b/GitHubConfiguration.ps1 new file mode 100644 index 00000000..7b894bfb --- /dev/null +++ b/GitHubConfiguration.ps1 @@ -0,0 +1,1034 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# The GitHub API token is stored in the password field. +[PSCredential] $script:accessTokenCredential = $null + +# The location of the file that we'll store any settings that can/should roam with the user. +[string] $script:configurationFilePath = [System.IO.Path]::Combine( + [Environment]::GetFolderPath('ApplicationData'), + 'Microsoft', + 'PowerShellForGitHub', + 'config.json') + +# The location of the file that we'll store the Access Token SecureString +# which cannot/should not roam with the user. +[string] $script:accessTokenFilePath = [System.IO.Path]::Combine( + [Environment]::GetFolderPath('LocalApplicationData'), + 'Microsoft', + 'PowerShellForGitHub', + 'accessToken.txt') + +# Only tell users about needing to configure an API token once per session. +$script:seenTokenWarningThisSession = $false + +# The session-cached copy of the module's configuration properties +[PSCustomObject] $script:configuration = $null + +function Initialize-GitHubConfiguration +{ +<# + .SYNOPSIS + Populates the configuration of the module for this session, loading in any values + that may have been saved to disk. + + .DESCRIPTION + Populates the configuration of the module for this session, loading in any values + that may have been saved to disk. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .NOTES + Internal helper method. This is actually invoked at the END of this file. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param() + + $script:seenTokenWarningThisSession = $false + $script:configuration = Import-GitHubConfiguration -Path $script:configurationFilePath +} + +function Set-GitHubConfiguration +{ +<# + .SYNOPSIS + Change the value of a configuration property for the PowerShellForGitHub module, + for the sesion only, or globally for this user. + + .DESCRIPTION + Change the value of a configuration property for the PowerShellForGitHub module, + for the sesion only, or globally for this user. + + A single call to this method can set any number or combination of properties. + + To change any of the boolean/switch properties to false, specify the switch, + immediately followed by ":$false" with no space. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER ApplicationInsightsKey + Change the Application Insights instance that telemetry will be reported to (if telemetry + hasn't been disabled via DisableTelemetry). + + .PARAMETER AssemblyPath + The location that any dependent assemblies that this module depends on can be located. + If the assemblies can't be found at this location, nor in a temporary cache or in + the module's directory, the assemblies will be downloaded and temporarily cached. + + .PARAMETER DefaultNoStatus + Control if the -NoStatus switch should be passed-in by default to all methods. + + .PARAMETER DefaultOwnerName + The owner name that should be used with a command that takes OwnerName as a parameter + when no value has been supplied. + + .PARAMETER DefaultRepositoryName + The owner name that should be used with a command that takes RepositoryName as a parameter + when no value has been supplied. + + .PARAMETER DisableLogging + Specify this switch to stop the module from logging all activity to a log file located + at the location specified by LogPath. + + .PARAMETER DisablePiiProtection + Specify this switch to disable the hashing of potential PII data prior to submitting the + data to telemetry (if telemetry hasn't been disabled via DisableTelemetry). + + .PARAMETER DisableParameterLogging + Specify this switch to stop the module from logging every individual parameter value + when it logs a function call. + + .PARAMETER DisableSmarterObjects + By deffault, this module will modify all objects returned by the API calls to update + any properties that can be converted to objects (like strings for Date/Time's being + converted to real DateTime objects). Enable this property if you desire getting back + the unmodified version of the object from the API. + + .PARAMETER DisableTelemetry + Specify this switch to stop the module from reporting any of its usage (which would be used + for diagnostics purposes). + + .PARAMETER LogPath + The location of the log file that all activity will be written to if DisableLogging remains + $false. + + .PARAMETER LogProcessId + If specified, the Process ID of the current PowerShell session will be included in each + log entry. This can be useful if you have concurrent PowerShell sessions all logging + to the same file, as it would then be possible to filter results based on ProcessId. + + .PARAMETER LogTimeAsUtc + If specified, all times logged will be logged as UTC instead of the local timezone. + + .PARAMETER RetryDelaySeconds + The number of seconds to wait before retrying a command again after receiving a 202 response. + + .PARAMETER SuppressNoTokenWarning + If an Access Token has not been configured, this module will provide a warning to the user + informing them of this, once per session. If it is expected that this module will regularly + be used without configuring an Access Token, specify this switch to always suppress that + warning message. + + .PARAMETER SuppressTelemetryReminder + When telemetry is enabled, a warning will be printed to the console once per session + informing users that telemetry is occurring. Setting this value will suppress that + message from showing up ever again. + + .PARAMETER WebRequestTimeoutSec + The number of seconds that should be allowed before an API request times out. A value of + 0 indicates an infinite timeout, however experience has shown that PowerShell doesn't seem + to always honor inifinite timeouts. Hence, this value can be configured if need be. + + .PARAMETER SessionOnly + By default, this method will store the configuration values in a local file so that changes + persist across PowerShell sessions. If this switch is provided, the file will not be + created/updated and the specified configuration changes will only remain in memory/effect + for the duration of this PowerShell session. + + .EXAMPLE + Set-GitHubConfiguration -WebRequestTimeoutSec 120 -SuppressNoTokenWarning + + Changes the timeout permitted for a web request to two minutes, and additionally tells + the module to never warn about no Access Token being configured. These settings will be + persisted across future PowerShell sessions. + + .EXAMPLE + Set-GitHubConfiguration -DisableLogging -SessionOnly + + Disables the logging of any activity to the logfile specified in LogPath, but for this + session only. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $ApplicationInsightsKey, + + [string] $AssemblyPath, + + [switch] $DefaultNoStatus, + + [string] $DefaultOwnerName, + + [string] $DefaultRepositoryName, + + [switch] $DisableLogging, + + [switch] $DisablePiiProtection, + + [switch] $DisableParameterLogging, + + [switch] $DisableSmarterObjects, + + [switch] $DisableTelemetry, + + [string] $LogPath, + + [switch] $LogProcessId, + + [switch] $LogTimeAsUtc, + + [int] $RetryDelaySeconds, + + [switch] $SuppressNoTokenWarning, + + [switch] $SuppressTelemetryReminder, + + [ValidateRange(0, 3600)] + [int] $WebRequestTimeoutSec, + + [switch] $SessionOnly + ) + + $persistedConfig = $null + if (-not $SessionOnly) + { + $persistedConfig = Read-GitHubConfiguration -Path $script:configurationFilePath + } + + $properties = Get-Member -InputObject $script:configuration -MemberType NoteProperty | Select-Object -ExpandProperty Name + foreach ($name in $properties) + { + if ($PSBoundParameters.ContainsKey($name)) + { + $value = $PSBoundParameters.$name + if ($value -is [switch]) { $value = $value.ToBool() } + $script:configuration.$name = $value + + if (-not $SessionOnly) + { + Add-Member -InputObject $persistedConfig -Name $name -Value $value -MemberType NoteProperty -Force + } + } + } + + if (-not $SessionOnly) + { + Save-GitHubConfiguration -Configuration $persistedConfig -Path $script:configurationFilePath + } +} + +function Get-GitHubConfiguration +{ +<# + .SYNOPSIS + Gets the currently configured value for the requested configuration setting. + + .DESCRIPTION + Gets the currently configured value for the requested configuration setting. + + Always returns the value for this session, which may or may not be the persisted + setting (that all depends on whether or not the setting was previously modified + during this session using Set-GitHubCOnfiguration -SessionOnly). + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Name + The name of the configuration whose value is desired. + + .EXAMPLE + Get-GitHubConfiguration -Name WebRequestTimeoutSec + + Gets the currently configured value for WebRequestTimeoutSec for this PowerShell session + (which may or may not be the same as the persisted configuration value, depending on + whether this value was modified during this session with Set-GitHubConfiguration -SessionOnly). +#> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet( + 'ApplicationInsightsKey', + 'AssemblyPath', + 'DefaultNoStatus', + 'DefaultOwnerName', + 'DefaultRepositoryName', + 'DisableLogging', + 'DisableParameterLogging', + 'DisablePiiProtection', + 'DisableSmarterObjects', + 'DisableTelemetry', + 'LogPath', + 'LogProcessId', + 'LogTimeAsUtc', + 'RetryDelaySeconds', + 'SuppressNoTokenWarning', + 'SuppressTelemetryReminder', + 'WebRequestTimeoutSec')] + [string] $Name + ) + + return $script:configuration.$Name +} + +function Save-GitHubConfiguration +{ +<# + .SYNOPSIS + Serializes the provided settings object to disk as a JSON file. + + .DESCRIPTION + Serializes the provided settings object to disk as a JSON file. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Configuration + The configuration object to persist to disk. + + .PARAMETER Path + The path to the file on disk that Configuration should be persisted to. + + .NOTES + Internal helper method. + + .EXAMPLE + Save-GitHubConfiguration -Configuration $config -Path 'c:\foo\config.json' + + Serializes $config as a JSON object to 'c:\foo\config.json' +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [PSCustomObject] $Configuration, + + [Parameter(Mandatory)] + [string] $Path + ) + + $null = New-Item -Path $Path -Force + $Configuration | + ConvertTo-Json | + Set-Content -Path $Path -Force -ErrorAction SilentlyContinue -ErrorVariable ev + + if (($null -ne $ev) -and ($ev.Count -gt 0)) + { + Write-Log -Message "Failed to persist these updated settings to disk. They will remain for this PowerShell session only." -Level Warning -Exception $ev[0] + } +} + +function Test-PropertyExists +{ +<# + .SYNOPSIS + Determines if an object contains a property with a specified name. + + .DESCRIPTION + Determines if an object contains a property with a specified name. + + This is essentially using Get-Member to verify that a property exists, + but additionally adds a check to ensure that InputObject isn't null. + + .PARAMETER InputObject + The object to check to see if it has a property named Name. + + .PARAMETER Name + The name of the property on InputObject that is being tested for. + + .EXAMPLE + Test-PropertyExists -InputObject $listing -Name 'title' + + Returns $true if $listing is non-null and has a property named 'title'. + Returns $false otherwise. + + .NOTES + Internal-only helper method. +#> + [CmdletBinding()] + [OutputType([bool])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Exists isn't a noun and isn't violating the intention of this rule.")] + param( + [Parameter(Mandatory)] + [AllowNull()] + $InputObject, + + [Parameter(Mandatory)] + [String] $Name + ) + + return (($null -ne $InputObject) -and + ($null -ne (Get-Member -InputObject $InputObject -Name $Name -MemberType Properties))) +} + +function Resolve-PropertyValue +{ +<# + .SYNOPSIS + Returns the requested property from the provided object, if it exists and is a valid + value. Otherwise, returns the default value. + + .DESCRIPTION + Returns the requested property from the provided object, if it exists and is a valid + value. Otherwise, returns the default value. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER InputObject + The object to check the value of the requested property. + + .PARAMETER Name + The name of the property on InputObject whose value is desired. + + .PARAMETER Type + The type of the value stored in the Name property on InputObject. Used to validate + that the property has a valid value. + + .PARAMETER DefaultValue + The value to return if Name doesn't exist on InputObject or is of an invalid type. + + .EXAMPLE + Resolve-PropertyValue -InputObject $config -Name defaultOwnerName -Type String -DefaultValue $null + + Checks $config to see if it has a property named "defaultOwnerName". If it does, and it's a + string, returns that value, otherwise, returns $null (the DefaultValue). +#> + [CmdletBinding()] + param( + [PSCustomObject] $InputObject, + + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [ValidateSet('String', 'Boolean', 'Int32', 'Int64')] + [String] $Type, + + $DefaultValue + ) + + if ($null -eq $InputObject) + { + return $DefaultValue + } + + $typeType = [String] + if ($Type -eq 'Boolean') { $typeType = [Boolean] } + if ($Type -eq 'Int32') { $typeType = [Int32] } + if ($Type -eq 'Int64') { $typeType = [Int64] } + + if (Test-PropertyExists -InputObject $InputObject -Name $Name) + { + if ($InputObject.$Name -is $typeType) + { + return $InputObject.$Name + } + else + { + Write-Log "The locally cached $Name configuration was not of type $Type. Reverting to default value." -Level Warning + return $DefaultValue + } + } + else + { + return $DefaultValue + } +} + +function Reset-GitHubConfiguration +{ +<# + .SYNOPSIS + Clears out the user's configuration file and configures this session with all default + configuration values. + + .DESCRIPTION + Clears out the user's configuration file and configures this session with all default + configuration values. + + This would be the functional equivalent of using this on a completely different computer. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER SessionOnly + By default, this will delete the location configuration file so that all defaults are used + again. If this is specified, then only the configuration values that were made during + this session will be discarded. + + .EXAMPLE + Reset-GitHubConfiguration + + Deletes the local configuration file and loads in all default configration values. +#> + [CmdletBinding(SupportsShouldProcess)] + param( + [switch] $SessionOnly + ) + + Set-TelemetryEvent -EventName Reset-GitHubConfiguration + + if (-not $SessionOnly) + { + if ($PSCmdlet.ShouldProcess($script:configurationFilePath, "Delete configuration file")) + { + $null = Remove-Item -Path $script:configurationFilePath -Force -ErrorAction SilentlyContinue -ErrorVariable ev + } + + if (($null -ne $ev) -and ($ev.Count -gt 0) -and ($ev[0].FullyQualifiedErrorId -notlike 'PathNotFound*')) + { + Write-Log -Message "Reset was unsuccessful. Experienced a problem trying to remove the file [$script:configurationFilePath]." -Level Warning -Exception $ev[0] + } + } + + Initialize-GitHubConfiguration + + Write-Log -Message "This has not cleared your authentication token. Call Clear-GitHubAuthentication to accomplish that." -Level Warning +} + +function Read-GitHubConfiguration +{ +<# + .SYNOPSIS + Loads in the default configuration values and returns the deserialized object. + + .DESCRIPTION + Loads in the default configuration values and returns the deserialized object. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Path + The file that may or may not exist with a serialized version of the configuration + values for this module. + + .OUTPUTS + PSCustomObject + + .NOTES + Internal helper method. + No side-effects. + + .EXAMPLE + Read-GitHubConfiguration -Path 'c:\foo\config.json' + + Returns back an object with the deserialized object contained in the specified file, + if it exists and is valid. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $Path + ) + + $content = Get-Content -Path $Path -Encoding UTF8 -ErrorAction Ignore + if (-not [String]::IsNullOrEmpty($content)) + { + try + { + return ($content | ConvertFrom-Json) + } + catch + { + Write-Log -Message 'The configuration file for this module is in an invalid state. Use Reset-GitHubConfiguration to recover.' -Level Warning + } + } + + return [PSCustomObject]@{} +} + +function Import-GitHubConfiguration +{ +<# + .SYNOPSIS + Loads in the default configuration values, and then updates the individual properties + with values that may exist in a file. + + .DESCRIPTION + Loads in the default configuration values, and then updates the individual properties + with values that may exist in a file. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Path + The file that may or may not exist with a serialized version of the configuration + values for this module. + + .OUTPUTS + PSCustomObject + + .NOTES + Internal helper method. + No side-effects. + + .EXAMPLE + Import-GitHubConfiguration -Path 'c:\foo\config.json' + + Creates a new default config object and updates its values with any that are found + within a deserialized object from the content in $Path. The configuration object + is then returned. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $Path + ) + + # Create a configuration object with all the default values. We can then update the values + # with any that we find on disk. + $logPath = [String]::Empty + $documentsFolder = [System.Environment]::GetFolderPath('MyDocuments') + if (-not [System.String]::IsNullOrEmpty($documentsFolder)) + { + $logPath = Join-Path -Path $documentsFolder -ChildPath 'PowerShellForGitHub.log' + } + + $config = [PSCustomObject]@{ + 'applicationInsightsKey' = '66d83c52-3070-489b-886b-09860e05e78a' + 'assemblyPath' = [String]::Empty + 'disableLogging' = ([String]::IsNullOrEmpty($logPath)) + 'disableParameterLogging' = $false + 'disablePiiProtection' = $false + 'disableSmarterObjects' = $false + 'disableTelemetry' = $false + 'defaultNoStatus' = $false + 'defaultOwnerName' = [String]::Empty + 'defaultRepositoryName' = [String]::Empty + 'logPath' = $logPath + 'logProcessId' = $false + 'logTimeAsUtc' = $false + 'retryDelaySeconds' = 30 + 'suppressNoTokenWarning' = $false + 'suppressTelemetryReminder' = $false + 'webRequestTimeoutSec' = 0 + } + + $jsonObject = Read-GitHubConfiguration -Path $Path + Get-Member -InputObject $config -MemberType NoteProperty | + ForEach-Object { + $name = $_.Name + $type = $config.$name.GetType().Name + $config.$name = Resolve-PropertyValue -InputObject $jsonObject -Name $name -Type $type -DefaultValue $config.$name + } + + return $config +} + +function Backup-GitHubConfiguration +{ +<# + .SYNOPSIS + Exports the user's current configuration file. + + .DESCRIPTION + Exports the user's current configuration file. + + This is primarily used for unit testing scenarios. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Path + The path to store the user's current configuration file. + + .PARAMETER Force + If specified, will overwrite the contents of any file with the same name at th + location specified by Path. + + .EXAMPLE + Backup-GitHubConfiguration -Path 'c:\foo\config.json' + + Writes the user's current configuration file to c:\foo\config.json. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $Path, + + [switch] $Force + ) + + # Make sure that the path that we're going to be storing the file exists. + $null = New-Item -Path (Split-Path -Path $Path -Parent) -ItemType Directory -Force + + if (Test-Path -Path $script:configurationFilePath -PathType Leaf) + { + $null = Copy-Item -Path $script:configurationFilePath -Destination $Path -Force:$Force + } + else + { + @{} | ConvertTo-Json | Set-Content -Path $Path -Force:$Force + } +} + +function Restore-GitHubConfiguration +{ +<# + .SYNOPSIS + Sets the specified file to be the user's configuration file. + + .DESCRIPTION + Sets the specified file to be the user's configuration file. + + This is primarily used for unit testing scenarios. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Path + The path to store the user's current configuration file. + + .EXAMPLE + Restore-GitHubConfiguration -Path 'c:\foo\config.json' + + Makes the contents of c:\foo\config.json be the user's configuration for the module. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [ValidateScript({if (Test-Path -Path $_ -PathType Leaf) { $true } else { throw "$_ does not exist." }})] + [string] $Path + ) + + # Make sure that the path that we're going to be storing the file exists. + $null = New-Item -Path (Split-Path -Path $script:configurationFilePath -Parent) -ItemType Directory -Force + + $null = Copy-Item -Path $Path -Destination $script:configurationFilePath -Force + + Initialize-GitHubConfiguration +} + +function Resolve-ParameterWithDefaultConfigurationValue +{ +<# + .SYNOPSIS + Some of the configuration properties act as default values to be used for some functions. + This will determine what the correct final value should be by inspecting the calling + functions inbound parameters, along with the corresponding configuration value. + + .DESCRIPTION + Some of the configuration properties act as default values to be used for some functions. + This will determine what the correct final value should be by inspecting the calling + functions inbound parameters, along with the corresponding configuration value. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER BoundParameters + The inbound parameters from the calling method. + + .PARAMETER Name + The name of the parameter in BoundParameters. + + .PARAMETER ConfigValueName + The name of the cofiguration property that should be used as default if Name doesn't exist + in BoundParameters. + + .PARAMETER NonEmptyStringRequired + If specified, will throw an exception if the resolved value to be returned would end up + being null or an empty string. + + .EXAMPLE + Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus + + Checks to see if the NoStatus switch was provided by the user from the calling method. If + so, uses that value. otherwise uses the DefaultNoStatus value currently configured. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + $BoundParameters, + + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [String] $ConfigValueName, + + [switch] $NonEmptyStringRequired + ) + + $value = $null + if ($BoundParameters.ContainsKey($Name)) + { + $value = $BoundParameters[$Name] + } + else + { + $value = (Get-GitHubConfiguration -Name $ConfigValueName) + } + + if ($NonEmptyStringRequired -and [String]::IsNullOrEmpty($value)) + { + $message = "A value must be provided for $Name either as a parameter, or as a default configuration value ($ConfigValueName) via Set-GitHubConfiguration." + Write-Log -Message $message -Level Error + throw $message + } + else + { + return $value + } +} + +function Set-GitHubAuthentication +{ +<# + .SYNOPSIS + Allows the user to configure the API token that should be used for authentication + with the GitHub API. + + .DESCRIPTION + Allows the user to configure the API token that should be used for authentication + with the GitHub API. + + The token will be stored on the machine as a SecureString and will automatically + be read on future PowerShell sessions with this module. If the user ever wishes + to remove their authentication from the system, they simply need to call + Clear-GitHubAuthentication. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Credential + If provided, instead of prompting the user for their API Token, it will be extracted + from the password field of this credential object. + + .PARAMETER SessionOnly + By default, this method will store the provided API Token as a SecureString in a local + file so that it can be restored automatically in future PowerShell sessions. If this + switch is provided, the file will not be created/updated and the authentication information + will only remain in memory for the duration of this PowerShell session. + + .EXAMPLE + Set-GitHubAuthentication + + Prompts the user for their GitHub API Token and stores it in a file on the machine as a + SecureString for use in future PowerShell sessions. + + .EXAMPLE + $secureString = ("" | ConvertTo-SecureString) + $cred = New-Object System.Management.Automation.PSCredential "username is ignored", $secureString + Set-GitHubAuthentication -Credential $cred + + Uses the API token stored in the password field of the provided credential object for + authentication, and stores it in a file on the machine as a SecureString for use in + future PowerShell sessions. + + .EXAMPLE + Set-GitHubAuthentication -SessionOnly + + Prompts the user for their GitHub API Token, but keeps it in memory only for the duration + of this PowerShell session. + + .EXAMPLE + Set-GitHubAuthentication -Credential $cred -SessionOnly + + Uses the API token stored in the password field of the provided credential object for + authentication, but keeps it in memory only for the duration of this PowerShell session.. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUsePSCredentialType", "", Justification="The System.Management.Automation.Credential() attribute does not appear to work in PowerShell v4 which we need to support.")] + param( + [PSCredential] $Credential, + + [switch] $SessionOnly + ) + + Write-InvocationLog -Invocation $MyInvocation + + if (-not $PSBoundParameters.ContainsKey('Credential')) + { + $message = 'Please provide your GitHub API Token in the Password field. You can enter anything in the username field (it will be ignored).' + if (-not $SessionOnly) + { + $message = $message + ' ***The token is being cached across PowerShell sessions. To clear caching, call Clear-GitHubAuthentication.***' + } + + Write-Log -Message $message + $Credential = Get-Credential -Message $message + } + + if ([String]::IsNullOrWhiteSpace($Credential.GetNetworkCredential().Password)) + { + $message = "The API Token was not provided in the password field. Nothing to do." + Write-Log -Message $message -Level Error + throw $message + } + + $script:accessTokenCredential = $Credential + if (-not $SessionOnly) + { + if ($PSCmdlet.ShouldProcess("Store API token as a SecureString in a local file")) + { + $null = New-Item -Path $script:accessTokenFilePath -Force + $script:accessTokenCredential.Password | + ConvertFrom-SecureString | + Set-Content -Path $script:accessTokenFilePath -Force + } + } +} + +function Clear-GitHubAuthentication +{ +<# + .SYNOPSIS + Clears out any GitHub API token from memory, as well as from local file storage. + + .DESCRIPTION + Clears out any GitHub API token from memory, as well as from local file storage. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER SessionOnly + By default, this will clear out the cache in memory, as well as in the local + configuration file. If this switch is specified, authentication will be cleared out + in this session only -- the local configuration file cache will remain + (and thus still be available in a new PowerShell session). + + .EXAMPLE + Clear-GitHubAuthentication + + Clears out any GitHub API token from memory, as well as from local file storage. +#> + [CmdletBinding(SupportsShouldProcess)] + param( + [switch] $SessionOnly + ) + + Write-InvocationLog -Invocation $MyInvocation + + Set-TelemetryEvent -EventName Clear-GitHubAuthentication + + if ($PSCmdlet.ShouldProcess("Clear memory cache")) + { + $script:accessTokenCredential = $null + } + + if (-not $SessionOnly) + { + if ($PSCmdlet.ShouldProcess("Clear file-based cache")) + { + Remove-Item -Path $script:accessTokenFilePath -Force -ErrorAction SilentlyContinue -ErrorVariable ev + + if (($null -ne $ev) -and ($ev.Count -gt 0) -and ($ev[0].FullyQualifiedErrorId -notlike 'PathNotFound*')) + { + Write-Log -Message "Experienced a problem trying to remove the file that persists the Access Token [$script:accessTokenFilePath]." -Level Warning -Exception $ev[0] + } + } + } + + Write-Log -Message "This has not cleared your configuration settings. Call Reset-GitHubConfiguration to accomplish that." -Level Warning +} + +function Get-AccessToken +{ +<# + .SYNOPSIS + Retrieves the API token for use in the rest of the module. + + .DESCRIPTION + Retrieves the API token for use in the rest of the module. + + First will try to use the one that may have been provided as a parameter. + If not provided, then will try to use the one already cached in memory. + If still not found, will look to see if there is a file with the API token stored + as a SecureString. + Finally, if there is still no available token, none will be used. The user will then be + subjected to tighter hourly query restrictions. + + The Git repo for this module can be found here: http://aka.ms/PowershellForGitHub + + .PARAMETER AccessToken + If provided, this will be returned instead of using the cached/configured value + + .OUTPUTS + System.String +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="For back-compat with v0.1.0, this still supports the deprecated method of using a global variable for storing the Access Token.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [OutputType([String])] + param( + [string] $AccessToken + ) + + if (-not [String]::IsNullOrEmpty($AccessToken)) + { + return $AccessToken + } + + if ($null -ne $script:accessTokenCredential) + { + $token = $script:accessTokenCredential.GetNetworkCredential().Password + + if (-not [String]::IsNullOrEmpty($token)) + { + return $token + } + } + + $content = Get-Content -Path $script:accessTokenFilePath -ErrorAction Ignore + if (-not [String]::IsNullOrEmpty($content)) + { + try + { + $secureString = $content | ConvertTo-SecureString + + Write-Log -Message "Restoring Access Token from file. This value can be cleared in the future by calling Clear-GitHubAuthentication." -Level Verbose + $script:accessTokenCredential = New-Object System.Management.Automation.PSCredential "", $secureString + return $script:accessTokenCredential.GetNetworkCredential().Password + } + catch + { + Write-Log -Message 'The Access Token file for this module contains an invalid SecureString (files can''t be shared by users or computers). Use Set-GitHubAuthentication to update it.' -Level Warning + } + } + + if (-not [String]::IsNullOrEmpty($global:gitHubApiToken)) + { + Write-Log -Message 'Storing the Access Token in `$global:gitHubApiToken` is insecure and is no longer recommended. To cache your Access Token for use across future PowerShell sessions, please use Set-GitHubAuthentication instead.' -Level Warning + return $global:gitHubApiToken + } + + if ((-not (Get-GitHubConfiguration -Name SuppressNoTokenWarning)) -and + (-not $script:seenTokenWarningThisSession)) + { + $script:seenTokenWarningThisSession = $true + Write-Log -Message 'This module has not yet been configured with a personal GitHub Access token. The module can still be used, but GitHub will limit your usage to 60 queries per hour. You can get a GitHub API token from https://github.com/settings/tokens/new (provide a description and check any appropriate scopes).' -Level Warning + } + + return $null +} + +function Test-GitHubAuthenticationConfigured +{ +<# + .SYNOPSIS + Indicates if a GitHub API Token has been configured for this module via Set-GitHubAuthentication. + + .DESCRIPTION + Indicates if a GitHub API Token has been configured for this module via Set-GitHubAuthentication. + + The Git repo for this module can be found here: http://aka.ms/PowershellForGitHub + + .OUTPUTS + Boolean + + .EXAMPLE + Test-GitHubAuthenticationConfigured + + Returns $true if the session is authenticated; $false otherwise +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [OutputType([Boolean])] + param() + + return (-not [String]::IsNullOrWhiteSpace((Get-AccessToken))) +} + +Initialize-GitHubConfiguration diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 new file mode 100644 index 00000000..7e881cbf --- /dev/null +++ b/GitHubCore.ps1 @@ -0,0 +1,876 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$script:gitHubApiUrl = "https://api.github.com" +$script:gitHubApiReposUrl = "https://api.github.com/repos" +$script:gitHubApiOrgsUrl = "https://api.github.com/orgs" + +$script:defaultAcceptHeader = 'application/vnd.github.v3+json' + +function Invoke-GHRestMethod +{ +<# + .SYNOPSIS + A wrapper around Invoke-WebRequest that understands the Store API. + + .DESCRIPTION + A very heavy wrapper around Invoke-WebRequest that understands the Store API and + how to perform its operation with and without console status updates. It also + understands how to parse and handle errors from the REST calls. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER UriFragment + The unique, tail-end, of the REST URI that indicates what Store REST action will + be peformed. This should not start with a leading "/". + + .PARAMETER Method + The type of REST method being peformed. This only supports a reduced set of the + possible REST methods (delete, get, post, put). + + .PARAMETER Description + A friendly description of the operation being performed for logging and console + display purposes. + + .PARAMETER Body + This optional parameter forms the body of a PUT or POST request. It will be automatically + encoded to UTF8 and sent as Content Type: "application/json; charset=UTF-8" + + .PARAMETER AcceptHeader + Specify the media type in the Accept header. Different types of commands may require + different media types. + + .PARAMETER ExtendedResult + If specified, the result will be a PSObject that contains the normal result, along with + the response code and other relevant header detail content. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api as opposed to requesting a new one. + + .PARAMETER TelemetryEventName + If provided, the successful execution of this REST command will be logged to telemetry + using this event name. + + .PARAMETER TelemetryProperties + If provided, the successful execution of this REST command will be logged to telemetry + with these additional properties. This will be silently ignored if TelemetryEventName + is not provided as well. + + .PARAMETER TelemetryExceptionBucket + If provided, any exception that occurs will be logged to telemetry using this bucket. + It's possible that users will wish to log exceptions but not success (by providing + TelemetryEventName) if this is being executed as part of a larger scenario. If this + isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be + used as the exception bucket value in the event of an exception. If neither is specified, + no bucket value will be used. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .OUTPUTS + [PSCutomObject] - The result of the REST operation, in whatever form it comes in. + + .EXAMPLE + Invoke-GHRestMethod -UriFragment "applications/" -Method Get -Description "Get first 10 applications" + + Gets the first 10 applications for the connected dev account. + + .EXAMPLE + Invoke-GHRestMethod -UriFragment "applications/0ABCDEF12345/submissions/1234567890123456789/" -Method Delete -Description "Delete Submission" -NoStatus + + Deletes the specified submission, but the request happens in the foreground and there is + no additional status shown to the user until a response is returned from the REST request. + + .NOTES + This wraps Invoke-WebRequest as opposed to Invoke-RestMethod because we want access to the headers + that are returned in the response (specifically 'MS-ClientRequestId') for logging purposes, and + Invoke-RestMethod drops those headers. +#> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $UriFragment, + + [Parameter(Mandatory)] + [ValidateSet('delete', 'get', 'post', 'patch', 'put')] + [string] $Method, + + [string] $Description, + + [string] $Body = $null, + + [string] $AcceptHeader = $script:defaultAcceptHeader, + + [switch] $ExtendedResult, + + [string] $AccessToken, + + [string] $TelemetryEventName = $null, + + [hashtable] $TelemetryProperties = @{}, + + [string] $TelemetryExceptionBucket = $null, + + [switch] $NoStatus + ) + + # Normalize our Uri fragment. It might be coming from a method implemented here, or it might + # be coming from the Location header in a previous response. Either way, we don't want there + # to be a leading "/" or trailing '/' + if ($UriFragment.StartsWith('/')) { $UriFragment = $UriFragment.Substring(1) } + if ($UriFragment.EndsWIth('/')) { $UriFragment = $UriFragment.Substring(0, $UriFragment.Length - 1) } + + if ([String]::IsNullOrEmpty($Description)) + { + $Description = "Executing: $UriFragment" + } + + # Telemetry-related + $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch + $localTelemetryProperties = @{ + 'UriFragment' = $UriFragment + 'WaitForCompletion' = ($WaitForCompletion -eq $true) + } + $TelemetryProperties.Keys | ForEach-Object { $localTelemetryProperties[$_] = $TelemetryProperties[$_] } + $errorBucket = $TelemetryExceptionBucket + if ([String]::IsNullOrEmpty($errorBucket)) + { + $errorBucket = $TelemetryEventName + } + + # Since we have retry logic, we won't create a new stopwatch every time, + # we'll just always continue the existing one... + $stopwatch.Start() + + $url = "$script:gitHubApiUrl/$UriFragment" + + # It's possible that we are directly calling the "nextLink" from a previous command which + # provides the full URI. If that's the case, we'll just use exactly what was provided to us. + if ($UriFragment.StartsWith('http')) + { + $url = $UriFragment + } + + $headers = @{ + 'Accept' = $AcceptHeader + 'User-Agent' = 'PowerShellForGitHub' + } + + $AccessToken = Get-AccessToken -AccessToken $AccessToken + if (-not [String]::IsNullOrEmpty($AccessToken)) + { + $headers['Authorization'] = "token $AccessToken" + } + + if ($Method -in ('post', 'patch', 'put')) + { + $headers.Add("Content-Type", "application/json; charset=UTF-8") + } + + try + { + Write-Log -Message $Description -Level Verbose + Write-Log -Message "Accessing [$Method] $url [Timeout = $(Get-GitHubConfiguration -Name WebRequestTimeoutSec))]" -Level Verbose + + $NoStatus = Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus + if ($NoStatus) + { + if ($PSCmdlet.ShouldProcess($url, "Invoke-WebRequest")) + { + $params = @{} + $params.Add("Uri", $url) + $params.Add("Method", $Method) + $params.Add("Headers", $headers) + $params.Add("UseDefaultCredentials", $true) + $params.Add("UseBasicParsing", $true) + $params.Add("TimeoutSec", (Get-GitHubConfiguration -Name WebRequestTimeoutSec)) + + if ($Method -in ('post', 'put', 'patch') -and (-not [String]::IsNullOrEmpty($Body))) + { + $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) + $params.Add("Body", $bodyAsBytes) + Write-Log -Message "Request includes a body." -Level Verbose + } + + [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + $result = Invoke-WebRequest @params + if ($Method -eq 'delete') + { + Write-Log -Message "Successfully removed." -Level Verbose + } + } + } + else + { + $jobName = "Invoke-GHRestMethod-" + (Get-Date).ToFileTime().ToString() + + if ($PSCmdlet.ShouldProcess($jobName, "Start-Job")) + { + [scriptblock]$scriptBlock = { + param($Url, $method, $Headers, $Body, $TimeoutSec, $ScriptRootPath) + + # We need to "dot invoke" Helpers.ps1 and GitHubConfiguration.ps1 within + # the context of this script block since we're running in a different + # PowerShell process and need access to Get-HttpWebResponseContent and + # config values referenced within Write-Log. + . (Join-Path -Path $ScriptRootPath -ChildPath 'Helpers.ps1') + . (Join-Path -Path $ScriptRootPath -ChildPath 'GitHubConfiguration.ps1') + + $params = @{} + $params.Add("Uri", $Url) + $params.Add("Method", $Method) + $params.Add("Headers", $Headers) + $params.Add("UseDefaultCredentials", $true) + $params.Add("UseBasicParsing", $true) + $params.Add("TimeoutSec", $TimeoutSec) + + if ($Method -in ('post', 'put', 'patch') -and (-not [String]::IsNullOrEmpty($Body))) + { + $bodyAsBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) + $params.Add("Body", $bodyAsBytes) + Write-Log -Message "Request includes a body." -Level Verbose + } + + try + { + [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest @params + } + catch [System.Net.WebException] + { + # We need to access certain headers in the exception handling, + # but the actual *values* of the headers of a WebException don't get serialized + # when the RemoteException wraps it. To work around that, we'll extract the + # information that we actually care about *now*, and then we'll throw our own exception + # that is just a JSON object with the data that we'll later extract for processing in + # the main catch. + $ex = @{} + $ex.Message = $_.Exception.Message + $ex.StatusCode = $_.Exception.Response.StatusCode + $ex.StatusDescription = $_.Exception.Response.StatusDescription + $ex.InnerMessage = $_.ErrorDetails.Message + try + { + $ex.RawContent = Get-HttpWebResponseContent -WebResponse $_.Exception.Response + } + catch + { + Write-Log -Message "Unable to retrieve the raw HTTP Web Response:" -Exception $_ -Level Warning + } + + throw ($ex | ConvertTo-Json -Depth 20) + } + } + + $null = Start-Job -Name $jobName -ScriptBlock $scriptBlock -Arg @($url, $Method, $headers, $Body, (Get-GitHubConfiguration -Name WebRequestTimeoutSec), $PSScriptRoot) + + if ($PSCmdlet.ShouldProcess($jobName, "Wait-JobWithAnimation")) + { + Wait-JobWithAnimation -Name $jobName -Description $Description + } + + if ($PSCmdlet.ShouldProcess($jobName, "Receive-Job")) + { + $result = Receive-Job $jobName -AutoRemoveJob -Wait -ErrorAction SilentlyContinue -ErrorVariable remoteErrors + } + } + + if ($remoteErrors.Count -gt 0) + { + throw $remoteErrors[0].Exception + } + + if ($Method -eq 'delete') + { + Write-Log -Message "Successfully removed." -Level Verbose + } + } + + # Record the telemetry for this event. + $stopwatch.Stop() + if (-not [String]::IsNullOrEmpty($TelemetryEventName)) + { + $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $localTelemetryProperties -Metrics $telemetryMetrics + } + + $finalResult = $result.Content + try + { + $finalResult = $finalResult | ConvertFrom-Json + } + catch [ArgumentException] + { + # The content must not be JSON (which is a legitimate situation). We'll return the raw content result instead. + # We do this unnecessary assignment to avoid PSScriptAnalyzer's PSAvoidUsingEmptyCatchBlock. + $finalResult = $finalResult + } + + if (-not (Get-GitHubConfiguration -Name DisableSmarterObjects)) + { + $finalResult = ConvertTo-SmarterObject -InputObject $finalResult + } + + $links = $result.Headers['Link'] -split ',' + $nextLink = $null + foreach ($link in $links) + { + if ($link -match '<(.*)>; rel="next"') + { + $nextLink = $matches[1] + } + } + + $resultNotReadyStatusCode = 202 + if ($result.StatusCode -eq $resultNotReadyStatusCode) + { + $retryDelaySeconds = Get-GitHubConfiguration -Name RetryDelaySeconds + if ($retryDelaySeconds -gt 0) + { + Write-Log -Message "The server has indicated that the result is not yet ready (received status code of [$($result.StatusCode)]). Will retry in [$retryDelaySeconds] seconds." -Level Warning + Start-Sleep -Seconds ($retryDelaySeconds) + return (Invoke-GHRestMethod @PSBoundParameters) + } + else + { + Write-Log -Message "The server has indicated that the result is not yet ready (received status code of [$($result.StatusCode)]), however the module is currently configured to not retry in this scenario (RetryDelaySeconds is set to 0). Please try this command again later." -Level Warning + } + } + + if ($ExtendedResult) + { + $finalResultEx = @{ + 'result' = $finalResult + 'statusCode' = $result.StatusCode + 'requestId' = $result.Headers['X-GitHub-Request-Id'] + 'nextLink' = $nextLink + 'link' = $result.Headers['Link'] + 'lastModified' = $result.Headers['Last-Modified'] + 'ifNoneMatch' = $result.Headers['If-None-Match'] + 'ifModifiedSince' = $result.Headers['If-Modified-Since'] + 'eTag' = $result.Headers['ETag'] + 'rateLimit' = $result.Headers['X-RateLimit-Limit'] + 'rateLimitRemaining' = $result.Headers['X-RateLimit-Remaining'] + 'rateLimitReset' = $result.Headers['X-RateLimit-Reset'] + } + + return ([PSCustomObject] $finalResultEx) + } + else + { + return $finalResult + } + } + catch + { + # We only know how to handle WebExceptions, which will either come in "pure" when running with -NoStatus, + # or will come in as a RemoteException when running normally (since it's coming from the asynchronous Job). + $ex = $null + $message = $null + $statusCode = $null + $statusDescription = $null + $requestId = $null + $innerMessage = $null + $rawContent = $null + + if ($_.Exception -is [System.Net.WebException]) + { + $ex = $_.Exception + $message = $_.Exception.Message + $statusCode = $ex.Response.StatusCode.value__ # Note that value__ is not a typo. + $statusDescription = $ex.Response.StatusDescription + $innerMessage = $_.ErrorDetails.Message + try + { + $rawContent = Get-HttpWebResponseContent -WebResponse $ex.Response + } + catch + { + Write-Log -Message "Unable to retrieve the raw HTTP Web Response:" -Exception $_ -Level Warning + } + + if ($ex.Response.Headers.Count -gt 0) + { + $requestId = $ex.Response.Headers['X-GitHub-Request-Id'] + } + } + elseif (($_.Exception -is [System.Management.Automation.RemoteException]) -and + ($_.Exception.SerializedRemoteException.PSObject.TypeNames[0] -eq 'Deserialized.System.Management.Automation.RuntimeException')) + { + $ex = $_.Exception + try + { + $deserialized = $ex.Message | ConvertFrom-Json + $message = $deserialized.Message + $statusCode = $deserialized.StatusCode + $statusDescription = $deserialized.StatusDescription + $innerMessage = $deserialized.InnerMessage + $requestId = $deserialized['X-GitHub-Request-Id'] + $rawContent = $deserialized.RawContent + } + catch [System.ArgumentException] + { + # Will be thrown if $ex.Message isn't JSON content + Write-Log -Exception $_ -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + throw + } + } + else + { + Write-Log -Exception $_ -Level Error + Set-TelemetryException -Exception $_.Exception -ErrorBucket $errorBucket -Properties $localTelemetryProperties + throw + } + + $output = @() + $output += $message + + if (-not [string]::IsNullOrEmpty($statusCode)) + { + $output += "$statusCode | $($statusDescription.Trim())" + } + + if (-not [string]::IsNullOrEmpty($innerMessage)) + { + try + { + $innerMessageJson = ($innerMessage | ConvertFrom-Json) + if ($innerMessageJson -is [String]) + { + $output += $innerMessageJson.Trim() + } + elseif (-not [String]::IsNullOrWhiteSpace($innerMessageJson.message)) + { + $output += "$($innerMessageJson.message.Trim()) | $($innerMessageJson.documentation_url.Trim())" + if ($innerMessageJson.details) + { + $output += "$($innerMessageJson.details | Format-Table | Out-String)" + } + } + else + { + # In this case, it's probably not a normal message from the API + $output += ($innerMessageJson | Out-String) + } + } + catch [System.ArgumentException] + { + # Will be thrown if $innerMessage isn't JSON content + $output += $innerMessage.Trim() + } + } + + # It's possible that the API returned JSON content in its error response. + if (-not [String]::IsNullOrWhiteSpace($rawContent)) + { + $output += $rawContent + } + + if ($statusCode -eq 404) + { + $output += "This typically happens when the current user isn't properly authenticated. You may need an Access Token with additional scopes checked." + } + + if (-not [String]::IsNullOrEmpty($requestId)) + { + $localTelemetryProperties['RequestId'] = $requestId + $message = 'RequestId: ' + $requestId + $output += $message + Write-Log -Message $message -Level Verbose + } + + $newLineOutput = ($output -join [Environment]::NewLine) + Write-Log -Message $newLineOutput -Level Error + Set-TelemetryException -Exception $ex -ErrorBucket $errorBucket -Properties $localTelemetryProperties + throw $newLineOutput + } +} + +function Invoke-GHRestMethodMultipleResult +{ +<# + .SYNOPSIS + A special-case wrapper around Invoke-GHRestMethod that understands GET URI's + which support the 'top' and 'max' parameters. + + .DESCRIPTION + A special-case wrapper around Invoke-GHRestMethod that understands GET URI's + which support the 'top' and 'max' parameters. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER UriFragment + The unique, tail-end, of the REST URI that indicates what Store REST action will + be peformed. This should *not* include the 'top' and 'max' parameters. These + will be automatically added as needed. + + .PARAMETER Description + A friendly description of the operation being performed for logging and console + display purposes. + + .PARAMETER AcceptHeader + Specify the media type in the Accept header. Different types of commands may require + different media types. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api as opposed to requesting a new one. + + .PARAMETER TelemetryEventName + If provided, the successful execution of this REST command will be logged to telemetry + using this event name. + + .PARAMETER TelemetryProperties + If provided, the successful execution of this REST command will be logged to telemetry + with these additional properties. This will be silently ignored if TelemetryEventName + is not provided as well. + + .PARAMETER TelemetryExceptionBucket + If provided, any exception that occurs will be logged to telemetry using this bucket. + It's possible that users will wish to log exceptions but not success (by providing + TelemetryEventName) if this is being executed as part of a larger scenario. If this + isn't provided, but TelemetryEventName *is* provided, then TelemetryEventName will be + used as the exception bucket value in the event of an exception. If neither is specified, + no bucket value will be used. + + .PARAMETER SinglePage + By default, this function will automtically call any follow-up "nextLinks" provided by + the return value in order to retrieve the entire result set. If this switch is provided, + only the first "page" of results will be retrieved, and the "nextLink" links will not be + followed. + WARNING: This might take a while depending on how many results there are. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .OUTPUTS + [PSCutomObject[]] - The result of the REST operation, in whatever form it comes in. + + .EXAMPLE + Invoke-GHRestMethodMultipleResult -UriFragment "repos/PowerShell/PowerShellForGitHub/issues?state=all" -Description "Get all issues" + + Gets the first set of issues associated with this project, + with the console window showing progress while awaiting the response + from the REST request. + + .EXAMPLE + Invoke-GHRestMethodMultipleResult -UriFragment "repos/PowerShell/PowerShellForGitHub/issues?state=all" -Description "Get all issues" -NoStatus + + Gets the first set of issues associated with this project, + but the request happens in the foreground and there is no additional status + shown to the user until a response is returned from the REST request. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [OutputType([Object[]])] + param( + [Parameter(Mandatory)] + [string] $UriFragment, + + [Parameter(Mandatory)] + [string] $Description, + + [string] $AcceptHeader = $script:defaultAcceptHeader, + + [string] $AccessToken, + + [string] $TelemetryEventName = $null, + + [hashtable] $TelemetryProperties = @{}, + + [string] $TelemetryExceptionBucket = $null, + + [switch] $SinglePage, + + [switch] $NoStatus + ) + + $AccessToken = Get-AccessToken -AccessToken $AccessToken + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $errorBucket = $TelemetryExceptionBucket + if ([String]::IsNullOrEmpty($errorBucket)) + { + $errorBucket = $TelemetryEventName + } + + $finalResult = @() + + $currentDescription = $Description + $nextLink = $UriFragment + + try + { + do { + $params = @{ + 'UriFragment' = $nextLink + 'Method' = 'Get' + 'Description' = $currentDescription + 'AcceptHeader' = $AcceptHeader + 'ExtendedResult' = $true + 'AccessToken' = $AccessToken + 'TelemetryProperties' = $telemetryProperties + 'TelemetryExceptionBucket' = $errorBucket + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + $result = Invoke-GHRestMethod @params + $finalResult += $result.result + $nextLink = $result.nextLink + $currentDescription = "$Description (getting additional results)" + } + until ($SinglePage -or ([String]::IsNullOrWhiteSpace($nextLink))) + + # Record the telemetry for this event. + $stopwatch.Stop() + if (-not [String]::IsNullOrEmpty($TelemetryEventName)) + { + $telemetryMetrics = @{ 'Duration' = $stopwatch.Elapsed.TotalSeconds } + Set-TelemetryEvent -EventName $TelemetryEventName -Properties $TelemetryProperties -Metrics $telemetryMetrics + } + + # Ensure we're always returning our results as an array, even if there is a single result. + return @($finalResult) + } + catch + { + throw + } +} + +function Split-GitHubUri +{ +<# + .SYNOPSIS + Extracts the relevant elements of a GitHub repository Uri and returns the requested element. + + .DESCRIPTION + Extracts the relevant elements of a GitHub repository Uri and returns the requested element. + + Currently supports retrieving the OwnerName and the RepositoryName, when avaialable. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Uri + The GitHub repository Uri whose components should be returned. + + .PARAMETER OwnerName + Returns the Owner Name from the Uri if it can be identified. + + .PARAMETER RepositoryName + Returns the Repository Name from the Uri if it can be identified. + + .OUTPUTS + [PSCutomObject] - The OwnerName and RepositoryName elements from the provided URL + + .EXAMPLE + Split-GitHubUri -Uri 'https://github.com/PowerShell/PowerShellForGitHub' + + PowerShellForGitHub + + .EXAMPLE + Split-GitHubUri -Uri 'https://github.com/PowerShell/PowerShellForGitHub' -RepositoryName + + PowerShellForGitHub + + .EXAMPLE + Split-GitHubUri -Uri 'https://github.com/PowerShell/PowerShellForGitHub' -OwnerName + + PowerShell +#> + [CmdletBinding(DefaultParametersetName='RepositoryName')] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Uri, + + [Parameter(ParameterSetName='OwnerName')] + [switch] $OwnerName, + + [Parameter(ParameterSetName='RepositoryName')] + [switch] $RepositoryName + ) + + $components = @{ + ownerName = [String]::Empty + repositoryName = [String]::Empty + } + + if ($Uri -match '^https?://(?:www.)?github.com/([^/]+)/?([^/]+)?(?:/.*)?$') + { + $components.ownerName = $Matches[1] + if ($Matches.Count -gt 2) + { + $components.repositoryName = $Matches[2] + } + } + + if ($OwnerName) + { + return $components.ownerName + } + elseif ($RepositoryName -or ($PSCmdlet.ParameterSetName -eq 'RepositoryName')) + { + return $components.repositoryName + } +} + +function Resolve-RepositoryElements +{ +<# + .SYNOPSIS + Determines the OwnerName and RepositoryName from the possible parameter values. + + .DESCRIPTION + Determines the OwnerName and RepositoryName from the possible parameter values. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER BoundParameters + The inbound parameters from the calling method. + This is expecting values that may include 'Uri', 'OwnerName' and 'RepositoryName' + + .PARAMETER DisableValidation + By default, this function ensures that it returns with all elements provided, + otherwise an exception is thrown. If this is specified, that validation will + not occur, and it's possible to receive a result where one or more elements + have no value. + + .OUTPUTS + [PSCutomObject] - The OwnerName and RepositoryName elements to be used +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="This was the most accurate name that I could come up with. Internal only anyway.")] + param + ( + [Parameter(Mandatory)] + $BoundParameters, + + [switch] $DisableValidation + ) + + $validate = -not $DisableValidation + $elements = @{} + + if ($BoundParameters.ContainsKey('Uri') -and + ($BoundParameters.ContainsKey('OwnerName') -or $BoundParameters.ContainsKey('RepositoryName'))) + { + $message = "Cannot specify a Uri AND individual OwnerName/RepositoryName. Please choose one or the other." + Write-Log -Message $message -Level Error + throw $message + } + + if ($BoundParameters.ContainsKey('Uri')) + { + $elements.ownerName = Split-GitHubUri -Uri $BoundParameters.Uri -OwnerName + if ($validate -and [String]::IsNullOrEmpty($elements.ownerName)) + { + $message = "Provided Uri does not contain enough information: Owner Name." + Write-Log -Message $message -Level Error + throw $message + } + + $elements.repositoryName = Split-GitHubUri -Uri $BoundParameters.Uri -RepositoryName + if ($validate -and [String]::IsNullOrEmpty($elements.repositoryName)) + { + $message = "Provided Uri does not contain enough information: Repository Name." + Write-Log -Message $message -Level Error + throw $message + } + } + else + { + $elements.ownerName = Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $BoundParameters -Name OwnerName -ConfigValueName DefaultOwnerName -NonEmptyStringRequired:$validate + $elements.repositoryName = Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $BoundParameters -Name RepositoryName -ConfigValueName DefaultRepositoryName -NonEmptyStringRequired:$validate + } + + return ([PSCustomObject] $elements) +} + +# The list of property names across all of GitHub API v3 that are known to store dates as strings. +$script:datePropertyNames = @( + 'closed_at', + 'committed_at', + 'completed_at', + 'created_at', + 'date', + 'due_on', + 'last_edited_at', + 'last_read_at', + 'merged_at', + 'published_at', + 'pushed_at', + 'starred_at', + 'started_at', + 'submitted_at', + 'timestamp', + 'updated_at' +) + +filter ConvertTo-SmarterObject +{ +<# + .SYNOPSIS + Updates the properties of the input object to be object themselves when the conversion + is possible. + + .DESCRIPTION + Updates the properties of the input object to be object themselves when the conversion + is possible. + + At present, this only attempts to convert properties known to store dates as strings + into storing them as DateTime objects instead. + + .PARAMETER InputObject + The object to update +#> + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $InputObject + ) + + if ($null -eq $InputObject) + { + return $null + } + + if ($InputObject -is [array]) + { + foreach ($object in $InputObject) + { + Write-Output -InputObject (ConvertTo-SmarterObject -InputObject $object) + } + } + elseif ($InputObject -is [PSCustomObject]) + { + $properties = $InputObject.PSObject.Properties | Where-Object { $null -ne $_.Value } + foreach ($property in $properties) + { + # Convert known date properties from dates to real DateTime objects + if ($property.Name -in $script:datePropertyNames) + { + $property.Value = Get-Date -Date $property.Value + } + + if (($property.Value -is [array]) -or ($property.Value -is [PSCustomObject])) + { + $property.Value = ConvertTo-SmarterObject -InputObject $property.Value + } + } + + Write-Output -InputObject $InputObject + } + else + { + Write-Output -InputObject $InputObject + } +} diff --git a/GitHubIssues.ps1 b/GitHubIssues.ps1 new file mode 100644 index 00000000..e61e1c24 --- /dev/null +++ b/GitHubIssues.ps1 @@ -0,0 +1,863 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubIssue +{ +<# + .SYNOPSIS + Retrieve Issues from GitHub. + + .DESCRIPTION + Retrieve Issues from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER OrganizationName + The organization whose issues should be retrieved. + + .PARAMETER RepositoryType + all: Retrieve issues across owned, member and org repositories + ownedAndMember: Retrieve issues across owned and member repositories + + .PARAMETER Issue + The number of specic Issue to retrieve. If not supplied, will return back all + Issues for this Repository that match the specified criteria. + + .PARAMETER IgnorePullRequests + GitHub treats Pull Requests as Issues. Specify this switch to skip over any + Issue that is actually a Pull Request. + + .PARAMETER Filter + Indicates the type of Issues to return: + assigned: Issues assigned to the authenticated user. + created: Issues created by the authenticated user. + mentioned: Issues mentioning the authenticated user. + subscribed: Issues the authenticated user has been subscribed to updates for. + all: All issues the authenticated user can see, regardless of participation or creation. + + .PARAMETER State + Indicates the state of the issues to return. + + .PARAMETER Label + The label (or labels) that returned Issues should have. + + .PARAMETER Sort + The property to sort the returned Issues by. + + .PARAMETER Direction + The direction of the sort. + + .PARAMETER Since + If specified, returns only issues updated at or after this time. + + .PARAMETER MilestoneType + If specified, indicates what milestone Issues must be a part of to be returned: + specific: Only issues with the milestone specified via the Milestone parameter will be returned. + all: All milestones will be returned. + none: Only issues without milestones will be returned. + + .PARAMETER Milestone + Only issues with this milestone will be returned. + + .PARAMETER AssigneeType + If specified, indicates who Issues must be assigned to in order to be returned: + specific: Only issues assigned to the user specified by the Assignee parameter will be returned. + all: Issues assigned to any user will be returned. + none: Only issues without an assigned user will be returned. + + .PARAMETER Assignee + Only issues assigned to this user will be returned. + + .PARAMETER Creator + Only issues created by this specified user will be returned. + + .PARAMETER Mentioned + Only issues that mention this specified user will be returned. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -State open + + Gets all the currently open issues in the PowerShell\PowerShellForGitHub repository. + + .EXAMPLE + Get-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -State all -Assignee Octocat + + Gets every issue in the PowerShell\PowerShellForGitHub repository that is assigned to Octocat. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $OrganizationName, + + [ValidateSet('all', 'ownedAndMember')] + [string] $RepositoryType = 'all', + + [string] $Issue, + + [switch] $IgnorePullRequests, + + [ValidateSet('assigned', 'created', 'mentioned', 'subscribed', 'all')] + [string] $Filter = 'assigned', + + [ValidateSet('open', 'closed', 'all')] + [string] $State = 'open', + + [string[]] $Label, + + [ValidateSet('created', 'updated', 'comments')] + [string] $Sort = 'created', + + [ValidateSet('asc', 'desc')] + [string] $Direction = 'desc', + + [DateTime] $Since, + + [ValidateSet('specific', 'all', 'none')] + [string] $MilestoneType, + + [string] $Milestone, + + [ValidateSet('specific', 'all', 'none')] + [string] $AssigneeType, + + [string] $Assignee, + + [string] $Creator, + + [string] $Mentioned, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters -DisableValidation + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + 'ProvidedIssue' = $PSBoundParameters.ContainsKey('Issue') + } + + $uriFragment = [String]::Empty + $description = [String]::Empty + if ($OwnerName -xor $RepositoryName) + { + $message = 'You must specify BOTH Owner Name and Repository Name when one is provided.' + Write-Log -Message $message -Level Error + throw $message + } + + if (-not [String]::IsNullOrEmpty($RepositoryName)) + { + $uriFragment = "/repos/$OwnerName/$RepositoryName/issues" + $description = "Getting issues for $RepositoryName" + if (-not [String]::IsNullOrEmpty($Issue)) + { + $uriFragment = $uriFragment + "/$Issue" + $description = "Getting issue $Issue for $RepositoryName" + } + } + elseif (-not [String]::IsNullOrEmpty($OrganizationName)) + { + $uriFragment = "/orgs/$OrganizationName/issues" + $description = "Getting issues for $OrganizationName" + } + elseif ($RepositoryType -eq 'all') + { + $uriFragment = "/issues" + $description = "Getting issues across owned, member and org repositories" + } + elseif ($RepositoryType -eq 'ownedAndMember') + { + $uriFragment = "/user/issues" + $description = "Getting issues across owned and member repositories" + } + else + { + throw "Parameter set not supported." + } + + $getParams = @( + "filter=$Filter", + "state=$State", + "sort=$Sort", + "direction=$Direction" + ) + + if ($PSBoundParameters.ContainsKey('Label')) + { + $getParams += "labels=$($Label -join ',')" + } + + if ($PSBoundParameters.ContainsKey('Since')) + { + $getParams += "since=$($Since.ToUniversalTime().ToString('o'))" + } + + if ($PSBoundParameters.ContainsKey('Mentioned')) + { + $getParams += "mentioned=$Mentioned" + } + + if ($PSBoundParameters.ContainsKey('MilestoneType')) + { + if ($MilestoneType -eq 'all') + { + $getParams += 'mentioned=*' + } + elseif ($MilestoneType -eq 'none') + { + $getParams += 'mentioned=none' + } + elseif ([String]::IsNullOrEmpty($Milestone)) + { + $message = "MilestoneType was set to [$MilestoneType], but no value for Milestone was provided." + Write-Log -Message $message -Level Error + throw $message + } + } + + if ($PSBoundParameters.ContainsKey('Milestone')) + { + $getParams += "milestone=$Milestone" + } + + if ($PSBoundParameters.ContainsKey('AssigneeType')) + { + if ($AssigneeType -eq 'all') + { + $getParams += 'assignee=*' + } + elseif ($AssigneeType -eq 'none') + { + $getParams += 'assignee=none' + } + elseif ([String]::IsNullOrEmpty($Assignee)) + { + $message = "AssigneeType was set to [$AssigneeType], but no value for Assignee was provided." + Write-Log -Message $message -Level Error + throw $message + } + } + + if ($PSBoundParameters.ContainsKey('Assignee')) + { + $getParams += "assignee=$Assignee" + } + + if ($PSBoundParameters.ContainsKey('Creator')) + { + $getParams += "creator=$Creator" + } + + if ($PSBoundParameters.ContainsKey('Mentioned')) + { + $getParams += "mentioned=$Mentioned" + } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = $description + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + $result = Invoke-GHRestMethodMultipleResult @params + + if ($IgnorePullRequests) + { + return ($result | Where-Object { $null -eq (Get-Member -InputObject $_ -Name pull_request) }) + } + else + { + return $result + } +} + +function Get-GitHubIssueTimeline +{ +<# + .SYNOPSIS + Retrieves various events that occur around an issue or pull request on GitHub. + + .DESCRIPTION + Retrieves various events that occur around an issue or pull request on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Issue + The Issue to get the timeline for. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubIssueTimeline -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Issue 24 +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Issue, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/issues/$Issue/timeline" + 'Description' = "Getting timeline for Issue #$Issue in $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.mockingbird-preview' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function New-GitHubIssue +{ +<# + .SYNOPSIS + Create a new Issue on GitHub. + + .DESCRIPTION + Create a new Issue on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Title + The title of the issue + + .PARAMETER Body + The contents of the issue + + .PARAMETER Assignee + Login(s) for Users to assign to the issue. + + .PARAMETER Milestone + The number of the mileston to associate this issue with. + + .PARAMETER Label + Label(s) to associate with this issue. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + New-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Title 'Test Issue' +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Title, + + [string] $Body, + + [string[]] $Assignee, + + [int] $Milestone, + + [string[]] $Label, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{ + 'title' = $Title + } + + if ($PSBoundParameters.ContainsKey('Body')) { $hashBody['body'] = $Body } + if ($PSBoundParameters.ContainsKey('Assignee')) { $hashBody['assignees'] = @($Assignee) } + if ($PSBoundParameters.ContainsKey('Milestone')) { $hashBody['milestone'] = $Milestone } + if ($PSBoundParameters.ContainsKey('Label')) { $hashBody['label'] = @($Label) } + + $params = @{ + 'UriFragment' = "/repos/$OwnerName/$RepositoryName/issues" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Post' + 'Description' = "Creating new Issue ""$Title"" on $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Update-GitHubIssue +{ +<# + .SYNOPSIS + Create a new Issue on GitHub. + + .DESCRIPTION + Create a new Issue on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Issue + The issue to be updated. + + .PARAMETER Title + The title of the issue + + .PARAMETER Body + The contents of the issue + + .PARAMETER Assignee + Login(s) for Users to assign to the issue. + Provide an empty array to clear all existing assignees. + + .PARAMETER Milestone + The number of the mileston to associate this issue with. + Set to 0/$null to remove current. + + .PARAMETER Label + Label(s) to associate with this issue. + Provide an empty array to clear all existing labels. + + .PARAMETER State + Modify the current state of the issue. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Update-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Issue 4 -Title 'Test Issue' -State closed +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [int] $Issue, + + [string] $Title, + + [string] $Body, + + [string[]] $Assignee, + + [int] $Milestone, + + [string[]] $Label, + + [ValidateSet('open', 'closed')] + [string] $State, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{} + + if ($PSBoundParameters.ContainsKey('Title')) { $hashBody['title'] = $Title } + if ($PSBoundParameters.ContainsKey('Body')) { $hashBody['body'] = $Body } + if ($PSBoundParameters.ContainsKey('Assignee')) { $hashBody['assignees'] = @($Assignee) } + if ($PSBoundParameters.ContainsKey('Label')) { $hashBody['label'] = @($Label) } + if ($PSBoundParameters.ContainsKey('State')) { $hashBody['state'] = $State } + if ($PSBoundParameters.ContainsKey('Milestone')) + { + $hashBody['milestone'] = $Milestone + if ($Milestone -in (0, $null)) + { + $hashBody['milestone'] = $null + } + } + + $params = @{ + 'UriFragment' = "/repos/$OwnerName/$RepositoryName/issues/$Issue" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Patch' + 'Description' = "Updating Issue #$Issue on $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Lock-GitHubIssue +{ +<# + .SYNOPSIS + Lock an Issue or Pull Request conversation on GitHub. + + .DESCRIPTION + Lock an Issue or Pull Request conversation on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Issue + The issue to be locked. + + .PARAMETER Reason + The reason for locking the issue or pull request conversation. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Lock-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Issue 4 -Title 'Test Issue' -Reason spam +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [int] $Issue, + + [ValidateSet('off-topic', 'too heated', 'resolved', 'spam')] + [string] $Reason, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{ + 'locked' = $true + } + + if ($PSBoundParameters.ContainsKey('Reason')) + { + $telemetryProperties['Reason'] = $Reason + $hashBody['active_lock_reason'] = $Reason + } + + $params = @{ + 'UriFragment' = "/repos/$OwnerName/$RepositoryName/issues/$Issue/lock" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Put' + 'Description' = "Locking Issue #$Issue on $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.sailor-v-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Unlock-GitHubIssue +{ +<# + .SYNOPSIS + Unlocks an Issue or Pull Request conversation on GitHub. + + .DESCRIPTION + Unlocks an Issue or Pull Request conversation on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Issue + The issue to be unlocked. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Unlock-GitHubIssue -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Issue 4 +#> +[CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] +param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [int] $Issue, + + [string] $AccessToken, + + [switch] $NoStatus +) + +Write-InvocationLog -Invocation $MyInvocation + +$elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters +$OwnerName = $elements.ownerName +$RepositoryName = $elements.repositoryName + +$telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) +} + +$params = @{ + 'UriFragment' = "/repos/$OwnerName/$RepositoryName/issues/$Issue/lock" + 'Method' = 'Delete' + 'Description' = "Unlocking Issue #$Issue on $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.sailor-v-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) +} + +return Invoke-GHRestMethod @params +} diff --git a/GitHubLabels.ps1 b/GitHubLabels.ps1 new file mode 100644 index 00000000..b168e4ae --- /dev/null +++ b/GitHubLabels.ps1 @@ -0,0 +1,628 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubLabel +{ +<# + .SYNOPSIS + Retrieve label(s) of a given GitHub repository. + + .DESCRIPTION + Retrieve label(s) of a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Name of the specific label to be retieved. If not supplied, all labels will be retrieved. + Emoji and codes are supported. For more information, see here: https://www.webpagefx.com/tools/emoji-cheat-sheet/ + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubLabel -OwnerName Powershell -RepositoryName PowerShellForGitHub + + Gets the information for every label from the PowerShell\PowerShellForGitHub project. + + .EXAMPLE + Get-GitHubLabel -OwnerName Powershell -RepositoryName PowerShellForGitHub -LabelName TestLabel + + Gets the information for the label named "TestLabel" from the PowerShell\PowerShellForGitHub + project. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Alias('LabelName')] + [string] $Name, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/labels/$Name" + 'Description' = "Getting all labels for $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + if (-not [String]::IsNullOrWhiteSpace($Name)) + { + $params["Description"] = "Getting label $Name for $RepositoryName" + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function New-GitHubLabel +{ +<# + .SYNOPSIS + Create a new label on a given GitHub repository. + + .DESCRIPTION + Create a new label on a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Name of the label to be created. + Emoji and codes are supported. For more information, see here: https://www.webpagefx.com/tools/emoji-cheat-sheet/ + + .PARAMETER Color + Color (in HEX) for the new label, without the leading # sign. + + .PARAMETER Description + A short description of the label. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + New-GitHubLabel -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Name TestLabel -Color BBBBBB + + Creates a new, grey-colored label called "TestLabel" in the PowerShellForGitHub project. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [Alias('LabelName')] + [string] $Name, + + [Parameter(Mandatory)] + [Alias('LabelColor')] + [ValidateScript({if ($_ -match '^#?[ABCDEF0-9]{6}$') { $true } else { throw "Color must be provided in hex." }})] + [string] $Color = "EEEEEE", + + [string] $Description, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + # Be robust to users who choose to provide a color in hex by specifying the leading # sign + # (by just stripping it out). + if ($Color.StartsWith('#')) + { + $Color = $Color.Substring(1) + } + + $hashBody = @{ + 'name' = $Name + 'color' = $Color + 'description' = $Description + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/labels" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Post' + 'Description' = "Creating label $Name in $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Remove-GitHubLabel +{ +<# + .SYNOPSIS + Deletes a label from a given GitHub repository. + + .DESCRIPTION + Deletes a label from a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Name of the label to be deleted. + Emoji and codes are supported. For more information, see here: https://www.webpagefx.com/tools/emoji-cheat-sheet/ + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Remove-GitHubLabel -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Name TestLabel + + Removes the label called "TestLabel" from the PowerShellForGitHub project. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [Alias('Delete-GitHubLabel')] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [Alias('LabelName')] + [string] $Name, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/labels/$Name" + 'Method' = 'Delete' + 'Description' = "Deleting label $Name from $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Update-GitHubLabel +{ +<# + .SYNOPSIS + Updates an existing label on a given GitHub repository. + + .DESCRIPTION + Updates an existing label on a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Current name of the label to be updated. + Emoji and codes are supported. For more information, see here: https://www.webpagefx.com/tools/emoji-cheat-sheet/ + + .PARAMETER NewName + New name for the label being updated. + Emoji and codes are supported. For more information, see here: https://www.webpagefx.com/tools/emoji-cheat-sheet/ + + .PARAMETER Color + Color (in HEX) for the new label, without the leading # sign. + + .PARAMETER Description + A short description of the label. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Update-GitHubLabel -OwnerName Powershell -RepositoryName PowerShellForGitHub -Name TestLabel -NewName NewTestLabel -LabelColor BBBB00 + + Updates the existing label called TestLabel in the PowerShellForGitHub project to be called + 'NewTestLabel' and be colored yellow. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [Alias('LabelName')] + [string] $Name, + + [Parameter(Mandatory)] + [Alias('NewLabelName')] + [string] $NewName, + + [Parameter(Mandatory)] + [Alias('LabelColor')] + [ValidateScript({if ($_ -match '^#?[ABCDEF0-9]{6}$') { $true } else { throw "Color must be provided in hex." }})] + [string] $Color = "EEEEEE", + + [string] $Description, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{} + if ($PSBoundParameters.ContainsKey('NewName')) { $hashBody['name'] = $NewName } + if ($PSBoundParameters.ContainsKey('Color')) { $hashBody['color'] = $Color } + if ($PSBoundParameters.ContainsKey('Description')) { $hashBody['description'] = $Description } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/labels/$Name" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Patch' + 'Description' = "Updating label $Name" + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Set-GitHubLabel +{ +<# + .SYNOPSIS + Sets the entire set of Labels on the given GitHub repository to match the provided list + of Labels. + + .DESCRIPTION + Sets the entire set of Labels on the given GitHub repository to match the provided list + of Labels. + + Will update the color/description for any Labels already in the repository that match the + name of a Label in the provided list. All other existing Labels will be removed, and then + new Labels will be created to match the others in the Label list. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Label + The array of Labels (name, color, description) that the repository should be aligning to. + A default list of labels will be used if no Labels are provided. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .NOTES + This method does not rename any existing labels, as it doesn't have any context regarding + which Issue the new name is for. Therefore, it is possible that by running this function + on a repository with Issues that have already been assigned Labels, you may experience data + loss as a minor correction to you (maye fixing a typo) will result in the old Label being + removed (and thus unassigned from existing Issues) and then the new one created. + + .EXAMPLE + Set-GitHubLabel -OwnerName Powershell -RepositoryName PowerShellForGitHub -Label @(@{'name' = 'TestLabel'; 'color' = 'EEEEEE'}, @{'name' = 'critical'; 'color' = 'FF000000'; 'description' = 'Needs immediate attention'}) + + Removes any labels not in this Label array, ensure the current assigned color and descriptions + match what's in the array for the labels that do already exist, and then creates new labels + for any remaining ones in the Label list. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [object[]] $Label, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + if (($null -eq $Label) -or ($Label.Count -eq 0)) + { + $Label = $script:defaultGitHubLabels + } + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $NoStatus = Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus + + $commonParams = @{ + 'OwnerName' = $OwnerName + 'RepositoryName' = $RepositoryName + 'AccessToken' = $AccessToken + 'NoStatus' = $NoStatus + } + + $labelNames = $Label.name + $existingLabels = Get-GitHubLabel @commonParams + $existingLabelNames = $existingLabels.name + + foreach ($labelToConfigure in $Label) + { + if ($labelToConfigure.name -notin $existingLabelNames) + { + # Create label if it doesn't exist + $null = New-GitHubLabel -Name $labelToConfigure.name -Color $labelToConfigure.color @commonParams + } + else + { + # Update label's color if it already exists + $null = Update-GitHubLabel -Name $labelToConfigure.name -NewName $labelToConfigure.name -Color $labelToConfigure.color @commonParams + } + } + + foreach ($labelName in $existingLabelNames) + { + if ($labelName -notin $labelNames) + { + # Remove label if it exists but is not in desired label list + $null = Remove-GitHubLabel -Name $labelName @commonParams + } + } +} + +# A set of labels that a project might want to initially populate their repository with +# Used by Set-GitHubLabel when no Label list is provided by the user. +# This list exists to support v0.1.0 users. +$script:defaultGitHubLabels = @( + @{ + 'name' = 'pri:lowest' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:low' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:medium' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:high' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:highest' + 'color' = '4285F4' + }, + @{ + 'name' = 'bug' + 'color' = 'fc2929' + }, + @{ + 'name' = 'duplicate' + 'color' = 'cccccc' + }, + @{ + 'name' = 'enhancement' + 'color' = '121459' + }, + @{ + 'name' = 'up for grabs' + 'color' = '159818' + }, + @{ + 'name' = 'question' + 'color' = 'cc317c' + }, + @{ + 'name' = 'discussion' + 'color' = 'fe9a3d' + }, + @{ + 'name' = 'wontfix' + 'color' = 'dcb39c' + }, + @{ + 'name' = 'in progress' + 'color' = 'f0d218' + }, + @{ + 'name' = 'ready' + 'color' = '145912' + } +) diff --git a/GitHubLabels.psm1 b/GitHubLabels.psm1 deleted file mode 100644 index b902de28..00000000 --- a/GitHubLabels.psm1 +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -<# - .SYNOPSIS PowerShell module for GitHub labels -#> - -# Import module which defines $global:gitHubApiToken with GitHub API access token. Create this file it if it doesn't exist. -$apiTokensFilePath = "$PSScriptRoot\ApiTokens.psm1" -if (Test-Path $apiTokensFilePath) -{ - Write-Host "Importing $apiTokensFilePath" - Import-Module -force $apiTokensFilePath -} -else -{ - Write-Warning "$apiTokensFilePath does not exist, skipping import" - Write-Warning @' -This module should define $global:gitHubApiToken with your GitHub API access token in ApiTokens.psm1. Create this file if it doesn't exist. -You can simply rename ApiTokensTemplate.psm1 to ApiTokens.psm1 and update value of $global:gitHubApiToken, then reimport this module with -Force switch. -You can get GitHub token from https://github.com/settings/tokens -If you don't provide it, you can still use this module, but you will be limited to 60 queries per hour. -'@ -} - -$script:gitHubToken = $global:gitHubApiToken -$script:gitHubApiUrl = "https://api.github.com" -$script:gitHubApiReposUrl = "https://api.github.com/repos" - -<# - .SYNOPSIS Function to get single or all labels of given repository - .PARAM - RepositoryName Name of the repository - .PARAM - OwnerName Owner of the repository - .PARAM - LabelName Name of the label to get. Function will return all labels for given repository if LabelName is not specified. - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Get-GitHubLabel -RepositoryName DesiredStateConfiguration -OwnerName Powershell -LabelName TestLabel - Get-GitHubLabel -RepositoryName DesiredStateConfiguration -OwnerName Powershell -#> -function Get-GitHubLabel -{ - param( - [Parameter(Mandatory=$true)] - [string]$RepositoryName, - [Parameter(Mandatory=$true)] - [string]$OwnerName, - [string]$LabelName, - [string]$GitHubAccessToken = $script:gitHubToken - ) - - $resultToReturn = @() - $index = 0 - $headers = @{"Authorization"="token $GitHubAccessToken"} - - if ($LabelName -eq "") - { - $query = "$script:gitHubApiReposUrl/{0}/{1}/labels" -f $OwnerName, $RepositoryName - Write-Host "Getting all labels for repository $RepositoryName" - - do - { - try - { - $jsonResult = Invoke-WebRequest $query -Method Get -Headers $headers - $labels = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - foreach ($label in $labels) - { - Write-Verbose "$index. $($label.name)" - $index++ - $resultToReturn += $label - } - $query = Get-NextResultPage -JsonResult $jsonResult - } while ($query -ne $null) - } - else - { - $query = "$script:gitHubApiReposUrl/{0}/{1}/labels/{2}" -f $OwnerName, $RepositoryName, $LabelName - Write-Host "Getting label $LabelName for repository $RepositoryName" - - try - { - $jsonResult = Invoke-WebRequest $query -Method Get -Headers $headers - $label = ConvertFrom-Json -InputObject $jsonResult.content - } - catch [System.Net.WebException] { - Write-Error "Failed to execute query with exception: $($_.Exception)`nHTTP status code: $($_.Exception.Response.StatusCode)" - return $null - } - catch { - Write-Error "Failed to execute query with exception: $($_.Exception)" - return $null - } - - Write-Verbose "$index. $($label.name)" - $resultToReturn = $label - } - - return $resultToReturn -} - -<# - .SYNOPSIS Function to create label in given repository - .PARAM - RepositoryName Name of the repository - .PARAM - OwnerName Owner of the repository - .PARAM - LabelName Name of the label to create - .PARAM - LabelColor New color of the label - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - New-GitHubLabel -RepositoryName DesiredStateConfiguration -OwnerName PowerShell -LabelName TestLabel -LabelColor BBBBBB -#> -function New-GitHubLabel -{ - param( - [Parameter(Mandatory=$true)] - [string]$RepositoryName, - [Parameter(Mandatory=$true)] - [string]$OwnerName, - [Parameter(Mandatory=$true)] - [string]$LabelName, - [string]$LabelColor = "EEEEEE", - [string]$GitHubAccessToken = $script:gitHubToken - ) - - $headers = @{"Authorization"="token $GitHubAccessToken"} - $hashTable = @{"name"=$LabelName; "color"=$LabelColor} - $data = $hashTable | ConvertTo-Json - $url = "$script:gitHubApiReposUrl/{0}/{1}/labels" -f $OwnerName, $RepositoryName - - Write-Host "Creating Label:" $LabelName - $result = Invoke-WebRequest $url -Method Post -Body $data -Headers $headers - - if ($result.StatusCode -eq 201) - { - Write-Host $LabelName "was created" - } - else - { - Write-Error $LabelName "was not created. Result: $result" - } -} - -<# - .SYNOPSIS Function to remove label from given repository - .PARAM - RepositoryName Name of the repository - .PARAM - OwnerName Owner of the repository - .PARAM - LabelName Name of the label to delete - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Remove-GitHubLabel -RepositoryName desiredstateconfiguration -OwnerName powershell -LabelName TestLabel -#> -function Remove-GitHubLabel -{ - param( - [Parameter(Mandatory=$true)] - [string]$RepositoryName, - [Parameter(Mandatory=$true)] - [string]$OwnerName, - [Parameter(Mandatory=$true)] - [string]$LabelName, - [string]$GitHubAccessToken = $script:gitHubToken - ) - - $headers = @{"Authorization"="token $GitHubAccessToken"} - $url = "$script:gitHubApiReposUrl/{0}/{1}/labels/{2}" -f $OwnerName, $RepositoryName, $LabelName - - Write-Host "Deleting Label:" $LabelName - $result = Invoke-WebRequest $url -Method Delete -Headers $headers - - if ($result.StatusCode -eq 204) - { - Write-Host $LabelName "was deleted" - } - else - { - Write-Error $LabelName "was not deleted. Result: $result" - } -} - -<# - .SYNOPSIS Function to update label in given repository - .PARAM - RepositoryName Name of the repository - .PARAM - OwnerName Owner of the repository - .PARAM - LabelName Name of the label to update - .PARAM - NewLabelName New name of the label - .PARAM - LabelColor New color of the label - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - Update-GitHubLabel -RepositoryName DesiredStateConfiguration -OwnerName Powershell -LabelName TestLabel -NewLabelName NewTestLabel -LabelColor BBBB00 -#> -function Update-GitHubLabel -{ - param( - [Parameter(Mandatory=$true)] - [string]$RepositoryName, - [Parameter(Mandatory=$true)] - [string]$OwnerName, - [Parameter(Mandatory=$true)] - [string]$LabelName, - [Parameter(Mandatory=$true)] - [string]$NewLabelName, - [string]$LabelColor = "EEEEEE", - [string]$GitHubAccessToken = $script:gitHubToken - ) - - $headers = @{"Authorization"="token $GitHubAccessToken"} - $hashTable = @{"name"=$NewLabelName; "color"=$LabelColor} - $data = $hashTable | ConvertTo-Json - $url = "$script:gitHubApiReposUrl/{0}/{1}/labels/{2}" -f $OwnerName, $RepositoryName, $LabelName - - Write-Host "Updating label '$LabelName' to name '$NewLabelName' and color '$LabelColor'" - $result = Invoke-WebRequest $url -Method Patch -Body $data -Headers $headers - - if ($result.StatusCode -eq 200) - { - Write-Host $LabelName "was updated" - } - else - { - Write-Error $LabelName "was not updated. Result: $result" - } -} - -<# - .SYNOPSIS Function to create labels for given repository. - It get all labels from repo, remove the ones which aren't on our approved label list, update the ones which already exist to desired color and add the ones which weren't there before. - .PARAM - RepositoryName Name of the repository - .PARAM - OwnerName Owner of the repository - .PARAM - GitHubAccessToken GitHub API Access Token. - Get github token from https://github.com/settings/tokens - If you don't provide it, you can still use this script, but you will be limited to 60 queries per hour. - .EXAMPLE - New-GitHubLabels -RepositoryName DesiredStateConfiguration -OwnerName Powershell -#> -function New-GitHubLabels -{ - param( - [Parameter(Mandatory=$true)] - [string]$RepositoryName, - [Parameter(Mandatory=$true)] - [string]$OwnerName, - [string]$GitHubAccessToken = $script:gitHubToken - ) - -$labelJson = @" -[ - { - "name": "pri:lowest", - "color": "4285F4" - }, - { - "name": "pri:low", - "color": "4285F4" - }, - { - "name": "pri:medium", - "color": "4285F4" - }, - { - "name": "pri:high", - "color": "4285F4" - }, - { - "name": "pri:highest", - "color": "4285F4" - }, - { - "name": "bug", - "color": "fc2929" - }, - { - "name": "duplicate", - "color": "cccccc" - }, - { - "name": "enhancement", - "color": "121459" - }, - { - "name": "up for grabs", - "color": "159818" - }, - { - "name": "question", - "color": "cc317c" - }, - { - "name": "discussion", - "color": "fe9a3d" - }, - { - "name": "wontfix", - "color": "dcb39c" - }, - { - "name": "in progress", - "color": "f0d218" - }, - { - "name": "ready", - "color": "145912" - } -] - -"@ - - $labelList = $labelJson | ConvertFrom-Json - $labelListNames = $labelList.name - $existingLabels = Get-GitHubLabel -RepositoryName $RepositoryName -OwnerName $OwnerName -GitHubAccessToken $GitHubAccessToken - $existingLabelsNames = $existingLabels.name - - - foreach ($label in $labelList) - { - if ($label.name -notin $existingLabelsNames) - { - # Create label if it doesn't exist - New-GitHubLabel -RepositoryName $RepositoryName -OwnerName $OwnerName -LabelName $label.name -LabelColor $label.color -GitHubAccessToken $GitHubAccessToken - } - else - { - # Update label's color if it already exists - Update-GitHubLabel -RepositoryName $RepositoryName -OwnerName $OwnerName -LabelName $label.name -NewLabelName $label.name -LabelColor $label.color -GitHubAccessToken $GitHubAccessToken - } - } - - foreach ($label in $existingLabelsNames) - { - if($label -notin $labelListNames) - { - # Remove label if it exists but is not on desired label list - Remove-GitHubLabel -RepositoryName $RepositoryName -OwnerName $OwnerName -LabelName $label -GitHubAccessToken $GitHubAccessToken - } - } -} - -<# - .SYNOPSIS Function to get next page with results from query to GitHub API - - .PARAM - JsonResult Result from the query to GitHub API -#> -function Get-NextResultPage -{ - param - ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $JsonResult - ) - - if($JsonResult.Headers.Link -eq $null) - { - return $null - } - - $nextLinkString = $JsonResult.Headers.Link.Split(',')[0] - - # Get url query for the next page - $query = $nextLinkString.Split(';')[0].replace('<','').replace('>','') - if ($query -notmatch 'page=1') - { - - return $query - } - else - { - return $null - } -} \ No newline at end of file diff --git a/GitHubMiscellaneous.ps1 b/GitHubMiscellaneous.ps1 new file mode 100644 index 00000000..c6f9f1c1 --- /dev/null +++ b/GitHubMiscellaneous.ps1 @@ -0,0 +1,525 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubRateLimit +{ +<# + .SYNOPSIS + Gets the current rate limit status for the GitHub API based on the currently configured + authentication (Access Token). + + .DESCRIPTION + Gets the current rate limit status for the GitHub API based on the currently configured + authentication (Access Token). + + Use Set-GitHubAuthentication to change your current authentication (Access Token). + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject] + Limits returned are _per hour_. + + The Search API has a custom rate limit, separate from the rate limit + governing the rest of the REST API. The GraphQL API also has a custom + rate limit that is separate from and calculated differently than rate + limits in the REST API. + + For these reasons, the Rate Limit API response categorizes your rate limit. + Under resources, you'll see three objects: + + The core object provides your rate limit status for all non-search-related resources in the REST API. + The search object provides your rate limit status for the Search API. + The graphql object provides your rate limit status for the GraphQL API. + + Deprecation notice + The rate object is deprecated. + If you're writing new API client code or updating existing code, + you should use the core object instead of the rate object. + The core object contains the same information that is present in the rate object. + + .EXAMPLE + Get-GitHubRateLimit +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $params = @{ + 'UriFragment' = 'rate_limit' + 'Method' = 'Get' + 'Description' = "Getting your API rate limit" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function ConvertFrom-Markdown +{ +<# + .SYNOPSIS + Converts arbitrary Markdown into HTML. + + .DESCRIPTION + Converts arbitrary Markdown into HTML. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Content + The Markdown text to render to HTML. Content must be 400 KB or less. + + .PARAMETER Mode + The rendering mode for the Markdown content. + + markdown - Renders Content in plain Markdown, just like README.md files are rendered + + gfm (GitHub Flavored Markdown) - Creates links for user mentions as well as references to + SHA-1 hashes, issues, and pull requests. + + .PARAMETER Context + The repository to use when creating references in 'githubFlavoredMarkdown' mode. + Specify as [ownerName]/[repositoryName]. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [String] The HTML version of the Markdown content. + + .EXAMPLE + Get-GitHubRateLimit +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [ValidateScript({if ([System.Text.Encoding]::UTF8.GetBytes($_).Count -lt 400000) { $true } else { throw "Content must be less than 400 KB." }})] + [string] $Content, + + [ValidateSet('markdown', 'gfm')] + [string] $Mode = 'markdown', + + [string] $Context, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{ + 'Mode' = $Mode + } + + $hashBody = @{ + 'text' = $Content + 'mode' = $Mode + } + + if (-not [String]::IsNullOrEmpty($Context)) { $hashBody['context'] = $Context } + + $params = @{ + 'UriFragment' = 'markdown' + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Post' + 'Description' = "Converting Markdown to HTML" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubLicense +{ +<# + .SYNOPSIS + Gets a license list or license content from GitHub. + + .DESCRIPTION + Gets a license list or license content from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + The name of the license to retrieve the content for. If not specified, all licenses + will be returned. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubLicense + + Returns metadata about popular open source licenses + + .EXAMPLE + Get-GitHubLicense -Name mit + + Gets the content of the mit license file + + .EXAMPLE + Get-GitHubLicense -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + Gets the content of the license file for the PowerShell\PowerShellForGitHub repository. + It may be necessary to convert the content of the file. Check the 'encoding' property of + the result to know how 'content' is encoded. As an example, to convert from Base64, do + the following: + + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($result.content)) +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter( + Mandatory, + ParameterSetName='Individual')] + [string] $Name, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters -DisableValidation + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{} + + $uriFragment = 'licenses' + $description = 'Getting all licenses' + if ($PSBoundParameters.ContainsKey('Name')) + { + $telemetryProperties['Name'] = $Name + $uriFragment = "licenses/$Name" + $description = "Getting the $Name license" + } + elseif ((-not [String]::IsNullOrEmpty($OwnerName)) -and (-not [String]::IsNullOrEmpty($RepositoryName))) + { + $telemetryProperties['OwnerName'] = Get-PiiSafeString -PlainText $OwnerName + $telemetryProperties['RepositoryName'] = Get-PiiSafeString -PlainText $RepositoryName + $uriFragment = "repos/$OwnerName/$RepositoryName/license" + $description = "Getting the license for $RepositoryName" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Method' = 'Get' + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubEmoji +{ +<# + .SYNOPSIS + Gets all the emojis available to use on GitHub. + + .DESCRIPTION + Gets all the emojis available to use on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject] + The emoji name and a link to its image. + + .EXAMPLE + Get-GitHubEmoji +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $params = @{ + 'UriFragment' = 'emojis' + 'Method' = 'Get' + 'Description' = "Getting all GitHub emojis" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubCodeOfConduct +{ +<# + .SYNOPSIS + Gets license or license content from GitHub. + + .DESCRIPTION + Gets license or license content from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + The name of the license to retrieve the content for. If not specified, all licenses + will be returned. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubCodeOfConduct + + Returns metadata about popular Codes of Conduct + + .EXAMPLE + Get-GitHubCodeOfConduct -Name citizen_code_of_conduct + + Gets the content of the 'Citizen Code of Conduct' + + .EXAMPLE + Get-GitHubCodeOfConduct -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + Gets the content of the Code of Coduct file for the PowerShell\PowerShellForGitHub repository + if one is detected. + + It may be necessary to convert the content of the file. Check the 'encoding' property of + the result to know how 'content' is encoded. As an example, to convert from Base64, do + the following: + + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($result.content)) +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter( + Mandatory, + ParameterSetName='Individual')] + [string] $Name, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters -DisableValidation + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{} + + $uriFragment = 'codes_of_conduct' + $description = 'Getting all Codes of Conduct' + if ($PSBoundParameters.ContainsKey('Name')) + { + $telemetryProperties['Name'] = $Name + $uriFragment = "codes_of_conduct/$Name" + $description = "Getting the $Name Code of Conduct" + } + elseif ((-not [String]::IsNullOrEmpty($OwnerName)) -and (-not [String]::IsNullOrEmpty($RepositoryName))) + { + $telemetryProperties['OwnerName'] = Get-PiiSafeString -PlainText $OwnerName + $telemetryProperties['RepositoryName'] = Get-PiiSafeString -PlainText $RepositoryName + $uriFragment = "repos/$OwnerName/$RepositoryName/community/code_of_conduct" + $description = "Getting the Code of Conduct for $RepositoryName" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Method' = 'Get' + 'AcceptHeader' = 'application/vnd.github.scarlet-witch-preview+json' + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubGitIgnore +{ +<# + .SYNOPSIS + Gets the list of available .gitignore templates, or their content, from GitHub. + + .DESCRIPTION + Gets the list of available .gitignore templates, or their content, from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Name + The name of the .gitignore template whose content should be fetched. + Not providing this will cause a list of all available templates to be returned. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubGitIgnore + + Returns the list of all available .gitignore templates. + + .EXAMPLE + Get-GitHubGitIgnore -Name VisualStudio + + Returns the content of the VisualStudio.gitignore template. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $Name, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + $uriFragment = 'gitignore/templates' + $description = 'Getting all gitignore templates' + if ($PSBoundParameters.ContainsKey('Name')) + { + $telemetryProperties['Name'] = $Name + $uriFragment = "gitignore/templates/$Name" + $description = "Getting $Name.gitignore" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Method' = 'Get' + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} diff --git a/GitHubOrganizations.ps1 b/GitHubOrganizations.ps1 new file mode 100644 index 00000000..dfebb9b7 --- /dev/null +++ b/GitHubOrganizations.ps1 @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubOrganizationMember +{ +<# + .SYNOPSIS + Retrieve list of members within an organization. + + .DESCRIPTION + Retrieve list of members within an organization. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OrganizationName + The name of the organization + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of members within the organization. + + .EXAMPLE + Get-GitHubOrganizationMember -OrganizationName PowerShell +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String] $OrganizationName, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{ + 'OrganizationName' = (Get-PiiSafeString -PlainText $OrganizationName) + } + + $params = @{ + 'UriFragment' = "orgs/$OrganizationName/members" + 'Description' = "Getting members for $OrganizationName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} diff --git a/GitHubPullRequests.ps1 b/GitHubPullRequests.ps1 new file mode 100644 index 00000000..8d28f12c --- /dev/null +++ b/GitHubPullRequests.ps1 @@ -0,0 +1,153 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubPullRequest +{ +<# + .SYNOPSIS + Retrieve the pull requests in the specified repository. + + .DESCRIPTION + Retrieve the pull requests in the specified repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER PullRequest + The specic pull request id to return back. If not supplied, will return back all + pull requests for the specified Repository. + + .PARAMETER State + The state of the pull requests that should be returned back. + + .PARAMETER Head + Filter pulls by head user and branch name in the format of 'user:ref-name' + + .PARAMETER Base + Base branch name to filter the pulls by. + + .PARAMETER Sort + What to sort the results by. + * created + * updated + * popularity (comment count) + * long-running (age, filtering by pulls updated in the last month) + + .PARAMETER Direction + The direction to be used for Sort. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of Pull Requests that match the specified criteria. + + .EXAMPLE + $pullRequests = Get-GitHubPullRequest -Uri 'https://github.com/PowerShell/PowerShellForGitHub' + + .EXAMPLE + $pullRequests = Get-GitHubPullRequest -OwnerName PowerShell -RepositoryName PowerShellForGitHub -State closed +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $PullRequest, + + [ValidateSet('open', 'closed', 'all')] + [string] $State = 'open', + + [string] $Head, + + [string] $Base, + + [ValidateSet('created', 'updated', 'popularity', 'long-running')] + [string] $Sort = 'created', + + [ValidateSet('asc', 'desc')] + [string] $Direction = 'desc', + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + 'ProvidedPullRequest' = $PSBoundParameters.ContainsKey('PullRequest') + } + + $uriFragment = "/repos/$OwnerName/$RepositoryName/pulls" + $description = "Getting pull requests for $RepositoryName" + if (-not [String]::IsNullOrEmpty($PullRequest)) + { + $uriFragment = $uriFragment + "/$PullRequest" + $description = "Getting pull request $PullRequest for $RepositoryName" + } + + $getParams = @( + "state=$State", + "sort=$Sort", + "direction=$Direction" + ) + + if ($PSBoundParameters.ContainsKey('Head')) + { + $getParams += "head=$Head" + } + + if ($PSBoundParameters.ContainsKey('Base')) + { + $getParams += "base=$Base" + } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = $description + 'AcceptHeader' = 'application/vnd.github.symmetra-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} diff --git a/GitHubRepositories.ps1 b/GitHubRepositories.ps1 new file mode 100644 index 00000000..12686c06 --- /dev/null +++ b/GitHubRepositories.ps1 @@ -0,0 +1,1312 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function New-GitHubRepository +{ +<# + .SYNOPSIS + Creates a new repository on GitHub. + + .DESCRIPTION + Creates a new repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER RepositoryName + Name of the repository to be created. + + .PARAMETER OrganizationName + Name of the organization that the repository should be created under. + If not specified, will be created under the current user's account. + + .PARAMETER Description + A short description of the repository. + + .PARAMETER Homepage + A URL with more information about the repository. + + .PARAMETER GitIgnoreTemplate + Desired language or platform .gitignore template to apply. + For supported values, call Get-GitHubGitIgnore. + Values are case-sensitive. + + .PARAMETER LicenseTemplate + Choose an open source license template that best suits your needs. + For supported values, call Get-GitHubLicense + Values are case-sensitive. + + .PARAMETER TeamId + The id of the team that will be granted access to this repository. + This is only valid when creating a repository in an organization. + + .PARAMETER Private + By default, this repository will created Public. Specify this to create + a private repository. Creating private repositories requires a paid GitHub account. + + .PARAMETER NoIssues + By default, this repository will support Issues. Specify this to disable Issues. + + .PARAMETER NoProjects + By default, this repository will support Projects. Specify this to disable Projects. + If you're creating a repository in an organization that has disabled repository projects, + this will be true by default. + + .PARAMETER NoWiki + By default, this repository will have a Wiki. Specify this to disable the Wiki. + + .PARAMETER AutoInit + Specify this to create an initial commit with an empty README. + + .PARAMETER DisallowSquashMerge + By default, squash-merging pull requests will be allowed. + Specify this to disallow. + + .PARAMETER DisallowMergeCommit + By default, merging pull requests with a merge commit will be allowed. + Specify this to disallow. + + .PARAMETER DisallowRebaseMerge + By default, rebase-merge pull requests will be allowed. + Specify this to disallow. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + New-GitHubRepository -RepositoryName MyNewRepo -AutoInit + + .EXAMPLE + New-GitHubRepository -RepositoryName MyNewRepo -Organization MyOrg -DisallowRebaseMerge +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $RepositoryName, + + [string] $OrganizationName, + + [string] $Description, + + [string] $Homepage, + + [string] $GitIgnoreTemplate, + + [string] $LicenseTemplate, + + [int] $TeamId, + + [switch] $Private, + + [switch] $NoIssues, + + [switch] $NoProjects, + + [switch] $NoWiki, + + [switch] $AutoInit, + + [switch] $DisallowSquashMerge, + + [switch] $DisallowMergeCommit, + + [switch] $DisallowRebaseMerge, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{ + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $uriFragment = 'user/repos' + if ($PSBoundParameters.ContainsKey('OrganizationName') -and + (-not [String]::IsNullOrEmpty($OrganizationName))) + { + $telemetryProperties['OrganizationName'] = Get-PiiSafeString -PlainText $OrganizationName + $uriFragment = "orgs/$OrganizationName/repos" + } + + if ($PSBoundParameters.ContainsKey('TeamId') -and (-not $PSBoundParameters.Contains('OrganizationName'))) + { + $message = 'TeamId may only be specified when creating a repository under an organization.' + Write-Log -Message $message -Level Error + throw $message + } + + $hashBody = @{ + 'name' = $RepositoryName + } + + if ($PSBoundParameters.ContainsKey('Description')) { $hashBody['description'] = $Description } + if ($PSBoundParameters.ContainsKey('Homepage')) { $hashBody['homepage'] = $Homepage } + if ($PSBoundParameters.ContainsKey('GitIgnoreTemplate')) { $hashBody['gitignore_template'] = $GitIgnoreTemplate } + if ($PSBoundParameters.ContainsKey('LicenseTemplate')) { $hashBody['license_template'] = $LicenseTemplate } + if ($PSBoundParameters.ContainsKey('TeamId')) { $hashBody['team_id'] = $TeamId } + if ($PSBoundParameters.ContainsKey('Private')) { $hashBody['private'] = $Private.ToBool() } + if ($PSBoundParameters.ContainsKey('NoIssues')) { $hashBody['has_issues'] = (-not $NoIssues.ToBool()) } + if ($PSBoundParameters.ContainsKey('NoProjects')) { $hashBody['has_projects'] = (-not $NoProjects.ToBool()) } + if ($PSBoundParameters.ContainsKey('NoWiki')) { $hashBody['has_wiki'] = (-not $NoWiki.ToBool()) } + if ($PSBoundParameters.ContainsKey('AutoInit')) { $hashBody['auto_init'] = $AutoInit.ToBool() } + if ($PSBoundParameters.ContainsKey('DisallowSquashMerge')) { $hashBody['allow_squash_merge'] = (-not $DisallowSquashMerge.ToBool()) } + if ($PSBoundParameters.ContainsKey('DisallowMergeCommit')) { $hashBody['allow_merge_commit'] = (-not $DisallowMergeCommit.ToBool()) } + if ($PSBoundParameters.ContainsKey('DisallowRebaseMerge')) { $hashBody['allow_rebase_merge'] = (-not $DisallowRebaseMerge.ToBool()) } + + $params = @{ + 'UriFragment' = $uriFragment + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Post' + 'Description' = "Creating $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Remove-GitHubRepository +{ +<# + .SYNOPSIS + Removes/deletes a repository from GitHub. + + .DESCRIPTION + Removes/deletes a repository from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Remove-GitHubRepository -OwnerName You -RepositoryName YourRepoToDelete + + .EXAMPLE + Remove-GitHubRepository -Uri https://github.com/You/YourRepoToDelete +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Alias('Delete-GitHubRepository')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName" + 'Method' = 'Delete' + 'Description' = "Deleting $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubRepository +{ +<# + .SYNOPSIS + Retrieves information about a repository or list of repoistories on GitHub. + + .DESCRIPTION + Retrieves information about a repository or list of repoistories on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER OrganizationName + The name of the organization to retrieve the repositories for. + + .PARAMETER Visibility + The type of visibility/accessibility for the repositories to return. + + .PARAMETER Affiliation + Can be one or more of: + + owner - Repositories that are owned by the authenticated user + + collaborator - Repositories that the user has been added to as a collaborator + + organization_member - Repositories that the user has access to through being + a member of an organization. This includes every repository on every team that the user + is on. + + .PARAMETER Type + The type of repository to return. + + .PARAMETER Sort + Property that the results should be sorted by + + .PARAMETER Direction + Direction of the sort that is to be applied to the results. + + .PARAMETER GetAllPublicRepositories + If this is specified with no other parameter, then instead of returning back all + repositories for the current authenticated user, it will instead return back all + public repositories on GitHub. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubRepository + + Gets all repositories for the current authenticated user. + + .EXAMPLE + Get-GitHubRepository -GetAllPublicRepositories + + Gets all public repositories on GitHub. + + .EXAMPLE + Get-GitHubRepository -OctoCat OctoCat + + .EXAMPLE + Get-GitHubRepository -Uri https://github.com/PowerShell/PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepository -OrganizationName PowerShell + +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(ParameterSetName='Organization')] + [string] $OrganizationName, + + [ValidateSet('all', 'public', 'private')] + [string] $Visibility, + + [string[]] $Affiliation, + + [ValidateSet('all', 'owner', 'public', 'private', 'member', 'forks', 'sources')] + [string] $Type, + + [ValidateSet('created', 'updated', 'pushed', 'full_name')] + [string] $Sort, + + [ValidateSet('asc', 'desc')] + [string] $Direction, + + [switch] $GetAllPublicRepositories, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters -DisableValidation + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{} + + $uriFragment = [String]::Empty + $description = [String]::Empty + if ((-not [String]::IsNullOrEmpty($OwnerName)) -and (-not [String]::IsNullOrEmpty($RepositoryName))) + { + $telemetryProperties['OwnerName'] = Get-PiiSafeString -PlainText $OwnerName + $telemetryProperties['RepositoryName'] = Get-PiiSafeString -PlainText $RepositoryName + + $uriFragment = "repos/$OwnerName/$RepositoryName" + $description = "Getting repo $RepositoryName" + } + elseif ([String]::IsNullOrEmpty($OwnerName) -and [String]::IsNullOrEmpty($OrganizationName)) + { + $uriFragment = 'user/repos' + $description = 'Getting repos for current authenticated user' + } + elseif ([String]::IsNullOrEmpty($OwnerName)) + { + $telemetryProperties['OrganizationName'] = Get-PiiSafeString -PlainText $OrganizationName + + $uriFragment = "orgs/$OrganizationName/repos" + $description = "Getting repos for $OrganizationName" + } + else + { + $telemetryProperties['OwnerName'] = Get-PiiSafeString -PlainText $OwnerName + + $uriFragment = "users/$OwnerName/repos" + $description = "Getting repos for $OwnerName" + } + + $getParams = @() + if ($PSBoundParameters.ContainsKey('Visibility')) { $getParams += "visibility=$Visibility" } + if ($PSBoundParameters.ContainsKey('Sort')) { $getParams += "sort=$Sort" } + if ($PSBoundParameters.ContainsKey('Type')) { $getParams += "type=$Type" } + if ($PSBoundParameters.ContainsKey('Direction')) { $getParams += "direction=$Direction" } + if ($PSBoundParameters.ContainsKey('Affiliation') -and $Affiliation.Count -gt 0) + { + $getParams += "affiliation=$($Affiliation -join ',')" + } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Update-GitHubRepository +{ +<# + .SYNOPSIS + Updates the details of an existing repository on GitHub. + + .DESCRIPTION + Updates the details of an existing repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Description + A short description of the repository. + + .PARAMETER Homepage + A URL with more information about the repository. + + .PARAMETER DefaultBranch + Update the default branch for this repository. + + .PARAMETER Private + Specify this to make the repository repository. Creating private repositories requires a + paid GitHub account. + To change a repository to be public, specify -Private:$false + + .PARAMETER NoIssues + By default, this repository will support Issues. Specify this to disable Issues. + + .PARAMETER NoProjects + By default, this repository will support Projects. Specify this to disable Projects. + If you're creating a repository in an organization that has disabled repository projects, + this will be true by default. + + .PARAMETER NoWiki + By default, this repository will have a Wiki. Specify this to disable the Wiki. + + .PARAMETER DisallowSquashMerge + By default, squash-merging pull requests will be allowed. + Specify this to disallow. + + .PARAMETER DisallowMergeCommit + By default, merging pull requests with a merge commit will be allowed. + Specify this to disallow. + + .PARAMETER DisallowRebaseMerge + By default, rebase-merge pull requests will be allowed. + Specify this to disallow. + + .PARAMETER DisallowRebaseMerge + Specify this to archive this repository. + NOTE: You cannot unarchive repositories through the API / this module. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .NOTES + It's possible that the ValidateSet values for GitIgnoreTemplate and LicenseTemplate + can get out of sync if some are added or removed after this module is published. + It was considered to make these free-form entries, but the most likely scenario is + that entries won't be added/removed often, making it more convenient to the end-user + to have the finite set of options available directly via the ValidateSets. + + .EXAMPLE + Update-GitHubRepository -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Description 'The best way to automate your GitHub interactions' + + .EXAMPLE + Update-GitHubRepository -Uri https://github.com/PowerShell/PowerShellForGitHub -Private:$false +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $Description, + + [string] $Homepage, + + [string] $DefaultBranch, + + [switch] $Private, + + [switch] $NoIssues, + + [switch] $NoProjects, + + [switch] $NoWiki, + + [switch] $DisallowSquashMerge, + + [switch] $DisallowMergeCommit, + + [switch] $DisallowRebaseMerge, + + [switch] $Archived, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{ + 'name' = $RepositoryName + } + + if ($PSBoundParameters.ContainsKey('Description')) { $hashBody['description'] = $Description } + if ($PSBoundParameters.ContainsKey('Homepage')) { $hashBody['homepage'] = $Homepage } + if ($PSBoundParameters.ContainsKey('DefaultBranch')) { $hashBody['default_branch'] = $DefaultBranch } + if ($PSBoundParameters.ContainsKey('Private')) { $hashBody['private'] = $Private.ToBool() } + if ($PSBoundParameters.ContainsKey('NoIssues')) { $hashBody['has_issues'] = (-not $NoIssues.ToBool()) } + if ($PSBoundParameters.ContainsKey('NoProjects')) { $hashBody['has_projects'] = (-not $NoProjects.ToBool()) } + if ($PSBoundParameters.ContainsKey('NoWiki')) { $hashBody['has_wiki'] = (-not $NoWiki.ToBool()) } + if ($PSBoundParameters.ContainsKey('DisallowSquashMerge')) { $hashBody['allow_squash_merge'] = (-not $DisallowSquashMerge.ToBool()) } + if ($PSBoundParameters.ContainsKey('DisallowMergeCommit')) { $hashBody['allow_merge_commit'] = (-not $DisallowMergeCommit.ToBool()) } + if ($PSBoundParameters.ContainsKey('DisallowRebaseMerge')) { $hashBody['allow_rebase_merge'] = (-not $DisallowRebaseMerge.ToBool()) } + if ($PSBoundParameters.ContainsKey('Archived')) { $hashBody['archived'] = (-not $Archived.ToBool()) } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$ReposistoryName" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Patch' + 'Description' = "Updating $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubRepositoryTopic +{ +<# + .SYNOPSIS + Retrieves information about a repository on GitHub. + + .DESCRIPTION + Retrieves information about a repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubRepositoryTopic -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepositoryTopic -Uri https://github.com/PowerShell/PowerShellForGitHub +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/topics" + 'Method' = 'Get' + 'Description' = "Getting topics for $RepositoryName" + 'AcceptHeader' = 'application/vnd.github.mercy-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Set-GitHubRepositoryTopic +{ +<# + .SYNOPSIS + Replaces all topics for a repository on GitHub. + + .DESCRIPTION + Replaces all topics for a repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Name + Array of topics to add to the repository. + + .PARAMETER Clear + Specify this to clear all topics from the repository. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Set-GitHubRepositoryTopic -OwnerName PowerShell -RepositoryName PowerShellForGitHub -Clear + + .EXAMPLE + Set-GitHubRepositoryTopic -Uri https://github.com/PowerShell/PowerShellForGitHub -Name ('octocat', 'powershell', 'github') +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='ElementsName')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='ElementsName')] + [Parameter(ParameterSetName='ElementsClear')] + [string] $OwnerName, + + [Parameter(ParameterSetName='ElementsName')] + [Parameter(ParameterSetName='ElementsClear')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='UriName')] + [Parameter( + Mandatory, + ParameterSetName='UriClear')] + [string] $Uri, + + [Parameter( + Mandatory, + ParameterSetName='ElementsName')] + [Parameter( + Mandatory, + ParameterSetName='UriName')] + [string[]] $Name, + + [Parameter( + Mandatory, + ParameterSetName='ElementsClear')] + [Parameter( + Mandatory, + ParameterSetName='UriClear')] + [switch] $Clear, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + 'Clear' = $PSBoundParameters.ContainsKey('Clear') + } + + $description = "Replacing topics in $RepositoryName" + if ($Clear) { $description = "Clearing topics in $RepositoryName" } + + $names = @($Name) + $hashBody = @{ + 'names' = $names + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/topics" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Put' + 'Description' = $description + 'AcceptHeader' = 'application/vnd.github.mercy-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +function Get-GitHubRepositoryContributor +{ +<# + .SYNOPSIS + Retrieve list of contributors for a given repository. + + .DESCRIPTION + Retrieve list of contributors for a given repository. + + GitHub identifies contributors by author email address. + This groups contribution counts by GitHub user, which includes all associated email addresses. + To improve performance, only the first 500 author email addresses in the repository link to + GitHub users. The rest will appear as anonymous contributors without associated GitHub user + information. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER IncludeAnonymousContributors + If specified, anonymous contributors will be included in the results. + + .PARAMETER IncludeStatistics + If specified, each result will include statistics for the number of additions, deletions + and commit counts, by week (excluding merge commits and empty commits). + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of contributors for the repository. + + .EXAMPLE + Get-GitHubRepositoryContributor -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepositoryContributor -Uri 'https://github.com/PowerShell/PowerShellForGitHub' -IncludeStatistics +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [switch] $IncludeAnonymousContributors, + + [switch] $IncludeStatistics, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + 'IncludeAnonymousContributors' = $IncludeAnonymousContributors.ToBool() + 'IncludeStatistics' = $IncludeStatistics.ToBool() + } + + $getParams = @() + if ($IncludeAnonymousContributors) { $getParams += 'anon=true' } + + $uriFragment = "repos/$OwnerName/$RepositoryName/contributors" + if ($IncludeStatistics) { $uriFragment = "repos/$OwnerName/$RepositoryName/stats/contributors" } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = "Getting contributors for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Get-GitHubRepositoryCollaborator +{ +<# + .SYNOPSIS + Retrieve list of contributors for a given repository. + + .DESCRIPTION + Retrieve list of contributors for a given repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of collaborators for the repository. + + .EXAMPLE + Get-GitHubRepositoryCollaborator -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepositoryCollaborator -Uri 'https://github.com/PowerShell/PowerShellForGitHub' +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/collaborators" + 'Description' = "Getting collaborators for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Get-GitHubRepositoryLanguage +{ +<# + .SYNOPSIS + Retrieves a list of the programming languages used in a repository on GitHub. + + .DESCRIPTION + Retrieves a list of the programming languages used in a repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of languages for the specified repository. The value shown + for each language is the number of bytes of code written in that language. + + .EXAMPLE + Get-GitHubRepositoryLanguage -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepositoryLanguage -Uri https://github.com/PowerShell/PowerShellForGitHub +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/languages" + 'Description' = "Getting languages for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Get-GitHubRepositoryTag +{ +<# + .SYNOPSIS + Retrieves tags for a repository on GitHub. + + .DESCRIPTION + Retrieves tags for a repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubRepositoryTag -OwnerName PowerShell -RepositoryName PowerShellForGitHub + + .EXAMPLE + Get-GitHubRepositoryTag -Uri https://github.com/PowerShell/PowerShellForGitHub +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/tags" + 'Description' = "Getting tags for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Move-GitHubRepositoryOwnership +{ +<# + .SYNOPSIS + Creates a new repository on GitHub. + + .DESCRIPTION + Creates a new repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER NewOwnerName + The username or organization name the repository will be transferred to. + + .PARAMETER TeamId + ID of the team or teams to add to the repository. Teams can only be added to + organization-owned repositories. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .NOTES + It's possible that the ValidateSet values for GitIgnoreTemplate and LicenseTemplate + can get out of sync if some are added or removed after this module is published. + It was considered to make these free-form entries, but the most likely scenario is + that entries won't be added/removed often, making it more convenient to the end-user + to have the finite set of options available directly via the ValidateSets. + + .EXAMPLE + Move-GitHubRepositoryOwnership -OwnerName PowerShell -RepositoryName PowerShellForGitHub -NewOwnerName OctoCat +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + [Alias('Transfer-GitHubRepositoryOwnership')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $NewOwnerName, + + [int[]] $TeamId, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $hashBody = @{ + 'new_owner' = $NewOwnerName + } + + if ($TeamId.Count -gt 0) { $hashBody['team_ids'] = @($TeamId) } + + $params = @{ + 'UriFragment' = "repos/$OwnerName/$RepositoryName/transfer" + 'Body' = ($hashBody | ConvertTo-Json) + 'Method' = 'Post' + 'Description' = "Transferring ownership of $RepositoryName to $NewOwnerName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} diff --git a/GitHubTeams.ps1 b/GitHubTeams.ps1 new file mode 100644 index 00000000..8dcd4b74 --- /dev/null +++ b/GitHubTeams.ps1 @@ -0,0 +1,208 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubTeam +{ +<# + .SYNOPSIS + Retrieve a team or teams within an organization or repository on GitHub. + + .DESCRIPTION + Retrieve a team or teams within an organization or repository on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER OrganizationName + The name of the organization + + .PARAMETER TeamId + The ID of the speific team to retrieve + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] The team(s) that match the user's request. + + .EXAMPLE + Get-GitHubTeam -OrganizationName PowerShell +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='Elements')] + param + ( + [Parameter(ParameterSetName='Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName='Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [Parameter( + Mandatory, + ParameterSetName='Organization')] + [ValidateNotNullOrEmpty()] + [string] $OrganizationName, + + [Parameter( + Mandatory, + ParameterSetName='Single')] + [ValidateNotNullOrEmpty()] + [string] $TeamId, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + $uriFragment = [String]::Empty + $description = [String]::Empty + if ($PSCmdlet.ParameterSetName -in ('Elements', 'Uri')) + { + $elements = Resolve-RepositoryElements -BoundParameters $PSBoundParameters + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties['OwnerName'] = Get-PiiSafeString -PlainText $OwnerName + $telemetryProperties['RepositoryName'] = Get-PiiSafeString -PlainText $RepositoryName + + $uriFragment = "/repos/$OwnerName/$RepositoryName/teams" + $description = "Getting teams for $RepositoryName" + } + elseif ($PSCmdlet.ParameterSetName -eq 'Organization') + { + $telemetryProperties['OrganizationName'] = Get-PiiSafeString -PlainText $OrganizationName + + $uriFragment = "/orgs/$OrganizationName/teams" + $description = "Gettings teams in $OrganizationName" + } + else + { + $telemetryProperties['TeamId'] = Get-PiiSafeString -PlainText $TeamId + + $uriFragment = "/teams/$TeamId" + $description = "Getting team $TeamId" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'AcceptHeader' = 'application/vnd.github.hellcat-preview+json' + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethodMultipleResult @params +} + +function Get-GitHubTeamMember +{ +<# + .SYNOPSIS + Retrieve list of team members within an organization. + + .DESCRIPTION + Retrieve list of team members within an organization. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OrganizationName + The name of the organization + + .PARAMETER TeamName + The name of the team in the organization + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .OUTPUTS + [PSCustomObject[]] List of members on the team within the organization. + + .EXAMPLE + $members = Get-GitHubTeamMember -Organization PowerShell -TeamName Everybody +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String] $OrganizationName, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String] $TeamName, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $NoStatus = Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus + + $teams = Get-GitHubTeam -OrganizationName $OrganizationName -AccessToken $AccessToken -NoStatus:$NoStatus + $team = $teams | Where-Object {$_.name -eq $TeamName} + if ($null -eq $team) + { + $message = "Unable to find the team [$TeamName] within the organization [$OrganizationName]." + Write-Log -Message $message -Level Error + throw $message + } + + $telemetryProperties = @{ + 'OrganizationName' = (Get-PiiSafeString -PlainText $OrganizationName) + 'TeamName' = (Get-PiiSafeString -PlainText $TeamName) + } + + $params = @{ + 'UriFragment' = "teams/$($team.id)/members" + 'Description' = "Getting members of the team $TeamName $($team.id)" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = $NoStatus + } + + return Invoke-GHRestMethodMultipleResult @params +} diff --git a/GitHubUsers.ps1 b/GitHubUsers.ps1 new file mode 100644 index 00000000..3cbe8d2d --- /dev/null +++ b/GitHubUsers.ps1 @@ -0,0 +1,270 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Get-GitHubUser +{ +<# + .SYNOPSIS + Retrieves information about the specified user on GitHub. + + .DESCRIPTION + Retrieves information about the specified user on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER User + The GitHub user to retrieve information for. + If not specified, will retrieve information on all GitHub users (and may take a while to complete). + + .PARAMETER Current + If specified, gets information on the current user. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .NOTES + The email key in the following response is the publicly visible email address from the + user's GitHub profile page. You only see publicly visible email addresses when + authenticated with GitHub. + + When setting up your profile, a user can select a primary email address to be public + which provides an email entry for this endpoint. If the user does not set a public + email address for email, then it will have a value of null. + + .EXAMPLE + Get-GitHubUser -User octocat + + Gets information on just the user named 'octocat' + + .EXAMPLE + Get-GitHubUser + + Gets information on every GitHub user. + + .EXAMPLE + Get-GitHubUser -Current + + Gets information on the current authenticated user. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName='ListAndSearch')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(ParameterSetName='ListAndSearch')] + [string] $User, + + [Parameter(ParameterSetName='Current')] + [switch] $Current, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $params = @{ + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + if ($Current) + { + return Invoke-GHRestMethod -UriFragment "user" -Description "Getting current authenticated user" -Method 'Get' @params + } + elseif ([String]::IsNullOrEmpty($User)) + { + return Invoke-GHRestMethodMultipleResult -UriFragment 'users' -Description 'Getting all users' @params + } + else + { + return Invoke-GHRestMethod -UriFragment "users/$User" -Description "Getting user $User" -Method 'Get' @params + } +} + +function Get-GitHubUserContextualInformation +{ +<# + .SYNOPSIS + Retrieves contextual information about the specified user on GitHub. + + .DESCRIPTION + Retrieves contextual information about the specified user on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER User + The GitHub user to retrieve information for. + + .PARAMETER Subject + Identifies which additional information to receive about the user's hovercard. + + .PARAMETER SubjectId + The ID for the Subject. Required when Subject has been specified. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubUserContextualInformation -User octocat + + .EXAMPLE + Get-GitHubUserContextualInformation -User octocat -Subject repository -SubjectId 1300192 +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [string] $User, + + [ValidateSet('organization', 'repository', 'issue', 'pull_request')] + [string] $Subject, + + [string] $SubjectId, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $getParams = @() + + # Intentionally not using -xor here because we need to know if we're setting the GET parameters as well. + if ((-not [String]::IsNullOrEmpty($Subject)) -or (-not [String]::IsNullOrEmpty($SubjectId))) + { + if ([String]::IsNullOrEmpty($Subject) -or [String]::IsNullOrEmpty($SubjectId)) + { + $message = 'If either Subject or SubjectId has been provided, then BOTH must be provided.' + Write-Log -Message $message -Level Error + throw $message + } + + $getParams += "subject_type=$Subject" + $getParams += "subject_id=$SubjectId" + } + + $params = @{ + 'UriFragment' = "users/$User/hovercard`?" + ($getParams -join '&') + 'Method' = 'Get' + 'Description' = "Getting hovercard information for $User" + 'AcceptHeader' = 'application/vnd.github.hagar-preview+json' + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + Invoke-GHRestMethod @params +} + +function Update-GitHubCurrentUser +{ +<# + .SYNOPSIS + Updates information about the current authenticated user on GitHub. + + .DESCRIPTION + Updates information about the current authenticated user on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Name + The new name of the user. + + .PARAMETER Email + The publicly visible email address of the user. + + .PARAMETER Blog + The new blog URL of the user. + + .PARAMETER Company + The new company of the user. + + .PARAMETER Location + The new location of the user. + + .PARAMETER Bio + The new short biography of the user. + + .PARAMETER Hireable + Specify to indicate a change in hireable availability for the current authenticated user's + GitHub profile. To change to "not hireable", specify -Hireable:$false + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Update-GitHubCurrentUser -Location 'Seattle, WA' -Hireable:$false + + Updates the current user to indicate that their location is "Seattle, WA" and that they + are not currently hireable. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [string] $Name, + + [string] $Email, + + [string] $Blog, + + [string] $Company, + + [string] $Location, + + [string] $Bio, + + [switch] $Hireable, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $hashBody = @{} + if ($PSBoundParameters.ContainsKey('Name')) { $hashBody['name'] = $Name } + if ($PSBoundParameters.ContainsKey('Email')) { $hashBody['email'] = $Email } + if ($PSBoundParameters.ContainsKey('Blog')) { $hashBody['blog'] = $Blog } + if ($PSBoundParameters.ContainsKey('Company')) { $hashBody['company'] = $Company } + if ($PSBoundParameters.ContainsKey('Location')) { $hashBody['location'] = $Location } + if ($PSBoundParameters.ContainsKey('Bio')) { $hashBody['bio'] = $Bio } + if ($PSBoundParameters.ContainsKey('Hireable')) { $hashBody['hireable'] = $Hireable.ToBool() } + + $params = @{ + 'UriFragment' = 'user' + 'Method' = 'Patch' + 'Body' = ($hashBody | ConvertTo-Json) + 'Description' = "Updating current authenticated user" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} diff --git a/Helpers.ps1 b/Helpers.ps1 new file mode 100644 index 00000000..8050c78b --- /dev/null +++ b/Helpers.ps1 @@ -0,0 +1,735 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +function Wait-JobWithAnimation +{ +<# + .SYNOPSIS + Waits for a background job to complete by showing a cursor and elapsed time. + + .DESCRIPTION + Waits for a background job to complete by showing a cursor and elapsed time. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Name + The name of the job(s) that we are waiting to complete. + + .PARAMETER Description + The text displayed next to the spinning cursor, explaining what the job is doing. + + .PARAMETER StopAllOnAnyFailure + Will call Stop-Job on any jobs still Running if any of the specified jobs entered + the Failed state. + + .EXAMPLE + Wait-JobWithAnimation Job1 + Waits for a job named "Job1" to exit the "Running" state. While waiting, shows + a waiting cursor and the elapsed time. + + .NOTES + This is not a stand-in replacement for Wait-Job. It does not provide the full + set of configuration options that Wait-Job does. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [string[]] $Name, + + [string] $Description = "", + + [switch] $StopAllOnAnyFailure + ) + + [System.Collections.ArrayList]$runningJobs = $Name + $allJobsCompleted = $true + $hasFailedJob = $false + + $animationFrames = '|','/','-','\' + $framesPerSecond = 9 + + # We'll wrap the description (if provided) in brackets for display purposes. + if ($Description -ne "") + { + $Description = "[$Description]" + } + + $iteration = 0 + while ($runningJobs.Count -gt 0) + { + # We'll run into issues if we try to modify the same collection we're iterating over + $jobsToCheck = $runningJobs.ToArray() + foreach ($jobName in $jobsToCheck) + { + $state = (Get-Job -Name $jobName).state + if ($state -ne 'Running') + { + $runningJobs.Remove($jobName) + + if ($state -ne 'Completed') + { + $allJobsCompleted = $false + } + + if ($state -eq 'Failed') + { + $hasFailedJob = $true + if ($StopAllOnAnyFailure) + { + break + } + } + } + } + + if ($hasFailedJob -and $StopAllOnAnyFailure) + { + foreach ($jobName in $runningJobs) + { + Stop-Job -Name $jobName + } + + $runingJobs.Clear() + } + + Write-InteractiveHost "`r$($animationFrames[$($iteration % $($animationFrames.Length))]) Elapsed: $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Yellow + Start-Sleep -Milliseconds ([int](1000/$framesPerSecond)) + $iteration++ + } + + if ($allJobsCompleted) + { + Write-InteractiveHost "`rDONE - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Green + + # We forcibly set Verbose to false here since we don't need it printed to the screen, since we just did above -- we just need to log it. + Write-Log -Message "DONE - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -Level Verbose -Verbose:$false + } + else + { + Write-InteractiveHost "`rDONE (FAILED) - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -NoNewline -f Red + + # We forcibly set Verbose to false here since we don't need it printed to the screen, since we just did above -- we just need to log it. + Write-Log -Message "DONE (FAILED) - Operation took $([int]($iteration / $framesPerSecond)) second(s) $Description" -Level Verbose -Verbose:$false + } + + Write-InteractiveHost "" +} + +function Get-SHA512Hash +{ +<# + .SYNOPSIS + Gets the SHA512 hash of the requested string. + + .DESCRIPTION + Gets the SHA512 hash of the requested string. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER PlainText + The plain text that you want the SHA512 hash for. + + .EXAMPLE + Get-SHA512Hash -PlainText "Hello World" + + Returns back the string "2C74FD17EDAFD80E8447B0D46741EE243B7EB74DD2149A0AB1B9246FB30382F27E853D8585719E0E67CBDA0DAA8F51671064615D645AE27ACB15BFB1447F459B" + which represents the SHA512 hash of "Hello World" + + .OUTPUTS + System.String - A SHA512 hash of the provided string +#> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowNull()] + [AllowEmptyString()] + [string] $PlainText + ) + + $sha512= New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider + $utf8 = New-Object -TypeName System.Text.UTF8Encoding + return [System.BitConverter]::ToString($sha512.ComputeHash($utf8.GetBytes($PlainText))) -replace '-', '' +} + + +function Write-Log +{ +<# + .SYNOPSIS + Writes logging information to screen and log file simultaneously. + + .DESCRIPTION + Writes logging information to screen and log file simultaneously. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Message + The message(s) to be logged. Each element of the array will be written to a separate line. + + This parameter supports pipelining but there are no + performance benefits to doing so. For more information, see the .NOTES for this + cmdlet. + + .PARAMETER Level + The type of message to be logged. + + .PARAMETER Indent + The number of spaces to indent the line in the log file. + + .PARAMETER Path + The log file path. + Defaults to $env:USERPROFILE\Documents\PowerShellForGitHub.log + + .PARAMETER Exception + If present, the exception information will be logged after the messages provided. + The actual string that is logged is obtained by passing this object to Out-String. + + .EXAMPLE + Write-Log -Message "Everything worked." -Path C:\Debug.log + + Writes the message "Everything worked." to the screen as well as to a log file at "c:\Debug.log", + with the caller's username and a date/time stamp prepended to the message. + + .EXAMPLE + Write-Log -Message ("Everything worked.", "No cause for alarm.") -Path C:\Debug.log + + Writes the following message to the screen as well as to a log file at "c:\Debug.log", + with the caller's username and a date/time stamp prepended to the message: + + Everything worked. + No cause for alarm. + + .EXAMPLE + Write-Log -Message "There may be a problem..." -Level Warning -Indent 2 + + Writes the message "There may be a problem..." to the warning pipeline indented two spaces, + as well as to the default log file with the caller's username and a date/time stamp + prepended to the message. + + .EXAMPLE + try { $null.Do() } + catch { Write-Log -Message ("There was a problem.", "Here is the exception information:") -Exception $_ -Level Error } + + Logs the message: + + Write-Log : 2018-01-23 12:57:37 : dabelc : There was a problem. + Here is the exception information: + You cannot call a method on a null-valued expression. + At line:1 char:7 + + try { $null.Do() } catch { Write-Log -Message ("There was a problem." ... + + ~~~~~~~~~~ + + CategoryInfo : InvalidOperation: (:) [], RuntimeException + + FullyQualifiedErrorId : InvokeMethodOnNull + + .INPUTS + System.String + + .NOTES + The "LogPath" configuration value indicates where the log file will be created. + The "" determines if log entries will be made to the log file. + If $false, log entries will ONLY go to the relevant output pipeline. + + Note that, although this function supports pipeline input to the -Message parameter, + there is NO performance benefit to using the pipeline. This is because the pipeline + input is simply accumulated and not acted upon until all input has been received. + This behavior is intentional, in order for a statement like: + "Multiple", "messages" | Write-Log -Exception $ex -Level Error + to make sense. In this case, the cmdlet should accumulate the messages and, at the end, + include the exception information. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="We need to be able to access the PID for logging purposes, and it is accessed via a global variable.")] + param( + [Parameter(ValueFromPipeline)] + [AllowEmptyCollection()] + [AllowEmptyString()] + [AllowNull()] + [string[]] $Message = @(), + + [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')] + [string] $Level = 'Informational', + + [ValidateRange(1, 30)] + [Int16] $Indent = 0, + + [IO.FileInfo] $Path = (Get-GitHubConfiguration -Name LogPath), + + [System.Management.Automation.ErrorRecord] $Exception + ) + + Begin + { + # Accumulate the list of Messages, whether by pipeline or parameter. + $messages = @() + } + + Process + { + foreach ($m in $Message) + { + $messages += $m + } + } + + End + { + if ($null -ne $Exception) + { + # If we have an exception, add it after the accumulated messages. + $messages += Out-String -InputObject $Exception + } + elseif ($messages.Count -eq 0) + { + # If no exception and no messages, we should early return. + return + } + + # Finalize the string to be logged. + $finalMessage = $messages -join [Environment]::NewLine + + # Build the console and log-specific messages. + $date = Get-Date + $dateString = $date.ToString("yyyy-MM-dd HH:mm:ss") + if (Get-GitHubConfiguration -Name LogTimeAsUtc) + { + $dateString = $date.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ssZ") + } + + $consoleMessage = '{0}{1}' -f + (" " * $Indent), + $finalMessage + + if (Get-GitHubConfiguration -Name LogProcessId) + { + $maxPidDigits = 10 # This is an estimate (see https://stackoverflow.com/questions/17868218/what-is-the-maximum-process-id-on-windows) + $pidColumnLength = $maxPidDigits + "[]".Length + $logFileMessage = "{0}{1} : {2, -$pidColumnLength} : {3} : {4} : {5}" -f + (" " * $Indent), + $dateString, + "[$global:PID]", + $env:username, + $Level.ToUpper(), + $finalMessage + } + else + { + $logFileMessage = '{0}{1} : {2} : {3} : {4}' -f + (" " * $Indent), + $dateString, + $env:username, + $Level.ToUpper(), + $finalMessage + } + + # Write the message to screen/log. + # Note that the below logic could easily be moved to a separate helper function, but a concious + # decision was made to leave it here. When this cmdlet is called with -Level Error, Write-Error + # will generate a WriteErrorException with the origin being Write-Log. If this call is moved to + # a helper function, the origin of the WriteErrorException will be the helper function, which + # could confuse an end user. + switch ($Level) + { + # Need to explicitly say SilentlyContinue here so that we continue on, given that + # we've assigned a script-level ErrorActionPreference of "Stop" for the module. + 'Error' { Write-Error $consoleMessage -ErrorAction SilentlyContinue } + 'Warning' { Write-Warning $consoleMessage } + 'Verbose' { Write-Verbose $consoleMessage } + 'Debug' { Write-Debug $consoleMessage } + 'Informational' { + # We'd prefer to use Write-Information to enable users to redirect that pipe if + # they want, unfortunately it's only available on v5 and above. We'll fallback to + # using Write-Host for earlier versions (since we still need to support v4). + if ($PSVersionTable.PSVersion.Major -ge 5) + { + Write-Information $consoleMessage -InformationAction Continue + } + else + { + Write-InteractiveHost $consoleMessage + } + } + } + + try + { + if (-not (Get-GitHubConfiguration -Name DisableLogging)) + { + if ([String]::IsNullOrWhiteSpace($Path)) + { + Write-Warning 'Logging is currently enabled, however no path has been specified for the log file. Use "Set-GitHubConfiguration -LogPath" to set the log path, or "Set-GitHubConfiguration -DisableLogging" to disable logging.' + } + else + { + $logFileMessage | Out-File -FilePath $Path -Append + } + } + } + catch + { + $output = @() + $output += "Failed to add log entry to [$Path]. The error was:" + $output += Out-String -InputObject $_ + + if (Test-Path -Path $Path -PathType Leaf) + { + # The file exists, but likely is being held open by another process. + # Let's do best effort here and if we can't log something, just report + # it and move on. + $output += "This is non-fatal, and your command will continue. Your log file will be missing this entry:" + $output += $consoleMessage + Write-Warning ($output -join [Environment]::NewLine) + } + else + { + # If the file doesn't exist and couldn't be created, it likely will never + # be valid. In that instance, let's stop everything so that the user can + # fix the problem, since they have indicated that they want this logging to + # occur. + throw ($output -join [Environment]::NewLine) + } + } + } +} + +function Write-InvocationLog +{ +<# + .SYNOPSIS + Writes a log entry for the invoke command. + + .DESCRIPTION + Writes a log entry for the invoke command. + + To control whether or not this will also write out the details of every parameter, + users can call 'Set-GitHubConfiguration -DisableParameterLogging:<$true|$false>' + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER InvocationInfo + The '$MyInvocationInfo' object from the calling function. + + .EXAMPLE + Write-InvocationLog -Invocation $MyInvocation +#> + [CmdletBinding(SupportsShouldProcess)] + param( + [Management.Automation.InvocationInfo] $Invocation + ) + + Write-Log -Message "[$($Invocation.MyCommand.Module.Version)] Executing: $($Invocation.Line.Trim())" -Level Verbose + + if (-not (Get-GitHubConfiguration -Name DisableParameterLogging)) + { + # Get the length of all invoked parameter names so that the output can all be aligned. + $maxLength = 0 + $Invocation.BoundParameters.Keys | + ForEach-Object { $maxLength = [Math]::Max($maxLength, $_.Length + 1) } + + $Invocation.BoundParameters.GetEnumerator() | + Where-Object { ($null -ne $_) -and ($null -ne $_.Value) } | + ForEach-Object { + $value = $_.Value + if ($null -eq $value) + { + $value = '$null' + } + + Write-Log -Message ("{0, -$maxLength} {1}" -F ($_.Key + ":"), $value.ToString()) -Level Verbose + } + } +} + +function DeepCopy-Object +<# + .SYNOPSIS + Creates a deep copy of a serializable object. + + .DESCRIPTION + Creates a deep copy of a serializable object. + By default, PowerShell performs shallow copies (simple references) + when assigning objects from one variable to another. This will + create full exact copies of the provided object so that they + can be manipulated independently of each other, provided that the + object being copied is serializable. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER InputObject + The object that is to be copied. This must be serializable or this will fail. + + .EXAMPLE + $bar = DeepCopy-Object -InputObject $foo + Assuming that $foo is serializable, $bar will now be an exact copy of $foo, but + any changes that you make to one will not affect the other. + + .RETURNS + An exact copy of the PSObject that was just deep copied. +#> +{ + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Intentional. This isn't exported, and needed to be explicit relative to Copy-Object.")] + param( + [Parameter(Mandatory)] + [PSCustomObject] $InputObject + ) + + $memoryStream = New-Object System.IO.MemoryStream + $binaryFormatter = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + $binaryFormatter.Serialize($memoryStream, $InputObject) + $memoryStream.Position = 0 + $DeepCopiedObject = $binaryFormatter.Deserialize($memoryStream) + $memoryStream.Close() + + return $DeepCopiedObject +} + +function New-TemporaryDirectory +{ +<# + .SYNOPSIS + Creates a new subdirectory within the users's temporary directory and returns the path. + + .DESCRIPTION + Creates a new subdirectory within the users's temporary directory and returns the path. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .EXAMPLE + New-TemporaryDirectory + Creates a new directory with a GUID under $env:TEMP + + .OUTPUTS + System.String - The path to the newly created temporary directory +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param() + + $guid = [System.GUID]::NewGuid() + while (Test-Path -PathType Container (Join-Path -Path $env:TEMP -ChildPath $guid)) + { + $guid = [System.GUID]::NewGuid() + } + + $tempFolderPath = Join-Path -Path $env:TEMP -ChildPath $guid + + Write-Log -Message "Creating temporary directory: $tempFolderPath" -Level Verbose + New-Item -ItemType Directory -Path $tempFolderPath +} + +function Write-InteractiveHost +{ +<# + .SYNOPSIS + Forwards to Write-Host only if the host is interactive, else does nothing. + + .DESCRIPTION + A proxy function around Write-Host that detects if the host is interactive + before calling Write-Host. Use this instead of Write-Host to avoid failures in + non-interactive hosts. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .EXAMPLE + Write-InteractiveHost "Test" + Write-InteractiveHost "Test" -NoNewline -f Yellow + + .NOTES + Boilerplate is generated using these commands: + # $Metadata = New-Object System.Management.Automation.CommandMetaData (Get-Command Write-Host) + # [System.Management.Automation.ProxyCommand]::Create($Metadata) | Out-File temp +#> + + [CmdletBinding( + HelpUri='http://go.microsoft.com/fwlink/?LinkID=113426', + RemotingCapability='None')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="This provides a wrapper around Write-Host. In general, we'd like to use Write-Information, but it's not supported on PS 4.0 which we need to support.")] + param( + [Parameter( + Position=0, + ValueFromPipeline, + ValueFromRemainingArguments)] + [System.Object] $Object, + + [switch] $NoNewline, + + [System.Object] $Separator, + + [System.ConsoleColor] $ForegroundColor, + + [System.ConsoleColor] $BackgroundColor + ) + + # Determine if the host is interactive + if ([Environment]::UserInteractive -and ` + ![Bool]([Environment]::GetCommandLineArgs() -like '-noni*') -and ` + (Get-Host).Name -ne 'Default Host') + { + # Special handling for OutBuffer (generated for the proxy function) + $outBuffer = $null + if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) + { + $PSBoundParameters['OutBuffer'] = 1 + } + + Write-Host @PSBoundParameters + } +} + +function Resolve-UnverifiedPath +{ +<# + .SYNOPSIS + A wrapper around Resolve-Path that works for paths that exist as well + as for paths that don't (Resolve-Path normally throws an exception if + the path doesn't exist.) + + .DESCRIPTION + A wrapper around Resolve-Path that works for paths that exist as well + as for paths that don't (Resolve-Path normally throws an exception if + the path doesn't exist.) + + The Git repo for this module can be found here: https://aka.ms/PowerShellForGitHub + + .EXAMPLE + Resolve-UnverifiedPath -Path 'c:\windows\notepad.exe' + + Returns the string 'c:\windows\notepad.exe'. + + .EXAMPLE + Resolve-UnverifiedPath -Path '..\notepad.exe' + + Returns the string 'c:\windows\notepad.exe', assuming that it's executed from + within 'c:\windows\system32' or some other sub-directory. + + .EXAMPLE + Resolve-UnverifiedPath -Path '..\foo.exe' + + Returns the string 'c:\windows\foo.exe', assuming that it's executed from + within 'c:\windows\system32' or some other sub-directory, even though this + file doesn't exist. + + .OUTPUTS + [string] - The fully resolved path + +#> + [CmdletBinding()] + param( + [Parameter( + Position=0, + ValueFromPipeline)] + [string] $Path + ) + + $resolvedPath = Resolve-Path -Path $Path -ErrorVariable resolvePathError -ErrorAction SilentlyContinue + + if ($null -eq $resolvedPath) + { + return $resolvePathError[0].TargetObject + } + else + { + return $resolvedPath.ProviderPath + } +} + +function Ensure-Directory +{ +<# + .SYNOPSIS + A utility function for ensuring a given directory exists. + + .DESCRIPTION + A utility function for ensuring a given directory exists. + + If the directory does not already exist, it will be created. + + .PARAMETER Path + A full or relative path to the directory that should exist when the function exits. + + .NOTES + Uses the Resolve-UnverifiedPath function to resolve relative paths. +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification = "Unable to find a standard verb that satisfies describing the purpose of this internal helper method.")] + param( + [Parameter(Mandatory)] + [string] $Path + ) + + try + { + $Path = Resolve-UnverifiedPath -Path $Path + + if (-not (Test-Path -PathType Container -Path $Path)) + { + Write-Log -Message "Creating directory: [$Path]" -Level Verbose + New-Item -ItemType Directory -Path $Path | Out-Null + } + } + catch + { + Write-Log -Message "Could not ensure directory: [$Path]" -Level Error + + throw + } +} + +function Get-HttpWebResponseContent +{ +<# + .SYNOPSIS + Returns the content that may be contained within an HttpWebResponse object. + + .DESCRIPTION + Returns the content that may be contained within an HttpWebResponse object. + + This would commonly be used when trying to get the potential content + returned within a failing WebResponse. Normally, when you call + Invoke-WebRequest, it returns back a BasicHtmlWebResponseObject which + directly contains a Content property, however if the web request fails, + you get a WebException which contains a simpler WebResponse, which + requires a bit more effort in order to acccess the raw response content. + + .PARAMETER WebResponse + An HttpWebResponse object, typically the Response property on a WebException. + + .OUTPUTS + System.String - The raw content that was included in a WebResponse; $null otherwise. +#> + [CmdletBinding()] + [OutputType([String])] + param( + [System.Net.HttpWebResponse] $WebResponse + ) + + $streamReader = $null + + try + { + $content = $null + + if (($null -ne $WebResponse) -and ($WebResponse.ContentLength -gt 0)) + { + $stream = $WebResponse.GetResponseStream() + $encoding = [System.Text.Encoding]::UTF8 + if (-not [String]::IsNullOrWhiteSpace($WebResponse.ContentEncoding)) + { + $encoding = [System.Text.Encoding]::GetEncoding($WebResponse.ContentEncoding) + } + + $streamReader = New-Object -TypeName System.IO.StreamReader -ArgumentList ($stream, $encoding) + $content = $streamReader.ReadToEnd() + } + + return $content + } + finally + { + if ($null -ne $streamReader) + { + $streamReader.Close() + } + } +} diff --git a/NugetTools.ps1 b/NugetTools.ps1 new file mode 100644 index 00000000..eb414bbd --- /dev/null +++ b/NugetTools.ps1 @@ -0,0 +1,378 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# The cached location of nuget.exe +$script:nugetExePath = [String]::Empty + +# The directory where we'll store the assemblies that we dynamically download during this session. +$script:tempAssemblyCacheDir = [String]::Empty + +function Get-NugetExe +{ +<# + .SYNOPSIS + Downloads nuget.exe from http://nuget.org to a new local temporary directory + and returns the path to the local copy. + + .DESCRIPTION + Downloads nuget.exe from http://nuget.org to a new local temporary directory + and returns the path to the local copy. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .EXAMPLE + Get-NugetExe + Creates a new directory with a GUID under $env:TEMP and then downloads + http://nuget.org/nuget.exe to that location. + + .OUTPUTS + System.String - The path to the newly downloaded nuget.exe +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param() + + if ([String]::IsNullOrEmpty($script:nugetExePath)) + { + $sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" + $script:nugetExePath = Join-Path $(New-TemporaryDirectory) "nuget.exe" + + Write-Log -Message "Downloading $sourceNugetExe to $script:nugetExePath" -Level Verbose + Invoke-WebRequest $sourceNugetExe -OutFile $script:nugetExePath + } + + return $script:nugetExePath +} + +function Get-NugetPackage +{ +<# + .SYNOPSIS + Downloads a nuget package to the specified directory. + + .DESCRIPTION + Downloads a nuget package to the specified directory (or the current + directory if no TargetPath was specified). + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER PackageName + The name of the nuget package to download + + .PARAMETER TargetPath + The nuget package will be downloaded to this location. + + .PARAMETER Version + If provided, this indicates the version of the package to download. + If not specified, downloads the latest version. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-NugetPackage "Microsoft.AzureStorage" -Version "6.0.0.0" -TargetPath "c:\foo" + Downloads v6.0.0.0 of the Microsoft.AzureStorage nuget package to the c:\foo directory. + + .EXAMPLE + Get-NugetPackage "Microsoft.AzureStorage" "c:\foo" + Downloads the most recent version of the Microsoft.AzureStorage + nuget package to the c:\foo directory. +#> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [string] $PackageName, + + [Parameter(Mandatory)] + [ValidateScript({if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ does not exist." }})] + [string] $TargetPath, + + [string] $Version, + + [switch] $NoStatus + ) + + Write-Log -Message "Downloading nuget package [$PackageName] to [$TargetPath]" -Level Verbose + + $nugetPath = Get-NugetExe + + if ($NoStatus) + { + if ($PSCmdlet.ShouldProcess($PackageName, $nugetPath)) + { + if (-not [System.String]::IsNullOrEmpty($Version)) + { + & $nugetPath install $PackageName -o $TargetPath -version $Version -source nuget.org -NonInteractive | Out-Null + } + else + { + & $nugetPath install $PackageName -o $TargetPath -source nuget.org -NonInteractive | Out-Null + } + } + } + else + { + $jobName = "Get-NugetPackage-" + (Get-Date).ToFileTime().ToString() + + if ($PSCmdlet.ShouldProcess($jobName, "Start-Job")) + { + [scriptblock]$scriptBlock = { + param($NugetPath, $PackageName, $TargetPath, $Version) + + if (-not [System.String]::IsNullOrEmpty($Version)) + { + & $NugetPath install $PackageName -o $TargetPath -version $Version -source nuget.org + } + else + { + & $NugetPath install $PackageName -o $TargetPath -source nuget.org + } + } + + Start-Job -Name $jobName -ScriptBlock $scriptBlock -Arg @($nugetPath, $PackageName, $TargetPath, $Version) | Out-Null + + if ($PSCmdlet.ShouldProcess($jobName, "Wait-JobWithAnimation")) + { + Wait-JobWithAnimation -Name $jobName -Description "Retrieving nuget package: $PackageName" + } + + if ($PSCmdlet.ShouldProcess($jobName, "Receive-Job")) + { + Receive-Job $jobName -AutoRemoveJob -Wait -ErrorAction SilentlyContinue -ErrorVariable remoteErrors | Out-Null + } + } + + if ($remoteErrors.Count -gt 0) + { + throw $remoteErrors[0].Exception + } + } +} + +function Test-AssemblyIsDesiredVersion +{ + <# + .SYNOPSIS + Checks if the specified file is the expected version. + + .DESCRIPTION + Checks if the specified file is the expected version. + + Does a best effort match. If you only specify a desired version of "6", + any version of the file that has a "major" version of 6 will be considered + a match, where we use the terminology of a version being: + Major.Minor.Build.PrivateInfo. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER AssemblyPath + The full path to the assembly file being tested. + + .PARAMETER DesiredVersion + The desired version of the assembly. Specify the version as specifically as + necessary. + + .EXAMPLE + Test-AssemblyIsDesiredVersion "c:\Microsoft.WindowsAzure.Storage.dll" "6" + + Returns back $true if "c:\Microsoft.WindowsAzure.Storage.dll" has a major version + of 6, regardless of its Minor, Build or PrivateInfo numbers. + + .OUTPUTS + Boolean - $true if the assembly at the specified path exists and meets the specified + version criteria, $false otherwise. +#> + param( + [Parameter(Mandatory)] + [ValidateScript( { if (Test-Path -PathType Leaf -Path $_) { $true } else { throw "'$_' cannot be found." } })] + [string] $AssemblyPath, + + [Parameter(Mandatory)] + [ValidateScript( { if ($_ -match '^\d+(\.\d+){0,3}$') { $true } else { throw "'$_' not a valid version format." } })] + [string] $DesiredVersion + ) + + $splitTargetVer = $DesiredVersion.Split('.') + + $versionInfo = (Get-Item -Path $AssemblyPath).VersionInfo + $splitSourceVer = @( + $versionInfo.ProductMajorPart, + $versionInfo.ProductMinorPart, + $versionInfo.ProductBuildPart, + $versionInfo.ProductPrivatePart + ) + + # The cmdlet contract states that we only care about matching + # as much of the version number as the user has supplied. + for ($i = 0; $i -lt $splitTargetVer.Count; $i++) + { + if ($splitSourceVer[$i] -ne $splitTargetVer[$i]) + { + return $false + } + } + + return $true +} + +function Get-NugetPackageDllPath +{ +<# + .SYNOPSIS + Makes sure that the specified assembly from a nuget package is available + on the machine, and returns the path to it. + + .DESCRIPTION + Makes sure that the specified assembly from a nuget package is available + on the machine, and returns the path to it. + + This will first look for the assembly in the module's script directory. + + Next it will look for the assembly in the location defined by the configuration + property AssemblyPath. + + If not found there, it will look in a temp folder established during this + PowerShell session. + + If still not found, it will download the nuget package + for it to a temp folder accessible during this PowerShell session. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NugetPackageName + The name of the nuget package to download + + .PARAMETER NugetPackageVersion + Indicates the version of the package to download. + + .PARAMETER AssemblyPackageTailDirectory + The sub-path within the nuget package download location where the assembly should be found. + + .PARAMETER AssemblyName + The name of the actual assembly that the user is looking for. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-NugetPackageDllPath "WindowsAzure.Storage" "6.0.0" "WindowsAzure.Storage.6.0.0\lib\net40\" "Microsoft.WindowsAzure.Storage.dll" + + Returns back the path to "Microsoft.WindowsAzure.Storage.dll", which is part of the + "WindowsAzure.Storage" nuget package. If the package has to be downloaded via nuget, + the command prompt will show a time duration status counter while the package is being + downloaded. + + .EXAMPLE + Get-NugetPackageDllPath "WindowsAzure.Storage" "6.0.0" "WindowsAzure.Storage.6.0.0\lib\net40\" "Microsoft.WindowsAzure.Storage.dll" -NoStatus + + Returns back the path to "Microsoft.WindowsAzure.Storage.dll", which is part of the + "WindowsAzure.Storage" nuget package. If the package has to be downloaded via nuget, + the command prompt will appear to hang during this time. + + .OUTPUTS + System.String - The full path to $AssemblyName. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [string] $NugetPackageName, + + [Parameter(Mandatory)] + [string] $NugetPackageVersion, + + [Parameter(Mandatory)] + [string] $AssemblyPackageTailDirectory, + + [Parameter(Mandatory)] + [string] $AssemblyName, + + [switch] $NoStatus + ) + + Write-Log -Message "Looking for $AssemblyName" -Level Verbose + + # First we'll check to see if the user has cached the assembly into the module's script directory + $moduleAssembly = Join-Path -Path $PSScriptRoot -ChildPath $AssemblyName + if (Test-Path -Path $moduleAssembly -PathType Leaf) + { + if (Test-AssemblyIsDesiredVersion -AssemblyPath $moduleAssembly -DesiredVersion $NugetPackageVersion) + { + Write-Log -Message "Found $AssemblyName in module directory ($PSScriptRoot)." -Level Verbose + return $moduleAssembly + } + else + { + Write-Log -Message "Found $AssemblyName in module directory ($PSScriptRoot), but its version number [$moduleAssembly] didn't match required [$NugetPackageVersion]." -Level Verbose + } + } + + # Next, we'll check to see if the user has defined an alternate path to get the assembly from + $alternateAssemblyPath = Get-GitHubConfiguration -Name AssemblyPath + if (-not [System.String]::IsNullOrEmpty($alternateAssemblyPath)) + { + $assemblyPath = Join-Path -Path $alternateAssemblyPath -ChildPath $AssemblyName + if (Test-Path -Path $assemblyPath -PathType Leaf) + { + if (Test-AssemblyIsDesiredVersion -AssemblyPath $assemblyPath -DesiredVersion $NugetPackageVersion) + { + Write-Log -Message "Found $AssemblyName in alternate directory ($alternateAssemblyPath)." -Level Verbose + return $assemblyPath + } + else + { + Write-Log -Message "Found $AssemblyName in alternate directory ($alternateAssemblyPath), but its version number [$moduleAssembly] didn't match required [$NugetPackageVersion]." -Level Verbose + } + } + } + + # Then we'll check to see if we've previously cached the assembly in a temp folder during this PowerShell session + if ([System.String]::IsNullOrEmpty($script:tempAssemblyCacheDir)) + { + $script:tempAssemblyCacheDir = New-TemporaryDirectory + } + else + { + $cachedAssemblyPath = Join-Path -Path $(Join-Path $script:tempAssemblyCacheDir $AssemblyPackageTailDirectory) $AssemblyName + if (Test-Path -Path $cachedAssemblyPath -PathType Leaf) + { + if (Test-AssemblyIsDesiredVersion -AssemblyPath $cachedAssemblyPath -DesiredVersion $NugetPackageVersion) + { + Write-Log -Message "Found $AssemblyName in temp directory ($script:tempAssemblyCacheDir)." -Level Verbose + return $cachedAssemblyPath + } + else + { + Write-Log -Message "Found $AssemblyName in temp directory ($script:tempAssemblyCacheDir), but its version number [$moduleAssembly] didn't match required [$NugetPackageVersion]." -Level Verbose + } + } + } + + # Still not found, so we'll go ahead and download the package via nuget. + Write-Log -Message "$AssemblyName is needed and wasn't found. Acquiring it via nuget..." -Level Verbose + Get-NugetPackage -PackageName $NugetPackageName -Version $NugetPackageVersion -TargetPath $script:tempAssemblyCacheDir -NoStatus:$NoStatus + + $cachedAssemblyPath = Join-Path -Path $(Join-Path -Path $script:tempAssemblyCacheDir -ChildPath $AssemblyPackageTailDirectory) -ChildPath $AssemblyName + if (Test-Path -Path $cachedAssemblyPath -PathType Leaf) + { + Write-Log -Message @( + "To avoid this download delay in the future, copy the following file:", + " [$cachedAssemblyPath]", + "either to:", + " [$PSScriptRoot]", + "or to:", + " a directory of your choosing, and store that directory as 'AssemblyPath' with 'Set-GitHubConfiguration'") + + return $cachedAssemblyPath + } + + $message = "Unable to acquire a reference to $AssemblyName." + Write-Log -Message $message -Level Error + throw $message +} diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index c767abdb..be669e88 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -2,114 +2,147 @@ # Licensed under the MIT License. @{ - -# Script module or binary module file associated with this manifest. -# RootModule = '' - -# Version number of this module. -ModuleVersion = '0.1.0' - -# ID used to uniquely identify this module -GUID = '9e8dfd44-f782-445a-883c-70614f71519c' - -# Author of this module -Author = 'Microsoft Corporation' - -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' - -# Copyright statement for this module -Copyright = '(c) 2016 Microsoft Corporation. All rights reserved.' - -# Description of the functionality provided by this module -Description = 'PowerShell wrapper for GitHub API' - -# Minimum version of the Windows PowerShell engine required by this module -# PowerShellVersion = '' - -# Name of the Windows PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the Windows PowerShell host required by this module -# PowerShellHostVersion = '' - -# Minimum version of Microsoft .NET Framework required by this module -# DotNetFrameworkVersion = '' - -# Minimum version of the common language runtime (CLR) required by this module -# CLRVersion = '' - -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' - -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() - -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() - -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() - -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() - -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -NestedModules = @('GitHubAnalytics.psm1','GitHubLabels.psm1') - -# Functions to export from this module -FunctionsToExport = '*' - -# Cmdlets to export from this module -CmdletsToExport = '*' - -# Variables to export from this module -VariablesToExport = '*' - -# Aliases to export from this module -AliasesToExport = '*' - -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -# FileList = @() - -# 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. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('GitHub','API','PowerShell') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/PowerShell/PowerShellForGitHub/blob/master/LICENSE' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/PowerShell/PowerShellForGitHub' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' - + GUID = '9e8dfd44-f782-445a-883c-70614f71519c' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = 'Copyright (C) Microsoft Corporation. All rights reserved.' + + ModuleVersion = '0.2.0' + Description = 'PowerShell wrapper for GitHub API' + + # Script module or binary module file associated with this manifest. + # RootModule = 'GitHubCore.psm1' + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @( + # Ideally this list would be kept completely alphabetical, but other scripts (like + # GitHubConfiguration.ps1) depend on some of the code in Helpers being around at load time. + 'Helpers.ps1', + 'GitHubConfiguration.ps1', + 'GitHubAnalytics.ps1', + 'GitHubBranches.ps1', + 'GitHubCore.ps1', + 'GitHubIssues.ps1', + 'GitHubLabels.ps1', + 'GitHubMiscellaneous.ps1', + 'GitHubOrganizations.ps1', + 'GitHubPullRequests.ps1', + 'GitHubRepositories.ps1', + 'GitHubTeams.ps1', + 'GitHubUsers.ps1', + 'NugetTools.ps1', + 'Telemetry.ps1') + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Functions to export from this module + FunctionsToExport = @( + 'Backup-GitHubConfiguration', + 'Clear-GitHubAuthentication', + 'ConvertFrom-Markdown', + 'Get-GitHubCodeOfConduct', + 'Get-GitHubConfiguration', + 'Get-GitHubEmoji', + 'Get-GitHubGitIgnore', + 'Get-GitHubIssue', + 'Get-GitHubIssueTimeline', + 'Get-GitHubLabel', + 'Get-GitHubLicense', + 'Get-GitHubOrganizationMember', + 'Get-GitHubPullRequest', + 'Get-GitHubRateLimit', + 'Get-GitHubRepository', + 'Get-GitHubRepositoryBranch', + 'Get-GitHubRepositoryCollaborator', + 'Get-GitHubRepositoryContributor', + 'Get-GitHubRepositoryLanguage', + 'Get-GitHubRepositoryTag', + 'Get-GitHubRepositoryTopic', + 'Get-GitHubRepositoryUniqueContributor', + 'Get-GitHubTeam', + 'Get-GitHubTeamMember', + 'Get-GitHubUser', + 'Get-GitHubUserContextualInformation', + 'Group-GitHubIssue', + 'Invoke-GHRestMethod', + 'Invoke-GHRestMethodMultipleResult', + 'Lock-GitHubIssue', + 'Move-GitHubRepositoryOwnership', + 'New-GitHubIssue', + 'New-GitHubLabel', + 'New-GitHubRepository', + 'Remove-GitHubLabel', + 'Remove-GitHubRepository', + 'Reset-GitHubConfiguration', + 'Restore-GitHubConfiguration', + 'Set-GitHubAuthentication', + 'Set-GitHubConfiguration', + 'Set-GitHubLabel', + 'Set-GitHubRepositoryTopic', + 'Split-GitHubUri', + 'Test-GitHubAuthenticationConfigured', + 'Unlock-GitHubIssue', + 'Update-GitHubCurrentUser', + 'Update-GitHubIssue', + 'Update-GitHubLabel', + 'Update-GitHubRepository' + ) + + AliasesToExport = @( + 'Delete-GitHubLabel', + 'Delete-GitHubRepository', + 'Get-GitHubBranch', + 'Transfer-GitHubRepositoryOwnership' + ) + + # Cmdlets to export from this module + # CmdletsToExport = '*' + + # Variables to export from this module + # VariablesToExport = '*' + + # 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. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('GitHub', 'API', 'PowerShell') + + # A URL to the license for this module. + LicenseUri = 'https://aka.ms/PowerShellForGitHub_License' + + # A URL to the main website for this project. + ProjectUri = 'https://aka.ms/PowerShellForGitHub' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + } + } + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = 'GH' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # HelpInfo URI of this module + # HelpInfoURI = '' } diff --git a/README.md b/README.md index e9f08eaa..3f873fcc 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,142 @@ -# PowerShellForGitHub +# PowerShellForGitHub PowerShell Module -PowerShell wrapper for GitHub API. +[![Build status](https://ci.appveyor.com/api/projects/status/vsfq8kxo2et2dn7i?svg=true +)](https://ci.appveyor.com/project/HowardWolosky/powershellforgithub) -This repository currently contains two modules: -* GitHubAnalytics.psm1 - for querying issues, pull requests, collaborators, contributors, and organizations -* GitHubLabels.psm1 - for operations on GitHub labels +#### Table of Contents -Please scroll down to the "Examples" section for details on what operations are supported. +* [Overview](#overview) +* [Current API Support](#current-api-support) +* [Installation](#installation) +* [Configuration](#configuration) +* [Usage](#usage) +* [Logging](#logging) +* [Developing and Contributing](#developing-and-contributing) +* [Legal and Licensing](#legal-and-licensing) +* [Governance](#governance) +* [Code of Conduct](#code-of-conduct) +* [Privacy Policy](#privacy-policy) + +---------- + +## Overview + +This is a [PowerShell](https://microsoft.com/powershell) [module](https://technet.microsoft.com/en-us/library/dd901839.aspx) +that provides command-line interaction and automation for for the [GitHub v3 API](https://developer.github.com/v3/). + +---------- + +## Current API Support + +At present, this module can: + * Query issues + * Query [pull requests](https://developer.github.com/v3/pulls/) + * Query [collaborators](https://developer.github.com/v3/repos/collaborators/) + * Query [contributors](https://developer.github.com/v3/repos/statistics/) + * Query [organizations](https://developer.github.com/v3/orgs/) + * Query, create, update and remove [Issues](https://developer.github.com/v3/issues/) + * Query, create, update and remove [Labels](https://developer.github.com/v3/issues/labels/) + * Query, create, update and remove [Repositories](https://developer.github.com/v3/repos/) + * Query and update [Users](https://developer.github.com/v3/users/) + +Development is ongoing, with the goal to add broad support for the entire API set. +Review [examples](#examples) to see how the module can be used to accomplish some of these tasks. + +---------- ## Installation + You can get latest release of the PowerShellForGitHub on the [PowerShell Gallery](https://www.powershellgallery.com/packages/PowerShellForGitHub) + ```PowerShell Install-Module -Name PowerShellForGitHub ``` -## Usage -1) Rename ApiTokensTemplate.psm1 to ApiTokens.psm1 and update value of $global:gitHubApiToken with GitHub token for your account - * You can obtain it from https://github.com/settings/tokens. - * If you don't provide GitHub token, you can still use this module, but you will be limited to 60 queries per hour. - * You will need to edit this file from an elevated context. - -2) Import module you want to use and call it's function, e.g. +---------- - ```powershell -Import-Module .\GitHubAnalytics.psm1 -$issues = Get-GitHubIssueForRepository -repositoryUrl @('https://github.com/PowerShell/DscResources') -``` +## Configuration -## Running tests -1) Install [Pester](http://www.powershellgallery.com/packages/Pester/3.4.0) +To avoid severe API rate limiting by GitHub, you should configure the module with your own personal +access token. -```powershell -Install-Module -Name Pester -``` +1) Create a new API token by going to https://github.com/settings/tokens/new (provide a description + and check any appropriate scopes) +2) Call `Set-GitHubAuthentication`, enter anything as the username (the username is ignored but + required by the dialog that pops up), and paste in the API token as the password. That will be + securely cached to disk and will persist across all future PowerShell sessions. +If you ever wish to clear it in the future, just call `Clear-GitHubAuthentication`). -2) Start test pass +A number of additional configuration options exist with this module, and they can be configured +for just the current session or to persist across all future sessions with `Set-GitHubConfiguration`. +For a full explanation of all possible configurations, run the following: -Go to the Tests folder and run: -```powershell -Invoke-Pester + ```powershell +Get-Help Set-GitHubConfiguration -ShowWindow ``` -Make sure ApiTokens.psm1 exists and contains $global:gitHubApiToken with your GitHub key. -Please keep in mind some tests may fail on your machine, as they test private items (e.g. secret teams) which your key won't have access to. +For example, if you tend to work on the same repository, you can save yourself a lot of typing +by configuring the default OwnerName and/or RepositoryName that you work with. You can always +override these values by expicitly providing a value for the paramater in an individual command, +but for the common scenario, you'd have less typing to do. -## Contributing + ```powershell +Set-GitHubConfiguration -DefaultOwnerName PowerShell +Set-GitHubConfiguration -DefaultRepositoryName PowerShellForGitHub +``` -Contributions are welcome, please open issue on what functionality you would like to see added/contribute or simply send a pull request. +> Be warned that there are some commands where you may want to only ever supply the OwnerName +> (like if you're calling `Get-GitHubRepository` and want to see all the repositories owned +> by a particular user, as opposed to getting a single, specific repository). In cases like that, +> you'll need to explicitly pass in `$null` as the relevant parameter value as a temporary override +> for your default if you've set a default for one (or both) of these values. -## Examples +There are more great configuration options available. Just review the help for that command for +the most up-to-date list! -### GitHubAnalytics +---------- -#### Querying issues +## Usage -```powershell -$issues = Get-GitHubIssueForRepository ` --repositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration') -``` +Example command: ```powershell -$issues = Get-GitHubWeeklyIssueForRepository ` --repositoryUrl @('https://github.com/powershell/xpsdesiredstateconfiguration',` -'https://github.com/powershell/xactivedirectory') -datatype closed +$issues = Get-GitHubIssue -Uri 'https://github.com/PowerShell/PowerShellForGitHub' ``` -```powershell -$issues = Get-GitHubTopIssueRepository ` --repositoryUrl @('https://github.com/powershell/xsharepoint',` -'https://github.com/powershell/xCertificate', 'https://github.com/powershell/xwebadministration') -state open -``` +For more example commands, please refer to [USAGE](USAGE.md). -#### Querying pull requests +---------- -```powershell -$pullRequests = Get-GitHubPullRequestForRepository ` --repositoryUrl @('https://github.com/PowerShell/xPSDesiredStateConfiguration') -``` +## Developing and Contributing -```powershell -$pullRequests = Get-GitHubWeeklyPullRequestForRepository ` --repositoryUrl @('https://github.com/powershell/xpsdesiredstateconfiguration',` -'https://github.com/powershell/xwebadministration') -datatype merged -``` +Please see the [Contribution Guide](CONTRIBUTING.md) for information on how to develop and +contribute. -```powershell -$pullRequests = Get-GitHubTopPullRequestRepository ` --repositoryUrl @('https://github.com/powershell/xsharepoint', 'https://github.com/powershell/xwebadministration')` --state closed -mergedOnOrAfter 2015-04-20 -``` +If you have any problems, please consult [GitHub Issues](https://github.com/PowerShell/PowerShellForGitHub/issues) +to see if has already been discussed. -#### Querying collaborators +If you do not see your problem captured, please file [feedback](CONTRIBUTING.md#feedback). -```powershell -$collaborators = Get-GitHubRepositoryCollaborator` --repositoryUrl @('https://github.com/PowerShell/DscResources') -``` +---------- -#### Querying contributors +## Legal and Licensing -```powershell -$contributors = Get-GitHubRepositoryCollaborator` --repositoryUrl @('https://github.com/PowerShell/DscResources', 'https://github.com/PowerShell/xWebAdministration') -``` +PowerShellForGitHub is licensed under the [MIT license](LICENSE). -```powershell -$contributors = Get-GitHubRepositoryCollaborator` --repositoryUrl @('https://github.com/PowerShell/DscResources','https://github.com/PowerShell/xWebAdministration') +---------- -$uniqueContributors = Get-GitHubRepositoryUniqueContributor -contributors $contributors -``` +## Governance -#### Quering teams / organization membership +Governance policy for this project is described [here](GOVERNANCE.md). -```powershell -$organizationMembers = Get-GitHubOrganizationMember -organizationName 'OrganizationName' -$teamMembers = Get-GitHubTeamMember -organizationName 'OrganizationName' -teamName 'TeamName' -``` +---------- -### GitHubLabels +## Code of Conduct -#### Getting labels for given repository -```powershell -$labels = Get-GitHubLabel -repositoryName DesiredStateConfiguration -ownerName Powershell -``` +For more info, see [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) -#### Adding new label to the repository -```powershell -New-GitHubLabel -repositoryName DesiredStateConfiguration -ownerName PowerShell -labelName TestLabel -labelColor BBBBBB -``` +---------- -#### Removing specific label from the repository -```powershell -Remove-GitHubLabel -repositoryName desiredstateconfiguration -ownerName powershell -labelName TestLabel -``` +## Privacy Policy -#### Updating specific label with new name and color -```powershell -Update-GitHubLabel -repositoryName DesiredStateConfiguration -ownerName Powershell -labelName TestLabel -newLabelName NewTestLabel -labelColor BBBB00 -``` +For more information, refer to Microsoft's [Privacy Policy](https://go.microsoft.com/fwlink/?LinkID=521839). diff --git a/Telemetry.ps1 b/Telemetry.ps1 new file mode 100644 index 00000000..8046e443 --- /dev/null +++ b/Telemetry.ps1 @@ -0,0 +1,600 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Singleton telemetry client. Don't directly access this though....always get it +# by calling Get-TelemetryClient to ensure that the singleton is properly initialized. +$script:GHTelemetryClient = $null + +function Get-PiiSafeString +{ +<# + .SYNOPSIS + If PII protection is enabled, returns back an SHA512-hashed value for the specified string, + otherwise returns back the original string, untouched. + + .SYNOPSIS + If PII protection is enabled, returns back an SHA512-hashed value for the specified string, + otherwise returns back the original string, untouched. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER PlainText + The plain text that contains PII that may need to be protected. + + .EXAMPLE + Get-PiiSafeString -PlainText "Hello World" + + Returns back the string "B10A8DB164E0754105B7A99BE72E3FE5" which respresents + the SHA512 hash of "Hello World", but only if the "DisablePiiProtection" configuration + value is $false. If it's $true, "Hello World" will be returned. + + .OUTPUTS + System.String - A SHA512 hash of PlainText will be returned if the "DisablePiiProtection" + configuration value is $false, otherwise PlainText will be returned untouched. +#> + [CmdletBinding()] + [OutputType([String])] + param( + [Parameter(Mandatory)] + [AllowNull()] + [AllowEmptyString()] + [string] $PlainText + ) + + if (Get-GitHubConfiguration -Name DisablePiiProtection) + { + return $PlainText + } + else + { + return (Get-SHA512Hash -PlainText $PlainText) + } +} + +function Get-ApplicationInsightsDllPath +{ +<# + .SYNOPSIS + Makes sure that the Microsoft.ApplicationInsights.dll assembly is available + on the machine, and returns the path to it. + + .DESCRIPTION + Makes sure that the Microsoft.ApplicationInsights.dll assembly is available + on the machine, and returns the path to it. + + This will first look for the assembly in the module's script directory. + + Next it will look for the assembly in the location defined by + $SBAlternateAssemblyDir. This value would have to be defined by the user + prior to execution of this cmdlet. + + If not found there, it will look in a temp folder established during this + PowerShell session. + + If still not found, it will download the nuget package + for it to a temp folder accessible during this PowerShell session. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-ApplicationInsightsDllPath + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will show a time duration + status counter while the package is being downloaded. + + .EXAMPLE + Get-ApplicationInsightsDllPath -NoStatus + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will appear to hang during + this time. + + .OUTPUTS + System.String - The path to the Microsoft.ApplicationInsights.dll assembly. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [switch] $NoStatus + ) + + $nugetPackageName = "Microsoft.ApplicationInsights" + $nugetPackageVersion = "2.0.1" + $assemblyPackageTailDir = "Microsoft.ApplicationInsights.2.0.1\lib\net45" + $assemblyName = "Microsoft.ApplicationInsights.dll" + + return Get-NugetPackageDllPath -NugetPackageName $nugetPackageName -NugetPackageVersion $nugetPackageVersion -AssemblyPackageTailDirectory $assemblyPackageTailDir -AssemblyName $assemblyName -NoStatus:$NoStatus +} + +function Get-DiagnosticsTracingDllPath +{ +<# + .SYNOPSIS + Makes sure that the Microsoft.Diagnostics.Tracing.EventSource.dll assembly is available + on the machine, and returns the path to it. + + .DESCRIPTION + Makes sure that the Microsoft.Diagnostics.Tracing.EventSource.dll assembly is available + on the machine, and returns the path to it. + + This will first look for the assembly in the module's script directory. + + Next it will look for the assembly in the location defined by + $SBAlternateAssemblyDir. This value would have to be defined by the user + prior to execution of this cmdlet. + + If not found there, it will look in a temp folder established during this + PowerShell session. + + If still not found, it will download the nuget package + for it to a temp folder accessible during this PowerShell session. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-DiagnosticsTracingDllPath + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will show a time duration + status counter while the package is being downloaded. + + .EXAMPLE + Get-DiagnosticsTracingDllPath -NoStatus + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will appear to hang during + this time. + + .OUTPUTS + System.String - The path to the Microsoft.ApplicationInsights.dll assembly. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [switch] $NoStatus + ) + + $nugetPackageName = "Microsoft.Diagnostics.Tracing.EventSource.Redist" + $nugetPackageVersion = "1.1.24" + $assemblyPackageTailDir = "Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.24\lib\net35" + $assemblyName = "Microsoft.Diagnostics.Tracing.EventSource.dll" + + return Get-NugetPackageDllPath -NugetPackageName $nugetPackageName -NugetPackageVersion $nugetPackageVersion -AssemblyPackageTailDirectory $assemblyPackageTailDir -AssemblyName $assemblyName -NoStatus:$NoStatus +} + +function Get-ThreadingTasksDllPath +{ +<# + .SYNOPSIS + Makes sure that the Microsoft.Threading.Tasks.dll assembly is available + on the machine, and returns the path to it. + + .DESCRIPTION + Makes sure that the Microsoft.Threading.Tasks.dll assembly is available + on the machine, and returns the path to it. + + This will first look for the assembly in the module's script directory. + + Next it will look for the assembly in the location defined by + $SBAlternateAssemblyDir. This value would have to be defined by the user + prior to execution of this cmdlet. + + If not found there, it will look in a temp folder established during this + PowerShell session. + + If still not found, it will download the nuget package + for it to a temp folder accessible during this PowerShell session. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-ThreadingTasksDllPath + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will show a time duration + status counter while the package is being downloaded. + + .EXAMPLE + Get-ThreadingTasksDllPath -NoStatus + + Returns back the path to the assembly as found. If the package has to + be downloaded via nuget, the command prompt will appear to hang during + this time. + + .OUTPUTS + System.String - The path to the Microsoft.ApplicationInsights.dll assembly. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [switch] $NoStatus + ) + + $nugetPackageName = "Microsoft.Bcl.Async" + $nugetPackageVersion = "1.0.168.0" + $assemblyPackageTailDir = "Microsoft.Bcl.Async.1.0.168\lib\net40" + $assemblyName = "Microsoft.Threading.Tasks.dll" + + return Get-NugetPackageDllPath -NugetPackageName $nugetPackageName -NugetPackageVersion $nugetPackageVersion -AssemblyPackageTailDirectory $assemblyPackageTailDir -AssemblyName $assemblyName -NoStatus:$NoStatus +} + +function Get-TelemetryClient +{ +<# + .SYNOPSIS + Returns back the singleton instance of the Application Insights TelemetryClient for + this module. + + .DESCRIPTION + Returns back the singleton instance of the Application Insights TelemetryClient for + this module. + + If the singleton hasn't been initialized yet, this will ensure all dependenty assemblies + are available on the machine, create the client and initialize its properties. + + This will first look for the dependent assemblies in the module's script directory. + + Next it will look for the assemblies in the location defined by + $SBAlternateAssemblyDir. This value would have to be defined by the user + prior to execution of this cmdlet. + + If not found there, it will look in a temp folder established during this + PowerShell session. + + If still not found, it will download the nuget package + for it to a temp folder accessible during this PowerShell session. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Get-TelemetryClient + + Returns back the singleton instance to the TelemetryClient for the module. + If any nuget packages have to be downloaded in order to load the TelemetryClient, the + command prompt will show a time duration status counter during the download process. + + .EXAMPLE + Get-TelemetryClient -NoStatus + + Returns back the singleton instance to the TelemetryClient for the module. + If any nuget packages have to be downloaded in order to load the TelemetryClient, the + command prompt will appear to hang during this time. + + .OUTPUTS + Microsoft.ApplicationInsights.TelemetryClient +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [switch] $NoStatus + ) + + if ($null -eq $script:GHTelemetryClient) + { + if (-not (Get-GitHubConfiguration -Name SuppressTelemetryReminder)) + { + Write-Log -Message 'Telemetry is currently enabled. It can be disabled by calling "Set-GitHubConfiguration -DisableTelemetry". Refer to USAGE.md#telemetry for more information. Stop seeing this message in the future by calling "Set-GitHubConfiguration -SuppressTelemetryReminder".' + } + + Write-Log -Message "Initializing telemetry client." -Level Verbose + + $dlls = @( + (Get-ThreadingTasksDllPath -NoStatus:$NoStatus), + (Get-DiagnosticsTracingDllPath -NoStatus:$NoStatus), + (Get-ApplicationInsightsDllPath -NoStatus:$NoStatus) + ) + + foreach ($dll in $dlls) + { + $bytes = [System.IO.File]::ReadAllBytes($dll) + [System.Reflection.Assembly]::Load($bytes) | Out-Null + } + + $username = Get-PiiSafeString -PlainText $env:USERNAME + + $script:GHTelemetryClient = New-Object Microsoft.ApplicationInsights.TelemetryClient + $script:GHTelemetryClient.InstrumentationKey = (Get-GitHubConfiguration -Name ApplicationInsightsKey) + $script:GHTelemetryClient.Context.User.Id = $username + $script:GHTelemetryClient.Context.Session.Id = [System.GUID]::NewGuid().ToString() + $script:GHTelemetryClient.Context.Properties['Username'] = $username + $script:GHTelemetryClient.Context.Properties['DayOfWeek'] = (Get-Date).DayOfWeek + $script:GHTelemetryClient.Context.Component.Version = $MyInvocation.MyCommand.Module.Version.ToString() + } + + return $script:GHTelemetryClient +} + +function Set-TelemetryEvent +{ +<# + .SYNOPSIS + Posts a new telemetry event for this module to the configured Applications Insights instance. + + .DESCRIPTION + Posts a new telemetry event for this module to the configured Applications Insights instance. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER EventName + The name of the event that has occurred. + + .PARAMETER Properties + A collection of name/value pairs (string/string) that should be associated with this event. + + .PARAMETER Metrics + A collection of name/value pair metrics (string/double) that should be associated with + this event. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Set-TelemetryEvent "zFooTest1" + + Posts a "zFooTest1" event with the default set of properties and metrics. If the telemetry + client needs to be created to accomplish this, and the required assemblies are not available + on the local machine, the download status will be presented at the command prompt. + + .EXAMPLE + Set-TelemetryEvent "zFooTest1" @{"Prop1" = "Value1"} + + Posts a "zFooTest1" event with the default set of properties and metrics along with an + additional property named "Prop1" with a value of "Value1". If the telemetry client + needs to be created to accomplish this, and the required assemblies are not available + on the local machine, the download status will be presented at the command prompt. + + .EXAMPLE + Set-TelemetryEvent "zFooTest1" -NoStatus + + Posts a "zFooTest1" event with the default set of properties and metrics. If the telemetry + client needs to be created to accomplish this, and the required assemblies are not available + on the local machine, the command prompt will appear to hang while they are downloaded. + + .NOTES + Because of the short-running nature of this module, we always "flush" the events as soon + as they have been posted to ensure that they make it to Application Insights. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [string] $EventName, + + [hashtable] $Properties = @{}, + + [hashtable] $Metrics = @{}, + + [switch] $NoStatus + ) + + if (Get-GitHubConfiguration -Name DisableTelemetry) + { + Write-Log -Message "Telemetry has been disabled via configuration. Skipping reporting event [$EventName]." -Level Verbose + return + } + + Write-InvocationLog -Invocation $MyInvocation + + try + { + $telemetryClient = Get-TelemetryClient -NoStatus:$NoStatus + + $propertiesDictionary = New-Object 'System.Collections.Generic.Dictionary[string, string]' + $propertiesDictionary['DayOfWeek'] = (Get-Date).DayOfWeek + $Properties.Keys | ForEach-Object { $propertiesDictionary[$_] = $Properties[$_] } + + $metricsDictionary = New-Object 'System.Collections.Generic.Dictionary[string, double]' + $Metrics.Keys | ForEach-Object { $metricsDictionary[$_] = $Metrics[$_] } + + $telemetryClient.TrackEvent($EventName, $propertiesDictionary, $metricsDictionary); + + # Flushing should increase the chance of success in uploading telemetry logs + Flush-TelemetryClient -NoStatus:$NoStatus + } + catch + { + # Telemetry should be best-effort. Failures while trying to handle telemetry should not + # cause exceptions in the app itself. + Write-Log -Message "Set-TelemetryEvent failed:" -Exception $_ -Level Error + } +} + +function Set-TelemetryException +{ +<# + .SYNOPSIS + Posts a new telemetry event to the configured Application Insights instance indicating + that an exception occurred in this this module. + + .DESCRIPTION + Posts a new telemetry event to the configured Application Insights instance indicating + that an exception occurred in this this module. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Exception + The exception that just occurred. + + .PARAMETER ErrorBucket + A property to be added to the Exception being logged to make it easier to filter to + exceptions resulting from similar scenarios. + + .PARAMETER Properties + Additional properties that the caller may wish to be associated with this exception. + + .PARAMETER NoFlush + It's not recommended to use this unless the exception is coming from Flush-TelemetryClient. + By default, every time a new exception is logged, the telemetry client will be flushed + to ensure that the event is published to the Application Insights. Use of this switch + prevents that automatic flushing (helpful in the scenario where the exception occurred + when trying to do the actual Flush). + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Set-TelemetryException $_ + + Used within the context of a catch statement, this will post the exception that just + occurred, along with a default set of properties. If the telemetry client needs to be + created to accomplish this, and the required assemblies are not available on the local + machine, the download status will be presented at the command prompt. + + .EXAMPLE + Set-TelemetryException $_ -NoStatus + + Used within the context of a catch statement, this will post the exception that just + occurred, along with a default set of properties. If the telemetry client needs to be + created to accomplish this, and the required assemblies are not available on the local + machine, the command prompt will appear to hang while they are downloaded. + + .NOTES + Because of the short-running nature of this module, we always "flush" the events as soon + as they have been posted to ensure that they make it to Application Insights. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory)] + [System.Exception] $Exception, + + [string] $ErrorBucket, + + [hashtable] $Properties = @{}, + + [switch] $NoFlush, + + [switch] $NoStatus + ) + + if (Get-GitHubConfiguration -Name DisableTelemetry) + { + Write-Log -Message "Telemetry has been disabled via configuration. Skipping reporting exception." -Level Verbose + return + } + + Write-InvocationLog -Invocation $MyInvocation + + try + { + $telemetryClient = Get-TelemetryClient -NoStatus:$NoStatus + + $propertiesDictionary = New-Object 'System.Collections.Generic.Dictionary[string,string]' + $propertiesDictionary['Message'] = $Exception.Message + $propertiesDictionary['HResult'] = "0x{0}" -f [Convert]::ToString($Exception.HResult, 16) + $Properties.Keys | ForEach-Object { $propertiesDictionary[$_] = $Properties[$_] } + + if (-not [String]::IsNullOrWhiteSpace($ErrorBucket)) + { + $propertiesDictionary['ErrorBucket'] = $ErrorBucket + } + + $telemetryClient.TrackException($Exception, $propertiesDictionary); + + # Flushing should increase the chance of success in uploading telemetry logs + if (-not $NoFlush) + { + Flush-TelemetryClient -NoStatus:$NoStatus + } + } + catch + { + # Telemetry should be best-effort. Failures while trying to handle telemetry should not + # cause exceptions in the app itself. + Write-Log -Message "Set-TelemetryException failed:" -Exception $_ -Level Error + } +} + +function Flush-TelemetryClient +{ +<# + .SYNOPSIS + Flushes the buffer of stored telemetry events to the configured Applications Insights instance. + + .DESCRIPTION + Flushes the buffer of stored telemetry events to the configured Applications Insights instance. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + + .EXAMPLE + Flush-TelemetryClient + + Attempts to push all buffered telemetry events for this telemetry client immediately to + Application Insights. If the telemetry client needs to be created to accomplish this, + and the required assemblies are not available on the local machine, the download status + will be presented at the command prompt. + + .EXAMPLE + Flush-TelemetryClient -NoStatus + + Attempts to push all buffered telemetry events for this telemetry client immediately to + Application Insights. If the telemetry client needs to be created to accomplish this, + and the required assemblies are not available on the local machine, the command prompt + will appear to hang while they are downloaded. +#> + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Internal-only helper method. Matches the internal method that is called.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if (Get-GitHubConfiguration -Name DisableTelemetry) + { + Write-Log -Message "Telemetry has been disabled via configuration. Skipping flushing of the telemetry client." -Level Verbose + return + } + + $telemetryClient = Get-TelemetryClient -NoStatus:$NoStatus + + try + { + $telemetryClient.Flush() + } + catch [System.Net.WebException] + { + Write-Log -Message "Encountered exception while trying to flush telemetry events:" -Exception $_ -Level Warning + + Set-TelemetryException -Exception ($_.Exception) -ErrorBucket "TelemetryFlush" -NoFlush -NoStatus:$NoStatus + } + catch + { + # Any other scenario is one that we want to identify and fix so that we don't miss telemetry + Write-Log -Level Warning -Exception $_ -Message @( + "Encountered a problem while trying to record telemetry events.", + "This is non-fatal, but it would be helpful if you could report this problem", + "to the PowerShellForGitHub team for further investigation:") + } +} diff --git a/Tests/Config/Settings.ps1 b/Tests/Config/Settings.ps1 new file mode 100644 index 00000000..701eb7e4 --- /dev/null +++ b/Tests/Config/Settings.ps1 @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# The account that the tests will be running under +$script:ownerName = 'PowerShellForGitHubTeam' + +# The organization that the tests will be running under +$script:organizationName = 'PowerShellForGitHubTeamTestOrg' \ No newline at end of file diff --git a/Tests/GitHubAnalytics.tests.ps1 b/Tests/GitHubAnalytics.tests.ps1 index 819daf9e..f1724248 100644 --- a/Tests/GitHubAnalytics.tests.ps1 +++ b/Tests/GitHubAnalytics.tests.ps1 @@ -3,232 +3,364 @@ <# .Synopsis - Tests for GitHubAnalytics.psm1 module + Tests for GitHubAnalytics.ps1 module #> [String] $root = Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) +. (Join-Path -Path $root -ChildPath 'Tests\Config\Settings.ps1') +Import-Module -Name $root -Force -if ($env:AppVeyor) +function Initialize-AppVeyor { - $global:gitHubApiToken = $env:token - $message = 'This run is executed in the AppVeyor environment. -GitHubApiToken won''t be decrypted in PR runs causing some tests to fail. -403 errors possible due to GitHub hourly limit for unauthenticated queries. -Define $global:gitHubApiToken manually and run tests on your machine first.' - Write-Host $message -BackgroundColor Yellow -ForegroundColor Black -} +<# + .SYNOPSIS + Configures the tests to run with the authentication information stored in AppVeyor + (if that information exists in the environment). -$apiTokensFilePath = "$root\ApiTokens.psm1" -if (Test-Path $apiTokensFilePath) -{ - Write-Host "Importing $apiTokensFilePath" - Import-Module -Force $apiTokensFilePath -} -else -{ - Write-Host "$apiTokensFilePath does not exist, skipping import in tests" -} + .DESCRIPTION + Configures the tests to run with the authentication information stored in AppVeyor + (if that information exists in the environment). -$script:tokenExists = $true -if ($global:gitHubApiToken -eq $null) -{ - Write-Host "GitHubApiToken not defined, some of the tests will be skipped. `n403 errors possible due to GitHub hourly limit for unauthenticated queries." -BackgroundColor Yellow -ForegroundColor Black - $script:tokenExists = $false + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .NOTES + Internal-only helper method. + + The only reason this exists is so that we can leverage CodeAnalysis.SuppressMessageAttribute, + which can only be applied to functions. + + We call this immediately after the declaration so that AppVeyor initialization can heppen + (if applicable). + +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="Needed to configure with the stored, encrypted string value in AppVeyor.")] + param() + + if ($env:AppVeyor) + { + $secureString = $env:avAccessToken | ConvertTo-SecureString -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential "", $secureString + Set-GitHubAuthentication -Credential $cred + + $script:ownerName = $env:avOwnerName + $script:organizationName = $env:avOrganizationName + + $message = @( + 'This run is executed in the AppVeyor environment.', + 'The GitHub Api Token won''t be decrypted in PR runs causing some tests to fail.', + '403 errors possible due to GitHub hourly limit for unauthenticated queries.', + 'Use Set-GitHubAuthentication manually. modify the values in Tests\Config\Settings.ps1,', + 'and run tests on your machine first.') + Write-Warning -Message ($message -join [Environment]::NewLine) + } } -else + +Initialize-AppVeyor + +$script:accessTokenConfigured = Test-GitHubAuthenticationConfigured +if (-not $script:accessTokenConfigured) { - Write-Host "GitHubApiToken has been defined in tests" + $message = @( + 'GitHub API Token not defined, some of the tests will be skipped.', + '403 errors possible due to GitHub hourly limit for unauthenticated queries.') + Write-Warning -Message ($message -join [Environment]::NewLine) } -Import-Module (Join-Path -Path $root -ChildPath 'GitHubAnalytics.psm1') -Force +# Backup the user's configuration before we begin, and ensure we're at a pure state before running +# the tests. We'll restore it at the end. +$configFile = New-TemporaryFile +Backup-GitHubConfiguration -Path $configFile +Reset-GitHubConfiguration -$script:gitHubAccountUrl = "https://github.com/gipstestaccount" -$script:organizationName = "GiPSTestOrg" -$script:organizationTeamName = "TestTeam1" -$script:repositoryName = "TestRepository" -$script:repository2Name = "TestRepository2" -$script:repositoryUrl = "$script:gitHubAccountUrl/$script:repositoryName" -$script:repositoryUrl2 = "$script:gitHubAccountUrl/$script:repository2Name" +Describe 'Obtaining issues for repository' { + $repo = New-GitHubRepository -RepositoryName ([Guid]::NewGuid().Guid) -AutoInit - -Describe 'Obtaininig issues for repository' { - Context 'When no addional conditions specified' { - $issues = Get-GitHubIssueForRepository -RepositoryUrl @($repositoryUrl) + Context 'When initially created, there are no issues' { + $issues = Get-GitHubIssue -Uri $repo.svn_url It 'Should return expected number of issues' { - @($issues).Count | Should be 3 + @($issues).Count | Should be 0 } } - - Context 'When time range specified' { - $issues = Get-GitHubIssueForRepository -RepositoryUrl @($repositoryUrl) -CreatedOnOrAfter '2016-05-06' -CreatedOnOrBefore '2016-05-08' - It 'Should return expected number of issues' { - @($issues).Count | Should be 3 + Context 'When there are issues present' { + $newIssues = @() + for ($i = 0; $i -lt 4; $i++) + { + $newIssues += New-GitHubIssue -OwnerName $script:ownerName -RepositoryName $repo.name -Title ([guid]::NewGuid().Guid) + Start-Sleep -Seconds 5 } - } - - Context 'When state and time range specified' { - $issues = Get-GitHubIssueForRepository -RepositoryUrl @($repositoryUrl) -CreatedOnOrAfter '2016-04-01' -State closed - It 'Should return expected number of issues' { + $newIssues[0] = Update-GitHubIssue -OwnerName $script:ownerName -RepositoryName $repo.name -Issue $newIssues[0].number -State closed + $newIssues[-1] = Update-GitHubIssue -OwnerName $script:ownerName -RepositoryName $repo.name -Issue $newIssues[-1].number -State closed + + $issues = Get-GitHubIssue -Uri $repo.svn_url + It 'Should return only open issues' { @($issues).Count | Should be 2 } - } -} - -Describe 'Obtaininig repository with biggest number of issues' { - Context 'When no addional conditions specified' { - $issues = Get-GitHubTopIssueRepository -RepositoryUrl @($script:repositoryUrl,$script:repositoryUrl2) - It 'Should return expected number of issues for each repository' { - @($issues[0].Value) | Should be 3 - @($issues[1].Value) | Should be 0 + $issues = Get-GitHubIssue -Uri $repo.svn_url -State all + It 'Should return all issues' { + @($issues).Count | Should be 4 } - - It 'Should return expected repository names' { - @($issues[0].Name) | Should be $script:repositoryName - @($issues[1].Name) | Should be $script:repository2Name - } - } -} -Describe 'Obtaininig pull requests for repository' { - Context 'When no addional conditions specified' { - $pullRequests = Get-GitHubPullRequestForRepository -RepositoryUrl @($script:repositoryUrl) + $createdOnOrAfterDate = Get-Date -Date $newIssues[0].created_at + $createdOnOrBeforeDate = Get-Date -Date $newIssues[2].created_at + $issues = (Get-GitHubIssue -Uri $repo.svn_url) | Where-Object { ($_.created_at -ge $createdOnOrAfterDate) -and ($_.created_at -le $createdOnOrBeforeDate) } - It 'Should return expected number of PRs' { - @($pullRequests).Count | Should be 2 + It 'Smart object date conversion works for comparing dates' { + @($issues).Count | Should be 2 } - } - - Context 'When state and time range specified' { - $pullRequests = Get-GitHubPullRequestForRepository ` - -RepositoryUrl @($script:repositoryUrl) ` - -State closed -MergedOnOrAfter 2016-04-10 -MergedOnOrBefore 2016-05-07 - - It 'Should return expected number of PRs' { - @($pullRequests).Count | Should be 3 + + $createdDate = Get-Date -Date $newIssues[1].created_at + $issues = Get-GitHubIssue -Uri $repo.svn_url -State all | Where-Object { ($_.created_at -ge $createdDate) -and ($_.state -eq 'closed') } + + It 'Able to filter based on date and state' { + @($issues).Count | Should be 1 } } + + $null = Remove-GitHubRepository -Uri ($repo.svn_url) } -Describe 'Obtaininig repository with biggest number of pull requests' { - Context 'When no addional conditions specified' { - $pullRequests = Get-GitHubTopPullRequestRepository -RepositoryUrl @($script:repositoryUrl,$script:repositoryUrl2) +Describe 'Obtaining repository with biggest number of issues' { + $repo1 = New-GitHubRepository -RepositoryName ([Guid]::NewGuid().Guid) -AutoInit + $repo2 = New-GitHubRepository -RepositoryName ([Guid]::NewGuid().Guid) -AutoInit - It 'Should return expected number of pull requests for each repository' { - @($pullRequests[0].Value) | Should be 2 - @($pullRequests[1].Value) | Should be 0 - } - - It 'Should return expected repository names' { - @($pullRequests[0].Name) | Should be $script:repositoryName - @($pullRequests[1].Name) | Should be $script:repository2Name + Context 'When no addional conditions specified' { + for ($i = 0; $i -lt 3; $i++) + { + $null = New-GitHubIssue -OwnerName $script:ownerName -RepositoryName $repo1.name -Title ([guid]::NewGuid().Guid) } - } - - Context 'When state and time range specified' { - $pullRequests = Get-GitHubTopPullRequestRepository -RepositoryUrl @($script:repositoryUrl,$script:repositoryUrl2) -State closed -MergedOnOrAfter 2015-04-20 - - It 'Should return expected number of pull requests for each repository' { - @($pullRequests[0].Value) | Should be 3 - @($pullRequests[1].Value) | Should be 0 + + $repos = @(($repo1.svn_url), ($repo2.svn_url)) + $issueCounts = @() + $repos | ForEach-Object { $issueCounts = $issueCounts + ([PSCustomObject]@{ 'Uri' = $_; 'Count' = (Get-GitHubIssue -Uri $_).Count }) } + $issueCounts = $issueCounts | Sort-Object -Property Count -Descending + + It 'Should return expected number of issues for each repository' { + @($issueCounts[0].Count) | Should be 3 + @($issueCounts[1].Count) | Should be 0 } - + It 'Should return expected repository names' { - @($pullRequests[0].Name) | Should be $script:repositoryName - @($pullRequests[1].Name) | Should be $script:repository2Name + @($issueCounts[0].Uri) | Should be ($repo1.svn_url) + @($issueCounts[1].Uri) | Should be ($repo2.svn_url) } } + + $null = Remove-GitHubRepository -Uri ($repo1.svn_url) + $null = Remove-GitHubRepository -Uri ($repo2.svn_url) } -if ($script:tokenExists) + +# TODO: Re-enable these tests once the module has sufficient support getting the repository into the +# required state for testing, and to recover back to the original state at the conclusion of the test. + +# Describe 'Obtaining pull requests for repository' { +# Context 'When no addional conditions specified' { +# $pullRequests = Get-GitHubPullRequest -Uri $script:repositoryUrl + +# It 'Should return expected number of PRs' { +# @($pullRequests).Count | Should be 2 +# } +# } + +# Context 'When state and time range specified' { +# $mergedStartDate = Get-Date -Date '2016-04-10' +# $mergedEndDate = Get-Date -Date '2016-05-07' +# $pullRequests = Get-GitHubPullRequest -Uri $script:repositoryUrl -State closed | +# Where-Object { ($_.merged_at -ge $mergedStartDate) -and ($_.merged_at -le $mergedEndDate) } + +# It 'Should return expected number of PRs' { +# @($pullRequests).Count | Should be 3 +# } +# } +# } + +# Describe 'Obtaining repository with biggest number of pull requests' { +# Context 'When no addional conditions specified' { +# @($script:repositoryUrl, $script:repositoryUrl2) | +# ForEach-Object { +# $pullRequestCounts += ([PSCustomObject]@{ +# 'Uri' = $_; +# 'Count' = (Get-GitHubPullRequest -Uri $_).Count }) } +# $pullRequestCounts = $pullRequestCounts | Sort-Object -Property Count -Descending + +# It 'Should return expected number of pull requests for each repository' { +# @($pullRequestCounts[0].Count) | Should be 2 +# @($pullRequestCounts[1].Count) | Should be 0 +# } + +# It 'Should return expected repository names' { +# @($pullRequestCounts[0].Uri) | Should be $script:repositoryUrl +# @($pullRequestCounts[1].Uri) | Should be $script:repositoryUrl2 +# } +# } + +# Context 'When state and time range specified' { +# $mergedDate = Get-Date -Date '2015-04-20' +# $repos = @($script:repositoryUrl, $script:repositoryUrl2) +# $pullRequestCounts = @() +# $pullRequestSearchParams = @{ +# 'State' = 'closed' +# } +# $repos | +# ForEach-Object { +# $pullRequestCounts += ([PSCustomObject]@{ +# 'Uri' = $_; +# 'Count' = ( +# (Get-GitHubPullRequest -Uri $_ @pullRequestSearchParams) | +# Where-Object { $_.merged_at -ge $mergedDate } +# ).Count +# }) } + +# $pullRequestCounts = $pullRequestCounts | Sort-Object -Property Count -Descending +# $pullRequests = Get-GitHubTopPullRequestRepository -Uri @($script:repositoryUrl, $script:repositoryUrl2) -State closed -MergedOnOrAfter + +# It 'Should return expected number of pull requests for each repository' { +# @($pullRequests[0].Count) | Should be 3 +# @($pullRequests[1].Count) | Should be 0 +# } + +# It 'Should return expected repository names' { +# @($pullRequests[0].Uri) | Should be $script:repositoryUrl +# @($pullRequests[1].Uri) | Should be $script:repositoryUrl2 +# } +# } +# } + +if ($script:accessTokenConfigured) { - Describe 'Obtaininig collaborators for repository' { - $collaborators = Get-GitHubRepositoryCollaborator -RepositoryUrl @($script:repositoryUrl) + Describe 'Obtaining collaborators for repository' { + $repositoryName = [guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName -AutoInit + $repositoryUrl = "https://github.com/$script:ownerName/$repositoryName" + + $collaborators = Get-GitHubRepositoryCollaborator -Uri $repositoryUrl It 'Should return expected number of collaborators' { @($collaborators).Count | Should be 1 - } + } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } } -Describe 'Obtaininig contributors for repository' { - $contributors = Get-GitHubRepositoryContributor -RepositoryUrl @($script:repositoryUrl) +Describe 'Obtaining contributors for repository' { + $repositoryName = [guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName -AutoInit + $repositoryUrl = "https://github.com/$script:ownerName/$repositoryName" + + $contributors = Get-GitHubRepositoryContributor -Uri $repositoryUrl -IncludeStatistics It 'Should return expected number of contributors' { @($contributors).Count | Should be 1 } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } -if ($script:tokenExists) +if ($script:accessTokenConfigured) { - Describe 'Obtaininig organization members' { - $members = Get-GitHubOrganizationMember -OrganizationName $script:organizationName + # TODO: Re-enable these tests once the module has sufficient support getting the Organization + # and repository into the required state for testing, and to recover back to the original state + # at the conclusion of the test. - It 'Should return expected number of organization members' { - @($members).Count | Should be 1 - } - } + # Describe 'Obtaining organization members' { + # $members = Get-GitHubOrganizationMember -OrganizationName $script:organizationName - Describe 'Obtaininig organization teams' { - $teams = Get-GitHubTeam -OrganizationName $script:organizationName + # It 'Should return expected number of organization members' { + # @($members).Count | Should be 1 + # } + # } - It 'Should return expected number of organization teams' { - @($teams).Count | Should be 2 - } - } + # Describe 'Obtaining organization teams' { + # $teams = Get-GitHubTeam -OrganizationName $script:organizationName - Describe 'Obtaininig organization team members' { - $members = Get-GitHubTeamMember -OrganizationName $script:organizationName -TeamName $script:organizationTeamName + # It 'Should return expected number of organization teams' { + # @($teams).Count | Should be 2 + # } + # } - It 'Should return expected number of organization team members' { - @($members).Count | Should be 1 - } - } + # Describe 'Obtaining organization team members' { + # $members = Get-GitHubTeamMember -OrganizationName $script:organizationName -TeamName $script:organizationTeamName + + # It 'Should return expected number of organization team members' { + # @($members).Count | Should be 1 + # } + # } } Describe 'Getting repositories from organization' { - $repositories = Get-GitHubOrganizationRepository -OrganizationName $script:organizationName + $original = Get-GitHubRepository -OrganizationName $script:organizationName + + $repositoryName = [guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName -OrganizationName $script:organizationName + $current = Get-GitHubRepository -OrganizationName $script:organizationName It 'Should return expected number of organization repositories' { - @($repositories).Count | Should be 2 + (@($current).Count - @($original).Count) | Should be 1 } + + $null = Remove-GitHubRepository -OwnerName $script:organizationName -RepositoryName $repositoryName } Describe 'Getting unique contributors from contributors array' { - $contributors = Get-GitHubRepositoryContributor -RepositoryUrl @($script:repositoryUrl) - $uniqueContributors = Get-GitHubRepositoryUniqueContributor -Contributors $contributors + $repositoryName = [guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName -AutoInit + + $contributors = Get-GitHubRepositoryContributor -OwnerName $script:ownerName -RepositoryName $repositoryName -IncludeStatistics + + $uniqueContributors = $contributors | + Select-Object -ExpandProperty author | + Select-Object -ExpandProperty login -Unique + Sort-Object It 'Should return expected number of unique contributors' { @($uniqueContributors).Count | Should be 1 } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } Describe 'Getting repository name from url' { - $name = Get-GitHubRepositoryNameFromUrl -RepositoryUrl "https://github.com/KarolKaczmarek/TestRepository" + $repositoryName = [guid]::NewGuid().Guid + $url = "https://github.com/$script:ownerName/$repositoryName" + $name = Split-GitHubUri -Uri $url -RepositoryName It 'Should return expected repository name' { - $name | Should be "TestRepository" + $name | Should be $repositoryName } } Describe 'Getting repository owner from url' { - $owner = Get-GitHubRepositoryOwnerFromUrl -RepositoryUrl "https://github.com/KarolKaczmarek/TestRepository" + $repositoryName = [guid]::NewGuid().Guid + $url = "https://github.com/$script:ownerName/$repositoryName" + $owner = Split-GitHubUri -Uri $url -OwnerName It 'Should return expected repository owner' { - $owner | Should be "KarolKaczmarek" + $owner | Should be $script:ownerName } } Describe 'Getting branches for repository' { - $branches = Get-GitHubRepositoryBranch -OwnerName $script:organizationName -Repository $script:repositoryName + $repositoryName = [guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName -AutoInit + + $branches = Get-GitHubRepositoryBranch -OwnerName $script:ownerName -RepositoryName $repositoryName It 'Should return expected number of repository branches' { @($branches).Count | Should be 1 } - It 'Should return the name of the branches' { + + It 'Should return the name of the branches' { @($branches[0].name) | Should be "master" } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } + +# Restore the user's configuration to its pre-test state +Restore-GitHubConfiguration -Path $configFile diff --git a/Tests/GitHubLabels.tests.ps1 b/Tests/GitHubLabels.tests.ps1 index 3b0252d4..5f7a3dff 100644 --- a/Tests/GitHubLabels.tests.ps1 +++ b/Tests/GitHubLabels.tests.ps1 @@ -3,130 +3,231 @@ <# .Synopsis - Tests for GitHubLabels.psm1 module + Tests for GitHubLabels.ps1 module #> [String] $root = Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path) +. (Join-Path -Path $root -ChildPath 'Tests\Config\Settings.ps1') +Import-Module -Name $root -Force -if ($env:AppVeyor) +function Initialize-AppVeyor { - $global:gitHubApiToken = $env:token - $message = 'This run is executed in the AppVeyor environment. -GitHubApiToken won''t be decrypted in PR runs causing some tests to fail. -403 errors possible due to GitHub hourly limit for unauthenticated queries. -Define $global:gitHubApiToken manually and run tests on your machine first.' - Write-Host $message -BackgroundColor Yellow -ForegroundColor Black -} +<# + .SYNOPSIS + Configures the tests to run with the authentication information stored in AppVeyor + (if that information exists in the environment). -$apiTokensFilePath = "$root\ApiTokens.psm1" -if (Test-Path $apiTokensFilePath) -{ - Write-Host "Importing $apiTokensFilePath" - Import-Module -force $apiTokensFilePath -} -else -{ - Write-Host "$apiTokensFilePath does not exist, skipping import in tests" -} + .DESCRIPTION + Configures the tests to run with the authentication information stored in AppVeyor + (if that information exists in the environment). -$script:tokenExists = $true -if ($global:gitHubApiToken -eq $null) -{ - Write-Host "GitHubApiToken not defined, some of the tests will be skipped. `n403 errors possible due to GitHub hourly limit for unauthenticated queries." -BackgroundColor Yellow -ForegroundColor Black - $script:tokenExists = $false + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .NOTES + Internal-only helper method. + + The only reason this exists is so that we can leverage CodeAnalysis.SuppressMessageAttribute, + which can only be applied to functions. + + We call this immediately after the declaration so that AppVeyor initialization can heppen + (if applicable). + +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="Needed to configure with the stored, encrypted string value in AppVeyor.")] + param() + + if ($env:AppVeyor) + { + $secureString = $env:avAccessToken | ConvertTo-SecureString -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential "", $secureString + Set-GitHubAuthentication -Credential $cred + + $script:ownerName = $env:avOwnerName + $script:organizationName = $env:avOrganizationName + + $message = @( + 'This run is executed in the AppVeyor environment.', + 'The GitHub Api Token won''t be decrypted in PR runs causing some tests to fail.', + '403 errors possible due to GitHub hourly limit for unauthenticated queries.', + 'Use Set-GitHubAuthentication manually. modify the values in Tests\Config\Settings.ps1,', + 'and run tests on your machine first.') + Write-Warning -Message ($message -join [Environment]::NewLine) + } } -else + +Initialize-AppVeyor + +$script:accessTokenConfigured = Test-GitHubAuthenticationConfigured +if (-not $script:accessTokenConfigured) { - Write-Host "GitHubApiToken has been defined in tests" + $message = @( + 'GitHub API Token not defined, some of the tests will be skipped.', + '403 errors possible due to GitHub hourly limit for unauthenticated queries.') + Write-Warning -Message ($message -join [Environment]::NewLine) } -Import-Module (Join-Path -Path $root -ChildPath 'GitHubLabels.psm1') -Force - -$script:gitHubAccountUrl = "https://github.com/gipstestaccount" -$script:accountName = "gipstestaccount" -$script:repositoryName = "TestRepository" -$script:repositoryUrl = "$script:gitHubAccountUrl/$script:repositoryName" -$script:expectedNumberOfLabels = 14 +# Backup the user's configuration before we begin, and ensure we're at a pure state before running +# the tests. We'll restore it at the end. +$configFile = New-TemporaryFile +Backup-GitHubConfiguration -Path $configFile +Reset-GitHubConfiguration + +$script:defaultLabels = @( + @{ + 'name' = 'pri:lowest' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:low' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:medium' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:high' + 'color' = '4285F4' + }, + @{ + 'name' = 'pri:highest' + 'color' = '4285F4' + }, + @{ + 'name' = 'bug' + 'color' = 'fc2929' + }, + @{ + 'name' = 'duplicate' + 'color' = 'cccccc' + }, + @{ + 'name' = 'enhancement' + 'color' = '121459' + }, + @{ + 'name' = 'up for grabs' + 'color' = '159818' + }, + @{ + 'name' = 'question' + 'color' = 'cc317c' + }, + @{ + 'name' = 'discussion' + 'color' = 'fe9a3d' + }, + @{ + 'name' = 'wontfix' + 'color' = 'dcb39c' + }, + @{ + 'name' = 'in progress' + 'color' = 'f0d218' + }, + @{ + 'name' = 'ready' + 'color' = '145912' + } +) -if ($script:tokenExists) +if ($script:accessTokenConfigured) { - New-GitHubLabels -RepositoryName $script:repositoryName -OwnerName $script:accountName - Describe 'Getting labels from repository' { + $repositoryName = [Guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName + Set-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Label $script:defaultLabels + Context 'When querying for all labels' { - $labels = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName + $labels = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName It 'Should return expected number of labels' { - $($labels).Count | Should be $script:expectedNumberOfLabels + $($labels).Count | Should be $script:defaultLabels.Count } } Context 'When querying for specific label' { - $label = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName bug + $label = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name bug It 'Should return expected label' { $label.name | Should be "bug" } } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } Describe 'Creating new label' { - $labelName = "TestLabel" - New-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -LabelColor BBBBBB - $label = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName + $repositoryName = [Guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName - AfterEach { - Remove-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName - } + $labelName = [Guid]::NewGuid().Guid + New-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -Color BBBBBB + $label = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName It 'New label should be created' { $label.name | Should be $labelName } + + AfterEach { + Remove-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName + } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } Describe 'Removing label' { - $labelName = "TestLabel" + $repositoryName = [Guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName + Set-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Label $script:defaultLabels - New-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -LabelColor BBBBBB - $labels = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName + $labelName = [Guid]::NewGuid().Guid + New-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -Color BBBBBB + $labels = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName It 'Should return increased number of labels' { - $($labels).Count | Should be ($script:expectedNumberOfLabels + 1) + $($labels).Count | Should be ($script:defaultLabels.Count + 1) } - Remove-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName - $labels = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName + Remove-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName + $labels = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName It 'Should return expected number of labels' { - $($labels).Count | Should be $script:expectedNumberOfLabels + $($labels).Count | Should be $script:defaultLabels.Count } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } Describe 'Updating label' { - $labelName = "TestLabel" - + $repositoryName = [Guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName + + $labelName = [Guid]::NewGuid().Guid + Context 'Updating label color' { - New-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -LabelColor BBBBBB - Update-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -NewLabelName $labelName -LabelColor AAAAAA - $label = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName + New-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -Color BBBBBB + Update-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -NewName $labelName -Color AAAAAA + $label = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName - AfterEach { - Remove-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName + AfterEach { + Remove-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName } It 'Label should have different color' { $label.color | Should be AAAAAA } } - + Context 'Updating label name' { $newLabelName = $labelName + "2" - New-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -LabelColor BBBBBB - Update-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -NewLabelName $newLabelName -LabelColor BBBBBB - $label = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $newLabelName + New-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -Color BBBBBB + Update-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -NewName $newLabelName -Color BBBBBB + $label = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $newLabelName - AfterEach { - Remove-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $newLabelName + AfterEach { + Remove-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $newLabelName } It 'Label should have different color' { @@ -134,34 +235,43 @@ if ($script:tokenExists) $label.color | Should be BBBBBB } } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } Describe 'Applying set of labels on repository' { - $labelName = "TestLabel" + $repositoryName = [Guid]::NewGuid().Guid + $null = New-GitHubRepository -RepositoryName $repositoryName - New-GitHubLabels -RepositoryName $script:repositoryName -OwnerName $script:accountName + $labelName = [Guid]::NewGuid().Guid + Set-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Label $script:defaultLabels # Add new label - New-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName $labelName -LabelColor BBBBBB - $labels = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName + New-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name $labelName -Color BBBBBB + $labels = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName # Change color of existing label - Update-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName "bug" -NewLabelName "bug" -LabelColor BBBBBB + Update-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name "bug" -NewName "bug" -Color BBBBBB # Remove one of approved labels" - Remove-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName -LabelName "discussion" + Remove-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Name "discussion" It 'Should return increased number of labels' { - $($labels).Count | Should be ($script:expectedNumberOfLabels + 1) + $($labels).Count | Should be ($script:defaultLabels.Count + 1) } - New-GitHubLabels -RepositoryName $script:repositoryName -OwnerName $script:accountName - $labels = Get-GitHubLabel -RepositoryName $script:repositoryName -OwnerName $script:accountName + Set-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName -Label $script:defaultLabels + $labels = Get-GitHubLabel -OwnerName $script:ownerName -RepositoryName $repositoryName It 'Should return expected number of labels' { - $($labels).Count | Should be $script:expectedNumberOfLabels - $bugLabel = $labels | ?{$_.name -eq "bug"} + $($labels).Count | Should be $script:defaultLabels.Count + $bugLabel = $labels | Where-Object {$_.name -eq "bug"} $bugLabel.color | Should be "fc2929" } + + $null = Remove-GitHubRepository -OwnerName $script:ownerName -RepositoryName $repositoryName } -} \ No newline at end of file +} + +# Restore the user's configuration to its pre-test state +Restore-GitHubConfiguration -Path $configFile diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 00000000..cf91b528 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,298 @@ +# PowerShellForGitHub PowerShell Module +## Usage + +#### Table of Contents +* [Logging](#logging) +* [Telemetry](#telemetry) +* [Examples](#examples) + * [Analytics](#analytics) + * [Querying Issues](#querying-issues) + * [Querying Pull Requests](#querying-pull-requests) + * [Querying Collaborators](#querying-collaborators) + * [Querying Contributors](#querying-contributors) + * [Quering Team and Organization Membership](#querying-team-and-organization-membership) + * [Labels](#labels) + * [Getting Labels for a Repository](#getting-labels-for-a-repository) + * [Adding a New Label to a Repository](#adding-a-new-label-to-a-repository) + * [Removing a Label From a Repository](#removing-a-label-from-a-repository) + * [Updating a Label With a New Name and Color](#updating-a-label-with-a-new-name-and-color) + * [Bulk Updating Labels in a Repository](#bulk-updating-labels-in-a-repository) + * [Users](#users) + * [Getting the current authenticated user](#getting-the-current-authenticated-user) + * [Updating the current authenticated user](#updating-the-current-authenticated-user) + * [Getting any user](#getting-any-user) + * [Getting all users](#getting-all-users) + +---------- + +## Logging + +All commands will log to the console, as well as to a log file, by default. +The logging is affected by configuration properties (which can be checked with +`Get-GitHubConfiguration` and changed with `Set-GitHubConfiguration`). + + **`LogPath`** [string] The logfile. Defaults to + `$env:USERPROFILE\Documents\PowerShellForGitHub.log` + + **`DisableLogging`** [bool] Defaults to `$false`. + + **`LogTimeAsUtc`** [bool] Defaults to `$false`. If `$false`, times are logged in local time. + When `$true`, times are logged using UTC (and those timestamps will end with a Z per the + [W3C standard](http://www.w3.org/TR/NOTE-datetime)) + + **`LogProcessId`** [bool] Defaults to `$false`. If `$true`, the + Process ID (`$global:PID`) of the current PowerShell process will be added + to every log entry. This can be helpful if you have situations where + multiple instances of this module run concurrently and you want to + more easily isolate the log entries for one process. An alternative + solution would be to use `Set-GitHubConfiguration -LogPath -SessionOnly` to specify a + different log file for each PowerShell process. An easy way to view the filtered + entries for a session is (replacing `PID` with the PID that you are interested in): + + Get-Content -Path -Encoding UTF8 | Where { $_ -like '*[[]PID[]]*' } + +---------- + +## Telemetry + +In order to track usage, gauge performance and identify areas for improvement, telemetry is +employed during execution of commands within this module (via Application Insights). For more +information, refer to the [Privacy Policy](README.md#privacy-policy). + +> You may notice some needed assemblies for communicating with Application Insights being +> downloaded on first run of a command within each PowerShell session. The +> [automatic dependency downloads](#automatic-dependency-downloads) section of the setup +> documentation describes how you can avoid having to always re-download the telemetry assemblies +> in the future. + +We recommend that you always leave the telemetry feature enabled, but a situation may arise where +it must be disabled for some reason. In this scenario, you can disable telemetry by calling: + +```powershell +Set-GitHubConfiguration -DisableTelemetry -SessionOnly +``` + +The effect of that value will last for the duration of your session (until you close your +console window). To make that change permanent, remove `-SessionOnly` from that call. + +The following type of information is collected: + * Every major command executed (to gauge usefulness of the various commands) + * Types of parameters used with the command + * Error codes / information + +The following information is also collected, but the reported information is only reported +in the form of an SHA512 Hash (to protect PII (personal identifiable information)): + * Username + * OwnerName + * RepositoryName + * OrganizationName + +The hashing of the above items can be disabled (meaning that the plaint-text data will be reported +instead of the _hash_ of the data) by setting + +```powershell +Set-GitHubConfiguration -DisablePiiProtection -SessionOnly +``` + +Similar to `DisableTelemetry`, the effect of this value will only last for the duration of +your session (until you close your console window), unless you call it without `-SessionOnly`. + +The first time telemetry is tracked in a new PowerShell session, a reminder message will be displayed +to the user. To suppress this reminder in the future, call: + +```powershell +Set-GitHubConfiguration -SuppressTelemetryReminder +``` + +Finally, the Application Insights Key that the telemetry is reported to is exposed as + +```powershell +Get-GitHubConfiguration -Name ApplicationInsightsKey +``` +It is requested that you do not change this value, otherwise the telemetry will not be reported to +us for analysis. We expose it here for complete transparency. + +---------- + +## Examples + +### Analytics + +#### Querying Issues + +```powershell +# Getting all of the issues from the PowerShell\xPSDesiredStateConfiguration repository +$issues = Get-GitHubIssue -OwnerName PowerShell -RepositoryName 'xPSDesiredStateConfiguration' +``` + +```powershell +# An example of accomplishing what Get-GitHubIssueForRepository (from v0.1.0) used to do. +# Get all of the issues from multiple repos, but only return back the ones that were created within +# past two weeks. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$issues = @() +$repos | ForEach-Object { $issues += Get-GitHubIssue -Uri $_ } +$issues | Where-Object { $_.created_at -gt (Get-Date).AddDays(-14) } +``` + +```powershell +# An example of accomplishing what Get-GitHubWeeklyIssueForRepository (from v0.1.0) used to do. +# Get all of the issues from multiple repos, and group them by the week in which they were created. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$issues = @() +$repos | ForEach-Object { $issues += Get-GitHubIssue -Uri $_ } +$issues | Group-GitHubIssue -Weeks 12 -DateType 'created' +``` + +```powershell +# An example of accomplishing what Get-GitHubTopIssueRepository (from v0.1.0) used to do. +# Get all of the issues from multiple repos, and sort the repos by the number issues that they have. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$issueCounts = @() +$issueSearchParams = @{ + 'State' = 'open' +} +$repos | ForEach-Object { $issueCounts += ([PSCustomObject]@{ 'Uri' = $_; 'Count' = (Get-GitHubIssue -Uri $_ @issueSearchParams).Count }) } +$issueCounts | Sort-Object -Property Count -Descending +``` + +#### Querying Pull Requests + +```powershell +# Getting all of the pull requests from the PowerShell\PowerShellForGitHub repository +$issues = Get-GitHubIssue -OwnerName PowerShell -RepositoryName 'PowerShellForGitHub' +``` + +```powershell +# An example of accomplishing what Get-GitHubPullRequestForRepository (from v0.1.0) used to do. +# Get all of the pull requests from multiple repos, but only return back the ones that were created +# within the past two weeks. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$pullRequests = @() +$repos | ForEach-Object { $pullRequests += Get-GitHubPullRequest -Uri $_ } +$pullRequests | Where-Object { $_.created_at -gt (Get-Date).AddDays(-14) } +``` + +```powershell +# An example of accomplishing what Get-GitHubWeeklyPullRequestForRepository (from v0.1.0) used to do. +# Get all of the pull requests from multiple repos, and group them by the week in which they were merged. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$pullRequests = @() +$repos | ForEach-Object { $pullRequests += Get-GitHubPullRequest -Uri $_ } +$pullRequests | Group-GitHubPullRequest -Weeks 12 -DateType 'merged' +``` + +```powershell +# An example of accomplishing what Get-GitHubTopPullRequestRepository (from v0.1.0) used to do. +# Get all of the pull requests from multiple repos, and sort the repos by the number +# of closed pull requests that they have had within the past two weeks. +$repos = @('https://github.com/powershell/xpsdesiredstateconfiguration', 'https://github.com/powershell/xactivedirectory') +$pullRequestCounts = @() +$pullRequestSearchParams = @{ + 'State' = 'closed' +} +$repos | + ForEach-Object { + $pullRequestCounts += ([PSCustomObject]@{ + 'Uri' = $_; + 'Count' = ( + (Get-GitHubPullRequest -Uri $_ @pullRequestSearchParams) | + Where-Object { $_.completed_at -gt (Get-Date).AddDays(-14) } + ).Count + }) } + +$pullRequestCounts | Sort-Object -Property Count -Descending +``` + +#### Querying Collaborators + +```powershell +$collaborators = Get-GitHubRepositoryCollaborators` + -Uri @('https://github.com/PowerShell/DscResources') +``` + +#### Querying Contributors + +```powershell +# Getting all of the contributors for a single repository +$contributors = Get-GitHubRepositoryContributor -OwnerName 'PowerShell' -RepositoryName 'PowerShellForGitHub' } +``` + +```powershell +# An example of accomplishing what Get-GitHubRepositoryContributors (from v0.1.0) used to do. +# Getting all of the contributors for a set of repositories +$repos = @('https://github.com/PowerShell/DscResources', 'https://github.com/PowerShell/xWebAdministration') +$contributors = @() +$repos | ForEach-Object { $contributors += Get-GitHubRepositoryContributor -Uri $_ } +``` + +```powershell +# An example of accomplishing what Get-GitHubRepositoryUniqueContributor (from v0.1.0) used to do. +# Getting the unique set of contributors from the previous results of Get-GitHubRepositoryContributor +Get-GitHubRepositoryContributor -OwnerName 'PowerShell' -RepositoryName 'PowerShellForGitHub' } | + Select-Object -ExpandProperty author | + Select-Object -ExpandProperty login -Unique + Sort-Object +``` + +#### Quering Team and Organization Membership + +```powershell +$organizationMembers = Get-GitHubOrganizationMembers -OrganizationName 'OrganizationName' +$teamMembers = Get-GitHubTeamMembers -OrganizationName 'OrganizationName' -TeamName 'TeamName' +``` + +---------- + +### Labels + +#### Getting Labels for a Repository +```powershell +$labels = Get-GitHubLabel -OwnerName Powershell -RepositoryName DesiredStateConfiguration +``` + +#### Adding a New Label to a Repository +```powershell +New-GitHubLabel -OwnerName Powershell -RepositoryName DesiredStateConfiguration -Name TestLabel -Color BBBBBB +``` + +#### Removing a Label From a Repository +```powershell +Remove-GitHubLabel -OwnerName Powershell -RepositoryName desiredstateconfiguration -Name TestLabel +``` + +#### Updating a Label With a New Name and Color +```powershell +Update-GitHubLabel -OwnerName Powershell -RepositoryName DesiredStateConfiguration -Name TestLabel -NewName NewTestLabel -Color BBBB00 +``` + +#### Bulk Updating Labels in a Repository +```powershell +$labels = @( @{ 'name' = 'Label1'; 'color' = 'BBBB00'; 'description' = 'My label description' }, @{ 'name' = 'Label2'; 'color' = 'FF00000' }) +Set-GitHubLabel -OwnerName Powershell -RepositoryName DesiredStateConfiguration -Label $labels +``` + +---------- + +### Users + +#### Getting the current authenticated user +```powershell +Get-GitHubUser -Current +``` + +#### Updating the current authenticated user +```powershell +Update-GitHubCurrentUser -Location 'Seattle, WA' -Hireable:$false +``` + +#### Getting any user +```powershell +Get-GitHubUser -Name octocat +``` + +#### Getting all users +```powershell +Get-GitHubUser +``` +> Warning: This will take a while. It's getting _every_ GitHub user. diff --git a/appveyor.yml b/appveyor.yml index a1e9bd31..3ff0c1da 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,8 +4,10 @@ version: 0.1.0.{build} environment: - token: - secure: ea+zvlDJmekHVgOvilbf+ZARO2fuoAp07W63ZtEHBz3ke48LdC1odE8bgBk0nIuw + avOwnerName: PowerShellForGitHubTeam + avOrganizationName: PowerShellForGitHubTeamTestOrg + avAccessToken: + secure: 9Bjnb+a2KjzXF39rXmKmZl1s/Sf9tKR6fm+E40HCAiDe56fKMJ+rObjg1+joPe6f install: - cinst -y pester