diff --git a/cmd/server.go b/cmd/server.go index c7f2da065..afad5ace0 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -36,6 +36,10 @@ import ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. const ( // Flag names. + ADBasicPasswordFlag = "azuredevops-basic-password" // nolint: gosec + ADBasicUserFlag = "azuredevops-basic-user" + ADTokenFlag = "azuredevops-token" // nolint: gosec + ADUserFlag = "azuredevops-user" AllowForkPRsFlag = "allow-fork-prs" AllowRepoConfigFlag = "allow-repo-config" AtlantisURLFlag = "atlantis-url" @@ -72,8 +76,9 @@ const ( TFETokenFlag = "tfe-token" WriteGitCredsFlag = "write-git-creds" - // Flag defaults. // NOTE: Must manually set these as defaults in the setDefaults function. + DefaultADBasicUser = "" + DefaultADBasicPassword = "" DefaultCheckoutStrategy = "branch" DefaultBitbucketBaseURL = bitbucketcloud.BaseURL DefaultDataDir = "~/.atlantis" @@ -85,6 +90,24 @@ const ( ) var stringFlags = map[string]stringFlag{ + ADBasicPasswordFlag: { + description: "Azure Devops basic authentication password for inbound webhooks " + + "(see https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops)." + + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from your Azure Devops org. " + + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + + "Should be specified via the ATLANTIS_AZUREDEVOPS_BASIC_PASSWORD environment variable.", + defaultValue: "", + }, + ADBasicUserFlag: { + description: "Azure Devops basic authentication username for inbound webhooks.", + defaultValue: "", + }, + ADTokenFlag: { + description: "Azure Devops token of API user. Can also be specified via the ATLANTIS_DO_TOKEN environment variable.", + }, + ADUserFlag: { + description: "Azure Devops username of API user.", + }, AtlantisURLFlag: { description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ". Supports a base path ex. https://example.com/basepath.", }, @@ -454,14 +477,15 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { // 1. github user and token set // 2. gitlab user and token set // 3. bitbucket user and token set - // 4. any combination of the above - vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag) - if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) || ((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) { + // 4. azuredevops user and token set + // 5. any combination of the above + vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag) + if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) || ((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) || ((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "")) { return vcsErr } // At this point, we know that there can't be a single user/token without // its partner, but we haven't checked if any user/token is set at all. - if userConfig.GithubUser == "" && userConfig.GitlabUser == "" && userConfig.BitbucketUser == "" { + if userConfig.GithubUser == "" && userConfig.GitlabUser == "" && userConfig.BitbucketUser == "" && userConfig.AzureDevopsUser == "" { return vcsErr } @@ -550,6 +574,7 @@ func (s *ServerCmd) trimAtSymbolFromUsers(userConfig *server.UserConfig) { userConfig.GithubUser = strings.TrimPrefix(userConfig.GithubUser, "@") userConfig.GitlabUser = strings.TrimPrefix(userConfig.GitlabUser, "@") userConfig.BitbucketUser = strings.TrimPrefix(userConfig.BitbucketUser, "@") + userConfig.AzureDevopsUser = strings.TrimPrefix(userConfig.AzureDevopsUser, "@") } func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { @@ -565,6 +590,9 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput { s.Logger.Warn("Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are whitelisting Bitbucket IPs") } + if (userConfig.AzureDevopsWebhookBasicUser == "" || userConfig.AzureDevopsWebhookBasicPassword == "") && !s.SilenceOutput { + s.Logger.Warn("no Azure Devops webhook basic user and password set. This could allow attackers to spoof requests from Azure Devops.") + } } // deprecationWarnings prints a warning if flags that are deprecated are diff --git a/cmd/server_test.go b/cmd/server_test.go index e80af58e1..036888795 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -184,7 +184,7 @@ func TestExecute_ValidateSSLConfig(t *testing.T) { } func TestExecute_ValidateVCSConfig(t *testing.T) { - expErr := "--gh-user/--gh-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token must be set" + expErr := "--gh-user/--gh-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set" cases := []struct { description string flags map[string]interface{} @@ -216,6 +216,13 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { }, true, }, + { + "just azuredevops token set", + map[string]interface{}{ + cmd.ADTokenFlag: "token", + }, + true, + }, { "just github user set", map[string]interface{}{ @@ -237,6 +244,13 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { }, true, }, + { + "just azuredevops user set", + map[string]interface{}{ + cmd.ADUserFlag: "user", + }, + true, + }, { "github user and gitlab token set", map[string]interface{}{ @@ -285,6 +299,14 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { }, false, }, + { + "azuredevops user and azuredevops token set and should be successful", + map[string]interface{}{ + cmd.ADUserFlag: "user", + cmd.ADTokenFlag: "token", + }, + false, + }, { "all set should be successful", map[string]interface{}{ @@ -294,6 +316,8 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { cmd.GitlabTokenFlag: "token", cmd.BitbucketUserFlag: "user", cmd.BitbucketTokenFlag: "token", + cmd.ADUserFlag: "user", + cmd.ADTokenFlag: "token", }, false, }, @@ -316,13 +340,17 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { func TestExecute_Defaults(t *testing.T) { t.Log("Should set the defaults for all unspecified flags.") c := setup(map[string]interface{}{ - cmd.GHUserFlag: "user", - cmd.GHTokenFlag: "token", - cmd.GitlabUserFlag: "gitlab-user", - cmd.GitlabTokenFlag: "gitlab-token", - cmd.BitbucketUserFlag: "bitbucket-user", - cmd.BitbucketTokenFlag: "bitbucket-token", - cmd.RepoWhitelistFlag: "*", + cmd.GHUserFlag: "user", + cmd.GHTokenFlag: "token", + cmd.GitlabUserFlag: "gitlab-user", + cmd.GitlabTokenFlag: "gitlab-token", + cmd.BitbucketUserFlag: "bitbucket-user", + cmd.BitbucketTokenFlag: "bitbucket-token", + cmd.ADBasicUserFlag: "azuredevops-basic-user", + cmd.ADBasicPasswordFlag: "azuredevops-basic-password", + cmd.ADTokenFlag: "azuredevops-token", + cmd.ADUserFlag: "azuredevops-user", + cmd.RepoWhitelistFlag: "*", }) err := c.Execute() Ok(t, err) @@ -354,6 +382,8 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "https://api.bitbucket.org", passedConfig.BitbucketBaseURL) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "azuredevops-token", passedConfig.AzureDevopsToken) + Equals(t, "azuredevops-user", passedConfig.AzureDevopsUser) Equals(t, "", passedConfig.BitbucketWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 4141, passedConfig.Port) diff --git a/runatlantis.io/.vuepress/components/HomeCustom.vue b/runatlantis.io/.vuepress/components/HomeCustom.vue index bfac96b59..2686d2a55 100644 --- a/runatlantis.io/.vuepress/components/HomeCustom.vue +++ b/runatlantis.io/.vuepress/components/HomeCustom.vue @@ -129,7 +129,7 @@ Fargate, etc.
  • Listens for - webhooks from GitHub/GitLab/Bitbucket. + webhooks from GitHub/GitLab/Bitbucket/Azure Devops.
  • Runs terraform commands remotely and comments back with their output. diff --git a/runatlantis.io/docs/access-credentials.md b/runatlantis.io/docs/access-credentials.md index a09df508b..abe02174c 100644 --- a/runatlantis.io/docs/access-credentials.md +++ b/runatlantis.io/docs/access-credentials.md @@ -1,5 +1,6 @@ # Git Host Access Credentials -This page describes how to create credentials for your Git host (GitHub, GitLab or Bitbucket) +This page describes how to create credentials for your Git host (GitHub, GitLab, Bitbucket, or Azure Devops) + that Atlantis will use to make API calls. [[toc]] @@ -19,6 +20,7 @@ generate an access token. Read on for the instructions for your specific Git hos * [GitLab](#gitlab) * [Bitbucket Cloud (bitbucket.org)](#bitbucket-cloud-bitbucket-org) * [Bitbucket Server (aka Stash)](#bitbucket-server-aka-stash) +* [Azure Devops](#azure-devops) ### GitHub - Create a Personal Access Token by following: [https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token) @@ -48,5 +50,14 @@ Your Atlantis user must also have "Write permissions" (for repos in an organizat - Give the token **Read** Project permissions and **Write** Pull request permissions - Click **Create** and record the access token +### Azure Devops +- Create a Personal access token by following [https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops) +- Label the password "atlantis" +- The minimum scopes required for this token are: + - Code (Read) + - Code (Status) + - Work Items (Read & write) +- Record the access token + ## Next Steps Once you've got your user and access token, you're ready to create a webhook secret. See [Creating a Webhook Secret](webhook-secrets.html). diff --git a/runatlantis.io/docs/apply-requirements.md b/runatlantis.io/docs/apply-requirements.md index 1d30ca1f9..75189dc3b 100644 --- a/runatlantis.io/docs/apply-requirements.md +++ b/runatlantis.io/docs/apply-requirements.md @@ -49,6 +49,7 @@ Each VCS provider has different rules around who can approve: * **Bitbucket Cloud (bitbucket.org)** – A user can approve their own pull request but Atlantis does not count that as an approval and requires an approval from at least one user that is not the author of the pull request +* **Azure Devops** – **All builtin groups include the "Contribute to pull requests"** permission and can approve a pull request :::tip Tip If you want to require **certain people** to approve the pull request, look at the @@ -120,6 +121,20 @@ merge. We don't check anything else because Bitbucket's API doesn't support it. If you need a specific check, please [open an issue](https://github.com/runatlantis/atlantis/issues/new). +#### Azure Devops +In Azure Devops, all pull requests are mergeable unless there is a conflict. You can set a pull request to "Complete" right away, or set "Auto-Complete", which will merge after all branch policies are met. See [Review code with pull requests](https://docs.microsoft.com/en-us/azure/devops/repos/git/pull-requests?view=azure-devops). + +[Branch policies](https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops) can: +* Require a minimum number of reviewers +* Allow users to approve their own changes +* Allow completion even if some reviewers vote "Waiting" or "Reject" +* Reset code reviewer votes when there are new changes +* Require a specfic merge strategy (squash, rebase, etc.) + +::: warning +At this time, the Azure Devops client only supports merging using the default 'no fast-forward' strategy. Make sure your branch policies permit this type of merge. +::: + ## Setting Apply Requirements As mentioned above, you can set apply requirements via flags, in `repos.yaml`, or in `atlantis.yaml` if `repos.yaml` allows the override. @@ -186,3 +201,4 @@ request can run the actual `atlantis apply` command. * For more information on GitHub pull request reviews and approvals see: [https://help.github.com/articles/about-pull-request-reviews/](https://help.github.com/articles/about-pull-request-reviews/) * For more information on GitLab merge request reviews and approvals (only supported on GitLab Enterprise) see: [https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html). * For more information on Bitbucket pull request reviews and approvals see: [https://confluence.atlassian.com/bitbucket/pull-requests-and-code-review-223220593.html](https://confluence.atlassian.com/bitbucket/pull-requests-and-code-review-223220593.html) +* For more information on Azure Devops pull request reviews and approvals see: [https://docs.microsoft.com/en-us/azure/devops/repos/git/pull-requests-overview?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/repos/git/pull-requests-overview?view=azure-devops) diff --git a/runatlantis.io/docs/configuring-webhooks.md b/runatlantis.io/docs/configuring-webhooks.md index 51aba9dd1..6899d0021 100644 --- a/runatlantis.io/docs/configuring-webhooks.md +++ b/runatlantis.io/docs/configuring-webhooks.md @@ -85,7 +85,40 @@ If you're using GitLab, navigate to your project's home page in GitLab - Click **Save**Bitbucket Webhook - See [Next Steps](#next-steps) +## Azure Devops +Webhooks are installed at the [team project](https://docs.microsoft.com/en-us/azure/devops/organizations/projects/about-projects?view=azure-devops) level, but may be restricted to only fire based on events pertaining to [specific repos](https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops) within the team project. + +- Navigate anywhere within a team project, ie: `https://dev.azure.com/orgName/projectName/_git/repoName` +- Select **Project settings** in the lower-left corner +- Select **Service hooks** + - If you see the message "You do not have sufficient permissions to view or configure subscriptions." you need to ensure your user is a member of either the organization's "Project Collection Administrators" group or the project's "Project Administrators" group. + - To add your user to the Project Collection Build Administrators group, navigate to the organization level, click **Organization Settings** and then click **Permissions**. You should be at `https://dev.azure.com//_settings/groups`. Now click on the **/Project Collection Administrators** group and add your user as a member. + - To add your user to the Project Administrators group, navigate to the project level, click **Project Settings** and then click **Permissions**. You should be at `https://dev.azure.com///_settings/permissions`. Now click on the **[]/Project Administrators** group and add your user as a member. +- Click **Create subscription** or the green plus icon to add a new webhook +- Scroll to the bottom of the list and select **Web Hooks** +- Click **Next** +- Under "Trigger on this type of event", select **Pull request created** + - Optionally, select a repository under **Filters** to restrict the scope of this webhook subscription to a specific repository +- Click **Next** +- Set **URL** to `http://$URL/events` where `$URL` is where Atlantis is hosted. Note that SSL, or `https://$URL/events`, is required if you set a Basic username and password for the webhook). **Be sure to add `/events`** +- It is strongly recommended to set a Basic Username and Password for all webhooks +- Leave all three drop-down menus for `...to send` set to **All** +- Resource version should be set to **1.0** for `Pull request created` and `Pull request updated` event types and **2.0** for `Pull request commented on` +- **NOTE** If you're adding a webhook to multiple team projects or repositories (using filters), each repository will need to use the **same** basic username and password. +- Click **Finish** + +Repeat the process above until you have webhook subscriptions for the following event types that will trigger on all repositories Atlantis will manage: + +- Pull request created (you just added this one) +- Pull request updated +- Pull request commented on + +- See [Next Steps](#next-steps) + +## GitLab +If you're using GitLab, navigate to your project's home page in GitLab ## Next Steps * To verify that Atlantis is receiving your webhooks, create a test pull request - to your repo. You should see the request show up in the Atlantis logs at an `INFO` level. + to your repo. +* You should see the request show up in the Atlantis logs at an `INFO` level. * You'll now need to configure Atlantis to add your [Provider Credentials](provider-credentials.html) diff --git a/runatlantis.io/docs/deployment.md b/runatlantis.io/docs/deployment.md index afa0a0a96..940564aaa 100644 --- a/runatlantis.io/docs/deployment.md +++ b/runatlantis.io/docs/deployment.md @@ -17,7 +17,7 @@ Atlantis [Docker image](https://hub.docker.com/r/runatlantis/atlantis/). ### Routing Atlantis and your Git host need to be able to route and communicate with one another. Your Git host needs to be able to send webhooks to Atlantis and Atlantis needs to be able to make API calls to your Git host. If you're using -a public Git host like GitHub.com, GitLab.com or Bitbucket.org then you'll need to +a public Git host like GitHub.com, GitLab.com, Bitbucket.org, or Dev.azure.com then you'll need to expose Atlantis to the internet. If you're using a private Git host like GitHub Enterprise, GitLab Enterprise or @@ -100,13 +100,16 @@ up upgrading Atlantis by accident! for your Terraform repos. See [Repo Whitelist](server-configuration.html#repo-whitelist) for more details. 3. If you're using GitHub: 1. Replace `` with the username of your Atlantis GitHub user without the `@`. - 2. Delete all the `ATLANTIS_GITLAB_*` and `ATLANTIS_BITBUCKET_*` environment variables. + 2. Delete all the `ATLANTIS_GITLAB_*`, `ATLANTIS_BITBUCKET_*`, and `ATLANTIS_ADS_*` environment variables. 4. If you're using GitLab: 1. Replace `` with the username of your Atlantis GitLab user without the `@`. - 2. Delete all the `ATLANTIS_GH_*` and `ATLANTIS_BITBUCKET_*` environment variables. + 2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_BITBUCKET_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables. 5. If you're using Bitbucket: 1. Replace `` with the username of your Atlantis Bitbucket user without the `@`. - 2. Delete all the `ATLANTIS_GH_*` and `ATLANTIS_GITLAB_*` environment variables. + 2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITLAB_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables. +6. If you're using Azure Devops: + 1. Replace `` with the username of your Atlantis Azure Devops user without the `@`. + 2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITLAB_*`, and `ATLANTIS_BITBUCKET_*` environment variables. #### StatefulSet Manifest
    @@ -181,6 +184,26 @@ spec: key: token ### End Bitbucket Config ### + ### Azure Devops Config ### + - name: ATLANTIS_AZUREDEVOPS_USER + value: # 6i. If you're using Azure Devops replace with the username of your Atlantis Azure Devops user without the `@`. + - name: ATLANTIS_AZUREDEVOPS_TOKEN + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: token + - name: ATLANTIS_AZUREDEVOPS_BASIC_USER + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: basic-user + - name: ATLANTIS_AZUREDEVOPS_BASIC_PASS + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: basic-password + ### End Azure Devops Config ### + - name: ATLANTIS_DATA_DIR value: /atlantis - name: ATLANTIS_PORT @@ -309,6 +332,26 @@ spec: key: token ### End Bitbucket Config ### + ### Azure Devops Config ### + - name: ATLANTIS_AZUREDEVOPS_USER + value: # 6i. If you're using Azure Devops replace with the username of your Atlantis Azure Devops user without the `@`. + - name: ATLANTIS_AZUREDEVOPS_TOKEN + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: token + - name: ATLANTIS_AZUREDEVOPS_BASIC_USER + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: basic-user + - name: ATLANTIS_AZUREDEVOPS_BASIC_PASS + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: basic-password + ### End Azure Devops Config ### + - name: ATLANTIS_PORT value: "4141" # Kubernetes sets an ATLANTIS_PORT variable so we need to override. ports: @@ -484,14 +527,30 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` +##### Azure Devops + +A certificate and private key are required if using Basic authentication for webhooks. + +```bash +atlantis server \ +--atlantis-url="$URL" \ +--azuredevops-user="$USERNAME" \ +--azuredevops-token="$TOKEN" \ +--azuredevops-basic-user="$ATLANTIS_AZUREDEVOPS_BASIC_USER" \ +--azuredevops-basic-password="$ATLANTIS_AZUREDEVOPS_BASIC_PASS" \ +--repo-whitelist="$REPO_WHITELIST" +--ssl-cert-file=file.crt +--ssl-key-file=file.key +``` + Where - `$URL` is the URL that Atlantis can be reached at -- `$USERNAME` is the GitHub/GitLab/Bitbucket username you generated the token for +- `$USERNAME` is the GitHub/GitLab/Bitbucket/AzureDevops username you generated the token for - `$TOKEN` is the access token you created. If you don't want this to be passed in as an argument for security reasons you can specify it in a config file (see [Configuration](/docs/server-configuration.html#environment-variables)) or as an environment variable: `ATLANTIS_GH_TOKEN` or `ATLANTIS_GITLAB_TOKEN` - or `ATLANTIS_BITBUCKET_TOKEN` + or `ATLANTIS_BITBUCKET_TOKEN` or `ATLANTIS_AZUREDEVOPS_TOKEN` - `$SECRET` is the random key you used for the webhook secret. If you don't want this to be passed in as an argument for security reasons you can specify it in a config file diff --git a/runatlantis.io/docs/installation-guide.md b/runatlantis.io/docs/installation-guide.md index b6a93dc33..b37aff589 100644 --- a/runatlantis.io/docs/installation-guide.md +++ b/runatlantis.io/docs/installation-guide.md @@ -3,7 +3,7 @@ This guide is for installing a **production-ready** instance of Atlantis onto yo infrastructure: 1. First, ensure your Terraform setup meets the Atlantis **requirements** * See [Requirements](requirements.html) -1. Create **access credentials** for your Git host (GitHub, GitLab, Bitbucket) +1. Create **access credentials** for your Git host (GitHub, GitLab, Bitbucket, Azure Devops) * See [Generating Git Host Access Credentials](access-credentials.html) 1. Create a **webhook secret** so Atlantis can validate webhooks * See [Creating a Webhook Secret](webhook-secrets.html) diff --git a/runatlantis.io/docs/requirements.md b/runatlantis.io/docs/requirements.md index 22442dccf..7460a46c0 100644 --- a/runatlantis.io/docs/requirements.md +++ b/runatlantis.io/docs/requirements.md @@ -11,6 +11,7 @@ Atlantis integrates with the following Git hosts: * GitLab (public, private or enterprise) * Bitbucket Cloud aka bitbucket.org (public or private) * Bitbucket Server aka Stash +* Azure Devops ## Terraform State Atlantis supports all backend types **except for local state**. We don't support local state diff --git a/runatlantis.io/docs/security.md b/runatlantis.io/docs/security.md index 5acc6bd83..6bfc123b5 100644 --- a/runatlantis.io/docs/security.md +++ b/runatlantis.io/docs/security.md @@ -52,6 +52,13 @@ Atlantis should be run with Webhook secrets set via the `$ATLANTIS_GH_WEBHOOK_SE Even with the `--repo-whitelist` flag set, without a webhook secret, attackers could make requests to Atlantis posing as a repository that is whitelisted. Webhook secrets ensure that the webhook requests are actually coming from your VCS provider (GitHub or GitLab). +:::tip Tip +If you are using Azure Devops, instead of webhook secrets add a [basic username and password](#azure devops basic authentication) +::: + +### Azure Devops Basic Authentication +Azure Devops supports sending a basic authentication header in all webhook events. This requires using an HTTPS URL for your webhook location. + ### SSL/HTTPS If you're using webhook secrets but your traffic is over HTTP then the webhook secrets could be stolen. Enable SSL/HTTPS using the `--ssl-cert-file` and `--ssl-key-file` diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 0c4ca3e49..3172b134d 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -98,6 +98,38 @@ Values are chosen in this order: Automatically merge pull requests after all plans have been successfully applied. Defaults to `false`. See [Automerging](automerging.html) for more details. +* ### `--azuredevops-basic-password` + ```bash + atlantis server --azuredevops-basic-password="password123" + ``` + Azure Devops basic authentication password for inbound webhooks (see + https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops). + SECURITY WARNING: If not specified, Atlantis won't be able to validate that the + incoming webhook call came from your Azure Devops org. This means that an + attacker could spoof calls to Atlantis and cause it to perform malicious + actions. Should be specified via the ATLANTIS_AZUREDEVOPS_BASIC_AUTH environment + variable. + +* ### `--azuredevops-basic-user` + ```bash + atlantis server --azuredevops-basic-user="username@example.com" + ``` + Azure Devops basic authentication username for inbound webhooks. Can also be specified via the ATLANTIS_AZUREDEVOPS_BASIC_USER + environment variable. + +* ### `--azuredevops-token` + ```bash + atlantis server --azuredevops-token="username@example.com" + ``` + Azure Devops token of API user. Can also be specified via the ATLANTIS_AZUREDEVOPS_TOKEN + environment variable. + +* ### `--azuredevops-user` + ```bash + atlantis server --azuredevops-user="username@example.com" + ``` + Azure Devops username of API user. + * ### `--bitbucket-base-url` ```bash atlantis server --bitbucket-base-url="http://bitbucket.corp:7990/basepath" @@ -308,6 +340,8 @@ Values are chosen in this order: * Format is `{hostname}/{owner}/{repo}`, ex. `github.com/runatlantis/atlantis` * `*` matches any characters, ex. `github.com/runatlantis/*` will match all repos in the runatlantis organization * For Bitbucket Server: `{hostname}` is the domain without scheme and port, `{owner}` is the name of the project (not the key), and `{repo}` is the repo name + * For Azure Devops the whitelist takes one of two forms: `{owner}.visualstudio.com/{project}/{repo}` or `dev.azure.com/{owner}/{project}/{repo} + * Microsoft is in the process of changing Azure Devops to the latter form, so it may be safest to always specify both formats in your repo whitelist for each repository until the change is complete. Examples: * Whitelist `myorg/repo1` and `myorg/repo2` on `github.com` @@ -316,6 +350,8 @@ Values are chosen in this order: * `--repo-whitelist='github.com/myorg/*'` * Whitelist all repos in my GitHub Enterprise installation * `--repo-whitelist='github.yourcompany.com/*'` + * Whitelist all repos under `myorg` project `myproject` on Azure Devops + * `--repo-whitelist='myorg.visualstudio.com/myproject/*,dev.azure.com/myorg/myproject/*'` * Whitelist all repositories * `--repo-whitelist='*'` diff --git a/runatlantis.io/docs/webhook-secrets.md b/runatlantis.io/docs/webhook-secrets.md index 83696b8a3..1e17abc82 100644 --- a/runatlantis.io/docs/webhook-secrets.md +++ b/runatlantis.io/docs/webhook-secrets.md @@ -12,6 +12,10 @@ Webhook secrets are actually optional. However they're highly recommended for security. ::: +::: tip NOTE +Azure Devops uses Basic authentication for webhooks rather than webhook secrets. +::: + ::: warning Bitbucket.org **does not** support webhook secrets. To mitigate, use repo whitelists and IP whitelists. See [Security](security.html#bitbucket-cloud-bitbucket-org) for more information. diff --git a/runatlantis.io/guide/testing-locally.md b/runatlantis.io/guide/testing-locally.md index adf326665..d2a73e701 100644 --- a/runatlantis.io/guide/testing-locally.md +++ b/runatlantis.io/guide/testing-locally.md @@ -262,6 +262,22 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` +##### Azure Devops + +A certificate and private key are required if using Basic authentication for webhooks. + +```bash +atlantis server \ +--atlantis-url="$URL" \ +--azuredevops-user="$USERNAME" \ +--azuredevops-token="$TOKEN" \ +--azuredevops-basic-user="$ATLANTIS_AZUREDEVOPS_BASIC_USER" \ +--azuredevops-basic-password="$ATLANTIS_AZUREDEVOPS_BASIC_PASS" \ +--repo-whitelist="$REPO_WHITELIST" +--ssl-cert-file=file.crt +--ssl-key-file=file.key +``` + ## Create a pull request Create a pull request so you can test Atlantis. ::: tip diff --git a/server/azuredevops_request_validator.go b/server/azuredevops_request_validator.go new file mode 100644 index 000000000..fdfcf61e5 --- /dev/null +++ b/server/azuredevops_request_validator.go @@ -0,0 +1,57 @@ +package server + +import ( + "fmt" + "io/ioutil" + "net/http" + + "github.com/mcdafydd/go-azuredevops/azuredevops" +) + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_azuredevops_request_validator.go AzureDevopsRequestValidator + +// AzureDevopsRequestValidator handles checking if Azure Devops requests +// contain a valid Basic authentication username and password. +type AzureDevopsRequestValidator interface { + // Validate returns the JSON payload of the request. + // If both username and password values have a length greater than zero, + // it checks that the credentials match those configured in Atlantis. + // If either username or password have a length of zero, the payload is + // returned without further checking. + Validate(r *http.Request, user []byte, pass []byte) ([]byte, error) +} + +// DefaultAzureDevopsRequestValidator handles checking if Azure Devops +// requests contain the correct Basic auth username and password. +type DefaultAzureDevopsRequestValidator struct{} + +// Validate returns the JSON payload of the request. +// If secret is not empty, it checks that the request was signed +// by secret and returns an error if it was not. +// If secret is empty, it does not check if the request was signed. +func (d *DefaultAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) ([]byte, error) { + if len(user) != 0 && len(pass) != 0 { + return d.validateWithBasicAuth(r, user, pass) + } + return d.validateWithoutBasicAuth(r) +} + +func (d *DefaultAzureDevopsRequestValidator) validateWithBasicAuth(r *http.Request, user []byte, pass []byte) ([]byte, error) { + payload, err := azuredevops.ValidatePayload(r, user, pass) + if err != nil { + return nil, err + } + return payload, nil +} + +func (d *DefaultAzureDevopsRequestValidator) validateWithoutBasicAuth(r *http.Request) ([]byte, error) { + ct := r.Header.Get("Content-Type") + if ct == "application/json" || ct == "application/json; charset=utf-8" { + payload, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("could not read body: %s", err) + } + return payload, nil + } + return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", ct) +} diff --git a/server/azuredevops_request_validator_test.go b/server/azuredevops_request_validator_test.go new file mode 100644 index 000000000..24b91e537 --- /dev/null +++ b/server/azuredevops_request_validator_test.go @@ -0,0 +1,69 @@ +package server_test + +import ( + "bytes" + "net/http" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server" + . "github.com/runatlantis/atlantis/testing" +) + +func TestAzureDevopsValidate_WithBasicAuthErr(t *testing.T) { + t.Log("if the request does not have a valid basic auth user and password there is an error") + RegisterMockTestingT(t) + g := server.DefaultAzureDevopsRequestValidator{} + buf := bytes.NewBufferString("") + req, err := http.NewRequest("POST", "http://localhost/event", buf) + Ok(t, err) + req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") // user:pass + req.Header.Set("Content-Type", "application/json") + + _, err = g.Validate(req, []byte("user"), []byte("wrongpass")) + Assert(t, err != nil, "error should not be nil") + Equals(t, "ValidatePayload authentication failed", err.Error()) +} + +func TestAzureDevopsValidate_WithBasicAuth(t *testing.T) { + t.Log("if the request has a valid basic auth user and password the payload is returned") + RegisterMockTestingT(t) + g := server.DefaultAzureDevopsRequestValidator{} + buf := bytes.NewBufferString(`{"yo":true}`) + req, err := http.NewRequest("POST", "http://localhost/event", buf) + Ok(t, err) + req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") // user:pass + req.Header.Set("Content-Type", "application/json") + + bs, err := g.Validate(req, []byte("user"), []byte("pass")) + Ok(t, err) + Equals(t, `{"yo":true}`, string(bs)) +} + +func TestAzureDevopsValidate_WithoutSecretInvalidContentType(t *testing.T) { + t.Log("if the request has an invalid content type an error is returned") + RegisterMockTestingT(t) + g := server.DefaultAzureDevopsRequestValidator{} + buf := bytes.NewBufferString("") + req, err := http.NewRequest("POST", "http://localhost/event", buf) + Ok(t, err) + req.Header.Set("Content-Type", "invalid") + + _, err = g.Validate(req, nil, nil) + Assert(t, err != nil, "error should not be nil") + Equals(t, "webhook request has unsupported Content-Type \"invalid\"", err.Error()) +} + +func TestAzureDevopsValidate_WithoutSecretJSON(t *testing.T) { + t.Log("if the request is JSON the body is returned") + RegisterMockTestingT(t) + g := server.DefaultAzureDevopsRequestValidator{} + buf := bytes.NewBufferString(`{"yo":true}`) + req, err := http.NewRequest("POST", "http://localhost/event", buf) + Ok(t, err) + req.Header.Set("Content-Type", "application/json") + + bs, err := g.Validate(req, nil, nil) + Ok(t, err) + Equals(t, `{"yo":true}`, string(bs)) +} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 53311c6dc..828f133fb 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -17,6 +17,7 @@ import ( "fmt" "github.com/google/go-github/v28/github" + "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/db" "github.com/runatlantis/atlantis/server/events/models" @@ -45,6 +46,14 @@ type GithubPullGetter interface { GetPullRequest(repo models.Repo, pullNum int) (*github.PullRequest, error) } +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_azuredevops_pull_getter.go AzureDevopsPullGetter + +// AzureDevopsPullGetter makes API calls to get pull requests. +type AzureDevopsPullGetter interface { + // GetPullRequest gets the pull request with id pullNum for the repo. + GetPullRequest(repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error) +} + //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_gitlab_merge_request_getter.go GitlabMergeRequestGetter // GitlabMergeRequestGetter makes API calls to get merge requests. @@ -57,6 +66,7 @@ type GitlabMergeRequestGetter interface { type DefaultCommandRunner struct { VCSClient vcs.Client GithubPullGetter GithubPullGetter + AzureDevopsPullGetter AzureDevopsPullGetter GitlabMergeRequestGetter GitlabMergeRequestGetter CommitStatusUpdater CommitStatusUpdater DisableApplyAll bool @@ -167,6 +177,8 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead err = errors.New("pull request should not be nil–this is a bug") } pull = *maybePull + case models.AzureDevops: + pull, headRepo, err = c.getAzureDevopsData(baseRepo, pullNum) default: err = errors.New("Unknown VCS type–this is a bug") } @@ -353,6 +365,21 @@ func (c *DefaultCommandRunner) getGitlabData(baseRepo models.Repo, pullNum int) return pull, nil } +func (c *DefaultCommandRunner) getAzureDevopsData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) { + if c.AzureDevopsPullGetter == nil { + return models.PullRequest{}, models.Repo{}, errors.New("atlantis not configured to support Azure Devops") + } + adPull, err := c.AzureDevopsPullGetter.GetPullRequest(baseRepo, pullNum) + if err != nil { + return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to Azure Devops") + } + pull, _, headRepo, err := c.EventParser.ParseAzureDevopsPull(adPull) + if err != nil { + return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data") + } + return pull, headRepo, nil +} + func (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { src := fmt.Sprintf("%s#%d", repoFullName, pullNum) return c.Logger.NewLogger(src, true, c.Logger.GetLevel()) diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index c09daa3a3..261184731 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -36,6 +36,7 @@ import ( var projectCommandBuilder *mocks.MockProjectCommandBuilder var projectCommandRunner *mocks.MockProjectCommandRunner var eventParsing *mocks.MockEventParsing +var azuredevopsGetter *mocks.MockAzureDevopsPullGetter var githubGetter *mocks.MockGithubPullGetter var gitlabGetter *mocks.MockGitlabMergeRequestGetter var ch events.DefaultCommandRunner @@ -50,6 +51,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { vcsClient := vcsmocks.NewMockClient() githubGetter = mocks.NewMockGithubPullGetter() gitlabGetter = mocks.NewMockGitlabMergeRequestGetter() + azuredevopsGetter = mocks.NewMockAzureDevopsPullGetter() logger := logmocks.NewMockSimpleLogging() pullLogger = logging.NewSimpleLogger("runatlantis/atlantis#1", true, logging.Info) projectCommandRunner = mocks.NewMockProjectCommandRunner() @@ -65,6 +67,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { MarkdownRenderer: &events.MarkdownRenderer{}, GithubPullGetter: githubGetter, GitlabMergeRequestGetter: gitlabGetter, + AzureDevopsPullGetter: azuredevopsGetter, Logger: logger, AllowForkPRs: false, AllowForkPRsFlag: "allow-fork-prs-flag", diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index b6bf14949..203ab7f58 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -65,9 +65,10 @@ type CommentBuilder interface { // CommentParser implements CommentParsing type CommentParser struct { - GithubUser string - GitlabUser string - BitbucketUser string + GithubUser string + GitlabUser string + BitbucketUser string + AzureDevopsUser string } // CommentParseResult describes the result of parsing a comment as a command. @@ -125,6 +126,8 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen vcsUser = e.GitlabUser case models.BitbucketCloud, models.BitbucketServer: vcsUser = e.BitbucketUser + case models.AzureDevops: + vcsUser = e.AzureDevopsUser } executableNames := []string{"run", atlantisExecutable, "@" + vcsUser} if !e.stringInSlice(args[0], executableNames) { diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 297bc9b39..b9c203f2a 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -635,9 +635,10 @@ func TestBuildPlanApplyComment(t *testing.T) { func TestParse_VCSUsername(t *testing.T) { cp := events.CommentParser{ - GithubUser: "gh", - GitlabUser: "gl", - BitbucketUser: "bb", + GithubUser: "gh", + GitlabUser: "gl", + BitbucketUser: "bb", + AzureDevopsUser: "ad", } cases := []struct { vcs models.VCSHostType @@ -659,6 +660,10 @@ func TestParse_VCSUsername(t *testing.T) { vcs: models.BitbucketCloud, user: "bb", }, + { + vcs: models.AzureDevops, + user: "ad", + }, } for _, c := range cases { diff --git a/server/events/event_parser.go b/server/events/event_parser.go index d0d6044ff..50cbe5c98 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -16,10 +16,12 @@ package events import ( "encoding/json" "fmt" + "net/url" "path" "strings" "github.com/google/go-github/v28/github" + "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" @@ -234,6 +236,28 @@ type EventParsing interface { // GetBitbucketServerPullEventType returns the type of the pull request // event given the Bitbucket Server header. GetBitbucketServerPullEventType(eventTypeHeader string) models.PullRequestEventType + + // ParseAzureDevopsPull parses the response from the Azure Devops API endpoint (not + // from a webhook) that returns a pull request. + // pull is the parsed pull request. + // baseRepo is the repo the pull request will be merged into. + // headRepo is the repo the pull request branch is from. + ParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) ( + pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) + + // ParseAzureDevopsPullEvent parses Azure Devops pull request events. + // pull is the parsed pull request. + // pullEventType is the type of event, for example opened/closed. + // baseRepo is the repo the pull request will be merged into. + // headRepo is the repo the pull request branch is from. + // user is the pull request author. + ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) ( + pull models.PullRequest, pullEventType models.PullRequestEventType, + baseRepo models.Repo, headRepo models.Repo, user models.User, err error) + + // ParseAzureDevopsRepo parses the response from the Azure Devops API endpoint that + // returns a repo into the Atlantis model. + ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error) } // EventParser parses VCS events. @@ -245,6 +269,8 @@ type EventParser struct { BitbucketUser string BitbucketToken string BitbucketServerURL string + AzureDevopsToken string + AzureDevopsUser string } // GetBitbucketCloudPullEventType returns the type of the pull request @@ -671,3 +697,145 @@ func (e *EventParser) ParseBitbucketServerPullEvent(body []byte) (pull models.Pu pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketServerEventData(event.CommonEventData) return } + +// ParseAzureDevopsPullEvent parses Azure Devops pull request events. +// See EventParsing for return value docs. +func (e *EventParser) ParseAzureDevopsPullEvent(event azuredevops.Event) (pull models.PullRequest, pullEventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { + pullResource, ok := event.Resource.(*azuredevops.GitPullRequest) + if !ok { + errMsg := fmt.Sprintf("failed to type assert event.Resource.") + err = errors.New(errMsg) + return pull, pullEventType, baseRepo, headRepo, user, err + } + pull, baseRepo, headRepo, err = e.ParseAzureDevopsPull(pullResource) + if err != nil { + return pull, pullEventType, baseRepo, headRepo, user, err + } + createdBy := pullResource.GetCreatedBy() + if createdBy == nil { + err = errors.New("CreatedBy is null") + return pull, pullEventType, baseRepo, headRepo, user, err + } + senderUsername := createdBy.GetUniqueName() + if senderUsername == "" { + err = errors.New("CreatedBy.UniqueName is null") + return pull, pullEventType, baseRepo, headRepo, user, err + } + switch event.EventType { + case "git.pullrequest.created": + pullEventType = models.OpenedPullEvent + case "git.pullrequest.updated": + pullEventType = models.UpdatedPullEvent + if pull.State == models.ClosedPullState { + pullEventType = models.ClosedPullEvent + } + default: + pullEventType = models.OtherPullEvent + } + user = models.User{Username: senderUsername} + return pull, pullEventType, baseRepo, headRepo, user, err +} + +// ParseAzureDevopsPull parses the response from the Azure Devops API endpoint (not +// from a webhook) that returns a pull request. +// See EventParsing for return value docs. +func (e *EventParser) ParseAzureDevopsPull(pull *azuredevops.GitPullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) { + commit := pull.LastMergeSourceCommit.GetCommitID() + if commit == "" { + err = errors.New("lastMergeSourceCommit.commitID is null") + return pullModel, baseRepo, headRepo, err + } + url := pull.GetURL() + if url == "" { + err = errors.New("url is null") + return pullModel, baseRepo, headRepo, err + } + headBranch := pull.GetSourceRefName() + if headBranch == "" { + err = errors.New("sourceRefName (branch name) is null") + return pullModel, baseRepo, headRepo, err + } + baseBranch := pull.GetTargetRefName() + if baseBranch == "" { + err = errors.New("targetRefName (branch name) is null") + return pullModel, baseRepo, headRepo, err + } + num := pull.GetPullRequestID() + if num == 0 { + err = errors.New("pullRequestId is null") + return pullModel, baseRepo, headRepo, err + } + createdBy := pull.GetCreatedBy() + if createdBy == nil { + err = errors.New("CreatedBy is null") + return pullModel, baseRepo, headRepo, err + } + authorUsername := createdBy.GetUniqueName() + if authorUsername == "" { + err = errors.New("CreatedBy.UniqueName is null") + return pullModel, baseRepo, headRepo, err + } + baseRepo, err = e.ParseAzureDevopsRepo(pull.GetRepository()) + if err != nil { + return pullModel, baseRepo, headRepo, err + } + headRepo, err = e.ParseAzureDevopsRepo(pull.GetRepository()) + if err != nil { + return pullModel, baseRepo, headRepo, err + } + pullState := models.ClosedPullState + if *pull.Status == azuredevops.PullActive.String() { + pullState = models.OpenPullState + } + + pullModel = models.PullRequest{ + Author: authorUsername, + HeadBranch: path.Base(headBranch), + HeadCommit: commit, + URL: url, + Num: num, + State: pullState, + BaseRepo: baseRepo, + BaseBranch: path.Base(baseBranch), + } + return +} + +// ParseAzureDevopsRepo parses the response from the Azure Devops API endpoint that +// returns a repo into the Atlantis model. +// If the event payload doesn't contain a parent repository reference, extract the owner +// name from the URL. The URL will match one of two different formats: +// +// https://runatlantis.visualstudio.com/project/_git/repo +// https://dev.azure.com/runatlantis/project/_git/repo +// +// See EventParsing for return value docs. +func (e *EventParser) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error) { + teamProject := adRepo.GetProject() + parent := adRepo.GetParentRepository() + owner := "" + if parent != nil { + owner = parent.GetName() + } else { + uri, err := url.Parse(adRepo.GetWebURL()) + if err != nil { + return models.Repo{}, err + } + if strings.Contains(uri.Host, "visualstudio.com") { + owner = strings.Split(uri.Host, ".")[0] + } else if strings.Contains(uri.Host, "dev.azure.com") { + owner = strings.Split(uri.Path, "/")[1] + } else { + owner = "" + } + } + + // Construct our own clone URL so we always get the new dev.azure.com + // hostname for now. + // https://docs.microsoft.com/en-us/azure/devops/release-notes/2018/sep-10-azure-devops-launch#switch-existing-organizations-to-use-the-new-domain-name-url + project := teamProject.GetName() + repo := adRepo.GetName() + cloneURL := fmt.Sprintf("https://dev.azure.com/%s/%s/_git/%s", owner, project, repo) + fullName := fmt.Sprintf("%s/%s/%s", owner, project, repo) + return models.NewRepo(models.AzureDevops, fullName, cloneURL, e.AzureDevopsUser, e.AzureDevopsToken) +} diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index 6c864f7c5..10af0f1fc 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -17,11 +17,13 @@ import ( "encoding/json" "fmt" "io/ioutil" + "path" "path/filepath" "strings" "testing" "github.com/google/go-github/v28/github" + "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/mohae/deepcopy" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/models" @@ -38,6 +40,8 @@ var parser = events.EventParser{ BitbucketUser: "bitbucket-user", BitbucketToken: "bitbucket-token", BitbucketServerURL: "http://mycorp.com:7490", + AzureDevopsUser: "azuredevops-user", + AzureDevopsToken: "azuredevops-token", } func TestParseGithubRepo(t *testing.T) { @@ -1071,3 +1075,196 @@ func TestGetBitbucketServerEventType(t *testing.T) { }) } } + +func TestParseAzureDevopsRepo(t *testing.T) { + // this should be successful + repo := ADRepo + repo.ParentRepository = nil + r, err := parser.ParseAzureDevopsRepo(&repo) + Ok(t, err) + Equals(t, models.Repo{ + Owner: "owner/project", + FullName: "owner/project/repo", + CloneURL: "https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo", + SanitizedCloneURL: "https://azuredevops-user:@dev.azure.com/owner/project/_git/repo", + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "dev.azure.com", + Type: models.AzureDevops, + }, + }, r) + + // this should be successful + repo = ADRepo + repo.WebURL = nil + r, err = parser.ParseAzureDevopsRepo(&repo) + Ok(t, err) + Equals(t, models.Repo{ + Owner: "owner/project", + FullName: "owner/project/repo", + CloneURL: "https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo", + SanitizedCloneURL: "https://azuredevops-user:@dev.azure.com/owner/project/_git/repo", + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "dev.azure.com", + Type: models.AzureDevops, + }, + }, r) + +} + +func TestParseAzureDevopsPullEvent(t *testing.T) { + _, _, _, _, _, err := parser.ParseAzureDevopsPullEvent(ADPullEvent) + Ok(t, err) + + testPull := deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.LastMergeSourceCommit.CommitID = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "lastMergeSourceCommit.commitID is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.URL = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "url is null", err) + testEvent := deepcopy.Copy(ADPullEvent).(azuredevops.Event) + resource := deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest) + resource.CreatedBy = nil + testEvent.Resource = resource + _, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent) + ErrEquals(t, "CreatedBy is null", err) + + testEvent = deepcopy.Copy(ADPullEvent).(azuredevops.Event) + resource = deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest) + resource.CreatedBy.UniqueName = azuredevops.String("") + testEvent.Resource = resource + _, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent) + ErrEquals(t, "CreatedBy.UniqueName is null", err) + + actPull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseAzureDevopsPullEvent(ADPullEvent) + Ok(t, err) + expBaseRepo := models.Repo{ + Owner: "owner/project", + FullName: "owner/project/repo", + CloneURL: "https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo", + SanitizedCloneURL: "https://azuredevops-user:@dev.azure.com/owner/project/_git/repo", + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "dev.azure.com", + Type: models.AzureDevops, + }, + } + Equals(t, expBaseRepo, actBaseRepo) + Equals(t, expBaseRepo, actHeadRepo) + Equals(t, models.PullRequest{ + URL: ADPull.GetURL(), + Author: ADPull.CreatedBy.GetUniqueName(), + HeadBranch: path.Base(ADPull.GetSourceRefName()), + BaseBranch: path.Base(ADPull.GetTargetRefName()), + HeadCommit: ADPull.LastMergeSourceCommit.GetCommitID(), + Num: ADPull.GetPullRequestID(), + State: models.OpenPullState, + BaseRepo: expBaseRepo, + }, actPull) + Equals(t, models.OpenedPullEvent, evType) + Equals(t, models.User{Username: "user@example.com"}, actUser) +} + +func TestParseAzureDevopsPullEvent_EventType(t *testing.T) { + cases := []struct { + action string + exp models.PullRequestEventType + }{ + { + action: "git.pullrequest.updated", + exp: models.UpdatedPullEvent, + }, + { + action: "git.pullrequest.created", + exp: models.OpenedPullEvent, + }, + { + action: "git.pullrequest.updated", + exp: models.ClosedPullEvent, + }, + { + action: "anything_else", + exp: models.OtherPullEvent, + }, + } + + for _, c := range cases { + t.Run(c.action, func(t *testing.T) { + event := deepcopy.Copy(ADPullEvent).(azuredevops.Event) + if c.exp == models.ClosedPullEvent { + event = deepcopy.Copy(ADPullClosedEvent).(azuredevops.Event) + } + event.EventType = c.action + _, actType, _, _, _, err := parser.ParseAzureDevopsPullEvent(event) + Ok(t, err) + Equals(t, c.exp, actType) + }) + } +} + +func TestParseAzureDevopsPull(t *testing.T) { + testPull := deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.LastMergeSourceCommit.CommitID = nil + _, _, _, err := parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "lastMergeSourceCommit.commitID is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.URL = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "url is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.SourceRefName = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "sourceRefName (branch name) is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.TargetRefName = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "targetRefName (branch name) is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.CreatedBy = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "CreatedBy is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.CreatedBy.UniqueName = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "CreatedBy.UniqueName is null", err) + + testPull = deepcopy.Copy(ADPull).(azuredevops.GitPullRequest) + testPull.PullRequestID = nil + _, _, _, err = parser.ParseAzureDevopsPull(&testPull) + ErrEquals(t, "pullRequestId is null", err) + + actPull, actBaseRepo, actHeadRepo, err := parser.ParseAzureDevopsPull(&ADPull) + Ok(t, err) + expBaseRepo := models.Repo{ + Owner: "owner/project", + FullName: "owner/project/repo", + CloneURL: "https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo", + SanitizedCloneURL: "https://azuredevops-user:@dev.azure.com/owner/project/_git/repo", + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "dev.azure.com", + Type: models.AzureDevops, + }, + } + Equals(t, models.PullRequest{ + URL: ADPull.GetURL(), + Author: ADPull.CreatedBy.GetUniqueName(), + HeadBranch: path.Base(ADPull.GetSourceRefName()), + BaseBranch: path.Base(ADPull.GetTargetRefName()), + HeadCommit: ADPull.LastMergeSourceCommit.GetCommitID(), + Num: ADPull.GetPullRequestID(), + State: models.OpenPullState, + BaseRepo: expBaseRepo, + }, actPull) + Equals(t, expBaseRepo, actBaseRepo) + Equals(t, expBaseRepo, actHeadRepo) +} diff --git a/server/events/matchers/models_pullrequest.go b/server/events/matchers/models_pullrequest.go index 37e478013..dd1fb0d4e 100644 --- a/server/events/matchers/models_pullrequest.go +++ b/server/events/matchers/models_pullrequest.go @@ -3,7 +3,6 @@ package matchers import ( "reflect" - "github.com/petergtz/pegomock" models "github.com/runatlantis/atlantis/server/events/models" ) diff --git a/server/events/matchers/models_repo.go b/server/events/matchers/models_repo.go index e985fd3a9..418f13cfc 100644 --- a/server/events/matchers/models_repo.go +++ b/server/events/matchers/models_repo.go @@ -3,7 +3,6 @@ package matchers import ( "reflect" - "github.com/petergtz/pegomock" models "github.com/runatlantis/atlantis/server/events/models" ) diff --git a/server/events/matchers/ptr_to_logging_simplelogger.go b/server/events/matchers/ptr_to_logging_simplelogger.go index 095fa65a7..04c72791b 100644 --- a/server/events/matchers/ptr_to_logging_simplelogger.go +++ b/server/events/matchers/ptr_to_logging_simplelogger.go @@ -3,7 +3,6 @@ package matchers import ( "reflect" - "github.com/petergtz/pegomock" logging "github.com/runatlantis/atlantis/server/logging" ) diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index 58d91d8d8..c0b745f72 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -4,12 +4,11 @@ package events import ( - "reflect" - "time" - pegomock "github.com/petergtz/pegomock" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" + "reflect" + "time" ) type MockWorkingDir struct { diff --git a/server/events/mocks/matchers/azuredevops_event.go b/server/events/mocks/matchers/azuredevops_event.go new file mode 100644 index 000000000..e8bb43d1c --- /dev/null +++ b/server/events/mocks/matchers/azuredevops_event.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" +) + +func AnyAzuredevopsEvent() azuredevops.Event { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(azuredevops.Event))(nil)).Elem())) + var nullValue azuredevops.Event + return nullValue +} + +func EqAzuredevopsEvent(value azuredevops.Event) azuredevops.Event { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue azuredevops.Event + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_azuredevops_gitpullrequest.go b/server/events/mocks/matchers/ptr_to_azuredevops_gitpullrequest.go new file mode 100644 index 000000000..8c5088273 --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_azuredevops_gitpullrequest.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" +) + +func AnyPtrToAzuredevopsGitPullRequest() *azuredevops.GitPullRequest { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*azuredevops.GitPullRequest))(nil)).Elem())) + var nullValue *azuredevops.GitPullRequest + return nullValue +} + +func EqPtrToAzuredevopsGitPullRequest(value *azuredevops.GitPullRequest) *azuredevops.GitPullRequest { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *azuredevops.GitPullRequest + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_azuredevops_gitrepository.go b/server/events/mocks/matchers/ptr_to_azuredevops_gitrepository.go new file mode 100644 index 000000000..f9499615e --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_azuredevops_gitrepository.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" +) + +func AnyPtrToAzuredevopsGitRepository() *azuredevops.GitRepository { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*azuredevops.GitRepository))(nil)).Elem())) + var nullValue *azuredevops.GitRepository + return nullValue +} + +func EqPtrToAzuredevopsGitRepository(value *azuredevops.GitRepository) *azuredevops.GitRepository { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *azuredevops.GitRepository + return nullValue +} diff --git a/server/events/mocks/mock_azuredevops_pull_getter.go b/server/events/mocks/mock_azuredevops_pull_getter.go new file mode 100644 index 000000000..683403c7a --- /dev/null +++ b/server/events/mocks/mock_azuredevops_pull_getter.go @@ -0,0 +1,114 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: AzureDevopsPullGetter) + +package mocks + +import ( + azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockAzureDevopsPullGetter struct { + fail func(message string, callerSkip ...int) +} + +func NewMockAzureDevopsPullGetter(options ...pegomock.Option) *MockAzureDevopsPullGetter { + mock := &MockAzureDevopsPullGetter{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockAzureDevopsPullGetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockAzureDevopsPullGetter) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockAzureDevopsPullGetter) GetPullRequest(repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockAzureDevopsPullGetter().") + } + params := []pegomock.Param{repo, pullNum} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetPullRequest", params, []reflect.Type{reflect.TypeOf((**azuredevops.GitPullRequest)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 *azuredevops.GitPullRequest + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(*azuredevops.GitPullRequest) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockAzureDevopsPullGetter) VerifyWasCalledOnce() *VerifierMockAzureDevopsPullGetter { + return &VerifierMockAzureDevopsPullGetter{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockAzureDevopsPullGetter) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockAzureDevopsPullGetter { + return &VerifierMockAzureDevopsPullGetter{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockAzureDevopsPullGetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAzureDevopsPullGetter { + return &VerifierMockAzureDevopsPullGetter{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockAzureDevopsPullGetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockAzureDevopsPullGetter { + return &VerifierMockAzureDevopsPullGetter{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockAzureDevopsPullGetter struct { + mock *MockAzureDevopsPullGetter + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockAzureDevopsPullGetter) GetPullRequest(repo models.Repo, pullNum int) *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification { + params := []pegomock.Param{repo, pullNum} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullRequest", params, verifier.timeout) + return &MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification struct { + mock *MockAzureDevopsPullGetter + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification) GetCapturedArguments() (models.Repo, int) { + repo, pullNum := c.GetAllCapturedArguments() + return repo[len(repo)-1], pullNum[len(pullNum)-1] +} + +func (c *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []int) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]int, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.(int) + } + } + return +} diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index c87687997..74be6eaea 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -5,6 +5,7 @@ package mocks import ( github "github.com/google/go-github/v28/github" + azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" pegomock "github.com/petergtz/pegomock" models "github.com/runatlantis/atlantis/server/events/models" go_gitlab "github.com/xanzy/go-gitlab" @@ -374,6 +375,87 @@ func (mock *MockEventParsing) GetBitbucketServerPullEventType(eventTypeHeader st return ret0 } +func (mock *MockEventParsing) ParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) (models.PullRequest, models.Repo, models.Repo, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockEventParsing().") + } + params := []pegomock.Param{adPull} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAzureDevopsPull", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.Repo + var ret2 models.Repo + var ret3 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.Repo) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(error) + } + } + return ret0, ret1, ret2, ret3 +} + +func (mock *MockEventParsing) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockEventParsing().") + } + params := []pegomock.Param{pullEvent} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAzureDevopsPullEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.PullRequestEventType + var ret2 models.Repo + var ret3 models.Repo + var ret4 models.User + var ret5 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.PullRequestEventType) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(models.Repo) + } + if result[4] != nil { + ret4 = result[4].(models.User) + } + if result[5] != nil { + ret5 = result[5].(error) + } + } + return ret0, ret1, ret2, ret3, ret4, ret5 +} + +func (mock *MockEventParsing) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockEventParsing().") + } + params := []pegomock.Param{adRepo} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAzureDevopsRepo", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.Repo + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.Repo) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockEventParsing) VerifyWasCalledOnce() *VerifierMockEventParsing { return &VerifierMockEventParsing{ mock: mock, @@ -765,3 +847,84 @@ func (c *MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification) G } return } + +func (verifier *VerifierMockEventParsing) ParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) *MockEventParsing_ParseAzureDevopsPull_OngoingVerification { + params := []pegomock.Param{adPull} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseAzureDevopsPull", params, verifier.timeout) + return &MockEventParsing_ParseAzureDevopsPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockEventParsing_ParseAzureDevopsPull_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockEventParsing_ParseAzureDevopsPull_OngoingVerification) GetCapturedArguments() *azuredevops.GitPullRequest { + adPull := c.GetAllCapturedArguments() + return adPull[len(adPull)-1] +} + +func (c *MockEventParsing_ParseAzureDevopsPull_OngoingVerification) GetAllCapturedArguments() (_param0 []*azuredevops.GitPullRequest) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*azuredevops.GitPullRequest, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*azuredevops.GitPullRequest) + } + } + return +} + +func (verifier *VerifierMockEventParsing) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification { + params := []pegomock.Param{pullEvent} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseAzureDevopsPullEvent", params, verifier.timeout) + return &MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification) GetCapturedArguments() azuredevops.Event { + pullEvent := c.GetAllCapturedArguments() + return pullEvent[len(pullEvent)-1] +} + +func (c *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []azuredevops.Event) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]azuredevops.Event, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(azuredevops.Event) + } + } + return +} + +func (verifier *VerifierMockEventParsing) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification { + params := []pegomock.Param{adRepo} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseAzureDevopsRepo", params, verifier.timeout) + return &MockEventParsing_ParseAzureDevopsRepo_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockEventParsing_ParseAzureDevopsRepo_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification) GetCapturedArguments() *azuredevops.GitRepository { + adRepo := c.GetAllCapturedArguments() + return adRepo[len(adRepo)-1] +} + +func (c *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification) GetAllCapturedArguments() (_param0 []*azuredevops.GitRepository) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*azuredevops.GitRepository, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*azuredevops.GitRepository) + } + } + return +} diff --git a/server/events/models/models.go b/server/events/models/models.go index 36c7d938e..7194a0d6f 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -20,6 +20,7 @@ import ( "fmt" "net/url" paths "path" + "regexp" "strings" "time" @@ -33,11 +34,13 @@ import ( // Repo is a VCS repository. type Repo struct { // FullName is the owner and repo name separated - // by a "/", ex. "runatlantis/atlantis", "gitlab/subgroup/atlantis", "Bitbucket Server/atlantis". + // by a "/", ex. "runatlantis/atlantis", "gitlab/subgroup/atlantis", + // "Bitbucket Server/atlantis", "azuredevops/project/atlantis". FullName string - // Owner is just the repo owner, ex. "runatlantis" or "gitlab/subgroup". - // This may contain /'s in the case of GitLab subgroups. - // This may contain spaces in the case of Bitbucket Server. + // Owner is just the repo owner, ex. "runatlantis" or "gitlab/subgroup" + // or azuredevops/project. This may contain /'s in the case of GitLab + // subgroups or Azure Devops Team Projects. This may contain spaces in + // the case of Bitbucket Server. Owner string // Name is just the repo name, ex. "atlantis". This will never have // /'s in it. @@ -71,7 +74,20 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU return Repo{}, errors.New("cloneURL can't be empty") } - if !strings.HasSuffix(cloneURL, ".git") { + if vcsHostType == AzureDevops { + // https://docs.microsoft.com/en-us/azure/devops/organizations/settings/naming-restrictions?view=azure-devops + matched, err := regexp.MatchString("[A-z0-9][A-z0-9._]+/[A-z0-9][A-z0-9._]+/[A-z0-9][A-z0-9._]+", repoFullName) + if err != nil { + e := fmt.Sprintf("newRepo error vcsHostType = AzureDevops: %s", err) + return Repo{}, errors.New(e) + } + if !matched { + return Repo{}, errors.New("AzureDevops repoFullName must be of the format owner/project/repo") + } + } + + // Azure Devops doesn't work with .git suffix on clone URLs + if !strings.HasSuffix(cloneURL, ".git") && vcsHostType != AzureDevops { cloneURL += ".git" } @@ -84,7 +100,8 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU // We skip this check for Bitbucket Server because its format is different // and because the caller in that case actually constructs the clone url // from the repo name and so there's no point checking if they match. - if vcsHostType != BitbucketServer { + // Azure Devops also does not require .git at the end of clone urls. + if vcsHostType != BitbucketServer && vcsHostType != AzureDevops { expClonePath := fmt.Sprintf("/%s.git", repoFullName) if expClonePath != cloneURLParsed.Path { return Repo{}, fmt.Errorf("expected clone url to have path %q but had %q", expClonePath, cloneURLParsed.Path) @@ -110,9 +127,9 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU if owner == "" || repo == "" { return Repo{}, fmt.Errorf("invalid repo format %q, owner %q or repo %q was empty", repoFullName, owner, repo) } - // Only GitLab repos can have /'s in their owners. This is for GitLab - // subgroups. - if strings.Contains(owner, "/") && vcsHostType != Gitlab { + // Only GitLab repos can have /'s in their owners. + // This is for GitLab subgroups and Azure Devops Team Projects. + if strings.Contains(owner, "/") && vcsHostType != Gitlab && vcsHostType != AzureDevops { return Repo{}, fmt.Errorf("invalid repo format %q, owner %q should not contain any /'s", repoFullName, owner) } if strings.Contains(repo, "/") { @@ -273,6 +290,7 @@ const ( Gitlab BitbucketCloud BitbucketServer + AzureDevops ) func (h VCSHostType) String() string { @@ -285,6 +303,8 @@ func (h VCSHostType) String() string { return "BitbucketCloud" case BitbucketServer: return "BitbucketServer" + case AzureDevops: + return "AzureDevops" } return "" } @@ -347,16 +367,18 @@ type ProjectCommandContext struct { Workspace string } -// SplitRepoFullName splits a repo full name up into its owner and repo name -// segments. If the repoFullName is malformed, may return empty strings -// for owner or repo. +// SplitRepoFullName splits a repo full name up into its owner and repo +// name segments. If the repoFullName is malformed, may return empty +// strings for owner or repo. // Ex. runatlantis/atlantis => (runatlantis, atlantis) // gitlab/subgroup/runatlantis/atlantis => (gitlab/subgroup/runatlantis, atlantis) +// azuredevops/project/atlantis => (azuredevops, project, atlantis) func SplitRepoFullName(repoFullName string) (owner string, repo string) { lastSlashIdx := strings.LastIndex(repoFullName, "/") if lastSlashIdx == -1 || lastSlashIdx == len(repoFullName)-1 { return "", "" } + return repoFullName[:lastSlashIdx], repoFullName[lastSlashIdx+1:] } diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index b6241ece6..3d35ef9df 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" . "github.com/runatlantis/atlantis/testing" ) @@ -42,6 +43,11 @@ func TestNewRepo_CloneURLWrongRepo(t *testing.T) { ErrEquals(t, `expected clone url to have path "/owner/repo.git" but had "/notowner/repo.git"`, err) } +func TestNewRepo_EmptyAzureDevopsProject(t *testing.T) { + _, err := models.NewRepo(models.AzureDevops, "", "https://dev.azure.com/notowner/project/_git/repo", "u", "p") + ErrEquals(t, "repoFullName can't be empty", err) +} + // For bitbucket server we don't validate the clone URL because the callers // are actually constructing it. func TestNewRepo_CloneURLBitbucketServer(t *testing.T) { @@ -99,7 +105,7 @@ func TestNewRepo_FullNameWrongFormat(t *testing.T) { } } -// If the clone url doesn't end with .git it is appended +// If the clone url doesn't end with .git, and VCS is not Azure Devops, it is appended func TestNewRepo_MissingDotGit(t *testing.T) { repo, err := models.NewRepo(models.BitbucketCloud, "owner/repo", "https://bitbucket.org/owner/repo", "u", "p") Ok(t, err) @@ -196,6 +202,10 @@ func TestVCSHostType_ToString(t *testing.T) { models.BitbucketServer, "BitbucketServer", }, + { + models.AzureDevops, + "AzureDevops", + }, } for _, c := range cases { @@ -257,6 +267,82 @@ func TestSplitRepoFullName(t *testing.T) { } } +// These test cases should cover the same behavior as TestSplitRepoFullName +// and only produce different output in the AzureDevops case of +// owner/project/repo. +func TestAzureDevopsSplitRepoFullName(t *testing.T) { + cases := []struct { + input string + expOwner string + expRepo string + expProject string + }{ + { + "owner/repo", + "owner", + "repo", + "", + }, + { + "group/subgroup/owner/repo", + "group/subgroup/owner", + "repo", + "", + }, + { + "group/subgroup/owner/project/repo", + "group/subgroup/owner/project", + "repo", + "", + }, + { + "", + "", + "", + "", + }, + { + "/", + "", + "", + "", + }, + { + "owner/", + "", + "", + "", + }, + { + "/repo", + "", + "repo", + "", + }, + { + "group/subgroup/", + "", + "", + "", + }, + { + "owner/project/repo", + "owner", + "repo", + "project", + }, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + owner, project, repo := vcs.SplitAzureDevopsRepoFullName(c.input) + Equals(t, c.expOwner, owner) + Equals(t, c.expProject, project) + Equals(t, c.expRepo, repo) + }) + } +} + func TestProjectResult_IsSuccessful(t *testing.T) { cases := map[string]struct { pr models.ProjectResult diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go new file mode 100644 index 000000000..ac918c0cb --- /dev/null +++ b/server/events/vcs/azuredevops_client.go @@ -0,0 +1,287 @@ +package vcs + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + version "github.com/hashicorp/go-version" + "github.com/mcdafydd/go-azuredevops/azuredevops" + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/events/vcs/common" + + "github.com/runatlantis/atlantis/server/events/models" + + "gopkg.in/russross/blackfriday.v2" +) + +// AzureDevopsClient represents an Azure Devops VCS client +type AzureDevopsClient struct { + Client *azuredevops.Client + // Version is set to the server version. + Version *version.Version + ctx context.Context +} + +// NewAzureDevopsClient returns a valid Azure Devops client. +func NewAzureDevopsClient(hostname string, token string) (*AzureDevopsClient, error) { + tp := azuredevops.BasicAuthTransport{ + Username: "", + Password: strings.TrimSpace(token), + } + httpClient := tp.Client() + httpClient.Timeout = time.Second * 10 + var adClient, err = azuredevops.NewClient(httpClient) + if err != nil { + return nil, errors.Wrapf(err, "azuredevops.NewClient() %p", adClient) + } + + if hostname != "dev.azure.com" { + baseURL := fmt.Sprintf("https://%s/", hostname) + base, err := url.Parse(baseURL) + if err != nil { + return nil, errors.Wrapf(err, "invalid azure devops hostname trying to parse %s", baseURL) + } + adClient.BaseURL = *base + } + + client := &AzureDevopsClient{ + Client: adClient, + ctx: context.Background(), + } + + return client, nil +} + +// GetModifiedFiles returns the names of files that were modified in the merge request. +// The names include the path to the file from the repo root, ex. parent/child/file.txt. +func (g *AzureDevopsClient) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) { + var files []string + + opts := azuredevops.PullRequestGetOptions{ + IncludeWorkItemRefs: true, + } + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + commitIDResponse, _, _ := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts) + + commitID := commitIDResponse.GetLastMergeSourceCommit().GetCommitID() + + r, _, _ := g.Client.Git.GetChanges(g.ctx, owner, project, repoName, commitID) + + for _, change := range r.Changes { + item := change.GetItem() + files = append(files, item.GetPath()) + + // If the file was renamed, we'll want to run plan in the directory + // it was moved from as well. + changeType := azuredevops.Rename.String() + if change.ChangeType == &changeType { + files = append(files, change.GetSourceServerItem()) + } + } + + return files, nil +} + +// CreateComment creates a comment on a pull request. +// +// If comment length is greater than the max comment length we split into +// multiple comments. +// Azure Devops doesn't support markdown in Work Item comments, but it will +// convert text to HTML. We use the blackfriday library to convert Atlantis +// comment markdown before submission. +func (g *AzureDevopsClient) CreateComment(repo models.Repo, pullNum int, comment string) (err error) { + sepEnd := "\n```\n
    " + + "\n
    \n\n**Warning**: Output length greater than max comment size. Continued in next comment." + sepStart := "Continued from previous comment.\n
    Show Output\n\n" + + "```diff\n" + + // maxCommentLength is the maximum number of chars allowed in a single comment + // This length was copied from the Github client - haven't found documentation + // or tested limit in Azure Devops. + const maxCommentLength = 65536 + + comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart) + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + + for _, c := range comments { + input := blackfriday.Run([]byte(c)) + s := string(input) + commentType := "text" + parentCommentID := 0 + + prComment := azuredevops.Comment{ + CommentType: &commentType, + Content: &s, + ParentCommentID: &parentCommentID, + } + prComments := []*azuredevops.Comment{&prComment} + body := azuredevops.GitPullRequestCommentThread{ + Comments: prComments, + } + _, _, err := g.Client.PullRequests.CreateComments(g.ctx, owner, project, repoName, pullNum, &body) + if err != nil { + return err + } + } + return +} + +// PullIsApproved returns true if the merge request was approved. +// https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops#require-a-minimum-number-of-reviewers +func (g *AzureDevopsClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) { + opts := azuredevops.PullRequestGetOptions{ + IncludeWorkItemRefs: true, + } + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + adPull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts) + if err != nil { + return false, errors.Wrap(err, "getting pull request") + } + for _, review := range adPull.Reviewers { + if review == nil { + continue + } + if review.GetVote() == azuredevops.VoteApproved { + return true, nil + } + } + return false, nil +} + +// PullIsMergeable returns true if the merge request can be merged. +func (g *AzureDevopsClient) PullIsMergeable(repo models.Repo, pull models.PullRequest) (bool, error) { + + opts := azuredevops.PullRequestGetOptions{ + IncludeWorkItemRefs: true, + } + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + adPull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts) + if err != nil { + return false, errors.Wrap(err, "getting pull request") + } + if *adPull.MergeStatus != azuredevops.MergeConflicts.String() && + *adPull.MergeStatus != azuredevops.MergeRejectedByPolicy.String() { + return true, nil + } + return false, nil +} + +// GetPullRequest returns the pull request. +func (g *AzureDevopsClient) GetPullRequest(repo models.Repo, num int) (*azuredevops.GitPullRequest, error) { + opts := azuredevops.PullRequestGetOptions{ + IncludeWorkItemRefs: true, + } + + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + pull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, num, &opts) + return pull, err +} + +// UpdateStatus updates the build status of a commit. +func (g *AzureDevopsClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error { + adState := azuredevops.GitError.String() + switch state { + case models.PendingCommitStatus: + adState = azuredevops.GitPending.String() + case models.SuccessCommitStatus: + adState = azuredevops.GitSucceeded.String() + case models.FailedCommitStatus: + adState = azuredevops.GitFailed.String() + } + + genreStr := "Atlantis Bot" + status := azuredevops.GitStatus{ + State: &adState, + Description: &description, + Context: &azuredevops.GitStatusContext{ + Name: &src, + Genre: &genreStr, + }, + } + if url != "" { + status.TargetURL = &url + } + owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName) + _, _, err := g.Client.Git.CreateStatus(g.ctx, owner, project, repoName, pull.HeadCommit, status) + return err +} + +// MergePull merges the merge request using the default no fast-forward strategy +// If the user has set a branch policy that disallows no fast-forward, the merge will fail +// until we handle branch policies +// https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops +func (g *AzureDevopsClient) MergePull(pull models.PullRequest) error { + + descriptor := "Atlantis Terraform Pull Request Automation" + i := "atlantis" + imageURL := "https://github.com/runatlantis/atlantis/raw/master/runatlantis.io/.vuepress/public/hero.png" + id := azuredevops.IdentityRef{ + Descriptor: &descriptor, + ID: &i, + ImageURL: &imageURL, + } + // Set default pull request completion options + mcm := azuredevops.NoFastForward.String() + twi := new(bool) + *twi = true + completionOpts := azuredevops.GitPullRequestCompletionOptions{ + BypassPolicy: new(bool), + BypassReason: azuredevops.String(""), + DeleteSourceBranch: new(bool), + MergeCommitMessage: azuredevops.String(common.AutomergeCommitMsg), + MergeStrategy: &mcm, + SquashMerge: new(bool), + TransitionWorkItems: twi, + TriggeredByAutoComplete: new(bool), + } + + // Construct request body from supplied parameters + mergePull := new(azuredevops.GitPullRequest) + mergePull.AutoCompleteSetBy = &id + mergePull.CompletionOptions = &completionOpts + + owner, project, repoName := SplitAzureDevopsRepoFullName(pull.BaseRepo.FullName) + mergeResult, _, err := g.Client.PullRequests.Merge( + g.ctx, + owner, + project, + repoName, + pull.Num, + mergePull, + completionOpts, + id, + ) + if err != nil { + return errors.Wrap(err, "merging pull request") + } + if *mergeResult.MergeStatus != azuredevops.MergeSucceeded.String() { + return fmt.Errorf("could not merge pull request: %s", mergeResult.GetMergeFailureMessage()) + } + return nil +} + +// SplitAzureDevopsRepoFullName splits a repo full name up into its owner, +// repo and project name segments. If the repoFullName is malformed, may +// return empty strings for owner, repo, or project. Azure Devops uses +// repoFullName format owner/project/repo. +// +// Ex. runatlantis/atlantis => (runatlantis, atlantis) +// gitlab/subgroup/runatlantis/atlantis => (gitlab/subgroup/runatlantis, atlantis) +// azuredevops/project/atlantis => (azuredevops, project, atlantis) +func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project string, repo string) { + firstSlashIdx := strings.Index(repoFullName, "/") + lastSlashIdx := strings.LastIndex(repoFullName, "/") + slashCount := strings.Count(repoFullName, "/") + if lastSlashIdx == -1 || lastSlashIdx == len(repoFullName)-1 { + return "", "", "" + } + if firstSlashIdx != lastSlashIdx && slashCount == 2 { + return repoFullName[:firstSlashIdx], + repoFullName[firstSlashIdx+1 : lastSlashIdx], + repoFullName[lastSlashIdx+1:] + } + return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:] +} diff --git a/server/events/vcs/azuredevops_client_test.go b/server/events/vcs/azuredevops_client_test.go new file mode 100644 index 000000000..1af352f41 --- /dev/null +++ b/server/events/vcs/azuredevops_client_test.go @@ -0,0 +1,473 @@ +package vcs_test + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/mcdafydd/go-azuredevops/azuredevops" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/events/vcs/fixtures" + . "github.com/runatlantis/atlantis/testing" +) + +func TestAzureDevopsClient_MergePull(t *testing.T) { + cases := []struct { + description string + response string + code int + expErr string + }{ + { + "success", + adMergeSuccess, + 200, + "", + }, + { + "405", + `{"message":"405 Method Not Allowed"}`, + 405, + "405 {message: 405 Method Not Allowed}", + }, + { + "406", + `{"message":"406 Branch cannot be merged"}`, + 406, + "406 {message: 406 Branch cannot be merged}", + }, + } + + // Set default pull request completion options + mcm := azuredevops.NoFastForward.String() + twi := new(bool) + *twi = true + completionOptions := azuredevops.GitPullRequestCompletionOptions{ + BypassPolicy: new(bool), + BypassReason: azuredevops.String(""), + DeleteSourceBranch: new(bool), + MergeCommitMessage: azuredevops.String("commit message"), + MergeStrategy: &mcm, + SquashMerge: new(bool), + TransitionWorkItems: twi, + TriggeredByAutoComplete: new(bool), + } + + id := azuredevops.IdentityRef{} + pull := azuredevops.GitPullRequest{ + PullRequestID: azuredevops.Int(22), + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + // The first request should hit this URL. + case "/owner/project/_apis/git/repositories/repo/pullrequests/22?api-version=5.1-preview.1": + w.WriteHeader(c.code) + w.Write([]byte(c.response)) // nolint: errcheck + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + } + })) + + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + merge, _, err := client.Client.PullRequests.Merge(context.Background(), + "owner", + "project", + "repo", + pull.GetPullRequestID(), + &pull, + completionOptions, + id, + ) + + if err != nil { + fmt.Printf("Merge failed: %+v\n", err) + return + } + fmt.Printf("Successfully merged pull request: %+v\n", merge) + + err = client.MergePull(models.PullRequest{ + Num: 22, + BaseRepo: models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + }, + }) + if c.expErr == "" { + Ok(t, err) + } else { + ErrContains(t, c.expErr, err) + ErrContains(t, "unable to merge merge request, it may not be in a mergeable state", err) + } + }) + } +} + +func TestAzureDevopsClient_UpdateStatus(t *testing.T) { + cases := []struct { + status models.CommitStatus + expState string + }{ + { + models.PendingCommitStatus, + "pending", + }, + { + models.SuccessCommitStatus, + "succeeded", + }, + { + models.FailedCommitStatus, + "failed", + }, + } + response := `{"context":{"genre":"Atlantis Bot","name":"src"},"description":"description","state":"%s","targetUrl":"https://google.com"} +` + for _, c := range cases { + t.Run(c.expState, func(t *testing.T) { + gotRequest := false + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/owner/project/_apis/git/repositories/repo/commits/sha/statuses?api-version=5.1-preview.1": + gotRequest = true + body, err := ioutil.ReadAll(r.Body) + Ok(t, err) + exp := fmt.Sprintf(response, c.expState) + Equals(t, exp, string(body)) + defer r.Body.Close() // nolint: errcheck + w.Write([]byte(response)) // nolint: errcheck + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + } + })) + + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + repo := models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + } + err = client.UpdateStatus(repo, models.PullRequest{ + Num: 22, + BaseRepo: repo, + HeadCommit: "sha", + }, c.status, "src", "description", "https://google.com") + Ok(t, err) + Assert(t, gotRequest, "expected to get the request") + }) + } +} + +// GetModifiedFiles should make multiple requests if more than one page +// and concat results. +func TestAzureDevopsClient_GetModifiedFiles(t *testing.T) { + itemRespTemplate := `{ + "changes": [ + { + "item": { + "gitObjectType": "blob", + "path": "%s", + "url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit" + }, + "changeType": "add" + }, + { + "item": { + "gitObjectType": "blob", + "path": "%s", + "url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit" + }, + "changeType": "add" + } +]}` + resp := fmt.Sprintf(itemRespTemplate, "file1.txt", "file1.txt", "file2.txt", "file2.txt") + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + // The first request should hit this URL. + case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true": + w.Write([]byte(fixtures.ADPullJSON)) // nolint: errcheck + // The second should hit this URL. + case "/owner/project/_apis/git/repositories/repo/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3/changes?api-version=5.1-preview.1": + // We write a header that means there's an additional page. + w.Write([]byte(resp)) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + files, err := client.GetModifiedFiles(models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.AzureDevops, + Hostname: "dev.azure.com", + }, + }, models.PullRequest{ + Num: 1, + }) + Ok(t, err) + Equals(t, []string{"file1.txt", "file2.txt"}, files) +} + +func TestAzureDevopsClient_PullIsMergeable(t *testing.T) { + cases := []struct { + state string + expMergeable bool + }{ + { + azuredevops.MergeConflicts.String(), + false, + }, + { + azuredevops.MergeRejectedByPolicy.String(), + false, + }, + { + azuredevops.MergeFailure.String(), + true, + }, + { + azuredevops.MergeNotSet.String(), + true, + }, + { + azuredevops.MergeQueued.String(), + true, + }, + { + azuredevops.MergeSucceeded.String(), + true, + }, + } + + // Use a real Azure Devops json response and edit the mergeable_state field. + jsBytes, err := ioutil.ReadFile("fixtures/azuredevops-pr.json") + Ok(t, err) + json := string(jsBytes) + + for _, c := range cases { + t.Run(c.state, func(t *testing.T) { + response := strings.Replace(json, + `"mergeStatus": "NotSet"`, + fmt.Sprintf(`"mergeStatus": "%s"`, c.state), + 1, + ) + + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true": + w.Write([]byte(response)) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + actMergeable, err := client.PullIsMergeable(models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.AzureDevops, + Hostname: "dev.azure.com", + }, + }, models.PullRequest{ + Num: 1, + }) + Ok(t, err) + Equals(t, c.expMergeable, actMergeable) + }) + } +} + +func TestAzureDevopsClient_PullIsApproved(t *testing.T) { + cases := []struct { + testName string + vote int + expApproved bool + }{ + { + "approved", + azuredevops.VoteApproved, + true, + }, + { + "approved with suggestions", + azuredevops.VoteApprovedWithSuggestions, + false, + }, + { + "no vote", + azuredevops.VoteNone, + false, + }, + { + "vote waiting for author", + azuredevops.VoteWaitingForAuthor, + false, + }, + { + "vote rejected", + azuredevops.VoteRejected, + false, + }, + } + + // Use a real Azure Devops json response and edit the mergeable_state field. + jsBytes, err := ioutil.ReadFile("fixtures/azuredevops-pr.json") + Ok(t, err) + json := string(jsBytes) + + for _, c := range cases { + t.Run(c.testName, func(t *testing.T) { + response := strings.Replace(json, + `"vote": 0,`, + fmt.Sprintf(`"vote": %d,`, c.vote), + 1, + ) + + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true": + w.Write([]byte(response)) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + actApproved, err := client.PullIsApproved(models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.AzureDevops, + Hostname: "dev.azure.com", + }, + }, models.PullRequest{ + Num: 1, + }) + Ok(t, err) + Equals(t, c.expApproved, actApproved) + }) + } +} + +func TestAzureDevopsClient_GetPullRequest(t *testing.T) { + // Use a real Azure Devops json response and edit the mergeable_state field. + jsBytes, err := ioutil.ReadFile("fixtures/azuredevops-pr.json") + Ok(t, err) + response := string(jsBytes) + + t.Run("get pull request", func(t *testing.T) { + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true": + w.Write([]byte(response)) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "token") + Ok(t, err) + defer disableSSLVerification()() + + _, err = client.GetPullRequest(models.Repo{ + FullName: "owner/project/repo", + Owner: "owner", + Name: "repo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.AzureDevops, + Hostname: "dev.azure.com", + }, + }, 1) + Ok(t, err) + }) +} + +var adMergeSuccess = `{ + "status": "completed", + "mergeStatus": "succeeded", + "autoCompleteSetBy": { + "id": "54d125f7-69f7-4191-904f-c5b96b6261c8", + "displayName": "Jamal Hartnett", + "uniqueName": "fabrikamfiber4@hotmail.com", + "url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/54d125f7-69f7-4191-904f-c5b96b6261c8", + "imageUrl": "https://dev.azure.com/fabrikam/DefaultCollection/_api/_common/identityImage?id=54d125f7-69f7-4191-904f-c5b96b6261c8" + }, + "pullRequestId": 22, + "completionOptions": { + "bypassPolicy":false, + "bypassReason":"", + "deleteSourceBranch":false, + "mergeCommitMessage":"TEST MERGE COMMIT MESSAGE", + "mergeStrategy":"noFastForward", + "squashMerge":false, + "transitionWorkItems":true, + "triggeredByAutoComplete":false + } +}` diff --git a/server/events/vcs/fixtures/azuredevops-pr.json b/server/events/vcs/fixtures/azuredevops-pr.json new file mode 100644 index 000000000..8e5cc5c9c --- /dev/null +++ b/server/events/vcs/fixtures/azuredevops-pr.json @@ -0,0 +1,33 @@ +{ + "status": "completed", + "mergeStatus": "NotSet", + "autoCompleteSetBy": { + "id": "02228777-0784-420f-bdaa-8bc4830a14a6", + "displayName": "Atlantis User", + "uniqueName": "atlantis.user@example.com", + "url": "https://vssps.dev.azure.com/owner/_apis/Identities/02228777-0784-420f-bdaa-8bc4830a14a6", + "imageUrl": "https://dev.azure.com/owner/DefaultCollection/_api/_common/identityImage?id=02228777-0784-420f-bdaa-8bc4830a14a6" + }, + "pullRequestId": 22, + "completionOptions": { + "bypassPolicy":false, + "bypassReason":"", + "deleteSourceBranch":false, + "mergeCommitMessage":"TEST MERGE COMMIT MESSAGE", + "mergeStrategy":"noFastForward", + "squashMerge":false, + "transitionWorkItems":true, + "triggeredByAutoComplete":false + }, + "reviewers": [ + { + "reviewerUrl": "https://example:8080/tfs/_apis/git/repositories/8010495e-1002-438d-acbf-aaf245dac7c2/pullRequests/22/reviewers/8010495e-1002-438d-acbf-aaf245dac7c2", + "vote": 0, + "id": "8010495e-1002-438d-acbf-aaf245dac7c2", + "displayName": "Atlantis User", + "uniqueName": "atlantis.user@example.com", + "url": "https://owner:8080/tfs/_apis/Identities/8010495e-1002-438d-acbf-aaf245dac7c2", + "imageUrl": "https://owner:8080/tfs/_api/_common/identityImage?id=8010495e-1002-438d-acbf-aaf245dac7c2" + } + ] +} diff --git a/server/events/vcs/fixtures/fixtures.go b/server/events/vcs/fixtures/fixtures.go index 7effbab6b..6f3290fb7 100644 --- a/server/events/vcs/fixtures/fixtures.go +++ b/server/events/vcs/fixtures/fixtures.go @@ -13,7 +13,10 @@ package fixtures -import "github.com/google/go-github/v28/github" +import ( + "github.com/google/go-github/v28/github" + "github.com/mcdafydd/go-azuredevops/azuredevops" +) var PullEvent = github.PullRequestEvent{ Sender: &github.User{ @@ -49,3 +52,169 @@ var Repo = github.Repository{ Name: github.String("repo"), CloneURL: github.String("https://github.com/owner/repo.git"), } + +var ADPullEvent = azuredevops.Event{ + EventType: "git.pullrequest.created", + Resource: &ADPull, +} + +var ADPullUpdatedEvent = azuredevops.Event{ + EventType: "git.pullrequest.updated", + Resource: &ADPull, +} + +var ADPullClosedEvent = azuredevops.Event{ + EventType: "git.pullrequest.merged", + Resource: &ADPullCompleted, +} + +var ADPull = azuredevops.GitPullRequest{ + CreatedBy: &azuredevops.IdentityRef{ + ID: azuredevops.String("d6245f20-2af8-44f4-9451-8107cb2767db"), + DisplayName: azuredevops.String("User"), + UniqueName: azuredevops.String("user@example.com"), + }, + LastMergeSourceCommit: &azuredevops.GitCommitRef{ + CommitID: azuredevops.String("b60280bc6e62e2f880f1b63c1e24987664d3bda3"), + URL: azuredevops.String("https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3"), + }, + PullRequestID: azuredevops.Int(1), + Repository: &ADRepo, + SourceRefName: azuredevops.String("refs/heads/owner/sourceBranch"), + Status: azuredevops.String("active"), + TargetRefName: azuredevops.String("refs/heads/targetBranch"), + URL: azuredevops.String("https://dev.azure.com/fabrikam/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21"), +} + +var ADPullCompleted = azuredevops.GitPullRequest{ + CreatedBy: &azuredevops.IdentityRef{ + ID: azuredevops.String("d6245f20-2af8-44f4-9451-8107cb2767db"), + DisplayName: azuredevops.String("User"), + UniqueName: azuredevops.String("user@example.com"), + }, + LastMergeSourceCommit: &azuredevops.GitCommitRef{ + CommitID: azuredevops.String("b60280bc6e62e2f880f1b63c1e24987664d3bda3"), + URL: azuredevops.String("https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3"), + }, + PullRequestID: azuredevops.Int(1), + Repository: &ADRepo, + SourceRefName: azuredevops.String("refs/heads/owner/sourceBranch"), + Status: azuredevops.String("completed"), + TargetRefName: azuredevops.String("refs/heads/targetBranch"), + URL: azuredevops.String("https://dev.azure.com/fabrikam/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21"), +} + +var ADRepo = azuredevops.GitRepository{ + DefaultBranch: azuredevops.String("refs/heads/master"), + Name: azuredevops.String("repo"), + ParentRepository: &azuredevops.GitRepositoryRef{ + Name: azuredevops.String("owner"), + }, + Project: &azuredevops.TeamProjectReference{ + ID: azuredevops.String("a21f5f20-4a12-aaf4-ab12-9a0927cbbb90"), + Name: azuredevops.String("project"), + State: azuredevops.String("unchanged"), + }, + WebURL: azuredevops.String("https://dev.azure.com/owner/project/_git/repo"), +} + +var ADPullJSON = `{ + "repository": { + "id": "3411ebc1-d5aa-464f-9615-0b527bc66719", + "name": "repo", + "url": "https://dev.azure.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719", + "webUrl": "https://dev.azure.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719", + "project": { + "id": "a7573007-bbb3-4341-b726-0c4148a07853", + "name": "project", + "description": "test project created on Halloween 2016", + "url": "https://dev.azure.com/owner/_apis/projects/a7573007-bbb3-4341-b726-0c4148a07853", + "state": "wellFormed", + "revision": 7 + }, + "remoteUrl": "https://dev.azure.com/owner/project/_git/repo" + }, + "pullRequestId": 22, + "codeReviewId": 22, + "status": "active", + "createdBy": { + "id": "d6245f20-2af8-44f4-9451-8107cb2767db", + "displayName": "Normal Paulk", + "uniqueName": "fabrikamfiber16@hotmail.com", + "url": "https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db", + "imageUrl": "https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db" + }, + "creationDate": "2016-11-01T16:30:31.6655471Z", + "title": "A new feature", + "description": "Adding a new feature", + "sourceRefName": "refs/heads/npaulk/my_work", + "targetRefName": "refs/heads/new_feature", + "mergeStatus": "succeeded", + "mergeId": "f5fc8381-3fb2-49fe-8a0d-27dcc2d6ef82", + "lastMergeSourceCommit": { + "commitId": "b60280bc6e62e2f880f1b63c1e24987664d3bda3", + "url": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3" + }, + "lastMergeTargetCommit": { + "commitId": "f47bbc106853afe3c1b07a81754bce5f4b8dbf62", + "url": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62" + }, + "lastMergeCommit": { + "commitId": "39f52d24533cc712fc845ed9fd1b6c06b3942588", + "author": { + "name": "Normal Paulk", + "email": "fabrikamfiber16@hotmail.com", + "date": "2016-11-01T16:30:32Z" + }, + "committer": { + "name": "Normal Paulk", + "email": "fabrikamfiber16@hotmail.com", + "date": "2016-11-01T16:30:32Z" + }, + "comment": "Merge pull request 22 from npaulk/my_work into new_feature", + "url": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/39f52d24533cc712fc845ed9fd1b6c06b3942588" + }, + "reviewers": [ + { + "reviewerUrl": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/reviewers/d6245f20-2af8-44f4-9451-8107cb2767db", + "vote": 0, + "id": "d6245f20-2af8-44f4-9451-8107cb2767db", + "displayName": "Normal Paulk", + "uniqueName": "fabrikamfiber16@hotmail.com", + "url": "https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db", + "imageUrl": "https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db" + } + ], + "url": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22", + "_links": { + "self": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22" + }, + "repository": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719" + }, + "workItems": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/workitems" + }, + "sourceBranch": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs" + }, + "targetBranch": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs" + }, + "sourceCommit": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3" + }, + "targetCommit": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62" + }, + "createdBy": { + "href": "https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db" + }, + "iterations": { + "href": "https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/iterations" + } + }, + "supportsIterations": true, + "artifactId": "vstfs:///Git/PullRequestId/a7573007-bbb3-4341-b726-0c4148a07853%2f3411ebc1-d5aa-464f-9615-0b527bc66719%2f22" +}` diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 2db123b8c..976eb3e85 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -25,7 +25,7 @@ type ClientProxy struct { clients map[models.VCSHostType]Client } -func NewClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClient Client, bitbucketServerClient Client) *ClientProxy { +func NewClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClient Client, bitbucketServerClient Client, azuredevopsClient Client) *ClientProxy { if githubClient == nil { githubClient = &NotConfiguredVCSClient{} } @@ -38,12 +38,16 @@ func NewClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClie if bitbucketServerClient == nil { bitbucketServerClient = &NotConfiguredVCSClient{} } + if azuredevopsClient == nil { + azuredevopsClient = &NotConfiguredVCSClient{} + } return &ClientProxy{ clients: map[models.VCSHostType]Client{ models.Github: githubClient, models.Gitlab: gitlabClient, models.BitbucketCloud: bitbucketCloudClient, models.BitbucketServer: bitbucketServerClient, + models.AzureDevops: azuredevopsClient, }, } } diff --git a/server/events_controller.go b/server/events_controller.go index db521fc59..a47c0a66c 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -17,8 +17,11 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/google/go-github/v28/github" + "github.com/mcdafydd/go-azuredevops/azuredevops" + "github.com/microcosm-cc/bluemonday" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/models" @@ -31,6 +34,7 @@ import ( const githubHeader = "X-Github-Event" const gitlabHeader = "X-Gitlab-Event" +const azuredevopsHeader = "Request-Id" // bitbucketEventTypeHeader is the same in both cloud and server. const bitbucketEventTypeHeader = "X-Event-Key" @@ -69,6 +73,16 @@ type EventsController struct { // UI that identifies this call as coming from Bitbucket. If empty, no // request validation is done. BitbucketWebhookSecret []byte + // AzureDevopsWebhookBasicUser is the Basic authentication username added to this + // webhook via the Azure Devops UI that identifies this call as coming from your + // Azure Devops Team Project. If empty, no request validation is done. + // For more information, see https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops + AzureDevopsWebhookBasicUser []byte + // AzureDevopsWebhookBasicPassword is the Basic authentication password added to this + // webhook via the Azure Devops UI that identifies this call as coming from your + // Azure Devops Team Project. If empty, no request validation is done. + AzureDevopsWebhookBasicPassword []byte + AzureDevopsRequestValidator AzureDevopsRequestValidator } // Post handles POST webhook requests. @@ -109,6 +123,14 @@ func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) { e.handleBitbucketServerPost(w, r) return } + } else if r.Header.Get(azuredevopsHeader) != "" { + if !e.supportsHost(models.AzureDevops) { + e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support AzureDevops") + return + } + e.Logger.Debug("handling AzureDevops post") + e.handleAzureDevopsPost(w, r) + return } e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request") } @@ -195,6 +217,33 @@ func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *h } } +func (e *EventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.Request) { + // Validate the request against the optional basic auth username and password. + payload, err := e.AzureDevopsRequestValidator.Validate(r, e.AzureDevopsWebhookBasicUser, e.AzureDevopsWebhookBasicPassword) + if err != nil { + e.respond(w, logging.Warn, http.StatusUnauthorized, err.Error()) + return + } + e.Logger.Debug("request valid") + + azuredevopsReqID := "Request-Id=" + r.Header.Get("Request-Id") + event, err := azuredevops.ParseWebHook(payload) + if err != nil { + e.respond(w, logging.Error, http.StatusBadRequest, "Failed parsing webhook: %v %s", err, azuredevopsReqID) + return + } + switch event.PayloadType { + case azuredevops.PullRequestCommentedEvent: + e.Logger.Debug("handling as pull request commented event") + e.HandleAzureDevopsPullRequestCommentedEvent(w, event, azuredevopsReqID) + case azuredevops.PullRequestEvent: + e.Logger.Debug("handling as pull request event") + e.HandleAzureDevopsPullRequestEvent(w, event, azuredevopsReqID) + default: + e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event: %v %s", event.PayloadType, azuredevopsReqID) + } +} + // HandleGithubCommentEvent handles comment events from GitHub where Atlantis // commands can come from. It's exported to make testing easier. func (e *EventsController) HandleGithubCommentEvent(w http.ResponseWriter, event *github.IssueCommentEvent, githubReqID string) { @@ -412,6 +461,57 @@ func (e *EventsController) HandleGitlabMergeRequestEvent(w http.ResponseWriter, e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType) } +// HandleAzureDevopsPullRequestCommentedEvent handles comment events from Azure Devops where Atlantis +// commands can come from. It's exported to make testing easier. +// Sometimes we may want data from the parent azuredevops.Event struct, so we handle type checking here. +// Requires Resource Version 2.0 of the Pull Request Commented On webhook payload. +func (e *EventsController) HandleAzureDevopsPullRequestCommentedEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) { + resource, ok := event.Resource.(*azuredevops.GitPullRequestWithComment) + if !ok || event.PayloadType != azuredevops.PullRequestCommentedEvent { + e.respond(w, logging.Debug, http.StatusBadRequest, "Event.Resource is nil or received bad event type %v; %s", event.Resource, azuredevopsReqID) + return + } + + if resource.Comment == nil { + e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since no comment is linked to payload; %s", azuredevopsReqID) + return + } + strippedComment := bluemonday.StrictPolicy().SanitizeBytes([]byte(*resource.Comment.Content)) + + if resource.PullRequest == nil { + e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since no pull request is linked to payload; %s", azuredevopsReqID) + return + } + + createdBy := resource.PullRequest.GetCreatedBy() + user := models.User{Username: createdBy.GetUniqueName()} + baseRepo, err := e.Parser.ParseAzureDevopsRepo(resource.PullRequest.GetRepository()) + if err != nil { + e.respond(w, logging.Debug, http.StatusOK, "Error parsing pull request repository field; %s", azuredevopsReqID) + return + } + e.handleCommentEvent(w, baseRepo, nil, nil, user, resource.PullRequest.GetPullRequestID(), string(strippedComment), models.AzureDevops) +} + +// HandleAzureDevopsPullRequestEvent will delete any locks associated with the pull +// request if the event is a pull request closed event. It's exported to make +// testing easier. +func (e *EventsController) HandleAzureDevopsPullRequestEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) { + if strings.Contains(event.Message.GetText(), "changed the reviewer list") { + msg := "pull request updated event is not a supported type [changed the reviewer list]" + e.respond(w, logging.Debug, http.StatusOK, "%s: %s", msg, azuredevopsReqID) + return + } + + pull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseAzureDevopsPullEvent(*event) + if err != nil { + e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s", err, azuredevopsReqID) + return + } + e.Logger.Info("identified event as type %q", pullEventType.String()) + e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType) +} + // supportsHost returns true if h is in e.SupportedVCSHosts and false otherwise. func (e *EventsController) supportsHost(h models.VCSHostType) bool { for _, supported := range e.SupportedVCSHosts { diff --git a/server/locks_controller.go b/server/locks_controller.go index 4f3f08444..a586c564c 100644 --- a/server/locks_controller.go +++ b/server/locks_controller.go @@ -51,18 +51,26 @@ func (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) { return } - owner, repo := models.SplitRepoFullName(lock.Project.RepoFullName) viewData := LockDetailData{ LockKeyEncoded: id, LockKey: idUnencoded, - RepoOwner: owner, - RepoName: repo, PullRequestLink: lock.Pull.URL, LockedBy: lock.Pull.Author, Workspace: lock.Workspace, AtlantisVersion: l.AtlantisVersion, CleanedBasePath: l.AtlantisURL.Path, } + var owner, project, repo string + if lock.Pull.BaseRepo.VCSHost.Type == models.AzureDevops { + owner, project, repo = vcs.SplitAzureDevopsRepoFullName(lock.Project.RepoFullName) + } else { + owner, repo = models.SplitRepoFullName(lock.Project.RepoFullName) + project = "" + } + viewData.RepoOwner = owner + viewData.RepoProject = project + viewData.RepoName = repo + err = l.LockDetailTemplate.Execute(w, viewData) if err != nil { l.Logger.Err(err.Error()) diff --git a/server/mocks/mock_azuredevops_request_validator.go b/server/mocks/mock_azuredevops_request_validator.go new file mode 100644 index 000000000..75e8db670 --- /dev/null +++ b/server/mocks/mock_azuredevops_request_validator.go @@ -0,0 +1,117 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server (interfaces: AzureDevopsRequestValidator) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + http "net/http" + "reflect" + "time" +) + +type MockAzureDevopsRequestValidator struct { + fail func(message string, callerSkip ...int) +} + +func NewMockAzureDevopsRequestValidator(options ...pegomock.Option) *MockAzureDevopsRequestValidator { + mock := &MockAzureDevopsRequestValidator{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockAzureDevopsRequestValidator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockAzureDevopsRequestValidator) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) ([]byte, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockAzureDevopsRequestValidator().") + } + params := []pegomock.Param{r, user, pass} + result := pegomock.GetGenericMockFrom(mock).Invoke("Validate", params, []reflect.Type{reflect.TypeOf((*[]byte)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 []byte + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].([]byte) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockAzureDevopsRequestValidator) VerifyWasCalledOnce() *VerifierMockAzureDevopsRequestValidator { + return &VerifierMockAzureDevopsRequestValidator{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockAzureDevopsRequestValidator) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockAzureDevopsRequestValidator { + return &VerifierMockAzureDevopsRequestValidator{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockAzureDevopsRequestValidator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAzureDevopsRequestValidator { + return &VerifierMockAzureDevopsRequestValidator{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockAzureDevopsRequestValidator) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockAzureDevopsRequestValidator { + return &VerifierMockAzureDevopsRequestValidator{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockAzureDevopsRequestValidator struct { + mock *MockAzureDevopsRequestValidator + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) *MockAzureDevopsRequestValidator_Validate_OngoingVerification { + params := []pegomock.Param{r, user, pass} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Validate", params, verifier.timeout) + return &MockAzureDevopsRequestValidator_Validate_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockAzureDevopsRequestValidator_Validate_OngoingVerification struct { + mock *MockAzureDevopsRequestValidator + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockAzureDevopsRequestValidator_Validate_OngoingVerification) GetCapturedArguments() (*http.Request, []byte, []byte) { + r, user, pass := c.GetAllCapturedArguments() + return r[len(r)-1], user[len(user)-1], pass[len(pass)-1] +} + +func (c *MockAzureDevopsRequestValidator_Validate_OngoingVerification) GetAllCapturedArguments() (_param0 []*http.Request, _param1 [][]byte, _param2 [][]byte) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*http.Request, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*http.Request) + } + _param1 = make([][]byte, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.([]byte) + } + _param2 = make([][]byte, len(params[2])) + for u, param := range params[2] { + _param2[u] = param.([]byte) + } + } + return +} diff --git a/server/server.go b/server/server.go index e17eed2f5..cdeeed5ee 100644 --- a/server/server.go +++ b/server/server.go @@ -115,6 +115,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { var gitlabClient *vcs.GitlabClient var bitbucketCloudClient *bitbucketcloud.Client var bitbucketServerClient *bitbucketserver.Client + var azuredevopsClient *vcs.AzureDevopsClient if userConfig.GithubUser != "" { supportedVCSHosts = append(supportedVCSHosts, models.Github) var err error @@ -153,6 +154,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } } + if userConfig.AzureDevopsUser != "" { + supportedVCSHosts = append(supportedVCSHosts, models.AzureDevops) + var err error + azuredevopsClient, err = vcs.NewAzureDevopsClient("dev.azure.com", userConfig.AzureDevopsToken) + if err != nil { + return nil, err + } + } if userConfig.WriteGitCreds { home, err := homedir.Dir() @@ -174,6 +183,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { return nil, err } } + if userConfig.AzureDevopsUser != "" { + if err := events.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, "https://dev.azure.com/", home, logger); err != nil { + return nil, err + } + } } var webhooksConfig []webhooks.Config @@ -190,7 +204,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, errors.Wrap(err, "initializing webhooks") } - vcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient) + vcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient, azuredevopsClient) commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient} terraformClient, err := terraform.NewClient( logger, @@ -265,11 +279,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { BitbucketUser: userConfig.BitbucketUser, BitbucketToken: userConfig.BitbucketToken, BitbucketServerURL: userConfig.BitbucketBaseURL, + AzureDevopsUser: userConfig.AzureDevopsUser, + AzureDevopsToken: userConfig.AzureDevopsToken, } commentParser := &events.CommentParser{ - GithubUser: userConfig.GithubUser, - GitlabUser: userConfig.GitlabUser, - BitbucketUser: userConfig.BitbucketUser, + GithubUser: userConfig.GithubUser, + GitlabUser: userConfig.GitlabUser, + BitbucketUser: userConfig.BitbucketUser, + AzureDevopsUser: userConfig.AzureDevopsUser, } defaultTfVersion := terraformClient.DefaultVersion() pendingPlanFinder := &events.DefaultPendingPlanFinder{} @@ -282,6 +299,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { VCSClient: vcsClient, GithubPullGetter: githubClient, GitlabMergeRequestGetter: gitlabClient, + AzureDevopsPullGetter: azuredevopsClient, CommitStatusUpdater: commitStatusUpdater, EventParser: eventParser, MarkdownRenderer: markdownRenderer, @@ -347,20 +365,23 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { DB: boltdb, } eventsController := &EventsController{ - CommandRunner: commandRunner, - PullCleaner: pullClosedExecutor, - Parser: eventParser, - CommentParser: commentParser, - Logger: logger, - GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret), - GithubRequestValidator: &DefaultGithubRequestValidator{}, - GitlabRequestParserValidator: &DefaultGitlabRequestParserValidator{}, - GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), - RepoWhitelistChecker: repoWhitelist, - SilenceWhitelistErrors: userConfig.SilenceWhitelistErrors, - SupportedVCSHosts: supportedVCSHosts, - VCSClient: vcsClient, - BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret), + CommandRunner: commandRunner, + PullCleaner: pullClosedExecutor, + Parser: eventParser, + CommentParser: commentParser, + Logger: logger, + GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret), + GithubRequestValidator: &DefaultGithubRequestValidator{}, + GitlabRequestParserValidator: &DefaultGitlabRequestParserValidator{}, + GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), + RepoWhitelistChecker: repoWhitelist, + SilenceWhitelistErrors: userConfig.SilenceWhitelistErrors, + SupportedVCSHosts: supportedVCSHosts, + VCSClient: vcsClient, + BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret), + AzureDevopsWebhookBasicUser: []byte(userConfig.AzureDevopsWebhookBasicUser), + AzureDevopsWebhookBasicPassword: []byte(userConfig.AzureDevopsWebhookBasicPassword), + AzureDevopsRequestValidator: &DefaultAzureDevopsRequestValidator{}, } return &Server{ AtlantisVersion: config.AtlantisVersion, diff --git a/server/user_config.go b/server/user_config.go index c6c194d42..bf7a6d14e 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -6,30 +6,34 @@ import "github.com/runatlantis/atlantis/server/logging" // The mapstructure tags correspond to flags in cmd/server.go and are used when // the config is parsed from a YAML file. type UserConfig struct { - AllowForkPRs bool `mapstructure:"allow-fork-prs"` - AllowRepoConfig bool `mapstructure:"allow-repo-config"` - AtlantisURL string `mapstructure:"atlantis-url"` - Automerge bool `mapstructure:"automerge"` - BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` - BitbucketToken string `mapstructure:"bitbucket-token"` - BitbucketUser string `mapstructure:"bitbucket-user"` - BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` - CheckoutStrategy string `mapstructure:"checkout-strategy"` - DataDir string `mapstructure:"data-dir"` - DisableApplyAll bool `mapstructure:"disable-apply-all"` - GithubHostname string `mapstructure:"gh-hostname"` - GithubToken string `mapstructure:"gh-token"` - GithubUser string `mapstructure:"gh-user"` - GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` - GitlabHostname string `mapstructure:"gitlab-hostname"` - GitlabToken string `mapstructure:"gitlab-token"` - GitlabUser string `mapstructure:"gitlab-user"` - GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` - LogLevel string `mapstructure:"log-level"` - Port int `mapstructure:"port"` - RepoConfig string `mapstructure:"repo-config"` - RepoConfigJSON string `mapstructure:"repo-config-json"` - RepoWhitelist string `mapstructure:"repo-whitelist"` + AllowForkPRs bool `mapstructure:"allow-fork-prs"` + AllowRepoConfig bool `mapstructure:"allow-repo-config"` + AtlantisURL string `mapstructure:"atlantis-url"` + Automerge bool `mapstructure:"automerge"` + AzureDevopsToken string `mapstructure:"azuredevops-token"` + AzureDevopsUser string `mapstructure:"azuredevops-user"` + AzureDevopsWebhookBasicPassword string `mapstructure:"azuredevops-basic-password"` + AzureDevopsWebhookBasicUser string `mapstructure:"azuredevops-basic-user"` + BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` + BitbucketToken string `mapstructure:"bitbucket-token"` + BitbucketUser string `mapstructure:"bitbucket-user"` + BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` + CheckoutStrategy string `mapstructure:"checkout-strategy"` + DataDir string `mapstructure:"data-dir"` + DisableApplyAll bool `mapstructure:"disable-apply-all"` + GithubHostname string `mapstructure:"gh-hostname"` + GithubToken string `mapstructure:"gh-token"` + GithubUser string `mapstructure:"gh-user"` + GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` + GitlabHostname string `mapstructure:"gitlab-hostname"` + GitlabToken string `mapstructure:"gitlab-token"` + GitlabUser string `mapstructure:"gitlab-user"` + GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` + LogLevel string `mapstructure:"log-level"` + Port int `mapstructure:"port"` + RepoConfig string `mapstructure:"repo-config"` + RepoConfigJSON string `mapstructure:"repo-config-json"` + RepoWhitelist string `mapstructure:"repo-whitelist"` // RequireApproval is whether to require pull request approval before // allowing terraform apply's to be run. RequireApproval bool `mapstructure:"require-approval"` diff --git a/server/web_templates.go b/server/web_templates.go index acabc6cde..19253b7d9 100644 --- a/server/web_templates.go +++ b/server/web_templates.go @@ -116,6 +116,7 @@ type LockDetailData struct { LockKeyEncoded string LockKey string RepoOwner string + RepoProject string RepoName string PullRequestLink string LockedBy string diff --git a/vendor/github.com/mcdafydd/go-azuredevops/LICENSE b/vendor/github.com/mcdafydd/go-azuredevops/LICENSE new file mode 100644 index 000000000..cd22318bb --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) for portions of this fork of go-azuredevops are held by Ben Selby, 2019. All other copyright for this project are held by David McPike, 2019. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mcdafydd/go-azuredevops/LICENSE.go-github b/vendor/github.com/mcdafydd/go-azuredevops/LICENSE.go-github new file mode 100644 index 000000000..7959bdc61 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/LICENSE.go-github @@ -0,0 +1,28 @@ +Copyright (c) 2013 The go-github AUTHORS. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops-accessors.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops-accessors.go new file mode 100644 index 000000000..b3213105a --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops-accessors.go @@ -0,0 +1,3580 @@ +// Copyright 2017 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by gen-accessors; DO NOT EDIT. + +package azuredevops + +import ( + "time" +) + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (a *AgentPoolQueue) GetID() int { + if a == nil || a.ID == nil { + return 0 + } + return *a.ID +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (a *AgentPoolQueue) GetLinks() map[string]Link { + if a == nil || a.Links == nil { + return map[string]Link{} + } + return *a.Links +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (a *AgentPoolQueue) GetName() string { + if a == nil || a.Name == nil { + return "" + } + return *a.Name +} + +// GetPool returns the Pool field. +func (a *AgentPoolQueue) GetPool() *TaskAgentPoolReference { + if a == nil { + return nil + } + return a.Pool +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (a *AgentPoolQueue) GetURL() string { + if a == nil || a.URL == nil { + return "" + } + return *a.URL +} + +// GetAllowedMappings returns the AllowedMappings field if it's non-nil, zero value otherwise. +func (b *Board) GetAllowedMappings() string { + if b == nil || b.AllowedMappings == nil { + return "" + } + return *b.AllowedMappings +} + +// GetCanEdit returns the CanEdit field if it's non-nil, zero value otherwise. +func (b *Board) GetCanEdit() bool { + if b == nil || b.CanEdit == nil { + return false + } + return *b.CanEdit +} + +// GetFields returns the Fields field. +func (b *Board) GetFields() *BoardFields { + if b == nil { + return nil + } + return b.Fields +} + +// GetIsValid returns the IsValid field if it's non-nil, zero value otherwise. +func (b *Board) GetIsValid() bool { + if b == nil || b.IsValid == nil { + return false + } + return *b.IsValid +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (b *Board) GetLinks() map[string]Link { + if b == nil || b.Links == nil { + return map[string]Link{} + } + return *b.Links +} + +// GetRevision returns the Revision field if it's non-nil, zero value otherwise. +func (b *Board) GetRevision() int { + if b == nil || b.Revision == nil { + return 0 + } + return *b.Revision +} + +// GetColumnType returns the ColumnType field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetColumnType() string { + if b == nil || b.ColumnType == nil { + return "" + } + return *b.ColumnType +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetDescription() string { + if b == nil || b.Description == nil { + return "" + } + return *b.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetID() string { + if b == nil || b.ID == nil { + return "" + } + return *b.ID +} + +// GetIsSplit returns the IsSplit field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetIsSplit() bool { + if b == nil || b.IsSplit == nil { + return false + } + return *b.IsSplit +} + +// GetItemLimit returns the ItemLimit field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetItemLimit() int { + if b == nil || b.ItemLimit == nil { + return 0 + } + return *b.ItemLimit +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BoardColumn) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetColumnField returns the ColumnField field. +func (b *BoardFields) GetColumnField() *FieldReference { + if b == nil { + return nil + } + return b.ColumnField +} + +// GetDoneField returns the DoneField field. +func (b *BoardFields) GetDoneField() *FieldReference { + if b == nil { + return nil + } + return b.DoneField +} + +// GetRowField returns the RowField field. +func (b *BoardFields) GetRowField() *FieldReference { + if b == nil { + return nil + } + return b.RowField +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BoardReference) GetID() string { + if b == nil || b.ID == nil { + return "" + } + return *b.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BoardReference) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (b *BoardReference) GetURL() string { + if b == nil || b.URL == nil { + return "" + } + return *b.URL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BoardRow) GetID() string { + if b == nil || b.ID == nil { + return "" + } + return *b.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BoardRow) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetBuildNumber returns the BuildNumber field if it's non-nil, zero value otherwise. +func (b *Build) GetBuildNumber() string { + if b == nil || b.BuildNumber == nil { + return "" + } + return *b.BuildNumber +} + +// GetBuildNumberRevision returns the BuildNumberRevision field if it's non-nil, zero value otherwise. +func (b *Build) GetBuildNumberRevision() int { + if b == nil || b.BuildNumberRevision == nil { + return 0 + } + return *b.BuildNumberRevision +} + +// GetChangedDate returns the ChangedDate field if it's non-nil, zero value otherwise. +func (b *Build) GetChangedDate() string { + if b == nil || b.ChangedDate == nil { + return "" + } + return *b.ChangedDate +} + +// GetController returns the Controller field. +func (b *Build) GetController() *BuildController { + if b == nil { + return nil + } + return b.Controller +} + +// GetDefinition returns the Definition field. +func (b *Build) GetDefinition() *BuildDefinition { + if b == nil { + return nil + } + return b.Definition +} + +// GetDeleted returns the Deleted field if it's non-nil, zero value otherwise. +func (b *Build) GetDeleted() bool { + if b == nil || b.Deleted == nil { + return false + } + return *b.Deleted +} + +// GetDeletedBy returns the DeletedBy field. +func (b *Build) GetDeletedBy() *IdentityRef { + if b == nil { + return nil + } + return b.DeletedBy +} + +// GetDeletedDate returns the DeletedDate field if it's non-nil, zero value otherwise. +func (b *Build) GetDeletedDate() string { + if b == nil || b.DeletedDate == nil { + return "" + } + return *b.DeletedDate +} + +// GetDeletedReason returns the DeletedReason field if it's non-nil, zero value otherwise. +func (b *Build) GetDeletedReason() string { + if b == nil || b.DeletedReason == nil { + return "" + } + return *b.DeletedReason +} + +// GetFinishTime returns the FinishTime field if it's non-nil, zero value otherwise. +func (b *Build) GetFinishTime() string { + if b == nil || b.FinishTime == nil { + return "" + } + return *b.FinishTime +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *Build) GetID() int { + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// GetKeepForever returns the KeepForever field if it's non-nil, zero value otherwise. +func (b *Build) GetKeepForever() bool { + if b == nil || b.KeepForever == nil { + return false + } + return *b.KeepForever +} + +// GetLastChangedBy returns the LastChangedBy field. +func (b *Build) GetLastChangedBy() *IdentityRef { + if b == nil { + return nil + } + return b.LastChangedBy +} + +// GetLogs returns the Logs field. +func (b *Build) GetLogs() *BuildLogReference { + if b == nil { + return nil + } + return b.Logs +} + +// GetOrchestrationPlan returns the OrchestrationPlan field. +func (b *Build) GetOrchestrationPlan() *TaskOrchestrationPlanReference { + if b == nil { + return nil + } + return b.OrchestrationPlan +} + +// GetParams returns the Params field if it's non-nil, zero value otherwise. +func (b *Build) GetParams() string { + if b == nil || b.Params == nil { + return "" + } + return *b.Params +} + +// GetPriority returns the Priority field if it's non-nil, zero value otherwise. +func (b *Build) GetPriority() string { + if b == nil || b.Priority == nil { + return "" + } + return *b.Priority +} + +// GetProject returns the Project field. +func (b *Build) GetProject() *TeamProjectReference { + if b == nil { + return nil + } + return b.Project +} + +// GetQuality returns the Quality field if it's non-nil, zero value otherwise. +func (b *Build) GetQuality() string { + if b == nil || b.Quality == nil { + return "" + } + return *b.Quality +} + +// GetQueue returns the Queue field. +func (b *Build) GetQueue() *AgentPoolQueue { + if b == nil { + return nil + } + return b.Queue +} + +// GetQueuePosition returns the QueuePosition field if it's non-nil, zero value otherwise. +func (b *Build) GetQueuePosition() int { + if b == nil || b.QueuePosition == nil { + return 0 + } + return *b.QueuePosition +} + +// GetQueueTime returns the QueueTime field if it's non-nil, zero value otherwise. +func (b *Build) GetQueueTime() string { + if b == nil || b.QueueTime == nil { + return "" + } + return *b.QueueTime +} + +// GetRepository returns the Repository field. +func (b *Build) GetRepository() *BuildRepository { + if b == nil { + return nil + } + return b.Repository +} + +// GetResult returns the Result field if it's non-nil, zero value otherwise. +func (b *Build) GetResult() string { + if b == nil || b.Result == nil { + return "" + } + return *b.Result +} + +// GetRetainedByRelease returns the RetainedByRelease field if it's non-nil, zero value otherwise. +func (b *Build) GetRetainedByRelease() bool { + if b == nil || b.RetainedByRelease == nil { + return false + } + return *b.RetainedByRelease +} + +// GetSourceBranch returns the SourceBranch field if it's non-nil, zero value otherwise. +func (b *Build) GetSourceBranch() string { + if b == nil || b.SourceBranch == nil { + return "" + } + return *b.SourceBranch +} + +// GetStartTime returns the StartTime field if it's non-nil, zero value otherwise. +func (b *Build) GetStartTime() string { + if b == nil || b.StartTime == nil { + return "" + } + return *b.StartTime +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (b *Build) GetStatus() string { + if b == nil || b.Status == nil { + return "" + } + return *b.Status +} + +// GetTriggerBuild returns the TriggerBuild field. +func (b *Build) GetTriggerBuild() *Build { + if b == nil { + return nil + } + return b.TriggerBuild +} + +// GetTriggerInfo returns the TriggerInfo field. +func (b *Build) GetTriggerInfo() *TriggerInfo { + if b == nil { + return nil + } + return b.TriggerInfo +} + +// GetURI returns the URI field if it's non-nil, zero value otherwise. +func (b *Build) GetURI() string { + if b == nil || b.URI == nil { + return "" + } + return *b.URI +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (b *Build) GetURL() string { + if b == nil || b.URL == nil { + return "" + } + return *b.URL +} + +// GetVersion returns the Version field if it's non-nil, zero value otherwise. +func (b *Build) GetVersion() string { + if b == nil || b.Version == nil { + return "" + } + return *b.Version +} + +// GetCreatedDate returns the CreatedDate field if it's non-nil, zero value otherwise. +func (b *BuildController) GetCreatedDate() string { + if b == nil || b.CreatedDate == nil { + return "" + } + return *b.CreatedDate +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (b *BuildController) GetDescription() string { + if b == nil || b.Description == nil { + return "" + } + return *b.Description +} + +// GetEnabled returns the Enabled field if it's non-nil, zero value otherwise. +func (b *BuildController) GetEnabled() bool { + if b == nil || b.Enabled == nil { + return false + } + return *b.Enabled +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BuildController) GetID() int { + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BuildController) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (b *BuildController) GetStatus() string { + if b == nil || b.Status == nil { + return "" + } + return *b.Status +} + +// GetUpdateDate returns the UpdateDate field if it's non-nil, zero value otherwise. +func (b *BuildController) GetUpdateDate() string { + if b == nil || b.UpdateDate == nil { + return "" + } + return *b.UpdateDate +} + +// GetURI returns the URI field if it's non-nil, zero value otherwise. +func (b *BuildController) GetURI() string { + if b == nil || b.URI == nil { + return "" + } + return *b.URI +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (b *BuildController) GetURL() string { + if b == nil || b.URL == nil { + return "" + } + return *b.URL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BuildDefinition) GetID() int { + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BuildDefinition) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetRepository returns the Repository field. +func (b *BuildDefinition) GetRepository() *BuildRepository { + if b == nil { + return nil + } + return b.Repository +} + +// GetIncludeAllProperties returns the IncludeAllProperties field if it's non-nil, zero value otherwise. +func (b *BuildDefinitionsListOptions) GetIncludeAllProperties() bool { + if b == nil || b.IncludeAllProperties == nil { + return false + } + return *b.IncludeAllProperties +} + +// GetPath returns the Path field if it's non-nil, zero value otherwise. +func (b *BuildDefinitionsListOptions) GetPath() string { + if b == nil || b.Path == nil { + return "" + } + return *b.Path +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BuildDemand) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetValue returns the Value field if it's non-nil, zero value otherwise. +func (b *BuildDemand) GetValue() string { + if b == nil || b.Value == nil { + return "" + } + return *b.Value +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BuildLogReference) GetID() int { + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (b *BuildLogReference) GetType() string { + if b == nil || b.Type == nil { + return "" + } + return *b.Type +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (b *BuildLogReference) GetURL() string { + if b == nil || b.URL == nil { + return "" + } + return *b.URL +} + +// GetCheckoutSubmodules returns the CheckoutSubmodules field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetCheckoutSubmodules() bool { + if b == nil || b.CheckoutSubmodules == nil { + return false + } + return *b.CheckoutSubmodules +} + +// GetClean returns the Clean field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetClean() string { + if b == nil || b.Clean == nil { + return "" + } + return *b.Clean +} + +// GetDefaultBranch returns the DefaultBranch field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetDefaultBranch() string { + if b == nil || b.DefaultBranch == nil { + return "" + } + return *b.DefaultBranch +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetID() string { + if b == nil || b.ID == nil { + return "" + } + return *b.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetName() string { + if b == nil || b.Name == nil { + return "" + } + return *b.Name +} + +// GetRootFolder returns the RootFolder field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetRootFolder() string { + if b == nil || b.RootFolder == nil { + return "" + } + return *b.RootFolder +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetType() string { + if b == nil || b.Type == nil { + return "" + } + return *b.Type +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (b *BuildRepository) GetURL() string { + if b == nil || b.URL == nil { + return "" + } + return *b.URL +} + +// GetBranch returns the Branch field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetBranch() string { + if b == nil || b.Branch == nil { + return "" + } + return *b.Branch +} + +// GetBuildIDs returns the BuildIDs field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetBuildIDs() string { + if b == nil || b.BuildIDs == nil { + return "" + } + return *b.BuildIDs +} + +// GetBuildNumber returns the BuildNumber field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetBuildNumber() string { + if b == nil || b.BuildNumber == nil { + return "" + } + return *b.BuildNumber +} + +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetCount() int { + if b == nil || b.Count == nil { + return 0 + } + return *b.Count +} + +// GetDefinitions returns the Definitions field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetDefinitions() string { + if b == nil || b.Definitions == nil { + return "" + } + return *b.Definitions +} + +// GetDeleted returns the Deleted field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetDeleted() string { + if b == nil || b.Deleted == nil { + return "" + } + return *b.Deleted +} + +// GetMaxPerDefinition returns the MaxPerDefinition field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetMaxPerDefinition() string { + if b == nil || b.MaxPerDefinition == nil { + return "" + } + return *b.MaxPerDefinition +} + +// GetMaxTime returns the MaxTime field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetMaxTime() string { + if b == nil || b.MaxTime == nil { + return "" + } + return *b.MaxTime +} + +// GetMinTime returns the MinTime field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetMinTime() string { + if b == nil || b.MinTime == nil { + return "" + } + return *b.MinTime +} + +// GetOrder returns the Order field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetOrder() string { + if b == nil || b.Order == nil { + return "" + } + return *b.Order +} + +// GetProps returns the Props field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetProps() string { + if b == nil || b.Props == nil { + return "" + } + return *b.Props +} + +// GetQueues returns the Queues field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetQueues() string { + if b == nil || b.Queues == nil { + return "" + } + return *b.Queues +} + +// GetReason returns the Reason field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetReason() string { + if b == nil || b.Reason == nil { + return "" + } + return *b.Reason +} + +// GetRepository returns the Repository field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetRepository() string { + if b == nil || b.Repository == nil { + return "" + } + return *b.Repository +} + +// GetRepoType returns the RepoType field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetRepoType() string { + if b == nil || b.RepoType == nil { + return "" + } + return *b.RepoType +} + +// GetResult returns the Result field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetResult() string { + if b == nil || b.Result == nil { + return "" + } + return *b.Result +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetStatus() string { + if b == nil || b.Status == nil { + return "" + } + return *b.Status +} + +// GetTags returns the Tags field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetTags() string { + if b == nil || b.Tags == nil { + return "" + } + return *b.Tags +} + +// GetToken returns the Token field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetToken() string { + if b == nil || b.Token == nil { + return "" + } + return *b.Token +} + +// GetUserID returns the UserID field if it's non-nil, zero value otherwise. +func (b *BuildsListOptions) GetUserID() string { + if b == nil || b.UserID == nil { + return "" + } + return *b.UserID +} + +// GetAuthor returns the Author field. +func (c *Comment) GetAuthor() *IdentityRef { + if c == nil { + return nil + } + return c.Author +} + +// GetCommentType returns the CommentType field if it's non-nil, zero value otherwise. +func (c *Comment) GetCommentType() string { + if c == nil || c.CommentType == nil { + return "" + } + return *c.CommentType +} + +// GetContent returns the Content field if it's non-nil, zero value otherwise. +func (c *Comment) GetContent() string { + if c == nil || c.Content == nil { + return "" + } + return *c.Content +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (c *Comment) GetID() int { + if c == nil || c.ID == nil { + return 0 + } + return *c.ID +} + +// GetIsDeleted returns the IsDeleted field if it's non-nil, zero value otherwise. +func (c *Comment) GetIsDeleted() bool { + if c == nil || c.IsDeleted == nil { + return false + } + return *c.IsDeleted +} + +// GetLastContentUpdatedDate returns the LastContentUpdatedDate field if it's non-nil, zero value otherwise. +func (c *Comment) GetLastContentUpdatedDate() time.Time { + if c == nil || c.LastContentUpdatedDate == nil { + return time.Time{} + } + return *c.LastContentUpdatedDate +} + +// GetLastUpdatedDate returns the LastUpdatedDate field if it's non-nil, zero value otherwise. +func (c *Comment) GetLastUpdatedDate() time.Time { + if c == nil || c.LastUpdatedDate == nil { + return time.Time{} + } + return *c.LastUpdatedDate +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (c *Comment) GetLinks() map[string]Link { + if c == nil || c.Links == nil { + return map[string]Link{} + } + return *c.Links +} + +// GetParentCommentID returns the ParentCommentID field if it's non-nil, zero value otherwise. +func (c *Comment) GetParentCommentID() int { + if c == nil || c.ParentCommentID == nil { + return 0 + } + return *c.ParentCommentID +} + +// GetPublishedDate returns the PublishedDate field if it's non-nil, zero value otherwise. +func (c *Comment) GetPublishedDate() time.Time { + if c == nil || c.PublishedDate == nil { + return time.Time{} + } + return *c.PublishedDate +} + +// GetLine returns the Line field if it's non-nil, zero value otherwise. +func (c *CommentPosition) GetLine() int { + if c == nil || c.Line == nil { + return 0 + } + return *c.Line +} + +// GetOffset returns the Offset field if it's non-nil, zero value otherwise. +func (c *CommentPosition) GetOffset() int { + if c == nil || c.Offset == nil { + return 0 + } + return *c.Offset +} + +// GetCommentID returns the CommentID field if it's non-nil, zero value otherwise. +func (c *CommentVersionRef) GetCommentID() int { + if c == nil || c.CommentID == nil { + return 0 + } + return *c.CommentID +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (c *CommentVersionRef) GetURL() string { + if c == nil || c.URL == nil { + return "" + } + return *c.URL +} + +// GetVersion returns the Version field if it's non-nil, zero value otherwise. +func (c *CommentVersionRef) GetVersion() int { + if c == nil || c.Version == nil { + return 0 + } + return *c.Version +} + +// GetCreated returns the Created field if it's non-nil, zero value otherwise. +func (d *DeliveryPlan) GetCreated() string { + if d == nil || d.Created == nil { + return "" + } + return *d.Created +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (d *DeliveryPlan) GetID() string { + if d == nil || d.ID == nil { + return "" + } + return *d.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (d *DeliveryPlan) GetName() string { + if d == nil || d.Name == nil { + return "" + } + return *d.Name +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (d *DeliveryPlan) GetType() string { + if d == nil || d.Type == nil { + return "" + } + return *d.Type +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (d *DeliveryPlan) GetURL() string { + if d == nil || d.URL == nil { + return "" + } + return *d.URL +} + +// GetEndDate returns the EndDate field if it's non-nil, zero value otherwise. +func (d *DeliveryPlanTimeLine) GetEndDate() string { + if d == nil || d.EndDate == nil { + return "" + } + return *d.EndDate +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (d *DeliveryPlanTimeLine) GetID() string { + if d == nil || d.ID == nil { + return "" + } + return *d.ID +} + +// GetRevision returns the Revision field if it's non-nil, zero value otherwise. +func (d *DeliveryPlanTimeLine) GetRevision() int { + if d == nil || d.Revision == nil { + return 0 + } + return *d.Revision +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (d *DeliveryPlanTimeLine) GetStartDate() string { + if d == nil || d.StartDate == nil { + return "" + } + return *d.StartDate +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (d *DeliveryTeam) GetID() string { + if d == nil || d.ID == nil { + return "" + } + return *d.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (d *DeliveryTeam) GetName() string { + if d == nil || d.Name == nil { + return "" + } + return *d.Name +} + +// GetArtifactID returns the ArtifactID field if it's non-nil, zero value otherwise. +func (f *Favourite) GetArtifactID() string { + if f == nil || f.ArtifactID == nil { + return "" + } + return *f.ArtifactID +} + +// GetArtifactName returns the ArtifactName field if it's non-nil, zero value otherwise. +func (f *Favourite) GetArtifactName() string { + if f == nil || f.ArtifactName == nil { + return "" + } + return *f.ArtifactName +} + +// GetArtifactType returns the ArtifactType field if it's non-nil, zero value otherwise. +func (f *Favourite) GetArtifactType() string { + if f == nil || f.ArtifactType == nil { + return "" + } + return *f.ArtifactType +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (f *Favourite) GetID() string { + if f == nil || f.ID == nil { + return "" + } + return *f.ID +} + +// GetReferenceName returns the ReferenceName field if it's non-nil, zero value otherwise. +func (f *FieldReference) GetReferenceName() string { + if f == nil || f.ReferenceName == nil { + return "" + } + return *f.ReferenceName +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (f *FieldReference) GetURL() string { + if f == nil || f.URL == nil { + return "" + } + return *f.URL +} + +// GetContentType returns the ContentType field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetContentType() string { + if f == nil || f.ContentType == nil { + return "" + } + return *f.ContentType +} + +// GetEncoding returns the Encoding field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetEncoding() int { + if f == nil || f.Encoding == nil { + return 0 + } + return *f.Encoding +} + +// GetExtension returns the Extension field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetExtension() string { + if f == nil || f.Extension == nil { + return "" + } + return *f.Extension +} + +// GetFileName returns the FileName field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetFileName() string { + if f == nil || f.FileName == nil { + return "" + } + return *f.FileName +} + +// GetIsBinary returns the IsBinary field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetIsBinary() bool { + if f == nil || f.IsBinary == nil { + return false + } + return *f.IsBinary +} + +// GetIsImage returns the IsImage field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetIsImage() bool { + if f == nil || f.IsImage == nil { + return false + } + return *f.IsImage +} + +// GetVSLink returns the VSLink field if it's non-nil, zero value otherwise. +func (f *FileContentMetadata) GetVSLink() string { + if f == nil || f.VSLink == nil { + return "" + } + return *f.VSLink +} + +// GetChangeID returns the ChangeID field if it's non-nil, zero value otherwise. +func (g *GitChange) GetChangeID() int { + if g == nil || g.ChangeID == nil { + return 0 + } + return *g.ChangeID +} + +// GetChangeType returns the ChangeType field if it's non-nil, zero value otherwise. +func (g *GitChange) GetChangeType() string { + if g == nil || g.ChangeType == nil { + return "" + } + return *g.ChangeType +} + +// GetItem returns the Item field. +func (g *GitChange) GetItem() *GitItem { + if g == nil { + return nil + } + return g.Item +} + +// GetNewContent returns the NewContent field. +func (g *GitChange) GetNewContent() *ItemContent { + if g == nil { + return nil + } + return g.NewContent +} + +// GetNewContentTemplate returns the NewContentTemplate field. +func (g *GitChange) GetNewContentTemplate() *GitTemplate { + if g == nil { + return nil + } + return g.NewContentTemplate +} + +// GetOriginalPath returns the OriginalPath field if it's non-nil, zero value otherwise. +func (g *GitChange) GetOriginalPath() string { + if g == nil || g.OriginalPath == nil { + return "" + } + return *g.OriginalPath +} + +// GetSourceServerItem returns the SourceServerItem field if it's non-nil, zero value otherwise. +func (g *GitChange) GetSourceServerItem() string { + if g == nil || g.SourceServerItem == nil { + return "" + } + return *g.SourceServerItem +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitChange) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetChangeCounts returns the ChangeCounts field if it's non-nil, zero value otherwise. +func (g *GitCommitChanges) GetChangeCounts() map[string]int { + if g == nil || g.ChangeCounts == nil { + return map[string]int{} + } + return *g.ChangeCounts +} + +// GetAuthor returns the Author field. +func (g *GitCommitRef) GetAuthor() *GitUserDate { + if g == nil { + return nil + } + return g.Author +} + +// GetChangeCounts returns the ChangeCounts field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetChangeCounts() map[string]int { + if g == nil || g.ChangeCounts == nil { + return map[string]int{} + } + return *g.ChangeCounts +} + +// GetChanges returns the Changes field. +func (g *GitCommitRef) GetChanges() *GitChange { + if g == nil { + return nil + } + return g.Changes +} + +// GetComment returns the Comment field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetComment() string { + if g == nil || g.Comment == nil { + return "" + } + return *g.Comment +} + +// GetCommentTruncated returns the CommentTruncated field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetCommentTruncated() bool { + if g == nil || g.CommentTruncated == nil { + return false + } + return *g.CommentTruncated +} + +// GetCommitID returns the CommitID field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetCommitID() string { + if g == nil || g.CommitID == nil { + return "" + } + return *g.CommitID +} + +// GetCommitter returns the Committer field. +func (g *GitCommitRef) GetCommitter() *GitUserDate { + if g == nil { + return nil + } + return g.Committer +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetPush returns the Push field. +func (g *GitCommitRef) GetPush() *GitPushRef { + if g == nil { + return nil + } + return g.Push +} + +// GetRemoteURL returns the RemoteURL field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetRemoteURL() string { + if g == nil || g.RemoteURL == nil { + return "" + } + return *g.RemoteURL +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitCommitRef) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetWorkItems returns the WorkItems field. +func (g *GitCommitRef) GetWorkItems() *ResourceRef { + if g == nil { + return nil + } + return g.WorkItems +} + +// GetCommitID returns the CommitID field if it's non-nil, zero value otherwise. +func (g *GitItem) GetCommitID() string { + if g == nil || g.CommitID == nil { + return "" + } + return *g.CommitID +} + +// GetContent returns the Content field if it's non-nil, zero value otherwise. +func (g *GitItem) GetContent() string { + if g == nil || g.Content == nil { + return "" + } + return *g.Content +} + +// GetContentMetadata returns the ContentMetadata field. +func (g *GitItem) GetContentMetadata() *FileContentMetadata { + if g == nil { + return nil + } + return g.ContentMetadata +} + +// GetGitObjectType returns the GitObjectType field if it's non-nil, zero value otherwise. +func (g *GitItem) GetGitObjectType() string { + if g == nil || g.GitObjectType == nil { + return "" + } + return *g.GitObjectType +} + +// GetIsFolder returns the IsFolder field if it's non-nil, zero value otherwise. +func (g *GitItem) GetIsFolder() bool { + if g == nil || g.IsFolder == nil { + return false + } + return *g.IsFolder +} + +// GetIsSymLink returns the IsSymLink field if it's non-nil, zero value otherwise. +func (g *GitItem) GetIsSymLink() bool { + if g == nil || g.IsSymLink == nil { + return false + } + return *g.IsSymLink +} + +// GetLatestProcessedChange returns the LatestProcessedChange field. +func (g *GitItem) GetLatestProcessedChange() *GitCommitRef { + if g == nil { + return nil + } + return g.LatestProcessedChange +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitItem) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetObjectID returns the ObjectID field if it's non-nil, zero value otherwise. +func (g *GitItem) GetObjectID() string { + if g == nil || g.ObjectID == nil { + return "" + } + return *g.ObjectID +} + +// GetOriginalObjectID returns the OriginalObjectID field if it's non-nil, zero value otherwise. +func (g *GitItem) GetOriginalObjectID() string { + if g == nil || g.OriginalObjectID == nil { + return "" + } + return *g.OriginalObjectID +} + +// GetPath returns the Path field if it's non-nil, zero value otherwise. +func (g *GitItem) GetPath() string { + if g == nil || g.Path == nil { + return "" + } + return *g.Path +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitItem) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetArtifactID returns the ArtifactID field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetArtifactID() string { + if g == nil || g.ArtifactID == nil { + return "" + } + return *g.ArtifactID +} + +// GetAutoCompleteSetBy returns the AutoCompleteSetBy field. +func (g *GitPullRequest) GetAutoCompleteSetBy() *IdentityRef { + if g == nil { + return nil + } + return g.AutoCompleteSetBy +} + +// GetClosedBy returns the ClosedBy field. +func (g *GitPullRequest) GetClosedBy() *IdentityRef { + if g == nil { + return nil + } + return g.ClosedBy +} + +// GetClosedDate returns the ClosedDate field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetClosedDate() time.Time { + if g == nil || g.ClosedDate == nil { + return time.Time{} + } + return *g.ClosedDate +} + +// GetCodeReviewID returns the CodeReviewID field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetCodeReviewID() int { + if g == nil || g.CodeReviewID == nil { + return 0 + } + return *g.CodeReviewID +} + +// GetCompletionOptions returns the CompletionOptions field. +func (g *GitPullRequest) GetCompletionOptions() *GitPullRequestCompletionOptions { + if g == nil { + return nil + } + return g.CompletionOptions +} + +// GetCompletionQueueTime returns the CompletionQueueTime field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetCompletionQueueTime() time.Time { + if g == nil || g.CompletionQueueTime == nil { + return time.Time{} + } + return *g.CompletionQueueTime +} + +// GetCreatedBy returns the CreatedBy field. +func (g *GitPullRequest) GetCreatedBy() *IdentityRef { + if g == nil { + return nil + } + return g.CreatedBy +} + +// GetCreationDate returns the CreationDate field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetCreationDate() time.Time { + if g == nil || g.CreationDate == nil { + return time.Time{} + } + return *g.CreationDate +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetDescription() string { + if g == nil || g.Description == nil { + return "" + } + return *g.Description +} + +// GetForkSource returns the ForkSource field. +func (g *GitPullRequest) GetForkSource() *GitRef { + if g == nil { + return nil + } + return g.ForkSource +} + +// GetIsDraft returns the IsDraft field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetIsDraft() bool { + if g == nil || g.IsDraft == nil { + return false + } + return *g.IsDraft +} + +// GetLastMergeCommit returns the LastMergeCommit field. +func (g *GitPullRequest) GetLastMergeCommit() *GitCommitRef { + if g == nil { + return nil + } + return g.LastMergeCommit +} + +// GetLastMergeSourceCommit returns the LastMergeSourceCommit field. +func (g *GitPullRequest) GetLastMergeSourceCommit() *GitCommitRef { + if g == nil { + return nil + } + return g.LastMergeSourceCommit +} + +// GetLastMergeTargetCommit returns the LastMergeTargetCommit field. +func (g *GitPullRequest) GetLastMergeTargetCommit() *GitCommitRef { + if g == nil { + return nil + } + return g.LastMergeTargetCommit +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetMergeFailureMessage returns the MergeFailureMessage field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetMergeFailureMessage() string { + if g == nil || g.MergeFailureMessage == nil { + return "" + } + return *g.MergeFailureMessage +} + +// GetMergeFailureType returns the MergeFailureType field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetMergeFailureType() string { + if g == nil || g.MergeFailureType == nil { + return "" + } + return *g.MergeFailureType +} + +// GetMergeID returns the MergeID field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetMergeID() string { + if g == nil || g.MergeID == nil { + return "" + } + return *g.MergeID +} + +// GetMergeOptions returns the MergeOptions field. +func (g *GitPullRequest) GetMergeOptions() *GitPullRequestMergeOptions { + if g == nil { + return nil + } + return g.MergeOptions +} + +// GetMergeStatus returns the MergeStatus field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetMergeStatus() string { + if g == nil || g.MergeStatus == nil { + return "" + } + return *g.MergeStatus +} + +// GetPullRequestID returns the PullRequestID field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetPullRequestID() int { + if g == nil || g.PullRequestID == nil { + return 0 + } + return *g.PullRequestID +} + +// GetRemoteURL returns the RemoteURL field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetRemoteURL() string { + if g == nil || g.RemoteURL == nil { + return "" + } + return *g.RemoteURL +} + +// GetRepository returns the Repository field. +func (g *GitPullRequest) GetRepository() *GitRepository { + if g == nil { + return nil + } + return g.Repository +} + +// GetSourceRefName returns the SourceRefName field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetSourceRefName() string { + if g == nil || g.SourceRefName == nil { + return "" + } + return *g.SourceRefName +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetStatus() string { + if g == nil || g.Status == nil { + return "" + } + return *g.Status +} + +// GetSupportsIterations returns the SupportsIterations field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetSupportsIterations() bool { + if g == nil || g.SupportsIterations == nil { + return false + } + return *g.SupportsIterations +} + +// GetTargetRefName returns the TargetRefName field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetTargetRefName() string { + if g == nil || g.TargetRefName == nil { + return "" + } + return *g.TargetRefName +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetTitle() string { + if g == nil || g.Title == nil { + return "" + } + return *g.Title +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitPullRequest) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetID() int { + if g == nil || g.ID == nil { + return 0 + } + return *g.ID +} + +// GetIsDeleted returns the IsDeleted field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetIsDeleted() bool { + if g == nil || g.IsDeleted == nil { + return false + } + return *g.IsDeleted +} + +// GetLastUpdatedDate returns the LastUpdatedDate field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetLastUpdatedDate() time.Time { + if g == nil || g.LastUpdatedDate == nil { + return time.Time{} + } + return *g.LastUpdatedDate +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetPublishedDate returns the PublishedDate field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetPublishedDate() time.Time { + if g == nil || g.PublishedDate == nil { + return time.Time{} + } + return *g.PublishedDate +} + +// GetPullRequestThreadContext returns the PullRequestThreadContext field. +func (g *GitPullRequestCommentThread) GetPullRequestThreadContext() *GitPullRequestCommentThreadContext { + if g == nil { + return nil + } + return g.PullRequestThreadContext +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThread) GetStatus() string { + if g == nil || g.Status == nil { + return "" + } + return *g.Status +} + +// GetFilePath returns the FilePath field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCommentThreadContext) GetFilePath() string { + if g == nil || g.FilePath == nil { + return "" + } + return *g.FilePath +} + +// GetLeftFileEnd returns the LeftFileEnd field. +func (g *GitPullRequestCommentThreadContext) GetLeftFileEnd() *CommentPosition { + if g == nil { + return nil + } + return g.LeftFileEnd +} + +// GetLeftFileStart returns the LeftFileStart field. +func (g *GitPullRequestCommentThreadContext) GetLeftFileStart() *CommentPosition { + if g == nil { + return nil + } + return g.LeftFileStart +} + +// GetRightFileEnd returns the RightFileEnd field. +func (g *GitPullRequestCommentThreadContext) GetRightFileEnd() *CommentPosition { + if g == nil { + return nil + } + return g.RightFileEnd +} + +// GetRightFileStart returns the RightFileStart field. +func (g *GitPullRequestCommentThreadContext) GetRightFileStart() *CommentPosition { + if g == nil { + return nil + } + return g.RightFileStart +} + +// GetBypassPolicy returns the BypassPolicy field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetBypassPolicy() bool { + if g == nil || g.BypassPolicy == nil { + return false + } + return *g.BypassPolicy +} + +// GetBypassReason returns the BypassReason field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetBypassReason() string { + if g == nil || g.BypassReason == nil { + return "" + } + return *g.BypassReason +} + +// GetDeleteSourceBranch returns the DeleteSourceBranch field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetDeleteSourceBranch() bool { + if g == nil || g.DeleteSourceBranch == nil { + return false + } + return *g.DeleteSourceBranch +} + +// GetMergeCommitMessage returns the MergeCommitMessage field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetMergeCommitMessage() string { + if g == nil || g.MergeCommitMessage == nil { + return "" + } + return *g.MergeCommitMessage +} + +// GetMergeStrategy returns the MergeStrategy field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetMergeStrategy() string { + if g == nil || g.MergeStrategy == nil { + return "" + } + return *g.MergeStrategy +} + +// GetSquashMerge returns the SquashMerge field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetSquashMerge() bool { + if g == nil || g.SquashMerge == nil { + return false + } + return *g.SquashMerge +} + +// GetTransitionWorkItems returns the TransitionWorkItems field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetTransitionWorkItems() bool { + if g == nil || g.TransitionWorkItems == nil { + return false + } + return *g.TransitionWorkItems +} + +// GetTriggeredByAutoComplete returns the TriggeredByAutoComplete field if it's non-nil, zero value otherwise. +func (g *GitPullRequestCompletionOptions) GetTriggeredByAutoComplete() bool { + if g == nil || g.TriggeredByAutoComplete == nil { + return false + } + return *g.TriggeredByAutoComplete +} + +// GetDetectRenameFalsePositives returns the DetectRenameFalsePositives field if it's non-nil, zero value otherwise. +func (g *GitPullRequestMergeOptions) GetDetectRenameFalsePositives() bool { + if g == nil || g.DetectRenameFalsePositives == nil { + return false + } + return *g.DetectRenameFalsePositives +} + +// GetDisableRenames returns the DisableRenames field if it's non-nil, zero value otherwise. +func (g *GitPullRequestMergeOptions) GetDisableRenames() bool { + if g == nil || g.DisableRenames == nil { + return false + } + return *g.DisableRenames +} + +// GetProperties returns the Properties field if it's non-nil, zero value otherwise. +func (g *GitPullRequestStatus) GetProperties() time.Time { + if g == nil || g.Properties == nil { + return time.Time{} + } + return *g.Properties +} + +// GetComment returns the Comment field. +func (g *GitPullRequestWithComment) GetComment() *Comment { + if g == nil { + return nil + } + return g.Comment +} + +// GetPullRequest returns the PullRequest field. +func (g *GitPullRequestWithComment) GetPullRequest() *GitPullRequest { + if g == nil { + return nil + } + return g.PullRequest +} + +// GetDate returns the Date field if it's non-nil, zero value otherwise. +func (g *GitPush) GetDate() time.Time { + if g == nil || g.Date == nil { + return time.Time{} + } + return *g.Date +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitPush) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetPushedBy returns the PushedBy field. +func (g *GitPush) GetPushedBy() *IdentityRef { + if g == nil { + return nil + } + return g.PushedBy +} + +// GetPushID returns the PushID field if it's non-nil, zero value otherwise. +func (g *GitPush) GetPushID() int { + if g == nil || g.PushID == nil { + return 0 + } + return *g.PushID +} + +// GetRepository returns the Repository field. +func (g *GitPush) GetRepository() *GitRepository { + if g == nil { + return nil + } + return g.Repository +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitPush) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetRepository returns the Repository field. +func (g *GitPushRef) GetRepository() *GitRepository { + if g == nil { + return nil + } + return g.Repository +} + +// GetCreator returns the Creator field. +func (g *GitRef) GetCreator() *IdentityRef { + if g == nil { + return nil + } + return g.Creator +} + +// GetIsLocked returns the IsLocked field if it's non-nil, zero value otherwise. +func (g *GitRef) GetIsLocked() bool { + if g == nil || g.IsLocked == nil { + return false + } + return *g.IsLocked +} + +// GetIsLockedBy returns the IsLockedBy field. +func (g *GitRef) GetIsLockedBy() *IdentityRef { + if g == nil { + return nil + } + return g.IsLockedBy +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitRef) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitRef) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetObjectID returns the ObjectID field if it's non-nil, zero value otherwise. +func (g *GitRef) GetObjectID() string { + if g == nil || g.ObjectID == nil { + return "" + } + return *g.ObjectID +} + +// GetPeeledObjectID returns the PeeledObjectID field if it's non-nil, zero value otherwise. +func (g *GitRef) GetPeeledObjectID() string { + if g == nil || g.PeeledObjectID == nil { + return "" + } + return *g.PeeledObjectID +} + +// GetRepository returns the Repository field. +func (g *GitRef) GetRepository() *GitRepository { + if g == nil { + return nil + } + return g.Repository +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitRef) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetIsLocked returns the IsLocked field if it's non-nil, zero value otherwise. +func (g *GitRefUpdate) GetIsLocked() bool { + if g == nil || g.IsLocked == nil { + return false + } + return *g.IsLocked +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitRefUpdate) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetNewObjectID returns the NewObjectID field if it's non-nil, zero value otherwise. +func (g *GitRefUpdate) GetNewObjectID() string { + if g == nil || g.NewObjectID == nil { + return "" + } + return *g.NewObjectID +} + +// GetOldObjectID returns the OldObjectID field if it's non-nil, zero value otherwise. +func (g *GitRefUpdate) GetOldObjectID() string { + if g == nil || g.OldObjectID == nil { + return "" + } + return *g.OldObjectID +} + +// GetRepositoryID returns the RepositoryID field if it's non-nil, zero value otherwise. +func (g *GitRefUpdate) GetRepositoryID() string { + if g == nil || g.RepositoryID == nil { + return "" + } + return *g.RepositoryID +} + +// GetDefaultBranch returns the DefaultBranch field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetDefaultBranch() string { + if g == nil || g.DefaultBranch == nil { + return "" + } + return *g.DefaultBranch +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetID() string { + if g == nil || g.ID == nil { + return "" + } + return *g.ID +} + +// GetIsFork returns the IsFork field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetIsFork() bool { + if g == nil || g.IsFork == nil { + return false + } + return *g.IsFork +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetParentRepository returns the ParentRepository field. +func (g *GitRepository) GetParentRepository() *GitRepositoryRef { + if g == nil { + return nil + } + return g.ParentRepository +} + +// GetProject returns the Project field. +func (g *GitRepository) GetProject() *TeamProjectReference { + if g == nil { + return nil + } + return g.Project +} + +// GetRemoteURL returns the RemoteURL field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetRemoteURL() string { + if g == nil || g.RemoteURL == nil { + return "" + } + return *g.RemoteURL +} + +// GetSize returns the Size field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetSize() int { + if g == nil || g.Size == nil { + return 0 + } + return *g.Size +} + +// GetSSHURL returns the SSHURL field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetSSHURL() string { + if g == nil || g.SSHURL == nil { + return "" + } + return *g.SSHURL +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetWebURL returns the WebURL field if it's non-nil, zero value otherwise. +func (g *GitRepository) GetWebURL() string { + if g == nil || g.WebURL == nil { + return "" + } + return *g.WebURL +} + +// GetCollection returns the Collection field. +func (g *GitRepositoryRef) GetCollection() *TeamProjectCollectionReference { + if g == nil { + return nil + } + return g.Collection +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetID() string { + if g == nil || g.ID == nil { + return "" + } + return *g.ID +} + +// GetIsFork returns the IsFork field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetIsFork() bool { + if g == nil || g.IsFork == nil { + return false + } + return *g.IsFork +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetProject returns the Project field. +func (g *GitRepositoryRef) GetProject() *TeamProjectReference { + if g == nil { + return nil + } + return g.Project +} + +// GetRemoteURL returns the RemoteURL field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetRemoteURL() string { + if g == nil || g.RemoteURL == nil { + return "" + } + return *g.RemoteURL +} + +// GetSSHURL returns the SSHURL field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetSSHURL() string { + if g == nil || g.SSHURL == nil { + return "" + } + return *g.SSHURL +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GitRepositoryRef) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetContext returns the Context field. +func (g *GitStatus) GetContext() *GitStatusContext { + if g == nil { + return nil + } + return g.Context +} + +// GetCreatedBy returns the CreatedBy field. +func (g *GitStatus) GetCreatedBy() *IdentityRef { + if g == nil { + return nil + } + return g.CreatedBy +} + +// GetCreationDate returns the CreationDate field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetCreationDate() time.Time { + if g == nil || g.CreationDate == nil { + return time.Time{} + } + return *g.CreationDate +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetDescription() string { + if g == nil || g.Description == nil { + return "" + } + return *g.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetID() int { + if g == nil || g.ID == nil { + return 0 + } + return *g.ID +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetLinks() map[string]Link { + if g == nil || g.Links == nil { + return map[string]Link{} + } + return *g.Links +} + +// GetState returns the State field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetState() string { + if g == nil || g.State == nil { + return "" + } + return *g.State +} + +// GetTargetURL returns the TargetURL field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetTargetURL() string { + if g == nil || g.TargetURL == nil { + return "" + } + return *g.TargetURL +} + +// GetUpdatedDate returns the UpdatedDate field if it's non-nil, zero value otherwise. +func (g *GitStatus) GetUpdatedDate() time.Time { + if g == nil || g.UpdatedDate == nil { + return time.Time{} + } + return *g.UpdatedDate +} + +// GetGenre returns the Genre field if it's non-nil, zero value otherwise. +func (g *GitStatusContext) GetGenre() string { + if g == nil || g.Genre == nil { + return "" + } + return *g.Genre +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitStatusContext) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitTemplate) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (g *GitTemplate) GetType() string { + if g == nil || g.Type == nil { + return "" + } + return *g.Type +} + +// GetDate returns the Date field if it's non-nil, zero value otherwise. +func (g *GitUserDate) GetDate() time.Time { + if g == nil || g.Date == nil { + return time.Time{} + } + return *g.Date +} + +// GetEmail returns the Email field if it's non-nil, zero value otherwise. +func (g *GitUserDate) GetEmail() string { + if g == nil || g.Email == nil { + return "" + } + return *g.Email +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (g *GitUserDate) GetName() string { + if g == nil || g.Name == nil { + return "" + } + return *g.Name +} + +// GetValue returns the Value field if it's non-nil, zero value otherwise. +func (g *GraphDescriptorResult) GetValue() string { + if g == nil || g.Value == nil { + return "" + } + return *g.Value +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetDescription() string { + if g == nil || g.Description == nil { + return "" + } + return *g.Description +} + +// GetIsCrossProject returns the IsCrossProject field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetIsCrossProject() bool { + if g == nil || g.IsCrossProject == nil { + return false + } + return *g.IsCrossProject +} + +// GetIsDeleted returns the IsDeleted field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetIsDeleted() bool { + if g == nil || g.IsDeleted == nil { + return false + } + return *g.IsDeleted +} + +// GetIsGlobalScope returns the IsGlobalScope field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetIsGlobalScope() bool { + if g == nil || g.IsGlobalScope == nil { + return false + } + return *g.IsGlobalScope +} + +// GetIsRestrictedVisible returns the IsRestrictedVisible field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetIsRestrictedVisible() bool { + if g == nil || g.IsRestrictedVisible == nil { + return false + } + return *g.IsRestrictedVisible +} + +// GetLocalScopeID returns the LocalScopeID field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetLocalScopeID() string { + if g == nil || g.LocalScopeID == nil { + return "" + } + return *g.LocalScopeID +} + +// GetScopeID returns the ScopeID field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetScopeID() string { + if g == nil || g.ScopeID == nil { + return "" + } + return *g.ScopeID +} + +// GetScopeName returns the ScopeName field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetScopeName() string { + if g == nil || g.ScopeName == nil { + return "" + } + return *g.ScopeName +} + +// GetScopeType returns the ScopeType field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetScopeType() string { + if g == nil || g.ScopeType == nil { + return "" + } + return *g.ScopeType +} + +// GetSecuringHostID returns the SecuringHostID field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetSecuringHostID() string { + if g == nil || g.SecuringHostID == nil { + return "" + } + return *g.SecuringHostID +} + +// GetSpecialType returns the SpecialType field if it's non-nil, zero value otherwise. +func (g *GraphGroup) GetSpecialType() string { + if g == nil || g.SpecialType == nil { + return "" + } + return *g.SpecialType +} + +// GetDomain returns the Domain field if it's non-nil, zero value otherwise. +func (g *GraphMember) GetDomain() string { + if g == nil || g.Domain == nil { + return "" + } + return *g.Domain +} + +// GetMailAddress returns the MailAddress field if it's non-nil, zero value otherwise. +func (g *GraphMember) GetMailAddress() string { + if g == nil || g.MailAddress == nil { + return "" + } + return *g.MailAddress +} + +// GetPrincipalName returns the PrincipalName field if it's non-nil, zero value otherwise. +func (g *GraphMember) GetPrincipalName() string { + if g == nil || g.PrincipalName == nil { + return "" + } + return *g.PrincipalName +} + +// GetLegacyDescriptor returns the LegacyDescriptor field if it's non-nil, zero value otherwise. +func (g *GraphSubject) GetLegacyDescriptor() string { + if g == nil || g.LegacyDescriptor == nil { + return "" + } + return *g.LegacyDescriptor +} + +// GetOrigin returns the Origin field if it's non-nil, zero value otherwise. +func (g *GraphSubject) GetOrigin() string { + if g == nil || g.Origin == nil { + return "" + } + return *g.Origin +} + +// GetOriginID returns the OriginID field if it's non-nil, zero value otherwise. +func (g *GraphSubject) GetOriginID() string { + if g == nil || g.OriginID == nil { + return "" + } + return *g.OriginID +} + +// GetSubjectKind returns the SubjectKind field if it's non-nil, zero value otherwise. +func (g *GraphSubject) GetSubjectKind() string { + if g == nil || g.SubjectKind == nil { + return "" + } + return *g.SubjectKind +} + +// GetDescriptor returns the Descriptor field if it's non-nil, zero value otherwise. +func (g *GraphSubjectBase) GetDescriptor() string { + if g == nil || g.Descriptor == nil { + return "" + } + return *g.Descriptor +} + +// GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise. +func (g *GraphSubjectBase) GetDisplayName() string { + if g == nil || g.DisplayName == nil { + return "" + } + return *g.DisplayName +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (g *GraphSubjectBase) GetURL() string { + if g == nil || g.URL == nil { + return "" + } + return *g.URL +} + +// GetIsDeletedInOrigin returns the IsDeletedInOrigin field if it's non-nil, zero value otherwise. +func (g *GraphUser) GetIsDeletedInOrigin() bool { + if g == nil || g.IsDeletedInOrigin == nil { + return false + } + return *g.IsDeletedInOrigin +} + +// GetMetadataUpdateDate returns the MetadataUpdateDate field if it's non-nil, zero value otherwise. +func (g *GraphUser) GetMetadataUpdateDate() time.Time { + if g == nil || g.MetadataUpdateDate == nil { + return time.Time{} + } + return *g.MetadataUpdateDate +} + +// GetMetaType returns the MetaType field if it's non-nil, zero value otherwise. +func (g *GraphUser) GetMetaType() string { + if g == nil || g.MetaType == nil { + return "" + } + return *g.MetaType +} + +// GetDescriptor returns the Descriptor field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetDescriptor() string { + if i == nil || i.Descriptor == nil { + return "" + } + return *i.Descriptor +} + +// GetDirectoryAlias returns the DirectoryAlias field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetDirectoryAlias() string { + if i == nil || i.DirectoryAlias == nil { + return "" + } + return *i.DirectoryAlias +} + +// GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetDisplayName() string { + if i == nil || i.DisplayName == nil { + return "" + } + return *i.DisplayName +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetID() string { + if i == nil || i.ID == nil { + return "" + } + return *i.ID +} + +// GetImageURL returns the ImageURL field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetImageURL() string { + if i == nil || i.ImageURL == nil { + return "" + } + return *i.ImageURL +} + +// GetInactive returns the Inactive field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetInactive() bool { + if i == nil || i.Inactive == nil { + return false + } + return *i.Inactive +} + +// GetIsAadIdentity returns the IsAadIdentity field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetIsAadIdentity() bool { + if i == nil || i.IsAadIdentity == nil { + return false + } + return *i.IsAadIdentity +} + +// GetIsContainer returns the IsContainer field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetIsContainer() bool { + if i == nil || i.IsContainer == nil { + return false + } + return *i.IsContainer +} + +// GetIsDeletedInOrigin returns the IsDeletedInOrigin field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetIsDeletedInOrigin() bool { + if i == nil || i.IsDeletedInOrigin == nil { + return false + } + return *i.IsDeletedInOrigin +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetLinks() map[string]Link { + if i == nil || i.Links == nil { + return map[string]Link{} + } + return *i.Links +} + +// GetProfileURL returns the ProfileURL field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetProfileURL() string { + if i == nil || i.ProfileURL == nil { + return "" + } + return *i.ProfileURL +} + +// GetUniqueName returns the UniqueName field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetUniqueName() string { + if i == nil || i.UniqueName == nil { + return "" + } + return *i.UniqueName +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (i *IdentityRef) GetURL() string { + if i == nil || i.URL == nil { + return "" + } + return *i.URL +} + +// GetIsRequired returns the IsRequired field if it's non-nil, zero value otherwise. +func (i *IdentityRefWithVote) GetIsRequired() bool { + if i == nil || i.IsRequired == nil { + return false + } + return *i.IsRequired +} + +// GetReviewerURL returns the ReviewerURL field if it's non-nil, zero value otherwise. +func (i *IdentityRefWithVote) GetReviewerURL() string { + if i == nil || i.ReviewerURL == nil { + return "" + } + return *i.ReviewerURL +} + +// GetVote returns the Vote field if it's non-nil, zero value otherwise. +func (i *IdentityRefWithVote) GetVote() int { + if i == nil || i.Vote == nil { + return 0 + } + return *i.Vote +} + +// GetContent returns the Content field if it's non-nil, zero value otherwise. +func (i *ItemContent) GetContent() string { + if i == nil || i.Content == nil { + return "" + } + return *i.Content +} + +// GetContentType returns the ContentType field. +func (i *ItemContent) GetContentType() *ItemContentType { + if i == nil { + return nil + } + return i.ContentType +} + +// GetBase64Encoded returns the Base64Encoded field if it's non-nil, zero value otherwise. +func (i *ItemContentType) GetBase64Encoded() string { + if i == nil || i.Base64Encoded == nil { + return "" + } + return *i.Base64Encoded +} + +// GetRawText returns the RawText field if it's non-nil, zero value otherwise. +func (i *ItemContentType) GetRawText() string { + if i == nil || i.RawText == nil { + return "" + } + return *i.RawText +} + +// GetEndDate returns the EndDate field if it's non-nil, zero value otherwise. +func (i *Iteration) GetEndDate() string { + if i == nil || i.EndDate == nil { + return "" + } + return *i.EndDate +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (i *Iteration) GetID() string { + if i == nil || i.ID == nil { + return "" + } + return *i.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (i *Iteration) GetName() string { + if i == nil || i.Name == nil { + return "" + } + return *i.Name +} + +// GetPath returns the Path field if it's non-nil, zero value otherwise. +func (i *Iteration) GetPath() string { + if i == nil || i.Path == nil { + return "" + } + return *i.Path +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (i *Iteration) GetStartDate() string { + if i == nil || i.StartDate == nil { + return "" + } + return *i.StartDate +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (i *Iteration) GetURL() string { + if i == nil || i.URL == nil { + return "" + } + return *i.URL +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (i *IterationWorkItems) GetLinks() map[string]Link { + if i == nil || i.Links == nil { + return map[string]Link{} + } + return *i.Links +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (i *IterationWorkItems) GetURL() string { + if i == nil || i.URL == nil { + return "" + } + return *i.URL +} + +// GetHref returns the Href field if it's non-nil, zero value otherwise. +func (l *Link) GetHref() string { + if l == nil || l.Href == nil { + return "" + } + return *l.Href +} + +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (l *ListBoardsResponse) GetCount() int { + if l == nil || l.Count == nil { + return 0 + } + return *l.Count +} + +// GetHTML returns the HTML field if it's non-nil, zero value otherwise. +func (m *Message) GetHTML() string { + if m == nil || m.HTML == nil { + return "" + } + return *m.HTML +} + +// GetMarkdown returns the Markdown field if it's non-nil, zero value otherwise. +func (m *Message) GetMarkdown() string { + if m == nil || m.Markdown == nil { + return "" + } + return *m.Markdown +} + +// GetText returns the Text field if it's non-nil, zero value otherwise. +func (m *Message) GetText() string { + if m == nil || m.Text == nil { + return "" + } + return *m.Text +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (p *Project) GetDescription() string { + if p == nil || p.Description == nil { + return "" + } + return *p.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *Project) GetID() string { + if p == nil || p.ID == nil { + return "" + } + return *p.ID +} + +// GetLastUpdateTime returns the LastUpdateTime field if it's non-nil, zero value otherwise. +func (p *Project) GetLastUpdateTime() time.Time { + if p == nil || p.LastUpdateTime == nil { + return time.Time{} + } + return *p.LastUpdateTime +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *Project) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetRevision returns the Revision field if it's non-nil, zero value otherwise. +func (p *Project) GetRevision() int { + if p == nil || p.Revision == nil { + return 0 + } + return *p.Revision +} + +// GetState returns the State field if it's non-nil, zero value otherwise. +func (p *Project) GetState() string { + if p == nil || p.State == nil { + return "" + } + return *p.State +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (p *Project) GetURL() string { + if p == nil || p.URL == nil { + return "" + } + return *p.URL +} + +// GetVisibility returns the Visibility field if it's non-nil, zero value otherwise. +func (p *Project) GetVisibility() string { + if p == nil || p.Visibility == nil { + return "" + } + return *p.Visibility +} + +// GetAccount returns the Account field. +func (r *ResourceContainers) GetAccount() *ResourceRef { + if r == nil { + return nil + } + return r.Account +} + +// GetCollection returns the Collection field. +func (r *ResourceContainers) GetCollection() *ResourceRef { + if r == nil { + return nil + } + return r.Collection +} + +// GetProject returns the Project field. +func (r *ResourceContainers) GetProject() *ResourceRef { + if r == nil { + return nil + } + return r.Project +} + +// GetBaseURL returns the BaseURL field if it's non-nil, zero value otherwise. +func (r *ResourceRef) GetBaseURL() string { + if r == nil || r.BaseURL == nil { + return "" + } + return *r.BaseURL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (r *ResourceRef) GetID() string { + if r == nil || r.ID == nil { + return "" + } + return *r.ID +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (r *ResourceRef) GetURL() string { + if r == nil || r.URL == nil { + return "" + } + return *r.URL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (t *TaskAgentPoolReference) GetID() int { + if t == nil || t.ID == nil { + return 0 + } + return *t.ID +} + +// GetIsHosted returns the IsHosted field if it's non-nil, zero value otherwise. +func (t *TaskAgentPoolReference) GetIsHosted() bool { + if t == nil || t.IsHosted == nil { + return false + } + return *t.IsHosted +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *TaskAgentPoolReference) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetPlanID returns the PlanID field if it's non-nil, zero value otherwise. +func (t *TaskOrchestrationPlanReference) GetPlanID() string { + if t == nil || t.PlanID == nil { + return "" + } + return *t.PlanID +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (t *TaskOrchestrationPlanReference) GetType() int { + if t == nil || t.Type == nil { + return 0 + } + return *t.Type +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (t *Team) GetDescription() string { + if t == nil || t.Description == nil { + return "" + } + return *t.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (t *Team) GetID() string { + if t == nil || t.ID == nil { + return "" + } + return *t.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *Team) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (t *Team) GetURL() string { + if t == nil || t.URL == nil { + return "" + } + return *t.URL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (t *TeamProjectCollectionReference) GetID() string { + if t == nil || t.ID == nil { + return "" + } + return *t.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *TeamProjectCollectionReference) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (t *TeamProjectCollectionReference) GetURL() string { + if t == nil || t.URL == nil { + return "" + } + return *t.URL +} + +// GetAbbreviation returns the Abbreviation field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetAbbreviation() string { + if t == nil || t.Abbreviation == nil { + return "" + } + return *t.Abbreviation +} + +// GetDefaultTeamImageURL returns the DefaultTeamImageURL field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetDefaultTeamImageURL() string { + if t == nil || t.DefaultTeamImageURL == nil { + return "" + } + return *t.DefaultTeamImageURL +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetDescription() string { + if t == nil || t.Description == nil { + return "" + } + return *t.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetID() string { + if t == nil || t.ID == nil { + return "" + } + return *t.ID +} + +// GetLastUpdateTime returns the LastUpdateTime field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetLastUpdateTime() time.Time { + if t == nil || t.LastUpdateTime == nil { + return time.Time{} + } + return *t.LastUpdateTime +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetRevision returns the Revision field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetRevision() int { + if t == nil || t.Revision == nil { + return 0 + } + return *t.Revision +} + +// GetState returns the State field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetState() string { + if t == nil || t.State == nil { + return "" + } + return *t.State +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetURL() string { + if t == nil || t.URL == nil { + return "" + } + return *t.URL +} + +// GetVisibility returns the Visibility field if it's non-nil, zero value otherwise. +func (t *TeamProjectReference) GetVisibility() string { + if t == nil || t.Visibility == nil { + return "" + } + return *t.Visibility +} + +// GetMine returns the Mine field if it's non-nil, zero value otherwise. +func (t *TeamsListOptions) GetMine() bool { + if t == nil || t.Mine == nil { + return false + } + return *t.Mine +} + +// GetSkip returns the Skip field if it's non-nil, zero value otherwise. +func (t *TeamsListOptions) GetSkip() int { + if t == nil || t.Skip == nil { + return 0 + } + return *t.Skip +} + +// GetTop returns the Top field if it's non-nil, zero value otherwise. +func (t *TeamsListOptions) GetTop() int { + if t == nil || t.Top == nil { + return 0 + } + return *t.Top +} + +// GetCompletedDate returns the CompletedDate field if it's non-nil, zero value otherwise. +func (t *Test) GetCompletedDate() string { + if t == nil || t.CompletedDate == nil { + return "" + } + return *t.CompletedDate +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (t *Test) GetID() int { + if t == nil || t.ID == nil { + return 0 + } + return *t.ID +} + +// GetIsAutomated returns the IsAutomated field if it's non-nil, zero value otherwise. +func (t *Test) GetIsAutomated() bool { + if t == nil || t.IsAutomated == nil { + return false + } + return *t.IsAutomated +} + +// GetIteration returns the Iteration field if it's non-nil, zero value otherwise. +func (t *Test) GetIteration() string { + if t == nil || t.Iteration == nil { + return "" + } + return *t.Iteration +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *Test) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetRevision returns the Revision field if it's non-nil, zero value otherwise. +func (t *Test) GetRevision() int { + if t == nil || t.Revision == nil { + return 0 + } + return *t.Revision +} + +// GetStartedDate returns the StartedDate field if it's non-nil, zero value otherwise. +func (t *Test) GetStartedDate() string { + if t == nil || t.StartedDate == nil { + return "" + } + return *t.StartedDate +} + +// GetState returns the State field if it's non-nil, zero value otherwise. +func (t *Test) GetState() string { + if t == nil || t.State == nil { + return "" + } + return *t.State +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (t *Test) GetURL() string { + if t == nil || t.URL == nil { + return "" + } + return *t.URL +} + +// GetBuildURI returns the BuildURI field if it's non-nil, zero value otherwise. +func (t *TestsListOptions) GetBuildURI() string { + if t == nil || t.BuildURI == nil { + return "" + } + return *t.BuildURI +} + +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (t *TestsListOptions) GetCount() int { + if t == nil || t.Count == nil { + return 0 + } + return *t.Count +} + +// GetCiMessage returns the CiMessage field if it's non-nil, zero value otherwise. +func (t *TriggerInfo) GetCiMessage() string { + if t == nil || t.CiMessage == nil { + return "" + } + return *t.CiMessage +} + +// GetCiSourceBranch returns the CiSourceBranch field if it's non-nil, zero value otherwise. +func (t *TriggerInfo) GetCiSourceBranch() string { + if t == nil || t.CiSourceBranch == nil { + return "" + } + return *t.CiSourceBranch +} + +// GetCiSourceSha returns the CiSourceSha field if it's non-nil, zero value otherwise. +func (t *TriggerInfo) GetCiSourceSha() string { + if t == nil || t.CiSourceSha == nil { + return "" + } + return *t.CiSourceSha +} + +// GetIsLocked returns the IsLocked field if it's non-nil, zero value otherwise. +func (u *UpdateRefsBody) GetIsLocked() bool { + if u == nil || u.IsLocked == nil { + return false + } + return *u.IsLocked +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (u *UpdateRefsBody) GetName() string { + if u == nil || u.Name == nil { + return "" + } + return *u.Name +} + +// GetNewObjectID returns the NewObjectID field if it's non-nil, zero value otherwise. +func (u *UpdateRefsBody) GetNewObjectID() string { + if u == nil || u.NewObjectID == nil { + return "" + } + return *u.NewObjectID +} + +// GetOldObjectID returns the OldObjectID field if it's non-nil, zero value otherwise. +func (u *UpdateRefsBody) GetOldObjectID() string { + if u == nil || u.OldObjectID == nil { + return "" + } + return *u.OldObjectID +} + +// GetRepositoryID returns the RepositoryID field if it's non-nil, zero value otherwise. +func (u *UpdateRefsBody) GetRepositoryID() string { + if u == nil || u.RepositoryID == nil { + return "" + } + return *u.RepositoryID +} + +// GetMessage returns the Message field if it's non-nil, zero value otherwise. +func (v *ValidationResult) GetMessage() string { + if v == nil || v.Message == nil { + return "" + } + return *v.Message +} + +// GetResult returns the Result field if it's non-nil, zero value otherwise. +func (v *ValidationResult) GetResult() string { + if v == nil || v.Result == nil { + return "" + } + return *v.Result +} + +// GetActive returns the Active field if it's non-nil, zero value otherwise. +func (w *WebAPITagDefinition) GetActive() bool { + if w == nil || w.Active == nil { + return false + } + return *w.Active +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WebAPITagDefinition) GetID() string { + if w == nil || w.ID == nil { + return "" + } + return *w.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (w *WebAPITagDefinition) GetName() string { + if w == nil || w.Name == nil { + return "" + } + return *w.Name +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WebAPITagDefinition) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetCommentVersionRef returns the CommentVersionRef field. +func (w *WorkItem) GetCommentVersionRef() *CommentVersionRef { + if w == nil { + return nil + } + return w.CommentVersionRef +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WorkItem) GetID() int { + if w == nil || w.ID == nil { + return 0 + } + return *w.ID +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (w *WorkItem) GetLinks() map[string]Link { + if w == nil || w.Links == nil { + return map[string]Link{} + } + return *w.Links +} + +// GetRev returns the Rev field if it's non-nil, zero value otherwise. +func (w *WorkItem) GetRev() int { + if w == nil || w.Rev == nil { + return 0 + } + return *w.Rev +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItem) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetCreatedBy returns the CreatedBy field. +func (w *WorkItemComment) GetCreatedBy() *IdentityRef { + if w == nil { + return nil + } + return w.CreatedBy +} + +// GetCreatedDate returns the CreatedDate field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetCreatedDate() time.Time { + if w == nil || w.CreatedDate == nil { + return time.Time{} + } + return *w.CreatedDate +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetID() int { + if w == nil || w.ID == nil { + return 0 + } + return *w.ID +} + +// GetModifiedBy returns the ModifiedBy field. +func (w *WorkItemComment) GetModifiedBy() *IdentityRef { + if w == nil { + return nil + } + return w.ModifiedBy +} + +// GetModifiedDate returns the ModifiedDate field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetModifiedDate() time.Time { + if w == nil || w.ModifiedDate == nil { + return time.Time{} + } + return *w.ModifiedDate +} + +// GetText returns the Text field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetText() string { + if w == nil || w.Text == nil { + return "" + } + return *w.Text +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetVersion returns the Version field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetVersion() int { + if w == nil || w.Version == nil { + return 0 + } + return *w.Version +} + +// GetWorkItemID returns the WorkItemID field if it's non-nil, zero value otherwise. +func (w *WorkItemComment) GetWorkItemID() int { + if w == nil || w.WorkItemID == nil { + return 0 + } + return *w.WorkItemID +} + +// GetContinuationToken returns the ContinuationToken field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetContinuationToken() string { + if w == nil || w.ContinuationToken == nil { + return "" + } + return *w.ContinuationToken +} + +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetCount() int { + if w == nil || w.Count == nil { + return 0 + } + return *w.Count +} + +// GetLinks returns the Links field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetLinks() map[string]Link { + if w == nil || w.Links == nil { + return map[string]Link{} + } + return *w.Links +} + +// GetNextPage returns the NextPage field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetNextPage() string { + if w == nil || w.NextPage == nil { + return "" + } + return *w.NextPage +} + +// GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetTotalCount() int { + if w == nil || w.TotalCount == nil { + return 0 + } + return *w.TotalCount +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItemCommentList) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetRel returns the Rel field if it's non-nil, zero value otherwise. +func (w *WorkItemLink) GetRel() string { + if w == nil || w.Rel == nil { + return "" + } + return *w.Rel +} + +// GetSource returns the Source field. +func (w *WorkItemLink) GetSource() *WorkItemReference { + if w == nil { + return nil + } + return w.Source +} + +// GetTarget returns the Target field. +func (w *WorkItemLink) GetTarget() *WorkItemReference { + if w == nil { + return nil + } + return w.Target +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WorkItemReference) GetID() int { + if w == nil || w.ID == nil { + return 0 + } + return *w.ID +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItemReference) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetRel returns the Rel field if it's non-nil, zero value otherwise. +func (w *WorkItemRelation) GetRel() string { + if w == nil || w.Rel == nil { + return "" + } + return *w.Rel +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItemRelation) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetFields returns the Fields field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetFields() map[string]WorkItemFieldUpdate { + if w == nil || w.Fields == nil { + return map[string]WorkItemFieldUpdate{} + } + return *w.Fields +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetID() int { + if w == nil || w.ID == nil { + return 0 + } + return *w.ID +} + +// GetRelations returns the Relations field. +func (w *WorkItemUpdate) GetRelations() *WorkItemRelationUpdates { + if w == nil { + return nil + } + return w.Relations +} + +// GetRev returns the Rev field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetRev() int { + if w == nil || w.Rev == nil { + return 0 + } + return *w.Rev +} + +// GetRevisedBy returns the RevisedBy field. +func (w *WorkItemUpdate) GetRevisedBy() *IdentityRef { + if w == nil { + return nil + } + return w.RevisedBy +} + +// GetRevisedDate returns the RevisedDate field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetRevisedDate() time.Time { + if w == nil || w.RevisedDate == nil { + return time.Time{} + } + return *w.RevisedDate +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + +// GetWorkItemID returns the WorkItemID field if it's non-nil, zero value otherwise. +func (w *WorkItemUpdate) GetWorkItemID() int { + if w == nil || w.WorkItemID == nil { + return 0 + } + return *w.WorkItemID +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops.go new file mode 100644 index 000000000..b62855de7 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops.go @@ -0,0 +1,304 @@ +package azuredevops + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "reflect" + "strings" + + "github.com/google/go-querystring/query" +) + +const ( + // DefaultBaseURL is the default URI base for most Azure Devops REST API endpoints + DefaultBaseURL string = "https://dev.azure.com/" + // DefaultVsspsBaseURL is the default URI base for some Azure Devops REST API endpoints + DefaultVsspsBaseURL string = "https://vssps.dev.azure.com/" + // userAgent our HTTP client's user-agent + userAgent string = "go-azuredevops" +) + +// Client for interacting with the Azure DevOps API +type Client struct { + client *http.Client + + // BaseURL Comprised of baseURL and account + BaseURL url.URL + + VsspsBaseURL url.URL + + UserAgent string + + // Account Default tenant identifier + Account string + + // Services used to proxy to other API endpoints + Boards *BoardsService + BuildDefinitions *BuildDefinitionsService + Builds *BuildsService + DeliveryPlans *DeliveryPlansService + Favourites *FavouritesService + Git *GitService + Iterations *IterationsService + PullRequests *PullRequestsService + Teams *TeamsService + Tests *TestsService + Users *UsersService + WorkItems *WorkItemsService +} + +// NewClient returns a new Azure DevOps API client. If a nil httpClient is +// provided, http.DefaultClient will be used. To use API methods which require +// authentication, provide an http.Client that will perform the authentication +// for you (such as that provided by the golang.org/x/oauth2 library). +func NewClient(httpClient *http.Client) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + + c := &Client{} + baseURL, _ := url.Parse(DefaultBaseURL) + vsspsBaseURL, _ := url.Parse(DefaultVsspsBaseURL) + + c.client = httpClient + c.BaseURL = *baseURL + c.VsspsBaseURL = *vsspsBaseURL + c.UserAgent = userAgent + + c.Boards = &BoardsService{client: c} + c.BuildDefinitions = &BuildDefinitionsService{client: c} + c.Builds = &BuildsService{client: c} + c.DeliveryPlans = &DeliveryPlansService{client: c} + c.Favourites = &FavouritesService{client: c} + c.Git = &GitService{client: c} + c.Iterations = &IterationsService{client: c} + c.PullRequests = &PullRequestsService{client: c} + c.Teams = &TeamsService{client: c} + c.Tests = &TestsService{client: c} + c.Users = &UsersService{client: c} + c.WorkItems = &WorkItemsService{client: c} + + return c, nil +} + +// NewRequest creates an API request. A relative URL can be provided in urlStr, +// in which case it is resolved relative to the BaseURL of the Client. +// Relative URLs should always be specified without a preceding slash. If +// specified, the value pointed to by body is JSON encoded and included as the +// request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + if !strings.HasSuffix(c.BaseURL.Path, "/") { + return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL.String()) + } + + u, err := c.BaseURL.Parse(urlStr) + if err != nil { + return nil, err + } + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err := enc.Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + return req, nil +} + +// Execute sends an API request and returns the API response. The API response is +// JSON decoded and stored in the value pointed to by r, or returned as an +// error if an API error has occurred. If r implements the io.Writer +// interface, the raw response body will be written to r, without attempting to +// first decode it. +// +// The provided ctx must be non-nil. If it is canceled or times out, +// ctx.Err() will be returned. +func (c *Client) Execute(ctx context.Context, req *http.Request, r interface{}) (*http.Response, error) { + req = req.WithContext(ctx) + debugReq(req) + resp, err := c.client.Do(req) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // If the error type is *url.Error, sanitize its URL before returning. + if e, ok := err.(*url.Error); ok { + if url, err := url.Parse(e.URL); err == nil { + e.URL = sanitizeURL(url).String() + return nil, e + } + } + + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + return nil, fmt.Errorf("Request to %s responded with status %d", req.URL, resp.StatusCode) + } + + if r != nil { + if w, ok := r.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + decErr := json.NewDecoder(resp.Body).Decode(r) + if decErr == io.EOF { + decErr = nil // ignore EOF errors caused by empty response body + } + if decErr != nil { + err = decErr + } + } + } + + return resp, err +} + +// BasicAuthTransport is an http.RoundTripper that authenticates all requests +// using HTTP Basic Authentication with the provided username and password. It +// additionally supports users who have two-factor authentication enabled on +// their GitHub account. +type BasicAuthTransport struct { + Username string // Azure Devops username + Password string // Azure Devops password + OTP string // one-time password for users with two-factor auth enabled + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip implements the RoundTripper interface. +func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // To set extra headers, we must make a copy of the Request so + // that we don't modify the Request we were given. This is required by the + // specification of http.RoundTripper. + // + // Since we are going to modify only req.Header here, we only need a deep copy + // of req.Header. + req2 := new(http.Request) + *req2 = *req + req2.Header = make(http.Header, len(req.Header)) + for k, s := range req.Header { + req2.Header[k] = append([]string(nil), s...) + } + + //req2.SetBasicAuth(t.Username, t.Password) + req2.SetBasicAuth("", t.Password) + + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using HTTP Basic Authentication. +func (t *BasicAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *BasicAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +// From: https://github.com/google/go-github/blob/master/github/github.go +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + for k, v := range u.Query() { + qs[k] = v + } + + u.RawQuery = qs.Encode() + return u.String(), nil +} + +// formatRef helper function for API calls that need a branch reference +// as an input parameter. Doesn't do much error checking. +// Examples: +// *ref = "mybranch" => *ref = "refs/heads/mybranch" +// *ref = "refs/heads/abranch" => *ref = "refs/heads/abranch" +func formatRef(ref *string) error { + matched, err := path.Match("refs/heads/*", *ref) + if matched && err == nil { + return nil + } else if err != nil { + return err + } else { + *ref = fmt.Sprintf("refs/heads/%s", *ref) + return nil + } +} + +// sanitizeURL redacts the client_secret parameter from the URL which may be +// exposed to the user. +func sanitizeURL(uri *url.URL) *url.URL { + if uri == nil { + return nil + } + params := uri.Query() + if len(params.Get("client_secret")) > 0 { + params.Set("client_secret", "REDACTED") + uri.RawQuery = params.Encode() + } + return uri +} + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { return &v } + +// Int is a helper routine that allocates a new int value +// to store v and returns a pointer to it. +func Int(v int) *int { return &v } + +// Int64 is a helper routine that allocates a new int64 value +// to store v and returns a pointer to it. +func Int64(v int64) *int64 { return &v } + +// String is a helper routine that allocates a new string value +// to store v and returns a pointer to it. +func String(v string) *string { return &v } diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/boards.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/boards.go new file mode 100644 index 000000000..9f74ac7a5 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/boards.go @@ -0,0 +1,124 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// BoardsService handles communication with the boards methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/boards +type BoardsService struct { + client *Client +} + +// ListBoardsResponse describes the boards response +type ListBoardsResponse struct { + Count *int `json:"count,omitempty"` + BoardReferences []*BoardReference `json:"value,omitempty"` +} + +// Board describes a board +type Board struct { + BoardReference + Links *map[string]Link `json:"_links,omitempty"` + AllowedMappings *string `json:"allowedMappings,omitempty"` + CanEdit *bool `json:"canEdit,omitempty"` + Columns []*BoardColumn `json:"columns,omitempty"` + Fields *BoardFields `json:"fields,omitempty"` + IsValid *bool `json:"isvalid,omitempty"` + Revision *int `json:"revision,omitempty"` + Rows []*BoardRow `json:"rows,omitempty"` +} + +// BoardColumn describes a column on the board +type BoardColumn struct { + ColumnType *string `json:"columnType,omitempty"` + Description *string `json:"description,omitempty"` + ID *string `json:"id,omitempty"` + IsSplit *bool `json:"isSplit,omitempty"` + ItemLimit *int `json:"itemLimit,omitempty"` + Name *string `json:"name,omitempty"` + StateMappings map[string]string `json:"stateMappings,omitempty"` +} + +// BoardColumnType describes a column on the board +type BoardColumnType int + +// BoardColumnType Enum values +const ( + Incoming BoardColumnType = iota + InProgress + Outgoing +) + +func (d BoardColumnType) String() string { + return [...]string{"Incoming", "InProgress", "Outgoing"}[d] +} + +// BoardFields describes a column on the board +type BoardFields struct { + ColumnField *FieldReference `json:"columnField,omitempty"` + DoneField *FieldReference `json:"doneField,omitempty"` + RowField *FieldReference `json:"rowField,omitempty"` +} + +// BoardReference Base object a board +type BoardReference struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"URL,omitempty"` +} + +// BoardRow describes a row on the board +type BoardRow struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +// FieldReference describes a row on the board +type FieldReference struct { + ReferenceName *string `json:"referenceName,omitempty"` + URL *string `json:"url,omitempty"` +} + +// List returns list of the boards +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/boards/list +func (s *BoardsService) List(ctx context.Context, owner, project, team string) ([]*BoardReference, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/%s/_apis/work/boards?api-version=5.1-preview.1", + owner, + project, + url.PathEscape(team), + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(ListBoardsResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.BoardReferences, resp, err +} + +// Get returns a single board utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/boards/get +func (s *BoardsService) Get(ctx context.Context, owner, project, team, id string) (*Board, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/%s/_apis/work/boards/%s?api-version=5.1-preview.1", + owner, + project, + url.PathEscape(team), + id, + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(Board) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_controller.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_controller.go new file mode 100644 index 000000000..a3688c450 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_controller.go @@ -0,0 +1,14 @@ +package azuredevops + +// BuildController represents a controller of the build service +type BuildController struct { + CreatedDate *string `json:"createdDate"` + Description *string `json:"description"` + Enabled *bool `json:"enabled"` + ID *int `json:"id"` + Name *string `json:"name"` + Status *string `json:"status"` + UpdateDate *string `json:"updateDate"` + URI *string `json:"uri"` + URL *string `json:"url"` +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_definitions.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_definitions.go new file mode 100644 index 000000000..624258479 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_definitions.go @@ -0,0 +1,64 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" +) + +// BuildDefinitionsService handles communication with the build definitions methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/build/definitions +type BuildDefinitionsService struct { + client *Client +} + +// BuildDefinitionsListResponse describes the build definitions list response +type BuildDefinitionsListResponse struct { + Count int `json:"count"` + BuildDefinitions []*BuildDefinition `json:"value"` +} + +// BuildRepository represents a repository used by a build definition +type BuildRepository struct { + ID *string `json:"id,omitempty"` + Type *string `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + RootFolder *string `json:"root_folder"` + Properties map[string]interface{} `json:"properties"` + Clean *string `json:"clean"` + DefaultBranch *string `json:"default_branch"` + CheckoutSubmodules *bool `json:"checkout_submodules"` +} + +// BuildDefinition represents a build definition +type BuildDefinition struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Repository *BuildRepository `json:"repository,omitempty"` +} + +// BuildDefinitionsListOptions describes what the request to the API should look like +type BuildDefinitionsListOptions struct { + Path *string `url:"path,omitempty"` + IncludeAllProperties *bool `url:"includeAllProperties,omitempty"` +} + +// List returns a list of build definitions +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/build/definitions/list +func (s *BuildDefinitionsService) List(ctx context.Context, owner string, project string, opts *BuildDefinitionsListOptions) ([]*BuildDefinition, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/build/definitions?api-version=5.1-preview.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(BuildDefinitionsListResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.BuildDefinitions, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/builds.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/builds.go new file mode 100644 index 000000000..f4dbab05d --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/builds.go @@ -0,0 +1,203 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" +) + +// BuildsService handles communication with the builds methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/build/builds +type BuildsService struct { + client *Client +} + +// BuildsListResponse is the wrapper around the main response for the List of Builds +type BuildsListResponse struct { + Count int `json:"count"` + Builds []*Build `json:"value"` +} + +// TaskOrchestrationPlanReference The orchestration plan for the build. +type TaskOrchestrationPlanReference struct { + Type *int `json:"orchestrationType,omitempty"` + PlanID *string `json:"planId,omitempty"` +} + +// Build Represents a build. +type Build struct { + Definition *BuildDefinition `json:"definition,omitempty"` + Controller *BuildController `json:"controller,omitempty"` + LastChangedBy *IdentityRef `json:"lastChangedBy,omitempty"` + DeletedBy *IdentityRef `json:"deletedBy,omitempty"` + BuildNumber *string `json:"buildNumber,omitempty"` + FinishTime *string `json:"finishTime,omitempty"` + SourceBranch *string `json:"sourceBranch,omitempty"` + Repository *BuildRepository `json:"repository,omitempty"` + Demands []*BuildDemand `json:"demands,omitempty"` + Logs *BuildLogReference `json:"logs,omitempty"` + Project *TeamProjectReference `json:"project,omitempty"` + Properties map[string]string `json:"properties,omitempty"` + Priority *string `json:"priority,omitempty"` + OrchestrationPlan *TaskOrchestrationPlanReference `json:"orchestrationPlan,omitempty"` + Plans []*TaskOrchestrationPlanReference `json:"plans,omitempty"` + BuildNumberRevision *int `json:"buildNumberRevision,omitempty"` + Deleted *bool `json:"deleted,omitempty"` + DeletedDate *string `json:"deletedDate,omitempty"` + DeletedReason *string `json:"deletedReason,omitempty"` + ID *int `json:"id,omitempty"` + KeepForever *bool `json:"keepForever,omitempty"` + ChangedDate *string `json:"lastChangedDate,omitempty"` + Params *string `json:"parameters,omitempty"` + Quality *string `json:"quality,omitempty"` + Queue *AgentPoolQueue `json:"queue,omitempty"` + QueueOptions map[string]string `json:"queue_options,omitempty"` + QueuePosition *int `json:"queuePosition,omitempty"` + QueueTime *string `json:"queueTime,omitempty"` + RetainedByRelease *bool `json:"retainedByRelease,omitempty"` + Version *string `json:"sourceVersion,omitempty"` + StartTime *string `json:"startTime,omitempty"` + Status *string `json:"status,omitempty"` + Result *string `json:"result,omitempty"` + ValidationResults []*ValidationResult `json:"validationResult,omitempty"` + Tags []*string `json:"tags,omitempty"` + TriggerBuild *Build `json:"triggeredByBuild,omitempty"` + URI *string `json:"uri,omitempty"` + URL *string `json:"url,omitempty"` + TriggerInfo *TriggerInfo `json:"triggerInfo,omitempty"` +} + +// AgentPoolQueue The queue. This is only set if the definition type is Build. +type AgentPoolQueue struct { + Links *map[string]Link `json:"_links,omitempty"` + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + Pool *TaskAgentPoolReference `json:"pool,omitempty"` +} + +// TaskAgentPoolReference Represents a reference to an agent pool. +type TaskAgentPoolReference struct { + ID *int `json:"id,omitempty"` + IsHosted *bool `json:"is_hosted,omitempty"` + Name *string `json:"name,omitempty"` +} + +// TriggerInfo Source provider-specific information about what triggered the build. +type TriggerInfo struct { + CiSourceBranch *string `json:"ci.sourceBranch,omitempty"` + CiSourceSha *string `json:"ci.sourceSha,omitempty"` + CiMessage *string `json:"ci.message,omitempty"` +} + +// ValidationResult Represents the result of validating a build request. +type ValidationResult struct { + Message *string `json:"message,omitempty"` + Result *string `json:"result,omitempty"` +} + +// BuildDemand Represents a demand used by a definition or build. +type BuildDemand struct { + Name *string `json:"name,omitempty"` + Value *string `json:"value,omitempty"` +} + +// BuildListOrder is enum type for build list order +type BuildListOrder string + +const ( + // FinishTimeAscending orders by finish build time asc + FinishTimeAscending BuildListOrder = "finishTimeAscending" + // FinishTimeDescending orders by finish build time desc + FinishTimeDescending BuildListOrder = "finishTimeDescending" + // QueueTimeAscending orders by build queue time asc + QueueTimeAscending BuildListOrder = "queueTimeAscending" + // QueueTimeDescending orders by build queue time desc + QueueTimeDescending BuildListOrder = "queueTimeDescending" + // StartTimeAscending orders by build start time asc + StartTimeAscending BuildListOrder = "startTimeAscending" + // StartTimeDescending orders by build start time desc + StartTimeDescending BuildListOrder = "startTimeDescending" +) + +// BuildsListOptions describes what the request to the API should look like +type BuildsListOptions struct { + Definitions *string `url:"definitions,omitempty"` + Branch *string `url:"branchName,omitempty"` + Count *int `url:"$top,omitempty"` + Repository *string `url:"repositoryId,omitempty"` + BuildIDs *string `url:"buildIds,omitempty"` + Order *string `url:"queryOrder,omitempty"` + Deleted *string `url:"deletedFilter,omitempty"` + MaxPerDefinition *string `url:"maxBuildsPerDefinition,omitempty"` + Token *string `url:"continuationToken,omitempty"` + Props *string `url:"properties,omitempty"` + Tags *string `url:"tagFilters,omitempty"` + Result *string `url:"resultFilter,omitempty"` + Status *string `url:"statusFilter,omitempty"` + Reason *string `url:"reasonFilter,omitempty"` + UserID *string `url:"requestedFor,omitempty"` + MaxTime *string `url:"maxTime,omitempty"` + MinTime *string `url:"minTime,omitempty"` + BuildNumber *string `url:"buildNumber,omitempty"` + Queues *string `url:"queues,omitempty"` + RepoType *string `url:"repositoryType,omitempty"` +} + +// BuildLogReference Information about the build logs. +type BuildLogReference struct { + ID *int `json:"id"` + Type *string `json:"type"` + URL *string `json:"url"` +} + +// List returns list of the builds +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/build/builds/list +func (s *BuildsService) List(ctx context.Context, owner string, project string, opts *BuildsListOptions) ([]*Build, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/build/builds?api-version=5.1-preview.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(BuildsListResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.Builds, resp, err +} + +// QueueBuildOptions describes what the request to the API should look like +type QueueBuildOptions struct { + IgnoreWarnings bool `url:"ignoreWarnings,omitempty"` + CheckInTicket string `url:"checkInTicket,omitempty"` +} + +// Queue inserts new build creation to queue +// Requires build ID number in build.definition +// Example body: +// {"definition": {"id": 1}, "sourceBranch": "refs/heads/master"} +// utilising https://docs.microsoft.com/en-us/rest/api/azure/devops/build/Builds/Queue +func (s *BuildsService) Queue(ctx context.Context, owner string, project string, build *Build, opts *QueueBuildOptions) (*Build, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/build/builds?api-version=5.1-preview.5", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("POST", URL, build) + if err != nil { + return nil, nil, err + } + r := new(Build) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/debug.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/debug.go new file mode 100644 index 000000000..e0b24a1e0 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/debug.go @@ -0,0 +1,20 @@ +// +build debug + +package azuredevops + +import ( + "log" + "net/http" + "net/http/httputil" +) + +// Idea from: +// https://dave.cheney.net/2014/09/28/using-build-to-switch-between-debug-and-release +func debug(fmt string, args ...interface{}) { + log.Printf(fmt, args...) +} + +func debugReq(req *http.Request) { + dump, err := httputil.DumpRequest(req, true) + debug(string(dump), err) +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/delivery_plans.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/delivery_plans.go new file mode 100644 index 000000000..9f857568b --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/delivery_plans.go @@ -0,0 +1,109 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// DeliveryPlansService handles communication with the deliverytimeline methods on the API +// utilising https://docs.microsoft.com/en-us/rest/api/vsts/work/deliverytimeline +type DeliveryPlansService struct { + client *Client +} + +// DeliveryPlansListResponse describes the delivery plans list response +type DeliveryPlansListResponse struct { + Count int `json:"count"` + DeliveryPlans []*DeliveryPlan `json:"value"` +} + +// DeliveryPlanTimeLine describes the delivery plan get response +type DeliveryPlanTimeLine struct { + StartDate *string `json:"startDate,omitempty"` + EndDate *string `json:"endDate,omitempty"` + ID *string `json:"id,omitempty"` + Revision *int `json:"revision,omitempty"` + Teams []*DeliveryTeam `json:"teams,omitempty"` +} + +// DeliveryPlan describes an plan +type DeliveryPlan struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Created *string `json:"createdDate,omitempty"` + URL *string `json:"url,omitempty"` +} + +// DeliveryTeam describes the teams in a specific plan +type DeliveryTeam struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Iterations []*Iteration `json:"iterations,omitempty"` +} + +const ( + // DeliveryPlanWorkItemIDKey is the key for which part of the workItems[] slice has the ID + DeliveryPlanWorkItemIDKey = 0 + // DeliveryPlanWorkItemIterationKey is the key for which part of the workItems[] slice has the Iteration + DeliveryPlanWorkItemIterationKey = 1 + // DeliveryPlanWorkItemTypeKey is the key for which part of the workItems[] slice has the Type + DeliveryPlanWorkItemTypeKey = 2 + // DeliveryPlanWorkItemNameKey is the key for which part of the workItems[] slice has the Name + DeliveryPlanWorkItemNameKey = 4 + // DeliveryPlanWorkItemStatusKey is the key for which part of the workItems[] slice has the Status + DeliveryPlanWorkItemStatusKey = 5 + // DeliveryPlanWorkItemTagKey is the key for which part of the workItems[] slice has the Tag + DeliveryPlanWorkItemTagKey = 6 +) + +// DeliveryPlansListOptions describes what the request to the API should look like +type DeliveryPlansListOptions struct { +} + +// List returns a list of delivery plans +func (s *DeliveryPlansService) List(ctx context.Context, owner string, project string, opts *DeliveryPlansListOptions) ([]*DeliveryPlan, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/work/plans?api-version=5.1-preview.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(DeliveryPlansListResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.DeliveryPlans, resp, err +} + +// GetTimeLine will fetch the details about a specific delivery plan +func (s *DeliveryPlansService) GetTimeLine(ctx context.Context, owner string, project string, ID string, startDate, endDate string) (*DeliveryPlanTimeLine, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/work/plans/%s/deliverytimeline?api-version=5.1-preview.1", + owner, + project, + ID, + ) + + if startDate == "" { + startDate = time.Now().Format("2006-01-02") + // The 65 date thing is arbitrary from the API + endDate = time.Now().AddDate(0, 0, 65).Format("2006-01-02") + } + + URL = fmt.Sprintf(URL+"&startDate=%s&endDate=%s", startDate, endDate) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(DeliveryPlanTimeLine) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/doc.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/doc.go new file mode 100644 index 000000000..c9a785850 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/doc.go @@ -0,0 +1,5 @@ +/* +Package azuredevops is a Go client library for accessing the Azure DevOps API. +Installation +*/ +package azuredevops diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/event_types.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/event_types.go new file mode 100644 index 000000000..cc218e3da --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/event_types.go @@ -0,0 +1,49 @@ +// Copyright 2016 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Adapted to handle a subset of Pull Request webooks from Azure Devops +// Azure Devops Events docs: https://docs.microsoft.com/en-us/azure/devops/service-hooks/events?view=azure-devops + +package azuredevops + +// ItemContent describes an item +type ItemContent struct { + Content *string `json:"content,omitempty"` + ContentType *ItemContentType `json:"contentType,omitempty"` +} + +// ItemContentType describes an item content type +type ItemContentType struct { + Base64Encoded *string `json:"base64Encoded,omitempty"` + RawText *string `json:"rawText,omitempty"` +} + +// Link A single item in a collection of Links. +type Link struct { + Href *string `json:"href,omitempty"` +} + +// ResourceContainers provides information related to the Resources in a payload +type ResourceContainers struct { + Collection *ResourceRef `json:"text,omitempty"` + Account *ResourceRef `json:"html,omitempty"` + Project *ResourceRef `json:"markdown,omitempty"` +} + +// ResourceRef Describes properties to identify a resource +type ResourceRef struct { + ID *string `json:"id,omitempty"` + BaseURL *string `json:"baseUrl,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WebAPITagDefinition The representation of a tag definition which is sent across +// the wire. +type WebAPITagDefinition struct { + Active *bool `json:"active,omitempty"` + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/events.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/events.go new file mode 100644 index 000000000..c3ea324c3 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/events.go @@ -0,0 +1,106 @@ +// Copyright 2018 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package azuredevops + +import ( + "encoding/json" + "errors" + "fmt" + "time" +) + +// Message represents an Azure Devops webhook message property +type Message struct { + Text *string `json:"text,omitempty"` + HTML *string `json:"html,omitempty"` + Markdown *string `json:"markdown,omitempty"` +} + +// Event - Describes an Azure Devops webhook payload parent +// Delay parsing Resource using *json.RawMessage +// until we know EventType. The contents of Resource change +// depending on EventType. +// PayloadType is filled with an enum that describes the type of resource +// payload. +type Event struct { + SubscriptionID string `json:"subscriptionId,omitempty"` + NotificationID int `json:"notificationId,omitempty"` + ID string `json:"id,omitempty"` + EventType string `json:"eventType,omitempty"` + Message Message `json:"message,omitempty"` + DetailedMessage Message `json:"detailedMessage,omitempty"` + RawPayload json.RawMessage `json:"resource,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` + ResourceContainers ResourceContainers `json:"resourceContainers,omitempty"` + CreatedDate time.Time `json:"createdDate,omitempty"` + Resource interface{} + PayloadType PayloadType +} + +// PayloadType Used to describe the event area +type PayloadType int + +const ( + // PullRequestCommentedEvent Resource field is parsed as a pull + // request commented event (note this maps to resourceVersion + // 2.0 of the payload struct, which is incompatible with 1.0 + PullRequestCommentedEvent PayloadType = iota + // PullRequestEvent Resource field is parsed as a pull request event + PullRequestEvent + // PushEvent Git push service event + PushEvent + // WorkItemCommentedEvent Resource field is parsed as a work item + // commented event + WorkItemCommentedEvent + // WorkItemUpdatedEvent Resource field is parsed as a work item + // updated event + WorkItemUpdatedEvent +) + +// ParsePayload parses the event payload. For recognized event types, +// it returns the webhook payload with a parsed struct in the +// Event.Resource field. +func (e *Event) ParsePayload() (payload interface{}, err error) { + switch e.EventType { + case "git.pullrequest.created": + e.PayloadType = PullRequestEvent + payload = &GitPullRequest{} + case "git.pullrequest.merged": + e.PayloadType = PullRequestEvent + payload = &GitPullRequest{} + case "git.pullrequest.updated": + e.PayloadType = PullRequestEvent + payload = &GitPullRequest{} + case "git.push": + e.PayloadType = PushEvent + payload = &GitPush{} + case "ms.vss-code.git-pullrequest-comment-event": + e.PayloadType = PullRequestCommentedEvent + payload = &GitPullRequestWithComment{} + case "workitem.commented": + e.PayloadType = WorkItemCommentedEvent + payload = &WorkItem{} + case "workitem.updated": + e.PayloadType = WorkItemUpdatedEvent + payload = &WorkItemUpdate{} + default: + return payload, errors.New("Unknown EventType in webhook payload") + } + + if len(e.RawPayload) == 0 { + msg := "JSON zero byte resource field in payload" + fmt.Printf("%s: %#v \n\n", msg, e) + return nil, errors.New(msg) + } + + err = json.Unmarshal(e.RawPayload, &payload) + if err != nil { + fmt.Printf("JSON unmarshal err: %#v \n\n %#v \n\n", payload, err) + return payload, err + } + e.Resource = payload + return payload, nil +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/favourites.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/favourites.go new file mode 100644 index 000000000..99383b717 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/favourites.go @@ -0,0 +1,47 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" +) + +// FavouritesService handles communication with the favourites methods on the API +// So far it looks like this is undocumented, so this could change +type FavouritesService struct { + client *Client +} + +// FavouritesResponse describes the favourites response +type FavouritesResponse struct { + Count int `json:"count"` + Favourites []*Favourite `json:"value"` +} + +// Favourite describes what a favourite is +type Favourite struct { + ID *string `json:"id,omitempty"` + ArtifactName *string `json:"artifactName,omitempty"` + ArtifactType *string `json:"artifactType,omitempty"` + ArtifactID *string `json:"artifactId,omitempty"` +} + +// List returns a list of the favourite items from for the user +func (s *FavouritesService) List(ctx context.Context, owner, project string) ([]*Favourite, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/Favorite/Favorites?artifactType=%s", + owner, + project, + "Microsoft.TeamFoundation.Git.Repository", // @todo This needs fixing + ) + + u, _ := s.client.BaseURL.Parse(URL) + req, err := s.client.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, nil, err + } + r := new(FavouritesResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.Favourites, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/fields.json b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/fields.json new file mode 100644 index 000000000..05d627109 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/fields.json @@ -0,0 +1,449 @@ +{ + "value" : [ + { + "helpText" : "The iteration within which this task will be completed", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationPath", + "alwaysRequired" : false, + "referenceName" : "System.IterationPath", + "defaultValue" : null, + "name" : "Iteration Path" + }, + { + "name" : "Iteration ID", + "alwaysRequired" : true, + "referenceName" : "System.IterationId", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationId" + }, + { + "referenceName" : "System.ExternalLinkCount", + "alwaysRequired" : false, + "name" : "External Link Count", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.ExternalLinkCount" + }, + { + "referenceName" : "System.IterationLevel7", + "name" : "Iteration Level 7", + "alwaysRequired" : false, + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel7" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel6", + "defaultValue" : null, + "referenceName" : "System.IterationLevel6", + "alwaysRequired" : false, + "name" : "Iteration Level 6" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel5", + "referenceName" : "System.IterationLevel5", + "alwaysRequired" : false, + "name" : "Iteration Level 5", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel4", + "alwaysRequired" : false, + "name" : "Iteration Level 4", + "referenceName" : "System.IterationLevel4", + "defaultValue" : null + }, + { + "defaultValue" : null, + "name" : "Iteration Level 3", + "alwaysRequired" : false, + "referenceName" : "System.IterationLevel3", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel3" + }, + { + "referenceName" : "System.IterationLevel2", + "alwaysRequired" : false, + "name" : "Iteration Level 2", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel2" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.IterationLevel1", + "defaultValue" : null, + "referenceName" : "System.IterationLevel1", + "name" : "Iteration Level 1", + "alwaysRequired" : false + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel7", + "referenceName" : "System.AreaLevel7", + "name" : "Area Level 7", + "alwaysRequired" : false, + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel6", + "defaultValue" : null, + "referenceName" : "System.AreaLevel6", + "alwaysRequired" : false, + "name" : "Area Level 6" + }, + { + "name" : "Area Level 5", + "alwaysRequired" : false, + "referenceName" : "System.AreaLevel5", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel5" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel4", + "alwaysRequired" : false, + "name" : "Area Level 4", + "referenceName" : "System.AreaLevel4", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel3", + "defaultValue" : null, + "name" : "Area Level 3", + "alwaysRequired" : false, + "referenceName" : "System.AreaLevel3" + }, + { + "defaultValue" : null, + "referenceName" : "System.AreaLevel2", + "name" : "Area Level 2", + "alwaysRequired" : false, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel2" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaLevel1", + "defaultValue" : null, + "alwaysRequired" : false, + "name" : "Area Level 1", + "referenceName" : "System.AreaLevel1" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.TeamProject", + "name" : "Team Project", + "alwaysRequired" : false, + "referenceName" : "System.TeamProject", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.RemoteLinkCount", + "defaultValue" : null, + "alwaysRequired" : false, + "name" : "Remote Link Count", + "referenceName" : "System.RemoteLinkCount" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.CommentCount", + "defaultValue" : null, + "referenceName" : "System.CommentCount", + "alwaysRequired" : false, + "name" : "Comment Count" + }, + { + "referenceName" : "System.HyperLinkCount", + "alwaysRequired" : false, + "name" : "Hyperlink Count", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.HyperLinkCount" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AttachedFileCount", + "alwaysRequired" : false, + "name" : "Attached File Count", + "referenceName" : "System.AttachedFileCount", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.NodeName", + "referenceName" : "System.NodeName", + "alwaysRequired" : false, + "name" : "Node Name", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaPath", + "helpText" : "The area of the product to which this task contributes", + "referenceName" : "System.AreaPath", + "alwaysRequired" : false, + "name" : "Area Path", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.RevisedDate", + "defaultValue" : null, + "name" : "Revised Date", + "alwaysRequired" : false, + "referenceName" : "System.RevisedDate" + }, + { + "referenceName" : "System.ChangedDate", + "alwaysRequired" : false, + "name" : "Changed Date", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.ChangedDate" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Id", + "defaultValue" : null, + "referenceName" : "System.Id", + "alwaysRequired" : false, + "name" : "ID" + }, + { + "referenceName" : "System.AreaId", + "alwaysRequired" : true, + "name" : "Area ID", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AreaId" + }, + { + "defaultValue" : null, + "referenceName" : "System.AuthorizedAs", + "name" : "Authorized As", + "alwaysRequired" : false, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AuthorizedAs" + }, + { + "defaultValue" : null, + "name" : "Title", + "referenceName" : "System.Title", + "alwaysRequired" : true, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Title", + "helpText" : "Work required and how this will implement a User Story" + }, + { + "name" : "State", + "defaultValue" : "New", + "helpText" : "New = New work not yet activated; Active = work remains to be done; Closed = tested and checked in.", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.State", + "alwaysRequired" : true, + "referenceName" : "System.State" + }, + { + "defaultValue" : null, + "name" : "Authorized Date", + "alwaysRequired" : false, + "referenceName" : "System.AuthorizedDate", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AuthorizedDate" + }, + { + "referenceName" : "System.Watermark", + "name" : "Watermark", + "alwaysRequired" : false, + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Watermark" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Rev", + "defaultValue" : null, + "name" : "Rev", + "alwaysRequired" : false, + "referenceName" : "System.Rev" + }, + { + "alwaysRequired" : false, + "name" : "Changed By", + "referenceName" : "System.ChangedBy", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.ChangedBy" + }, + { + "defaultValue" : null, + "name" : "Reason", + "alwaysRequired" : false, + "referenceName" : "System.Reason", + "helpText" : "The reason why the task is in its current state", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Reason" + }, + { + "helpText" : "The person currently working on this task", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.AssignedTo", + "referenceName" : "System.AssignedTo", + "alwaysRequired" : false, + "name" : "Assigned To", + "defaultValue" : null + }, + { + "defaultValue" : null, + "name" : "Work Item Type", + "alwaysRequired" : false, + "referenceName" : "System.WorkItemType", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.WorkItemType" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.CreatedDate", + "defaultValue" : null, + "referenceName" : "System.CreatedDate", + "alwaysRequired" : false, + "name" : "Created Date" + }, + { + "defaultValue" : null, + "alwaysRequired" : false, + "name" : "Created By", + "referenceName" : "System.CreatedBy", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.CreatedBy" + }, + { + "helpText" : "What to do, pointers to resources and inputs, design notes, exit criteria", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Description", + "alwaysRequired" : false, + "referenceName" : "System.Description", + "defaultValue" : null, + "name" : "Description" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.History", + "helpText" : "Discussion thread plus automatic record of changes", + "referenceName" : "System.History", + "alwaysRequired" : false, + "name" : "History", + "defaultValue" : null + }, + { + "defaultValue" : null, + "name" : "Related Link Count", + "alwaysRequired" : false, + "referenceName" : "System.RelatedLinkCount", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.RelatedLinkCount" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.Tags", + "referenceName" : "System.Tags", + "name" : "Tags", + "alwaysRequired" : false, + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.BoardColumn", + "alwaysRequired" : false, + "name" : "Board Column", + "referenceName" : "System.BoardColumn", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.BoardColumnDone", + "defaultValue" : null, + "referenceName" : "System.BoardColumnDone", + "alwaysRequired" : false, + "name" : "Board Column Done" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/System.BoardLane", + "alwaysRequired" : false, + "name" : "Board Lane", + "referenceName" : "System.BoardLane", + "defaultValue" : null + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.Activity", + "helpText" : "Type of work involved", + "referenceName" : "Microsoft.VSTS.Common.Activity", + "alwaysRequired" : false, + "name" : "Activity", + "defaultValue" : null + }, + { + "defaultValue" : null, + "name" : "Remaining Work", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Scheduling.RemainingWork", + "helpText" : "An estimate of the number of units of work remaining to complete this task", + "referenceName" : "Microsoft.VSTS.Scheduling.RemainingWork", + "alwaysRequired" : false + }, + { + "alwaysRequired" : false, + "referenceName" : "Microsoft.VSTS.Scheduling.OriginalEstimate", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Scheduling.OriginalEstimate", + "helpText" : "Initial value for Remaining Work - set once, when work begins", + "name" : "Original Estimate", + "defaultValue" : null + }, + { + "referenceName" : "Microsoft.VSTS.Scheduling.CompletedWork", + "alwaysRequired" : false, + "helpText" : "The number of units of work that have been spent on this task", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Scheduling.CompletedWork", + "defaultValue" : null, + "name" : "Completed Work" + }, + { + "alwaysRequired" : false, + "referenceName" : "Microsoft.VSTS.Common.Priority", + "helpText" : "Importance to business", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.Priority", + "name" : "Priority", + "defaultValue" : "2" + }, + { + "defaultValue" : null, + "name" : "Stack Rank", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.StackRank", + "helpText" : "Work first on items with lower-valued stack rank. Set in triage.", + "referenceName" : "Microsoft.VSTS.Common.StackRank", + "alwaysRequired" : false + }, + { + "defaultValue" : null, + "name" : "Start Date", + "helpText" : "The date to start the task", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Scheduling.StartDate", + "alwaysRequired" : false, + "referenceName" : "Microsoft.VSTS.Scheduling.StartDate" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Scheduling.FinishDate", + "helpText" : "The date to finish the task", + "alwaysRequired" : false, + "referenceName" : "Microsoft.VSTS.Scheduling.FinishDate", + "defaultValue" : null, + "name" : "Finish Date" + }, + { + "defaultValue" : null, + "referenceName" : "Microsoft.VSTS.Common.StateChangeDate", + "alwaysRequired" : false, + "name" : "State Change Date", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.StateChangeDate" + }, + { + "alwaysRequired" : false, + "name" : "Activated Date", + "referenceName" : "Microsoft.VSTS.Common.ActivatedDate", + "defaultValue" : null, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.ActivatedDate" + }, + { + "defaultValue" : null, + "alwaysRequired" : false, + "name" : "Activated By", + "referenceName" : "Microsoft.VSTS.Common.ActivatedBy", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.ActivatedBy" + }, + { + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.ClosedDate", + "referenceName" : "Microsoft.VSTS.Common.ClosedDate", + "alwaysRequired" : false, + "name" : "Closed Date", + "defaultValue" : null + }, + { + "defaultValue" : null, + "referenceName" : "Microsoft.VSTS.Common.ClosedBy", + "name" : "Closed By", + "alwaysRequired" : false, + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Common.ClosedBy" + }, + { + "helpText" : "The build in which the bug was fixed", + "url" : "https://dev.azure.com/linkconsulting/ee81749d-1a0f-4787-a694-3e55b7c5539e/_apis/wit/fields/Microsoft.VSTS.Build.IntegrationBuild", + "referenceName" : "Microsoft.VSTS.Build.IntegrationBuild", + "alwaysRequired" : false, + "name" : "Integration Build", + "defaultValue" : null + } + ], + "count" : 61 +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/gen-accessors.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/gen-accessors.go new file mode 100644 index 000000000..fe92206fc --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/gen-accessors.go @@ -0,0 +1,332 @@ +// Copyright 2017 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ignore + +// gen-accessors generates accessor methods for structs with pointer fields. +// +// It is meant to be used by the go-github authors in conjunction with the +// go generate tool before sending a commit to GitHub. +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "log" + "os" + "sort" + "strings" + "text/template" +) + +const ( + fileSuffix = "-accessors.go" +) + +var ( + verbose = flag.Bool("v", false, "Print verbose log messages") + + sourceTmpl = template.Must(template.New("source").Parse(source)) + + // blacklistStructMethod lists "struct.method" combos to skip. + blacklistStructMethod = map[string]bool{ + "RepositoryContent.GetContent": true, + "Client.GetBaseURL": true, + "Client.GetUploadURL": true, + "ErrorResponse.GetResponse": true, + "RateLimitError.GetResponse": true, + "AbuseRateLimitError.GetResponse": true, + } + // blacklistStruct lists structs to skip. + blacklistStruct = map[string]bool{ + "Client": true, + } +) + +func logf(fmt string, args ...interface{}) { + if *verbose { + log.Printf(fmt, args...) + } +} + +func main() { + flag.Parse() + fset := token.NewFileSet() + + pkgs, err := parser.ParseDir(fset, ".", sourceFilter, 0) + if err != nil { + log.Fatal(err) + return + } + + for pkgName, pkg := range pkgs { + t := &templateData{ + filename: pkgName + fileSuffix, + Year: 2017, + Package: pkgName, + Imports: map[string]string{}, + } + for filename, f := range pkg.Files { + logf("Processing %v...", filename) + if err := t.processAST(f); err != nil { + log.Fatal(err) + } + } + if err := t.dump(); err != nil { + log.Fatal(err) + } + } + logf("Done.") +} + +func (t *templateData) processAST(f *ast.File) error { + for _, decl := range f.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + // Skip unexported identifiers. + if !ts.Name.IsExported() { + logf("Struct %v is unexported; skipping.", ts.Name) + continue + } + // Check if the struct is blacklisted. + if blacklistStruct[ts.Name.Name] { + logf("Struct %v is blacklisted; skipping.", ts.Name) + continue + } + st, ok := ts.Type.(*ast.StructType) + if !ok { + continue + } + for _, field := range st.Fields.List { + se, ok := field.Type.(*ast.StarExpr) + if len(field.Names) == 0 || !ok { + continue + } + + fieldName := field.Names[0] + // Skip unexported identifiers. + if !fieldName.IsExported() { + logf("Field %v is unexported; skipping.", fieldName) + continue + } + // Check if "struct.method" is blacklisted. + if key := fmt.Sprintf("%v.Get%v", ts.Name, fieldName); blacklistStructMethod[key] { + logf("Method %v is blacklisted; skipping.", key) + continue + } + + switch x := se.X.(type) { + case *ast.ArrayType: + t.addArrayType(x, ts.Name.String(), fieldName.String()) + case *ast.Ident: + t.addIdent(x, ts.Name.String(), fieldName.String()) + case *ast.MapType: + t.addMapType(x, ts.Name.String(), fieldName.String()) + case *ast.SelectorExpr: + t.addSelectorExpr(x, ts.Name.String(), fieldName.String()) + default: + logf("processAST: type %q, field %q, unknown %T: %+v", ts.Name, fieldName, x, x) + } + } + } + } + return nil +} + +func sourceFilter(fi os.FileInfo) bool { + return !strings.HasSuffix(fi.Name(), "_test.go") && !strings.HasSuffix(fi.Name(), fileSuffix) +} + +func (t *templateData) dump() error { + if len(t.Getters) == 0 { + logf("No getters for %v; skipping.", t.filename) + return nil + } + + // Sort getters by ReceiverType.FieldName. + sort.Sort(byName(t.Getters)) + + var buf bytes.Buffer + if err := sourceTmpl.Execute(&buf, t); err != nil { + return err + } + clean, err := format.Source(buf.Bytes()) + if err != nil { + return err + } + + logf("Writing %v...", t.filename) + return ioutil.WriteFile(t.filename, clean, 0644) +} + +func newGetter(receiverType, fieldName, fieldType, zeroValue string, namedStruct bool) *getter { + return &getter{ + sortVal: strings.ToLower(receiverType) + "." + strings.ToLower(fieldName), + ReceiverVar: strings.ToLower(receiverType[:1]), + ReceiverType: receiverType, + FieldName: fieldName, + FieldType: fieldType, + ZeroValue: zeroValue, + NamedStruct: namedStruct, + } +} + +func (t *templateData) addArrayType(x *ast.ArrayType, receiverType, fieldName string) { + var eltType string + switch elt := x.Elt.(type) { + case *ast.Ident: + eltType = elt.String() + default: + logf("addArrayType: type %q, field %q: unknown elt type: %T %+v; skipping.", receiverType, fieldName, elt, elt) + return + } + + t.Getters = append(t.Getters, newGetter(receiverType, fieldName, "[]"+eltType, "nil", false)) +} + +func (t *templateData) addIdent(x *ast.Ident, receiverType, fieldName string) { + var zeroValue string + var namedStruct = false + switch x.String() { + case "int", "int64": + zeroValue = "0" + case "string": + zeroValue = `""` + case "bool": + zeroValue = "false" + case "Timestamp": + zeroValue = "Timestamp{}" + default: + zeroValue = "nil" + namedStruct = true + } + + t.Getters = append(t.Getters, newGetter(receiverType, fieldName, x.String(), zeroValue, namedStruct)) +} + +func (t *templateData) addMapType(x *ast.MapType, receiverType, fieldName string) { + var keyType string + switch key := x.Key.(type) { + case *ast.Ident: + keyType = key.String() + default: + logf("addMapType: type %q, field %q: unknown key type: %T %+v; skipping.", receiverType, fieldName, key, key) + return + } + + var valueType string + switch value := x.Value.(type) { + case *ast.Ident: + valueType = value.String() + default: + logf("addMapType: type %q, field %q: unknown value type: %T %+v; skipping.", receiverType, fieldName, value, value) + return + } + + fieldType := fmt.Sprintf("map[%v]%v", keyType, valueType) + zeroValue := fmt.Sprintf("map[%v]%v{}", keyType, valueType) + t.Getters = append(t.Getters, newGetter(receiverType, fieldName, fieldType, zeroValue, false)) +} + +func (t *templateData) addSelectorExpr(x *ast.SelectorExpr, receiverType, fieldName string) { + if strings.ToLower(fieldName[:1]) == fieldName[:1] { // Non-exported field. + return + } + + var xX string + if xx, ok := x.X.(*ast.Ident); ok { + xX = xx.String() + } + + switch xX { + case "time", "json": + if xX == "json" { + t.Imports["encoding/json"] = "encoding/json" + } else { + t.Imports[xX] = xX + } + fieldType := fmt.Sprintf("%v.%v", xX, x.Sel.Name) + zeroValue := fmt.Sprintf("%v.%v{}", xX, x.Sel.Name) + if xX == "time" && x.Sel.Name == "Duration" { + zeroValue = "0" + } + t.Getters = append(t.Getters, newGetter(receiverType, fieldName, fieldType, zeroValue, false)) + default: + logf("addSelectorExpr: xX %q, type %q, field %q: unknown x=%+v; skipping.", xX, receiverType, fieldName, x) + } +} + +type templateData struct { + filename string + Year int + Package string + Imports map[string]string + Getters []*getter +} + +type getter struct { + sortVal string // Lower-case version of "ReceiverType.FieldName". + ReceiverVar string // The one-letter variable name to match the ReceiverType. + ReceiverType string + FieldName string + FieldType string + ZeroValue string + NamedStruct bool // Getter for named struct. +} + +type byName []*getter + +func (b byName) Len() int { return len(b) } +func (b byName) Less(i, j int) bool { return b[i].sortVal < b[j].sortVal } +func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +const source = `// Copyright {{.Year}} The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by gen-accessors; DO NOT EDIT. + +package {{.Package}} +{{with .Imports}} +import ( + {{- range . -}} + "{{.}}" + {{end -}} +) +{{end}} +{{range .Getters}} +{{if .NamedStruct}} +// Get{{.FieldName}} returns the {{.FieldName}} field. +func ({{.ReceiverVar}} *{{.ReceiverType}}) Get{{.FieldName}}() *{{.FieldType}} { + if {{.ReceiverVar}} == nil { + return {{.ZeroValue}} + } + return {{.ReceiverVar}}.{{.FieldName}} +} +{{else}} +// Get{{.FieldName}} returns the {{.FieldName}} field if it's non-nil, zero value otherwise. +func ({{.ReceiverVar}} *{{.ReceiverType}}) Get{{.FieldName}}() {{.FieldType}} { + if {{.ReceiverVar}} == nil || {{.ReceiverVar}}.{{.FieldName}} == nil { + return {{.ZeroValue}} + } + return *{{.ReceiverVar}}.{{.FieldName}} +} +{{end}} +{{end}} +` diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/git.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/git.go new file mode 100644 index 000000000..c118ae8f3 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/git.go @@ -0,0 +1,481 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" +) + +// VersionControlChangeType enum declaration +type VersionControlChangeType int + +// VersionControlChangeType valid enum values +const ( + None VersionControlChangeType = iota + Add + Edit + Encoding + Rename + Delete + Undelete + Branch + Merge + Lock + Rollback + SourceRename + TargetRename + Property + All +) + +func (d VersionControlChangeType) String() string { + return [...]string{"none", "add", "edit", "encoding", "rename", "delete", "undelete", "branch", "merge", "lock", "rollback", "sourceRename", "targetRename", "property", "all"}[d] +} + +// GitObjectType enum declaration +type GitObjectType int + +// GitObjectType enum declaration +const ( + Bad GitObjectType = iota + Commit + Tree + Blob + Tag + Ext2 + OfsDelta + RefDelta +) + +func (d GitObjectType) String() string { + return [...]string{"bad", "commit", "tree", "blob", "tag", "ext2", "ofsDelta", "refDelta"}[d] +} + +// GitService handles communication with the git methods on the API +// See: https://docs.microsoft.com/en-us/rest/api/vsts/git/ +type GitService struct { + client *Client +} + +// FileContentMetadata Describes files referenced by a GitItem +type FileContentMetadata struct { + ContentType *string `json:"contentType,omitempty"` + Encoding *int `json:"encoding,omitempty"` + Extension *string `json:"extension,omitempty"` + FileName *string `json:"fileName,omitempty"` + IsBinary *bool `json:"isBinary,omitempty"` + IsImage *bool `json:"isImage,omitempty"` + VSLink *string `json:"vsLink,omitempty"` +} + +// GitRefsResponse describes the git list refs response +type GitRefsResponse struct { + Count int `json:"count"` + GitRefs []*GitRef `json:"value"` +} + +// GitRefsUpdateResponse describes the git list refs update response +type GitRefsUpdateResponse struct { + Count int `json:"count"` + GitRefsUpdate []*GitRefUpdate `json:"value"` +} + +// UpdateRefsBody Request body for UpdateRefs() +type UpdateRefsBody struct { + IsLocked *bool `json:"isLocked,omitempty"` + Name *string `json:"name,omitempty"` + NewObjectID *string `json:"newObjectId,omitempty"` + OldObjectID *string `json:"oldObjectId,omitempty"` + RepositoryID *string `json:"repositoryId,omitempty"` +} + +// GitStatusesResponse describes the git statuses response +type GitStatusesResponse struct { + Count int `json:"count"` + GitStatuses []*GitStatus `json:"value"` +} + +// GitChange describes file path and content changes +type GitChange struct { + ChangeID *int `json:"changeId,omitempty"` + ChangeType *string `json:"changeType,omitempty"` + Item *GitItem `json:"item,omitempty"` + NewContent *ItemContent `json:"newContent,omitempty"` + NewContentTemplate *GitTemplate `json:"newContentTemplate,omitempty"` + OriginalPath *string `json:"originalPath,omitempty"` + SourceServerItem *string `json:"sourceServerItem,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GitCommitChanges is a list of GitCommitRefs and count of all changes describes in +// the response from the API +type GitCommitChanges struct { + ChangeCounts *map[string]int `json:"changeCounts,omitempty"` + Changes []*GitChange `json:"changes,omitempty"` +} + +// GitCommitRef describes a single git commit reference +type GitCommitRef struct { + Links *map[string]Link `json:"_links,omitempty"` + CommitID *string `json:"commitId,omitempty"` + Author *GitUserDate `json:"author,omitempty"` + Committer *GitUserDate `json:"committer,omitempty"` + Comment *string `json:"comment,omitempty"` + CommentTruncated *bool `json:"commentTruncated,omitempty"` + URL *string `json:"url,omitempty"` + ChangeCounts *map[string]int `json:"changeCounts,omitempty"` + Changes *GitChange `json:"changes,omitempty"` + Parents []*string `json:"parents,omitempty"` + Push *GitPushRef `json:"push,omitempty"` + RemoteURL *string `json:"remoteUrl,omitempty"` + Statuses []*GitStatus `json:"statuses,omitempty"` + WorkItems *ResourceRef `json:"workItems,omitempty"` +} + +// GitRef provides information about a git/fork ref. +type GitRef struct { + Links *map[string]Link `json:"_links,omitempty"` + Creator *IdentityRef `json:"creator,omitempty"` + IsLocked *bool `json:"isLocked,omitempty"` + IsLockedBy *IdentityRef `json:"isLockedBy,omitempty"` + Name *string `json:"name,omitempty"` + ObjectID *string `json:"objectId,omitempty"` + PeeledObjectID *string `json:"peeledObjectId,omitempty"` + Repository *GitRepository `json:"repository,omitempty"` + Statuses []*GitStatus `json:"statuses,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GitItem describes a single git item +type GitItem struct { + Links *map[string]Link `json:"_links,omitempty"` + CommitID *string `json:"commitId,omitempty"` + Content *string `json:"content,omitempty"` + ContentMetadata *FileContentMetadata `json:"contentMetadata,omitempty"` + GitObjectType *string `json:"gitObjectType,omitempty"` + IsFolder *bool `json:"isFolder,omitempty"` + IsSymLink *bool `json:"isSymLink,omitempty"` + LatestProcessedChange *GitCommitRef `json:"latestProcessedChange,omitempty"` + ObjectID *string `json:"objectId,omitempty"` + OriginalObjectID *string `json:"originalObjectId,omitempty"` + Path *string `json:"path,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GitPullRequest represents all the data associated with a pull request. +type GitPullRequest struct { + Links *map[string]Link `json:"_links,omitempty"` + ArtifactID *string `json:"artifactId,omitempty"` + AutoCompleteSetBy *IdentityRef `json:"autoCompleteSetBy,omitempty"` + ClosedBy *IdentityRef `json:"closedBy,omitempty"` + ClosedDate *time.Time `json:"closedDate,omitempty"` + CodeReviewID *int `json:"codeReviewId,omitempty"` + Commits []*GitCommitRef `json:"commits,omitempty"` + CompletionOptions *GitPullRequestCompletionOptions `json:"completionOptions,omitempty"` + CompletionQueueTime *time.Time `json:"completionQueueTime, omitempty"` + CreatedBy *IdentityRef `json:"createdBy,omitempty"` + CreationDate *time.Time `json:"creationDate,omitempty"` + Description *string `json:"description,omitempty"` + ForkSource *GitRef `json:"forkSource,omitempty"` + IsDraft *bool `json:"isDraft,omitempty"` + Labels []*WebAPITagDefinition `json:"labels,omitempty"` + LastMergeCommit *GitCommitRef `json:"lastMergeCommit,omitempty"` + LastMergeSourceCommit *GitCommitRef `json:"lastMergeSourceCommit,omitempty"` + LastMergeTargetCommit *GitCommitRef `json:"lastMergeTargetCommit,omitempty"` + MergeFailureMessage *string `json:"mergeFailureMessage,omitempty"` + MergeFailureType *string `json:"mergeFailureType,omitempty"` + MergeID *string `json:"mergeId,omitempty"` + MergeOptions *GitPullRequestMergeOptions `json:"mergeOptions,omitempty"` + MergeStatus *string `json:"mergeStatus,omitempty"` + PullRequestID *int `json:"pullRequestId,omitempty"` + Repository *GitRepository `json:"repository,omitempty"` + Reviewers []*IdentityRefWithVote `json:"reviewers,omitempty"` + RemoteURL *string `json:"remoteUrl,omitempty"` + SourceRefName *string `json:"sourceRefName,omitempty"` + Status *string `json:"status,omitempty"` + SupportsIterations *bool `json:"supportsIterations,omitempty"` + TargetRefName *string `json:"targetRefName,omitempty"` + Title *string `json:"title,omitempty"` + URL *string `json:"url,omitempty"` + WorkItemRefs []*ResourceRef `json:"workItemRefs,omitempty"` +} + +// GitPullRequestCompletionOptions describes preferences about how the pull +// request should be completed. +// SquashMerge is deprecated. You should explicity set the value of MergeStrategy. If +// MergeStrategy is set to any value, the SquashMerge value will be ignored. If +// MergeStrategy is not set, the merge strategy will be no-fast-forward if this flag is false, or squash if true. +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/update?view=azure-devops-rest-5.1#pullrequeststatus +type GitPullRequestCompletionOptions struct { + BypassPolicy *bool `json:"bypassPolicy,omitempty"` + BypassReason *string `json:"bypassReason,omitempty"` + DeleteSourceBranch *bool `json:"deleteSourceBranch,omitempty"` + MergeCommitMessage *string `json:"mergeCommitMessage,omitempty"` + MergeStrategy *string `json:"mergeStrategy,omitempty"` + SquashMerge *bool `json:"squashMerge,omitempty"` + TransitionWorkItems *bool `json:"transitionWorkItems,omitempty"` + TriggeredByAutoComplete *bool `json:"triggeredByAutoComplete,omitempty"` +} + +// GitPullRequestMergeOptions describes the options which are used when a pull +// request merge is created. +type GitPullRequestMergeOptions struct { + DetectRenameFalsePositives *bool `json:"detectRenameFalsePositives,omitempty"` + DisableRenames *bool `json:"disableRenames,omitempty"` +} + +// GitPullRequestMergeStrategy specifies the strategy used to merge the pull request +// during completion. +type GitPullRequestMergeStrategy int + +// GitPullRequestMergeStrategy enum values +const ( + NoFastForward GitPullRequestMergeStrategy = iota + Rebase + SebaseMerge + Squash +) + +func (d GitPullRequestMergeStrategy) String() string { + return [...]string{"noFastForward", "rebase", "rebaseMerge", "squash"}[d] +} + +// GitPush describes a code push request event. +type GitPush struct { + Links *map[string]Link `json:"_links,omitempty"` + Commits []*GitCommitRef `json:"commits,omitempty"` + Date *time.Time `json:"date,omitempty"` + PushID *int `json:"pushId,omitempty"` + PushedBy *IdentityRef `json:"pushedBy,omitempty"` + RefUpdates []*GitRefUpdate `json:"refUpdates,omitempty"` + Repository *GitRepository `json:"repository,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GitPushRef Describes a push request +type GitPushRef struct { + Commits []*GitCommitRef `json:"commits,omitempty"` + RefUpdates []*GitRefUpdate `json:"refUpdates,omitempty"` + Repository *GitRepository `json:"repository,omitempty"` +} + +// GitRefUpdate Describes a ref update +type GitRefUpdate struct { + IsLocked *bool `json:"isLocked,omitempty"` + Name *string `json:"name,omitempty"` + NewObjectID *string `json:"newObjectId,omitempty"` + OldObjectID *string `json:"oldObjectId,omitempty"` + RepositoryID *string `json:"repositoryId,omitempty"` +} + +// GitRepository describes an Azure Devops Git repository. +type GitRepository struct { + Links *map[string]Link `json:"_links,omitempty"` + DefaultBranch *string `json:"defaultBranch,omitempty"` + ID *string `json:"id,omitempty"` + IsFork *bool `json:"isFork,omitempty"` + Name *string `json:"name,omitempty"` + ParentRepository *GitRepositoryRef `json:"parentRepository,omitempty"` + Project *TeamProjectReference `json:"project,omitempty"` + RemoteURL *string `json:"remoteUrl,omitempty"` + Size *int `json:"size,omitempty"` + SSHURL *string `json:"sshUrl,omitempty"` + URL *string `json:"url,omitempty"` + ValidRemoteURLs []*string `json:"validRemoteUrls,omitempty"` + WebURL *string `json:"webUrl,omitempty"` +} + +// GitRepositoryRef describes a repository ref +type GitRepositoryRef struct { + Collection *TeamProjectCollectionReference `json:"collection,omitempty"` + ID *string `json:"id,omitempty"` + IsFork *bool `json:"isFork,omitempty"` + Name *string `json:"name,omitempty"` + Project *TeamProjectReference `json:"project,omitempty"` + RemoteURL *string `json:"remoteUrl,omitempty"` + SSHURL *string `json:"sshUrl,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GitStatusState contains the metadata of a service/extension posting a status. +type GitStatusState int + +// GitStatusState enum values +const ( + GitNotSet GitStatusState = iota + GitPending + GitSucceeded + GitFailed + GitError + GitNotApplicable +) + +func (d GitStatusState) String() string { + return [...]string{"notSet", "pending", "succeeded", "failed", "error", "notApplicable"}[d] +} + +// GitStatus describes a git status entity +type GitStatus struct { + Links *map[string]Link `json:"_links,omitempty"` + Context *GitStatusContext `json:"context,omitempty"` + CreatedBy *IdentityRef `json:"createdBy,omitempty"` + CreationDate *time.Time `json:"creationDate,omitempty"` + Description *string `json:"description,omitempty"` + ID *int `json:"id,omitempty"` + State *string `json:"state,omitempty"` + TargetURL *string `json:"targetUrl,omitempty"` + UpdatedDate *time.Time `json:"updatedDate,omitempty"` +} + +// GitPullRequestStatus This class contains the metadata of a service/extension +// posting pull request status. Status can be associated with a pull request or +// an iteration. +type GitPullRequestStatus struct { + GitStatus + IterationID int `json:"iterationId,omitempty"` + Properties *time.Time `json:"properties,omitempty"` +} + +// GitRefListOptions describes what the request to the API should look like +type GitRefListOptions struct { + Filter string `url:"filter,omitempty"` + IncludeStatuses bool `url:"includeStatuses,omitempty"` + LatestStatusesOnly bool `url:"latestStatusesOnly,omitempty"` +} + +// GitStatusContext Status context that uniquely identifies the status. +type GitStatusContext struct { + Genre *string `json:"genre,omitempty"` + Name *string `json:"name,omitempty"` +} + +// GitTemplate describes a git template +type GitTemplate struct { + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` +} + +// GitUserDate User info and date for Git operations. +type GitUserDate struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Date *time.Time `json:"date,omitempty"` +} + +// UpdateRefs returns a list of the references for a git repo +func (s *GitService) UpdateRefs(ctx context.Context, owner, project, repo, refType string, opts *GitRefListOptions) ([]*GitRef, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/git/repositories/%s/refs/%s?api-version=5.1-preview.1", + owner, + project, + repo, + refType, + ) + + URL, err := addOptions(URL, opts) + + body := &UpdateRefsBody{ + IsLocked: Bool(false), + Name: String("refs/heads/vsts-api-sample/answer-woman-flame"), + NewObjectID: String(""), + OldObjectID: String(""), + RepositoryID: String(repo), + } + + req, err := s.client.NewRequest("POST", URL, body) + if err != nil { + return nil, nil, err + } + r := new(GitRefsResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.GitRefs, resp, err +} + +// ListRefs returns a list of the references for a git repo +func (s *GitService) ListRefs(ctx context.Context, owner, project, repo, refType string, opts *GitRefListOptions) ([]*GitRef, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/git/repositories/%s/refs/%s?api-version=5.1-preview.1", + owner, + project, + repo, + refType, + ) + + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(GitRefsResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.GitRefs, resp, err +} + +// GetRepository Return a single GitRepository +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/repositories/get%20repository?view=azure-devops-rest-5.1 +func (s *GitService) GetRepository(ctx context.Context, owner, project, repoName string) (*GitRepository, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/git/repositories/%s?api-version=5.1-preview.1", + owner, + project, + repoName, + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(GitRepository) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err + +} + +// GetChanges Return a single GitRepository +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get%20changes?view=azure-devops-rest-5.1 +func (s *GitService) GetChanges(ctx context.Context, owner, project, repoName, commitID string) (*GitCommitChanges, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/git/repositories/%s/commits/%s/changes?api-version=5.1-preview.1", + owner, + project, + repoName, + commitID, + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(GitCommitChanges) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// CreateStatus creates a new status for a repository at the specified +// reference. Ref can be a SHA, a branch name, or a tag name. +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/statuses/create?view=azure-devops-rest-5.0 +func (s *GitService) CreateStatus(ctx context.Context, owner, project, repoName, ref string, status GitStatus) (*GitStatus, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/git/repositories/%s/commits/%s/statuses?api-version=5.1-preview.1", + owner, + project, + repoName, + url.QueryEscape(ref), + ) + + req, err := s.client.NewRequest("POST", URL, status) + if err != nil { + return nil, nil, err + } + r := new(GitStatus) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/identity_ref.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/identity_ref.go new file mode 100644 index 000000000..14fd976a1 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/identity_ref.go @@ -0,0 +1,27 @@ +package azuredevops + +// IdentityRef describes an Azure Devops identity +type IdentityRef struct { + Links *map[string]Link `json:"_links,omitempty"` + Descriptor *string `json:"descriptor,omitempty"` + DirectoryAlias *string `json:"directoryAlias,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + ID *string `json:"id,omitempty"` + ImageURL *string `json:"imageUrl,omitempty"` + Inactive *bool `json:"inactive,omitempty"` + IsAadIdentity *bool `json:"isAadIdentity,omitempty"` + IsContainer *bool `json:"isContainer,omitempty"` + IsDeletedInOrigin *bool `json:"isDeletedInOrigin,omitempty"` + ProfileURL *string `json:"profileUrl,omitempty"` + URL *string `json:"url,omitempty"` + UniqueName *string `json:"uniqueName,omitempty"` +} + +// IdentityRefWithVote Identity information including a vote on a pull request. +type IdentityRefWithVote struct { + IdentityRef + IsRequired *bool `json:"isRequired,omitempty"` + ReviewerURL *string `json:"reviewerUrl,omitempty"` + Vote *int `json:"vote,omitempty"` + VotedFor []*IdentityRefWithVote `json:"votedFor,omitempty"` +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/iterations.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/iterations.go new file mode 100644 index 000000000..001f0be47 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/iterations.go @@ -0,0 +1,69 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// IterationsService handles communication with the work items methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/iterations +type IterationsService struct { + client *Client +} + +// IterationsResponse describes the iterations response +type IterationsResponse struct { + Count int `json:"count,omitempty"` + Iterations []*Iteration `json:"value,omitempty"` +} + +// Iteration describes an iteration +type Iteration struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` + URL *string `json:"url,omitempty"` + StartDate *string `json:"startDate,omitempty"` + EndDate *string `json:"finishDate,omitempty"` + WorkItems [][]interface{} `json:"workItems,omitempty"` +} + +// List returns list of the iterations available to the user +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/iterations/list +func (s *IterationsService) List(ctx context.Context, owner, project, team string) ([]*Iteration, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/%s/_apis/work/teamsettings/iterations?api-version=5.1-preview.1", + owner, + project, + url.PathEscape(team), + ) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(IterationsResponse) + resp, err := s.client.Execute(ctx, request, &r) + + return r.Iterations, resp, err +} + +// GetByName will search the iterations for the account and project +// and return a single iteration if the names match +func (s *IterationsService) GetByName(ctx context.Context, owner, project, team string, name string) (*Iteration, *http.Response, error) { + iterations, resp, err := s.List(ctx, owner, project, team) + if err != nil { + return nil, nil, err + } + + for index := 0; index < len(iterations); index++ { + if name == *iterations[index].Name { + iteration := iterations[index] + return iteration, resp, nil + } + } + + return nil, nil, nil +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/messages.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/messages.go new file mode 100644 index 000000000..1f4429864 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/messages.go @@ -0,0 +1,162 @@ +// Copyright 2016 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file provides functions for validating payloads from GitHub Webhooks. +// GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github + +// Adapted for Azure Devops + +package azuredevops + +import ( + "crypto/subtle" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +const ( + activityIDHeader = "X-VSS-ActivityId" + subscriptionIDHeader = "X-VSS-SubscriptionId" + // requestIDHeader is the Azure Devops header key used to pass the unique ID for the webhook event. + requestIDHeader = "Request-Id" +) + +var ( + // eventTypeMapping maps webhooks types to their corresponding go-azuredevops + // resource struct types. + eventTypeMapping = map[string]string{ + "git.pullrequest.created": "PullRequestEvent", + "git.pullrequest.merged": "PullRequestEvent", + "git.pullrequest.updated": "PullRequestEvent", + "git.push": "PushEvent", + "ms.vss-code.git-pullrequest-comment-event": "PullRequestCommentedEvent", + "workitem.commented": "WorkItemCommentedEvent", + "workitem.updated": "WorkItemUpdatedEvent", + } +) + +// GetActivityID returns the value of the activityIDHeader webhook header. +// +// Haven't found vendor documentation yet. This could be a GUID that identifies +// the webhook request ID. A different GUID is also present in the body of +// webhook requests. +func GetActivityID(r *http.Request) string { + return r.Header.Get(activityIDHeader) +} + +// GetRequestID returns the value of the requestIDHeader webhook header. +// +// Haven't found vendor documentation yet. This could be a GUID that identifies +// the webhook request ID. A different GUID is also present in the body of +// webhook requests. +func GetRequestID(r *http.Request) string { + return r.Header.Get(requestIDHeader) +} + +// GetSubscriptionID returns the value of the subscriptionIDHeader webhook header. +// +// Haven't found vendor documentation yet. This could be a GUID that identifies +// the webhook event type and settings in the Azure Devops tenant +func GetSubscriptionID(r *http.Request) string { + return r.Header.Get(subscriptionIDHeader) +} + +// ValidatePayload validates an incoming Azure Devops Webhook event request +// and returns the (JSON) payload. +// The Content-Type header of the payload must be "application/json" or +// an error is returned. A charset may be included with the content type. +// user is the supplied username for Basic authentication +// pass is the supplied password for Basic authentication +// If your webhook does not contain a username or password, you can pass nil or an empty slice. +// This is intended for local development purposes only as all webhooks should ideally +// set up a secret token. +// It is up to the caller to process failed validation and return a proper 401 response +// to the user, such as: +// +// w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) +// w.WriteHeader(401) +// w.Write([]byte("Unauthorized.\n")) +// +// +// Example usage: +// +// func (s *Event) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// payload, err := azuredevops.ValidatePayload(r, s.user, s.pass) +// if err != nil { ... } +// // Process payload... +// } +// +func ValidatePayload(r *http.Request, user, pass []byte) (payload []byte, err error) { + var body []byte // Raw body that GitHub uses to calculate the signature. + + switch ct := r.Header.Get("Content-Type"); ct { + case "application/json": + var err error + if body, err = ioutil.ReadAll(r.Body); err != nil { + return nil, err + } + + // If the content type is application/json, + // the JSON payload is just the original body. + payload = body + case "application/json; charset=utf-8": + var err error + if body, err = ioutil.ReadAll(r.Body); err != nil { + return nil, err + } + + // If the content type is application/json, + // the JSON payload is just the original body. + payload = body + default: + return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct) + } + + // Only validate the authentication if a username and password exist. This is + // intended for local development only and all webhooks should ideally set up + // a Basic authentication username and password. + username, password, ok := r.BasicAuth() + + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + return nil, errors.New("ValidatePayload authentication failed") + } + //Authorization: Basic + return payload, nil +} + +// ParseWebHook parses the event payload into a corresponding struct. +// An error will be returned for unrecognized event types. +// +// Example usage: +// +// func (s *EventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// payload, err := azuredevops.ValidatePayload(r, s.user, s.pass) +// if err != nil { ... } +// event, err := azuredevops.ParseWebHook(payload) +// if err != nil { ... } +// switch event.PayloadType { +// case azuredevops.WorkItemEvent: +// processWorkItemEvent(&event) +// case azuredevops.PullRequestEvent: +// processPullRequestEvent(&event) +// ... +// } +// } +// +// https://docs.microsoft.com/en-us/azure/devops/service-hooks/events?view=azure-devops +func ParseWebHook(payload []byte) (*Event, error) { + event := new(Event) // returns pointer + err := json.Unmarshal(payload, event) + if err != nil { + return nil, err + } + + _, err = event.ParsePayload() + + return event, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/pull_requests.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/pull_requests.go new file mode 100644 index 000000000..44e44aa2a --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/pull_requests.go @@ -0,0 +1,447 @@ +package azuredevops + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" +) + +// Vote identifiers +const ( + VoteApproved = 10 + VoteApprovedWithSuggestions = 5 + VoteNone = 0 + VoteWaitingForAuthor = -5 + VoteRejected = -10 +) + +// CommentType enum declaration +type CommentType int + +// CommentType enum declaration +const ( + // The comment type is not known. + CommentTypeUnknown CommentType = iota + // This is a regular user comment. + CommentTypeText + // The comment comes as a result of a code change. + CommentTypeCodeChange + // The comment represents a system message. + CommentTypeSystem +) + +func (d CommentType) String() string { + return [...]string{"unknown", "text", "codechange", "system"}[d] +} + +// CommentThreadStatus enum declaration +type CommentThreadStatus int + +// CommentThreadStatus enum declaration +const ( + StatusUnknown CommentThreadStatus = iota + StatusActive + Fixed + WontFix + Closed + ByDesign + Pending +) + +func (d CommentThreadStatus) String() string { + return [...]string{"unknown", "active", "fixed", "wontfix", "closed", "byDesign", "pending"}[d] +} + +// PullRequestAsyncStatus The current status of a pull request merge. +type PullRequestAsyncStatus int + +// PullRequestAsyncStatus enum values +const ( + MergeConflicts PullRequestAsyncStatus = iota + MergeFailure + MergeNotSet + MergeQueued + MergeRejectedByPolicy + MergeSucceeded +) + +func (d PullRequestAsyncStatus) String() string { + return [...]string{"conflicts", "failure", "notSet", "queued", "rejectedByPolicy", "succeeded"}[d] +} + +// PullRequestMergeFailureType The specific type of merge request failure +type PullRequestMergeFailureType int + +// PullRequestMergeFailureType enum values +const ( + NoFailure PullRequestMergeFailureType = iota + UnknownFailure + CaseSensitive + ObjectTooLarge +) + +func (d PullRequestMergeFailureType) String() string { + return [...]string{"none", "unknown", "caseSensitive", "objectTooLarge"}[d] +} + +// PullRequestStatus The current status of a pull request merge. +type PullRequestStatus int + +// PullRequestStatus enum values +const ( + PullAbandoned PullRequestStatus = iota + PullActive + PullIncludeAll + PullCompleted + PullNotSet +) + +func (d PullRequestStatus) String() string { + return [...]string{"abandoned", "active", "all", "completed", "notSet"}[d] +} + +// PullRequestsService handles communication with the pull requests methods on the API +// utilising https://docs.microsoft.com/en-us/rest/api/vsts/git/pull%20requests +type PullRequestsService struct { + client *Client +} + +// PullRequestsCommitsResponse describes a pull requests commits response +type PullRequestsCommitsResponse struct { + Count int `json:"count"` + GitCommitRefs []*GitCommitRef `json:"value"` +} + +// PullRequestGetOptions describes what the request to the API should look like +type PullRequestGetOptions struct { + IncludeCommits bool `url:"includeCommits,omitempty"` + IncludeWorkItemRefs bool `url:"includeWorkItemRefs,omitempty"` + Project string `url:"project,omitempty"` + Organization string `url:"organization,omitempty"` + RepositoryID string `url:"repositoryId,omitempty"` + // maxCommentLength Not used. + MaxCommentLength int `url:"maxCommentLength,omitempty"` + PullRequestID int `url:"pullRequestId,omitempty"` + // $skip Not used. + Skip int `url:"$skip,omitempty"` + // $top Not used. + Top int `url:"$top,omitempty"` +} + +// PullRequestsListResponse describes a pull requests list response +type PullRequestsListResponse struct { + Count int `json:"count"` + GitPullRequests []*GitPullRequest `json:"value"` +} + +// PullRequestsListCommitsResponse describes a pull requests list commits response +type PullRequestsListCommitsResponse struct { + Count int `json:"count"` + GitCommitRefs []*GitCommitRef `json:"value"` +} + +// PullRequestListOptions describes what the request to the API should look like +type PullRequestListOptions struct { + CreatorID string `url:"searchCriteria.creatorId,omitempty"` + IncludeLinks string `url:"searchCriteria.includeLinks,omitempty"` + Project string `url:"project,omitempty"` + RepositoryID string `url:"searchCriteria.repositoryId,omitempty"` + ReviewerID string `url:"searchCriteria.reviewerId,omitempty"` + Skip string `url:"$skip,omitempty"` + SourceRefName string `url:"searchCriteria.sourceRefName,omitempty"` + SourceRepositoryID string `url:"searchCriteria.sourceRepositoryId,omitempty"` + Status string `url:"searchCriteria.status,omitempty"` + TargetRefName string `url:"searchCriteria.targetRefName,omitempty"` + Top string `url:"$top,omitempty"` +} + +// List returns list of pull requests in the specified Team Project with optional +// filters +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20requests%20by%20project +func (s *PullRequestsService) List(ctx context.Context, owner, project string, opts *PullRequestListOptions) ([]*GitPullRequest, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/pullrequests?api-version=5.1-preview.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + r := new(PullRequestsListResponse) + resp, err := s.client.Execute(ctx, req, r) + if err != nil { + return nil, nil, err + } + + return r.GitPullRequests, resp, err +} + +// Get returns a single pull request +// utilising https://docs.microsoft.com/en-us/rest/api/vsts/git/pull%20requests/get%20pull%20requests%20by%20project +func (s *PullRequestsService) Get(ctx context.Context, owner, project string, pullNum int, opts *PullRequestListOptions) (*GitPullRequest, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/pullrequests/%d?api-version=5.1-preview.1", + owner, + project, + pullNum, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(GitPullRequest) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// GetWithRepo returns a single pull request with additional information +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20request?view=azure-devops-rest-5.1 +func (s *PullRequestsService) GetWithRepo(ctx context.Context, owner, project, repo string, pullNum int, opts *PullRequestGetOptions) (*GitPullRequest, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests/%d?api-version=5.1-preview.1", + owner, + project, + repo, + pullNum, + ) + + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(GitPullRequest) + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// Merge Completes a pull request +// pull may be nil +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/update?view=azure-devops-rest-5.1 +func (s *PullRequestsService) Merge(ctx context.Context, owner, project string, repoName string, pullNum int, pull *GitPullRequest, completionOpts GitPullRequestCompletionOptions, id IdentityRef) (*GitPullRequest, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests/%d?api-version=5.1-preview.1", + owner, + project, + repoName, + pullNum, + ) + + /* If pull not nil, prepare for merge + // otherwise create an empty GitPullRequest{} + if pull != nil { + } + */ + + // Construct request body from supplied parameters + body := &GitPullRequest{} + body.AutoCompleteSetBy = &id + body.CompletionOptions = &completionOpts + + // Now we're ready to make our API call to merge the pull request. + request, err := s.client.NewRequest("PATCH", URL, body) + if err != nil { + return nil, nil, err + } + r := new(GitPullRequest) + resp, err := s.client.Execute(ctx, request, r) + + return r, resp, err +} + +// Create Creates a pull request +// Required fields in the GitPullRequest{} are: +// * Title +// * Description +// * SourceRefName +// * TargetRefName +// +// SourceRefName can be either the full ref name "refs/heads/branchname" or +// just "branchname". The latter will be converted before submission. +// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/create?view=azure-devops-rest-5.1 +func (s *PullRequestsService) Create(ctx context.Context, owner, project string, repoName string, pull *GitPullRequest) (*GitPullRequest, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests?api-version=5.1-preview.1", + owner, + project, + repoName, + ) + + if pull.GetTitle() == "" || pull.GetDescription() == "" || + pull.GetSourceRefName() == "" || pull.GetTargetRefName() == "" { + return nil, nil, errors.New("PullRequests.Create: Missing required field") + } + + formatRef(pull.SourceRefName) + formatRef(pull.TargetRefName) + + // Now we're ready to make our API call to create the pull request. + request, err := s.client.NewRequest("POST", URL, pull) + if err != nil { + return nil, nil, err + } + r := new(GitPullRequest) + resp, err := s.client.Execute(ctx, request, r) + + return r, resp, err +} + +// Comment Represents a comment which is one of potentially many in a comment thread. +type Comment struct { + Links *map[string]Link `json:"_links,omitempty"` + Author *IdentityRef `json:"author,omitempty"` + CommentType *string `json:"commentType,omitempty"` + Content *string `json:"content,omitempty"` + ID *int `json:"id,omitempty"` + IsDeleted *bool `json:"isDeleted,omitempty"` + LastContentUpdatedDate *time.Time `json:"lastContentUpdatedDate,omitempty"` + LastUpdatedDate *time.Time `json:"lastUpdatedDate,omitempty"` + ParentCommentID *int `json:"parentCommentId,omitempty"` + PublishedDate *time.Time `json:"publishedDate,omitempty"` + UsersLiked []*IdentityRef `json:"usersLiked,omitempty"` +} + +// CommentPosition describes a comment position +type CommentPosition struct { + Line *int `json:"line,omitempty"` + Offset *int `json:"offset,omitempty"` +} + +// GitPullRequestCommentThread Represents a comment thread of a pull request. +// A thread contains meta data about the file it was left on along with one or +// more comments (an initial comment and the subsequent replies). +type GitPullRequestCommentThread struct { + Links *map[string]Link `json:"_links,omitempty"` + Comments []*Comment `json:"comments,omitempty"` + ID *int `json:"id,omitempty"` + Identities []*IdentityRef `json:"identities,omitempty"` + IsDeleted *bool `json:"isDeleted,omitempty"` + LastUpdatedDate *time.Time `json:"lastUpdatedDate,omitempty"` + Properties []*int `json:"properties,omitempty"` + PublishedDate *time.Time `json:"publishedDate,omitempty"` + Status *string `json:"status,omitempty"` + PullRequestThreadContext *GitPullRequestCommentThreadContext `json:"pullRequestThreadContext,omitempty"` +} + +// GitPullRequestCommentThreadContext Comment thread context contains details about what +// diffs were being viewed at the time of thread creation and whether or not the thread +// has been tracked from that original diff. +type GitPullRequestCommentThreadContext struct { + FilePath *string `json:"filePath,omitempty"` + LeftFileEnd *CommentPosition `json:"leftFileEnd,omitempty"` + LeftFileStart *CommentPosition `json:"leftFileStart,omitempty"` + RightFileEnd *CommentPosition `json:"rightFileEnd,omitempty"` + RightFileStart *CommentPosition `json:"rightFileStart,omitempty"` +} + +// GitPullRequestWithComment contains a reference to an existing pull request and a +// comment. +type GitPullRequestWithComment struct { + Comment *Comment `json:"comment,omitempty"` + PullRequest *GitPullRequest `json:"pullRequest,omitempty"` +} + +// ListCommits lists the commits in a pull request. +// Azure Devops API docs: https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20commits/get%20pull%20request%20commits +// +func (s *PullRequestsService) ListCommits(ctx context.Context, owner, project, repo string, pullNum int) ([]*GitCommitRef, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests/%d/commits?api-version=5.1-preview.1", + owner, + project, + repo, + pullNum, + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + r := new(PullRequestsListCommitsResponse) + resp, err := s.client.Execute(ctx, req, r) + if err != nil { + return nil, nil, err + } + + return r.GitCommitRefs, resp, err +} + +// CreateComment adds a comment to a pull request thread. +// Azure Devops API docs: https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20thread%20comments/create +// +func (s *PullRequestsService) CreateComment(ctx context.Context, owner, project, repo string, pullNum int, threadId int, comment *Comment) (*Comment, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests/%d/threads/%d/comments?api-version=5.1-preview.1", + owner, + project, + repo, + pullNum, + threadId, + ) + + if comment.GetContent() == "" { + return nil, nil, errors.New("PullRequests.CreateComment: Nil pointer or empty string in comment.Content field ") + } + + if comment.GetCommentType() == "" { + comment.CommentType = String("text") + } + + req, err := s.client.NewRequest("POST", URL, comment) + if err != nil { + return nil, nil, err + } + + r := new(Comment) + resp, err := s.client.Execute(ctx, req, r) + if err != nil { + return nil, nil, err + } + + return r, resp, err +} + +// CreateComments adds one or more comments to a new or existing thread +// and may include additional context +// Azure Devops API docs: https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20threads/create +// +func (s *PullRequestsService) CreateComments(ctx context.Context, owner, project, repo string, pullNum int, body *GitPullRequestCommentThread) (*GitPullRequestCommentThread, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/pullrequests/%d/threads?api-version=5.1-preview.1", + owner, + project, + repo, + pullNum, + ) + + if len(body.Comments) == 0 { + return nil, nil, errors.New("PullRequests.CreateComments: Must supply at least one comment in Comments field") + } + + for idx, comment := range body.Comments { + if comment.GetContent() == "" { + return nil, nil, errors.New("PullRequests.CreateComment: Nil pointer or empty string in comment.Content field ") + } + + if comment.GetCommentType() == "" { + body.Comments[idx].CommentType = String("text") + } + } + + req, err := s.client.NewRequest("POST", URL, body) + if err != nil { + return nil, nil, err + } + + r := new(GitPullRequestCommentThread) + resp, err := s.client.Execute(ctx, req, r) + if err != nil { + return nil, nil, err + } + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/release.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/release.go new file mode 100644 index 000000000..c21571c8d --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/release.go @@ -0,0 +1,11 @@ +// +build !debug + +package azuredevops + +import "net/http" + +// Idea from: +// https://dave.cheney.net/2014/09/28/using-build-to-switch-between-debug-and-release +func debug(fmt string, args ...interface{}) {} + +func debugReq(*http.Request) {} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/teams.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/teams.go new file mode 100644 index 000000000..d41702903 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/teams.go @@ -0,0 +1,89 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// TeamsService handles communication with the teams methods on the API +// utilising https://docs.microsoft.com/en-us/rest/api/vsts/core/teams/get%20all%20teams +type TeamsService struct { + client *Client +} + +// Project Describes a project +type Project struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + URL *string `json:"url,omitempty"` + State *string `json:"state,omitempty"` + Revision *int `json:"revision,omitempty"` + Visibility *string `json:"visibility,omitempty"` + LastUpdateTime *time.Time `json:"lastUpdateTime,omitempty"` +} + +// Team describes what a team looks like +type Team struct { + ID *string `url:"id,omitempty"` + Name *string `url:"name,omitempty"` + URL *string `url:"url,omitempty"` + Description *string `url:"description,omitempty"` +} + +// TeamsListOptions describes what the request to the API should look like +type TeamsListOptions struct { + Mine *bool `url:"$mine,omitempty"` + Top *int `url:"$top,omitempty"` + Skip *int `url:"$skip,omitempty"` +} + +// TeamsListResponse Requests that may return multiple entities use this format +type TeamsListResponse struct { + Count int `json:"count,omitempty"` + Teams []*Team `json:"value,omitempty"` +} + +// TeamProjectCollectionReference Reference object for a TeamProjectCollection. +type TeamProjectCollectionReference struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` +} + +// TeamProjectReference Represents a shallow reference to a TeamProject. +type TeamProjectReference struct { + Abbreviation *string `json:"abbreviation,omitempty"` + DefaultTeamImageURL *string `json:"defaultTeamImageUrl,omitempty"` + Description *string `json:"description,omitempty"` + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Revision *int `json:"revision,omitempty"` + State *string `json:"state,omitempty"` + URL *string `json:"url,omitempty"` + Visibility *string `json:"visibility,omitempty"` + LastUpdateTime *time.Time `json:"lastUpdateTime,omitempty"` +} + +// List returns list of the teams +// https://docs.microsoft.com/en-us/rest/api/azure/devops/core/teams/get%20teams +// GET https://dev.azure.com/{organization}/_apis/projects/{projectId}/teams?api-version=5.1-preview.2 +func (s *TeamsService) List(ctx context.Context, owner, project string, opts *TeamsListOptions) ([]*Team, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/teams?api-version=5.1-preview.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + u, _ := s.client.BaseURL.Parse(URL) + req, err := s.client.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, nil, err + } + r := new(TeamsListResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.Teams, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/tests.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/tests.go new file mode 100644 index 000000000..ac9a096b8 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/tests.go @@ -0,0 +1,160 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// TestsService handles communication with the Tests methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test +type TestsService struct { + client *Client +} + +// TestListResponse is the wrapper around the main response for the List of Tests +type TestListResponse struct { + Count int `json:"count,omitempty"` + Tests []*Test `json:"value,omitempty"` +} + +// Test represents a test +type Test struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + IsAutomated *bool `json:"isAutomated,omitempty"` + Iteration *string `json:"iteration,omitempty"` + Owner *struct { + ID string `json:"id,omitempty"` + DisplayName string `json:"displayName,omitempty"` + } `json:"owner,omitempty"` + StartedDate *string `json:"startedDate,omitempty"` + CompletedDate *string `json:"completedDate,omitempty"` + State *string `json:"state,omitempty"` + Plan *struct { + ID string `json:"id,omitempty"` + } `json:"plan,omitempty"` + Revision *int `json:"revision,omitempty"` +} + +// TestsListOptions describes what the request to the API should look like +type TestsListOptions struct { + Count *int `url:"$top,omitempty"` + BuildURI *string `url:"buildUri,omitempty"` +} + +// List returns list of the tests +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test/runs/list +func (s *TestsService) List(ctx context.Context, owner, project string, opts *TestsListOptions) ([]*Test, *http.Response, error) { + URL := fmt.Sprintf("%s/%s/_apis/test/runs?api-version=4.1", + owner, + project, + ) + URL, err := addOptions(URL, opts) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := new(TestListResponse) + resp, err := s.client.Execute(ctx, req, r) + + return r.Tests, resp, err +} + +// TestResultsListResponse is the wrapper around the main response for the List of Tests +type TestResultsListResponse struct { + Results []TestResult `json:"value"` +} + +// TestResult represents a test result +type TestResult struct { + ID int `json:"id"` + Project struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"project"` + StartedDate time.Time `json:"startedDate"` + CompletedDate time.Time `json:"completedDate"` + DurationInMs float64 `json:"durationInMs"` + Outcome string `json:"outcome"` + Revision int `json:"revision"` + RunBy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + UniqueName string `json:"uniqueName"` + URL string `json:"url"` + ImageURL string `json:"imageUrl"` + } `json:"runBy"` + State string `json:"state"` + TestCase struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"testCase"` + TestRun struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"testRun"` + LastUpdatedDate time.Time `json:"lastUpdatedDate"` + LastUpdatedBy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + UniqueName string `json:"uniqueName"` + URL string `json:"url"` + ImageURL string `json:"imageUrl"` + } `json:"lastUpdatedBy"` + Priority int `json:"priority"` + ComputerName string `json:"computerName"` + Build struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"build"` + CreatedDate time.Time `json:"createdDate"` + URL string `json:"url"` + FailureType string `json:"failureType"` + AutomatedTestStorage string `json:"automatedTestStorage"` + AutomatedTestType string `json:"automatedTestType"` + AutomatedTestTypeID string `json:"automatedTestTypeId"` + AutomatedTestID string `json:"automatedTestId"` + Area struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"area"` + TestCaseTitle string `json:"testCaseTitle"` + CustomFields []interface{} `json:"customFields"` + AutomatedTestName string `json:"automatedTestName"` + StackTrace string `json:"stackTrace"` +} + +// TestResultsListOptions describes what the request to the API should look like +type TestResultsListOptions struct { + Count int `url:"$top,omitempty"` + RunID string `url:"runId,omitempty"` +} + +// ResultsList returns list of the test results +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test/runs/list +func (s *TestsService) ResultsList(ctx context.Context, owner, project string, opts *TestResultsListOptions) ([]TestResult, error) { + URL := fmt.Sprintf("%s/%s/_apis/test/Runs/%s/results?api-version=4.1", + owner, + project, + opts.RunID, + ) + opts.RunID = "" + URL, err := addOptions(URL, opts) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + var response TestResultsListResponse + _, err = s.client.Execute(ctx, request, &response) + + return response.Results, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/users.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/users.go new file mode 100644 index 000000000..788ec3582 --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/users.go @@ -0,0 +1,189 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// UsersService handles communication with the Graph.Users methods on the API +// utilising https://docs.microsoft.com/en-us/rest/api/azure/devops/graph/users/get +type UsersService struct { + client *Client +} + +// GraphGroup is the parent struct describing a Microsoft Graph group for Azure Devops +type GraphGroup struct { + GraphMember + /** + * A short phrase to help human readers disambiguate groups with similar names + */ + Description *string `json:"description,omitempty"` + IsCrossProject *bool `json:"isCrossProject,omitempty"` + IsDeleted *bool `json:"isDeleted,omitempty"` + IsGlobalScope *bool `json:"isGlobalScope,omitempty"` + IsRestrictedVisible *bool `json:"isRestrictedVisible,omitempty"` + LocalScopeID *string `json:"localScopeId,omitempty"` + ScopeID *string `json:"scopeId,omitempty"` + ScopeName *string `json:"scopeName,omitempty"` + ScopeType *string `json:"scopeType,omitempty"` + SecuringHostID *string `json:"securingHostId,omitempty"` + SpecialType *string `json:"specialType,omitempty"` +} + +// GraphMember is a child of the GraphUser struct +type GraphMember struct { + GraphSubject + /** + * This represents the name of the container of origin for a graph member. + * (For MSA this is "Windows Live ID", for AD the name of the domain, for + * AAD the tenantID of the directory, for VSTS groups the ScopeId, etc) + */ + Domain *string `json:"domain,omitempty"` + /** + * The email address of record for a given graph member. This may be + * different than the principal name. + */ + MailAddress *string `json:"mailAddress,omitempty"` + /** + * This is the PrincipalName of this graph member from the source + * provider. The source provider may change this field over time and it + * is not guaranteed to be immutable for the life of the graph member by + * VSTS. + */ + PrincipalName *string `json:"principalName,omitempty"` +} + +// GraphSubjectBase Base struct for other graph entities +type GraphSubjectBase struct { + /* + * This field contains zero or more interesting links about the graph + * subject. These links may be invoked to obtain additional relationships + * or more detailed information about this graph subject. + */ + Links map[string]Link `json:"_links,omitempty"` + /** + * The descriptor is the primary way to reference the graph subject while + * the system is running. This field will uniquely identify the same + * graph subject across both Accounts and Organizations. + */ + Descriptor *string `json:"descriptor,omitempty"` + /** + * This is the non-unique display name of the graph subject. To change + * this field, you must alter its value in the source provider. + */ + DisplayName *string `json:"displayName,omitempty"` + /** + * This url is the full route to the source resource of this graph subject. + */ + URL *string `json:"url,omitempty"` +} + +// GraphSubject A graph subject entity +type GraphSubject struct { + GraphSubjectBase + /** + * [Internal Use Only] The legacy descriptor is here in case you need to + * access old version IMS using identity descriptor. + */ + LegacyDescriptor *string `json:"legacyDescriptor,omitempty"` + /** + * The type of source provider for the origin identifier (ex:AD, AAD, MSA) + */ + Origin *string `json:"origin,omitempty"` + /** + * The unique identifier from the system of origin. Typically a sid, object + * id or Guid. Linking and unlinking operations can cause this value to + * change for a user because the user is not backed by a different provider + * and has a different unique id in the new provider. + */ + OriginID *string `json:"originId,omitempty"` + /** + * This field identifies the type of the graph subject (ex: Group, Scope, User). + */ + SubjectKind *string `json:"subjectKind,omitempty"` +} + +// GraphUser is the parent struct describing a Microsoft Graph user for Azure Devops +type GraphUser struct { + GraphMember + IsDeletedInOrigin *bool `json:"isDeletedOrigin,omitempty"` + MetadataUpdateDate *time.Time `json:"metadataUpdateDate,omitempty"` + /** + * The meta type of the user in the origin, such as "member", "guest", + * etc. See UserMetaType for the set of possible values. + */ + MetaType *string `json:"metaType,omitempty"` +} + +// GraphUsersListResponse describes what a response from the Users.List() +// API should look like +type GraphUsersListResponse struct { + Count int `json:"count"` + GraphUsers []*GraphUser `json:"value"` +} + +// Get returns information about a single user in an org +// https://docs.microsoft.com/en-us/rest/api/azure/devops/graph/users/get +func (s *UsersService) Get(ctx context.Context, owner, descriptor string) (*GraphUser, *http.Response, error) { + URL := fmt.Sprintf("%s%s/_apis/graph/users/%s?api-version=5.1-preview.1", + s.client.VsspsBaseURL.String(), + owner, + descriptor, + ) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + var r GraphUser + resp, err := s.client.Execute(ctx, request, &r) + + return &r, resp, err +} + +// List returns a list of users in an org +// utilising https://docs.microsoft.com/en-us/rest/api/azure/devops/graph/users/list +func (s *UsersService) List(ctx context.Context, owner string) ([]*GraphUser, *http.Response, error) { + URL := fmt.Sprintf("%s%s/_apis/graph/users?api-version=5.1-preview.1", + s.client.VsspsBaseURL.String(), + owner, + ) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + var r GraphUsersListResponse + resp, err := s.client.Execute(ctx, request, &r) + + return r.GraphUsers, resp, err +} + +// GraphDescriptorResult Returns user descriptor and links related to the +// request +type GraphDescriptorResult struct { + Links map[string]Link `json:"_links,omitempty"` + Value *string `json:"value,omitempty"` +} + +// GetDescriptors returns descriptors for one or more users based on filter +// criteria +// https://docs.microsoft.com/en-us/rest/api/azure/devops/graph/descriptors/get?view=azure-devops-rest-5.1 +func (s *UsersService) GetDescriptors(ctx context.Context, owner, storageKey string) (*GraphDescriptorResult, *http.Response, error) { + URL := fmt.Sprintf("%s%s/_apis/graph/descriptors/%s?api-version=5.1-preview.1", + s.client.VsspsBaseURL.String(), + owner, + storageKey, + ) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + r := &GraphDescriptorResult{} + resp, err := s.client.Execute(ctx, request, r) + + return r, resp, err +} diff --git a/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/work_items.go b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/work_items.go new file mode 100644 index 000000000..57220474b --- /dev/null +++ b/vendor/github.com/mcdafydd/go-azuredevops/azuredevops/work_items.go @@ -0,0 +1,260 @@ +package azuredevops + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +// WorkItemsService handles communication with the work items methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/wit/work%20items +type WorkItemsService struct { + client *Client +} + +// IterationWorkItems Represents work items in an iteration backlog +type IterationWorkItems struct { + Links *map[string]Link `json:"_links,omitempty"` + WorkItemRelations []*WorkItemLink `json:"workItemRelations"` + URL *string `json:"url,omitempty"` +} + +// WorkItemComment Describes a response to CreateComment +type WorkItemComment struct { + CreatedBy *IdentityRef `json:"createdBy,omitempty"` + CreatedDate *time.Time `json:"createdDate,omitempty"` + ID *int `json:"id,omitempty"` + ModifiedBy *IdentityRef `json:"modifiedBy,omitempty"` + ModifiedDate *time.Time `json:"modifiedDate,omitempty"` + Text *string `json:"text,omitempty"` + URL *string `json:"url,omitempty"` + Version *int `json:"version,omitempty"` + WorkItemID *int `json:"workItemId,omitempty"` +} + +// WorkItemCommentList Represents a list of work item comments. +type WorkItemCommentList struct { + Links *map[string]Link `json:"_links,omitempty"` + Comments []*WorkItemComment `json:"comments,omitempty"` + ContinuationToken *string `json:"continuationToken,omitempty"` + Count *int `json:"count,omitempty"` + NextPage *string `json:"nextPage,omitempty"` + TotalCount *int `json:"totalCount,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WorkItemCommentListOptions URI parameters for ListComments +// Valid Expand strings are: +// all, mentions, none, reactions, renderedText, renderedTextOnly +type WorkItemCommentListOptions struct { + IDs []int `url:"ids,omitempty"` + IncludeDeleted bool `url:"includeDeleted,omitempty"` + Expand string `url:"$expand,omitempty"` +} + +// WorkItemLink A link between two work items. +type WorkItemLink struct { + Rel *string `json:"rel,omitempty"` + Source *WorkItemReference `json:"source,omitempty"` + Target *WorkItemReference `json:"target,omitempty"` +} + +// WorkItemListResponse describes the list response for work items +type WorkItemListResponse struct { + Count int `json:"count,omitempty"` + WorkItems []*WorkItem `json:"value,omitempty"` +} + +// WorkItem describes an individual work item in TFS +type WorkItem struct { + Links *map[string]Link `json:"_links,omitempty"` + CommentVersionRef *CommentVersionRef `json:"commentVersionRef,omitempty"` + Fields *map[string]interface{} `json:"fields,omitempty"` + ID *int `json:"id,omitempty"` + Relations []*WorkItemRelation `json:"relations,omitempty"` + Rev *int `json:"rev,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WorkItemFieldUpdate Describes an update to a work item field. +type WorkItemFieldUpdate struct { + NewValue interface{} `json:"newValue,omitempty"` + OldValue interface{} `json:"oldValue,omitempty"` +} + +// WorkItemRelationUpdates Describes updates to a work item's relations. +type WorkItemRelationUpdates struct { + Added []*WorkItemRelation `json:"added,omitempty"` + Removed []*WorkItemRelation `json:"removed,omitempty"` + Updated []*WorkItemRelation `json:"updated,omitempty"` +} + +// CommentVersionRef refers to the specific version of a comment +type CommentVersionRef struct { + CommentID *int `json:"commentId,omitempty"` + Version *int `json:"version,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WorkItemReference Contains reference to a work item. +type WorkItemReference struct { + ID *int `json:"id,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WorkItemRelation describes an intermediary between iterations and work items +type WorkItemRelation struct { + Attributes *map[string]interface{} `json:"attributes,omitempty"` + Rel *string `json:"rel,omitempty"` + URL *string `json:"url,omitempty"` +} + +// WorkItemUpdate Describes an update to a work item. +type WorkItemUpdate struct { + Links *map[string]interface{} `json:"attributes,omitempty"` + Fields *map[string]WorkItemFieldUpdate `json:"fields,omitempty"` + ID *int `json:"id,omitempty"` + Relations *WorkItemRelationUpdates `json:"relations,omitempty"` + Rev *int `json:"rev,omitempty"` + RevisedBy *IdentityRef `json:"revisedBy,omitempty"` + RevisedDate *time.Time `json:"revisedDate,omitempty"` + WorkItemID *int `json:"workItemId,omitempty"` + URL *string `json:"url,omitempty"` +} + +// GetForIteration will get a list of work items based on an iteration name +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/wit/work%20items/list +func (s *WorkItemsService) GetForIteration(ctx context.Context, owner, project, team string, iteration Iteration) ([]*WorkItem, *http.Response, error) { + iterationWorkItems, resp, err := s.GetIdsForIteration(ctx, owner, project, team, iteration) + if err != nil { + return nil, resp, err + } + + var workIds []string + for index := 0; index < len(iterationWorkItems.WorkItemRelations); index++ { + relationship := (iterationWorkItems.WorkItemRelations)[index] + workIds = append(workIds, strconv.Itoa(*relationship.Target.ID)) + } + + // https://docs.microsoft.com/en-us/rest/api/vsts/wit/work%20item%20types%20field/list + fields := []string{ + "System.Id", "System.Title", "System.State", "System.WorkItemType", + "Microsoft.VSTS.Scheduling.StoryPoints", "System.BoardColumn", + "System.CreatedBy", "System.AssignedTo", "System.Tags", + } + + // Now we want to pad out the fields for the work items + URL := fmt.Sprintf( + "%s/%s/_apis/wit/workitems?ids=%s&fields=%s&api-version=5.1-preview.1", + owner, + project, + strings.Join(workIds, ","), + strings.Join(fields, ","), + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + r := new(WorkItemListResponse) + resp, err = s.client.Execute(ctx, req, r) + + return r.WorkItems, resp, err +} + +// GetIdsForIteration will return an array of ids for a given iteration +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/work/iterations/get%20iteration%20work%20items +func (s *WorkItemsService) GetIdsForIteration(ctx context.Context, owner, project, team string, iteration Iteration) (*IterationWorkItems, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/%s/_apis/work/teamsettings/iterations/%s/workitems?api-version=5.1-preview.1", + owner, + project, + url.PathEscape(team), + *iteration.ID, + ) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + r := new(IterationWorkItems) + + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// ListComments Lists all comments on a work item +// https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/comments/get%20comment?view=azure-devops-rest-5.1#comment +func (s *WorkItemsService) ListComments(ctx context.Context, owner, project string, workItemID int, opts *WorkItemCommentListOptions) (*WorkItemCommentList, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/wit/workItems/%d/comments?api-version=5.1-preview.3", + owner, + project, + workItemID, + ) + + URL, err := addOptions(URL, opts) + + r := new(WorkItemCommentList) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// GetComment Gets a work item comment +// https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/comments/get%20comments%20batch?view=azure-devops-rest-5.1#commentlist +func (s *WorkItemsService) GetComment(ctx context.Context, owner, project string, workItemID, commentID int, opts *WorkItemCommentListOptions) (*WorkItemComment, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/wit/workItems/%d/comments/%d?api-version=5.1-preview.3", + owner, + project, + workItemID, + commentID, + ) + + r := new(WorkItemComment) + + req, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} + +// CreateComment Posts a comment to a work item +// https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/comments/add +func (s *WorkItemsService) CreateComment(ctx context.Context, owner, project string, workItemID int, comment *WorkItemComment) (*WorkItemComment, *http.Response, error) { + URL := fmt.Sprintf( + "%s/%s/_apis/wit/workItems/%d/comments?api-version=5.1-preview.3", + owner, + project, + workItemID, + ) + + r := new(WorkItemComment) + + req, err := s.client.NewRequest("POST", URL, comment) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Execute(ctx, req, r) + + return r, resp, err +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml b/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml new file mode 100644 index 000000000..e0c87602f --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml @@ -0,0 +1 @@ +repo_token: x2wlA1x0X8CK45ybWpZRCVRB4g7vtkhaw diff --git a/vendor/github.com/microcosm-cc/bluemonday/.travis.yml b/vendor/github.com/microcosm-cc/bluemonday/.travis.yml new file mode 100644 index 000000000..4f6664619 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/.travis.yml @@ -0,0 +1,22 @@ +language: go +go: + - 1.1.x + - 1.2.x + - 1.3.x + - 1.4.x + - 1.5.x + - 1.6.x + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x + - tip +matrix: + allow_failures: + - go: tip + fast_finish: true +install: + - go get . +script: + - go test -v ./... diff --git a/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md new file mode 100644 index 000000000..d2b12302f --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to bluemonday + +Third-party patches are essential for keeping bluemonday secure and offering the features developers want. However there are a few guidelines that we need contributors to follow so that we can maintain the quality of work that developers who use bluemonday expect. + +## Getting Started + +* Make sure you have a [Github account](https://github.com/signup/free) + +## Guidelines + +1. Do not vendor dependencies. As a security package, were we to vendor dependencies the projects that then vendor bluemonday may not receive the latest security updates to the dependencies. By not vendoring dependencies the project that implements bluemonday will vendor the latest version of any dependent packages. Vendoring is a project problem, not a package problem. bluemonday will be tested against the latest version of dependencies periodically and during any PR/merge. + +## Submitting an Issue + +* Submit a ticket for your issue, assuming one does not already exist +* Clearly describe the issue including the steps to reproduce (with sample input and output) if it is a bug + +If you are reporting a security flaw, you may expect that we will provide the code to fix it for you. Otherwise you may want to submit a pull request to ensure the resolution is applied sooner rather than later: + +* Fork the repository on Github +* Issue a pull request containing code to resolve the issue + +## Submitting a Pull Request + +* Submit a ticket for your issue, assuming one does not already exist +* Describe the reason for the pull request and if applicable show some example inputs and outputs to demonstrate what the patch does +* Fork the repository on Github +* Before submitting the pull request you should + 1. Include tests for your patch, 1 test should encapsulate the entire patch and should refer to the Github issue + 1. If you have added new exposed/public functionality, you should ensure it is documented appropriately + 1. If you have added new exposed/public functionality, you should consider demonstrating how to use it within one of the helpers or shipped policies if appropriate or within a test if modifying a helper or policy is not appropriate + 1. Run all of the tests `go test -v ./...` or `make test` and ensure all tests pass + 1. Run gofmt `gofmt -w ./$*` or `make fmt` + 1. Run vet `go tool vet *.go` or `make vet` and resolve any issues + 1. Install golint using `go get -u github.com/golang/lint/golint` and run vet `golint *.go` or `make lint` and resolve every warning +* When submitting the pull request you should + 1. Note the issue(s) it resolves, i.e. `Closes #6` in the pull request comment to close issue #6 when the pull request is accepted + +Once you have submitted a pull request, we *may* merge it without changes. If we have any comments or feedback, or need you to make changes to your pull request we will update the Github pull request or the associated issue. We expect responses from you within two weeks, and we may close the pull request is there is no activity. + +### Contributor Licence Agreement + +We haven't gone for the formal "Sign a Contributor Licence Agreement" thing that projects like [puppet](https://cla.puppetlabs.com/), [Mojito](https://developer.yahoo.com/cocktails/mojito/cla/) and companies like [Google](http://code.google.com/legal/individual-cla-v1.0.html) are using. + +But we do need to know that we can accept and merge your contributions, so for now the act of contributing a pull request should be considered equivalent to agreeing to a contributor licence agreement, specifically: + +You accept that the act of submitting code to the bluemonday project is to grant a copyright licence to the project that is perpetual, worldwide, non-exclusive, no-charge, royalty free and irrevocable. + +You accept that all who comply with the licence of the project (BSD 3-clause) are permitted to use your contributions to the project. + +You accept, and by submitting code do declare, that you have the legal right to grant such a licence to the project and that each of the contributions is your own original creation. diff --git a/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md b/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md new file mode 100644 index 000000000..b98873f3c --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md @@ -0,0 +1,6 @@ +1. Andrew Krasichkov @buglloc https://github.com/buglloc +1. John Graham-Cumming http://jgc.org/ +1. Mike Samuel mikesamuel@gmail.com +1. Dmitri Shuralyov shurcooL@gmail.com +1. https://github.com/opennota +1. https://github.com/Gufran \ No newline at end of file diff --git a/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md new file mode 100644 index 000000000..f822458ed --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md @@ -0,0 +1,28 @@ +Copyright (c) 2014, David Kitchen + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the organisation (Microcosm) nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/microcosm-cc/bluemonday/Makefile b/vendor/github.com/microcosm-cc/bluemonday/Makefile new file mode 100644 index 000000000..b15dc74f3 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/Makefile @@ -0,0 +1,42 @@ +# Targets: +# +# all: Builds the code locally after testing +# +# fmt: Formats the source files +# build: Builds the code locally +# vet: Vets the code +# lint: Runs lint over the code (you do not need to fix everything) +# test: Runs the tests +# cover: Gives you the URL to a nice test coverage report +# +# install: Builds, tests and installs the code locally + +.PHONY: all fmt build vet lint test cover install + +# The first target is always the default action if `make` is called without +# args we build and install into $GOPATH so that it can just be run + +all: fmt vet test install + +fmt: + @gofmt -s -w ./$* + +build: + @go build + +vet: + @go vet *.go + +lint: + @golint *.go + +test: + @go test -v ./... + +cover: COVERAGE_FILE := coverage.out +cover: + @go test -coverprofile=$(COVERAGE_FILE) && \ + cover -html=$(COVERAGE_FILE) && rm $(COVERAGE_FILE) + +install: + @go install ./... diff --git a/vendor/github.com/microcosm-cc/bluemonday/README.md b/vendor/github.com/microcosm-cc/bluemonday/README.md new file mode 100644 index 000000000..ce679c10b --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/README.md @@ -0,0 +1,350 @@ +# bluemonday [![Build Status](https://travis-ci.org/microcosm-cc/bluemonday.svg?branch=master)](https://travis-ci.org/microcosm-cc/bluemonday) [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge) + +bluemonday is a HTML sanitizer implemented in Go. It is fast and highly configurable. + +bluemonday takes untrusted user generated content as an input, and will return HTML that has been sanitised against a whitelist of approved HTML elements and attributes so that you can safely include the content in your web page. + +If you accept user generated content, and your server uses Go, you **need** bluemonday. + +The default policy for user generated content (`bluemonday.UGCPolicy().Sanitize()`) turns this: +```html +Hello World +``` + +Into a harmless: +```html +Hello World +``` + +And it turns this: +```html +XSS +``` + +Into this: +```html +XSS +``` + +Whilst still allowing this: +```html + + + +``` + +To pass through mostly unaltered (it gained a rel="nofollow" which is a good thing for user generated content): +```html + + + +``` + +It protects sites from [XSS](http://en.wikipedia.org/wiki/Cross-site_scripting) attacks. There are many [vectors for an XSS attack](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) and the best way to mitigate the risk is to sanitize user input against a known safe list of HTML elements and attributes. + +You should **always** run bluemonday **after** any other processing. + +If you use [blackfriday](https://github.com/russross/blackfriday) or [Pandoc](http://johnmacfarlane.net/pandoc/) then bluemonday should be run after these steps. This ensures that no insecure HTML is introduced later in your process. + +bluemonday is heavily inspired by both the [OWASP Java HTML Sanitizer](https://code.google.com/p/owasp-java-html-sanitizer/) and the [HTML Purifier](http://htmlpurifier.org/). + +## Technical Summary + +Whitelist based, you need to either build a policy describing the HTML elements and attributes to permit (and the `regexp` patterns of attributes), or use one of the supplied policies representing good defaults. + +The policy containing the whitelist is applied using a fast non-validating, forward only, token-based parser implemented in the [Go net/html library](https://godoc.org/golang.org/x/net/html) by the core Go team. + +We expect to be supplied with well-formatted HTML (closing elements for every applicable open element, nested correctly) and so we do not focus on repairing badly nested or incomplete HTML. We focus on simply ensuring that whatever elements do exist are described in the policy whitelist and that attributes and links are safe for use on your web page. [GIGO](http://en.wikipedia.org/wiki/Garbage_in,_garbage_out) does apply and if you feed it bad HTML bluemonday is not tasked with figuring out how to make it good again. + +### Supported Go Versions + +bluemonday is tested against Go 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, and tip. + +We do not support Go 1.0 as we depend on `golang.org/x/net/html` which includes a reference to `io.ErrNoProgress` which did not exist in Go 1.0. + +## Is it production ready? + +*Yes* + +We are using bluemonday in production having migrated from the widely used and heavily field tested OWASP Java HTML Sanitizer. + +We are passing our extensive test suite (including AntiSamy tests as well as tests for any issues raised). Check for any [unresolved issues](https://github.com/microcosm-cc/bluemonday/issues?page=1&state=open) to see whether anything may be a blocker for you. + +We invite pull requests and issues to help us ensure we are offering comprehensive protection against various attacks via user generated content. + +## Usage + +Install in your `${GOPATH}` using `go get -u github.com/microcosm-cc/bluemonday` + +Then call it: +```go +package main + +import ( + "fmt" + + "github.com/microcosm-cc/bluemonday" +) + +func main() { + // Do this once for each unique policy, and use the policy for the life of the program + // Policy creation/editing is not safe to use in multiple goroutines + p := bluemonday.UGCPolicy() + + // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines + html := p.Sanitize( + `Google`, + ) + + // Output: + // Google + fmt.Println(html) +} +``` + +We offer three ways to call Sanitize: +```go +p.Sanitize(string) string +p.SanitizeBytes([]byte) []byte +p.SanitizeReader(io.Reader) bytes.Buffer +``` + +If you are obsessed about performance, `p.SanitizeReader(r).Bytes()` will return a `[]byte` without performing any unnecessary casting of the inputs or outputs. Though the difference is so negligible you should never need to care. + +You can build your own policies: +```go +package main + +import ( + "fmt" + + "github.com/microcosm-cc/bluemonday" +) + +func main() { + p := bluemonday.NewPolicy() + + // Require URLs to be parseable by net/url.Parse and either: + // mailto: http:// or https:// + p.AllowStandardURLs() + + // We only allow

    and + p.AllowAttrs("href").OnElements("a") + p.AllowElements("p") + + html := p.Sanitize( + `Google`, + ) + + // Output: + // Google + fmt.Println(html) +} +``` + +We ship two default policies: + +1. `bluemonday.StrictPolicy()` which can be thought of as equivalent to stripping all HTML elements and their attributes as it has nothing on its whitelist. An example usage scenario would be blog post titles where HTML tags are not expected at all and if they are then the elements *and* the content of the elements should be stripped. This is a *very* strict policy. +2. `bluemonday.UGCPolicy()` which allows a broad selection of HTML elements and attributes that are safe for user generated content. Note that this policy does *not* whitelist iframes, object, embed, styles, script, etc. An example usage scenario would be blog post bodies where a variety of formatting is expected along with the potential for TABLEs and IMGs. + +## Policy Building + +The essence of building a policy is to determine which HTML elements and attributes are considered safe for your scenario. OWASP provide an [XSS prevention cheat sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) to help explain the risks, but essentially: + +1. Avoid anything other than the standard HTML elements +1. Avoid `script`, `style`, `iframe`, `object`, `embed`, `base` elements that allow code to be executed by the client or third party content to be included that can execute code +1. Avoid anything other than plain HTML attributes with values matched to a regexp + +Basically, you should be able to describe what HTML is fine for your scenario. If you do not have confidence that you can describe your policy please consider using one of the shipped policies such as `bluemonday.UGCPolicy()`. + +To create a new policy: +```go +p := bluemonday.NewPolicy() +``` + +To add elements to a policy either add just the elements: +```go +p.AllowElements("b", "strong") +``` + +Or add elements as a virtue of adding an attribute: +```go +// Not the recommended pattern, see the recommendation on using .Matching() below +p.AllowAttrs("nowrap").OnElements("td", "th") +``` + +Attributes can either be added to all elements: +```go +p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally() +``` + +Or attributes can be added to specific elements: +```go +// Not the recommended pattern, see the recommendation on using .Matching() below +p.AllowAttrs("value").OnElements("li") +``` + +It is **always** recommended that an attribute be made to match a pattern. XSS in HTML attributes is very easy otherwise: +```go +// \p{L} matches unicode letters, \p{N} matches unicode numbers +p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally() +``` + +You can stop at any time and call .Sanitize(): +```go +// string htmlIn passed in from a HTTP POST +htmlOut := p.Sanitize(htmlIn) +``` + +And you can take any existing policy and extend it: +```go +p := bluemonday.UGCPolicy() +p.AllowElements("fieldset", "select", "option") +``` + +### Links + +Links are difficult beasts to sanitise safely and also one of the biggest attack vectors for malicious content. + +It is possible to do this: +```go +p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a") +``` + +But that will not protect you as the regular expression is insufficient in this case to have prevented a malformed value doing something unexpected. + +We provide some additional global options for safely working with links. + +`RequireParseableURLs` will ensure that URLs are parseable by Go's `net/url` package: +```go +p.RequireParseableURLs(true) +``` + +If you have enabled parseable URLs then the following option will `AllowRelativeURLs`. By default this is disabled (bluemonday is a whitelist tool... you need to explicitly tell us to permit things) and when disabled it will prevent all local and scheme relative URLs (i.e. `href="localpage.html"`, `href="../home.html"` and even `href="//www.google.com"` are relative): +```go +p.AllowRelativeURLs(true) +``` + +If you have enabled parseable URLs then you can whitelist the schemes (commonly called protocol when thinking of `http` and `https`) that are permitted. Bear in mind that allowing relative URLs in the above option will allow for a blank scheme: +```go +p.AllowURLSchemes("mailto", "http", "https") +``` + +Regardless of whether you have enabled parseable URLs, you can force all URLs to have a rel="nofollow" attribute. This will be added if it does not exist, but only when the `href` is valid: +```go +// This applies to "a" "area" "link" elements that have a "href" attribute +p.RequireNoFollowOnLinks(true) +``` + +We provide a convenience method that applies all of the above, but you will still need to whitelist the linkable elements for the URL rules to be applied to: +```go +p.AllowStandardURLs() +p.AllowAttrs("cite").OnElements("blockquote", "q") +p.AllowAttrs("href").OnElements("a", "area") +p.AllowAttrs("src").OnElements("img") +``` + +An additional complexity regarding links is the data URI as defined in [RFC2397](http://tools.ietf.org/html/rfc2397). The data URI allows for images to be served inline using this format: + +```html + +``` + +We have provided a helper to verify the mimetype followed by base64 content of data URIs links: + +```go +p.AllowDataURIImages() +``` + +That helper will enable GIF, JPEG, PNG and WEBP images. + +It should be noted that there is a potential [security](http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/) [risk](https://capec.mitre.org/data/definitions/244.html) with the use of data URI links. You should only enable data URI links if you already trust the content. + +We also have some features to help deal with user generated content: +```go +p.AddTargetBlankToFullyQualifiedLinks(true) +``` + +This will ensure that anchor `` links that are fully qualified (the href destination includes a host name) will get `target="_blank"` added to them. + +Additionally any link that has `target="_blank"` after the policy has been applied will also have the `rel` attribute adjusted to add `noopener`. This means a link may start like `` and will end up as ``. It is important to note that the addition of `noopener` is a security feature and not an issue. There is an unfortunate feature to browsers that a browser window opened as a result of `target="_blank"` can still control the opener (your web page) and this protects against that. The background to this can be found here: [https://dev.to/ben/the-targetblank-vulnerability-by-example](https://dev.to/ben/the-targetblank-vulnerability-by-example) + +### Policy Building Helpers + +We also bundle some helpers to simplify policy building: +```go + +// Permits the "dir", "id", "lang", "title" attributes globally +p.AllowStandardAttributes() + +// Permits the "img" element and its standard attributes +p.AllowImages() + +// Permits ordered and unordered lists, and also definition lists +p.AllowLists() + +// Permits HTML tables and all applicable elements and non-styling attributes +p.AllowTables() +``` + +### Invalid Instructions + +The following are invalid: +```go +// This does not say where the attributes are allowed, you need to add +// .Globally() or .OnElements(...) +// This will be ignored without error. +p.AllowAttrs("value") + +// This does not say where the attributes are allowed, you need to add +// .Globally() or .OnElements(...) +// This will be ignored without error. +p.AllowAttrs( + "type", +).Matching( + regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"), +) +``` + +Both examples exhibit the same issue, they declare attributes but do not then specify whether they are whitelisted globally or only on specific elements (and which elements). Attributes belong to one or more elements, and the policy needs to declare this. + +## Limitations + +We are not yet including any tools to help whitelist and sanitize CSS. Which means that unless you wish to do the heavy lifting in a single regular expression (inadvisable), **you should not allow the "style" attribute anywhere**. + +It is not the job of bluemonday to fix your bad HTML, it is merely the job of bluemonday to prevent malicious HTML getting through. If you have mismatched HTML elements, or non-conforming nesting of elements, those will remain. But if you have well-structured HTML bluemonday will not break it. + +## TODO + +* Add support for CSS sanitisation to allow some CSS properties based on a whitelist, possibly using the [Gorilla CSS3 scanner](http://www.gorillatoolkit.org/pkg/css/scanner) - PRs welcome so long as testing covers XSS and demonstrates safety first +* Investigate whether devs want to blacklist elements and attributes. This would allow devs to take an existing policy (such as the `bluemonday.UGCPolicy()` ) that encapsulates 90% of what they're looking for but does more than they need, and to remove the extra things they do not want to make it 100% what they want +* Investigate whether devs want a validating HTML mode, in which the HTML elements are not just transformed into a balanced tree (every start tag has a closing tag at the correct depth) but also that elements and character data appear only in their allowed context (i.e. that a `table` element isn't a descendent of a `caption`, that `colgroup`, `thead`, `tbody`, `tfoot` and `tr` are permitted, and that character data is not permitted) + +## Development + +If you have cloned this repo you will probably need the dependency: + +`go get golang.org/x/net/html` + +Gophers can use their familiar tools: + +`go build` + +`go test` + +I personally use a Makefile as it spares typing the same args over and over whilst providing consistency for those of us who jump from language to language and enjoy just typing `make` in a project directory and watch magic happen. + +`make` will build, vet, test and install the library. + +`make clean` will remove the library from a *single* `${GOPATH}/pkg` directory tree + +`make test` will run the tests + +`make cover` will run the tests and *open a browser window* with the coverage report + +`make lint` will run golint (install via `go get github.com/golang/lint/golint`) + +## Long term goals + +1. Open the code to adversarial peer review similar to the [Attack Review Ground Rules](https://code.google.com/p/owasp-java-html-sanitizer/wiki/AttackReviewGroundRules) +1. Raise funds and pay for an external security review diff --git a/vendor/github.com/microcosm-cc/bluemonday/doc.go b/vendor/github.com/microcosm-cc/bluemonday/doc.go new file mode 100644 index 000000000..71dab6089 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/doc.go @@ -0,0 +1,104 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +Package bluemonday provides a way of describing a whitelist of HTML elements +and attributes as a policy, and for that policy to be applied to untrusted +strings from users that may contain markup. All elements and attributes not on +the whitelist will be stripped. + +The default bluemonday.UGCPolicy().Sanitize() turns this: + + Hello World + +Into the more harmless: + + Hello World + +And it turns this: + + XSS + +Into this: + + XSS + +Whilst still allowing this: + + + + + +To pass through mostly unaltered (it gained a rel="nofollow"): + + + + + +The primary purpose of bluemonday is to take potentially unsafe user generated +content (from things like Markdown, HTML WYSIWYG tools, etc) and make it safe +for you to put on your website. + +It protects sites against XSS (http://en.wikipedia.org/wiki/Cross-site_scripting) +and other malicious content that a user interface may deliver. There are many +vectors for an XSS attack (https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) +and the safest thing to do is to sanitize user input against a known safe list +of HTML elements and attributes. + +Note: You should always run bluemonday after any other processing. + +If you use blackfriday (https://github.com/russross/blackfriday) or +Pandoc (http://johnmacfarlane.net/pandoc/) then bluemonday should be run after +these steps. This ensures that no insecure HTML is introduced later in your +process. + +bluemonday is heavily inspired by both the OWASP Java HTML Sanitizer +(https://code.google.com/p/owasp-java-html-sanitizer/) and the HTML Purifier +(http://htmlpurifier.org/). + +We ship two default policies, one is bluemonday.StrictPolicy() and can be +thought of as equivalent to stripping all HTML elements and their attributes as +it has nothing on its whitelist. + +The other is bluemonday.UGCPolicy() and allows a broad selection of HTML +elements and attributes that are safe for user generated content. Note that +this policy does not whitelist iframes, object, embed, styles, script, etc. + +The essence of building a policy is to determine which HTML elements and +attributes are considered safe for your scenario. OWASP provide an XSS +prevention cheat sheet ( https://www.google.com/search?q=xss+prevention+cheat+sheet ) +to help explain the risks, but essentially: + + 1. Avoid whitelisting anything other than plain HTML elements + 2. Avoid whitelisting `script`, `style`, `iframe`, `object`, `embed`, `base` + elements + 3. Avoid whitelisting anything other than plain HTML elements with simple + values that you can match to a regexp +*/ +package bluemonday diff --git a/vendor/github.com/microcosm-cc/bluemonday/go.mod b/vendor/github.com/microcosm-cc/bluemonday/go.mod new file mode 100644 index 000000000..fa8453c5f --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/go.mod @@ -0,0 +1,5 @@ +module github.com/microcosm-cc/bluemonday + +go 1.9 + +require golang.org/x/net v0.0.0-20181220203305-927f97764cc3 diff --git a/vendor/github.com/microcosm-cc/bluemonday/go.sum b/vendor/github.com/microcosm-cc/bluemonday/go.sum new file mode 100644 index 000000000..bee241d1c --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/go.sum @@ -0,0 +1,2 @@ +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/vendor/github.com/microcosm-cc/bluemonday/helpers.go b/vendor/github.com/microcosm-cc/bluemonday/helpers.go new file mode 100644 index 000000000..dfa5868da --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/helpers.go @@ -0,0 +1,297 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "encoding/base64" + "net/url" + "regexp" +) + +// A selection of regular expressions that can be used as .Matching() rules on +// HTML attributes. +var ( + // CellAlign handles the `align` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-align + CellAlign = regexp.MustCompile(`(?i)^(center|justify|left|right|char)$`) + + // CellVerticalAlign handles the `valign` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-valign + CellVerticalAlign = regexp.MustCompile(`(?i)^(baseline|bottom|middle|top)$`) + + // Direction handles the `dir` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo#attr-dir + Direction = regexp.MustCompile(`(?i)^(rtl|ltr)$`) + + // ImageAlign handles the `align` attribute on the `image` tag + // http://www.w3.org/MarkUp/Test/Img/imgtest.html + ImageAlign = regexp.MustCompile( + `(?i)^(left|right|top|texttop|middle|absmiddle|baseline|bottom|absbottom)$`, + ) + + // Integer describes whole positive integers (including 0) used in places + // like td.colspan + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-colspan + Integer = regexp.MustCompile(`^[0-9]+$`) + + // ISO8601 according to the W3 group is only a subset of the ISO8601 + // standard: http://www.w3.org/TR/NOTE-datetime + // + // Used in places like time.datetime + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time#attr-datetime + // + // Matches patterns: + // Year: + // YYYY (eg 1997) + // Year and month: + // YYYY-MM (eg 1997-07) + // Complete date: + // YYYY-MM-DD (eg 1997-07-16) + // Complete date plus hours and minutes: + // YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) + // Complete date plus hours, minutes and seconds: + // YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) + // Complete date plus hours, minutes, seconds and a decimal fraction of a + // second + // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) + ISO8601 = regexp.MustCompile( + `^[0-9]{4}(-[0-9]{2}(-[0-9]{2}([ T][0-9]{2}(:[0-9]{2}){1,2}(.[0-9]{1,6})` + + `?Z?([\+-][0-9]{2}:[0-9]{2})?)?)?)?$`, + ) + + // ListType encapsulates the common value as well as the latest spec + // values for lists + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#attr-type + ListType = regexp.MustCompile(`(?i)^(circle|disc|square|a|A|i|I|1)$`) + + // SpaceSeparatedTokens is used in places like `a.rel` and the common attribute + // `class` which both contain space delimited lists of data tokens + // http://www.w3.org/TR/html-markup/datatypes.html#common.data.tokens-def + // Regexp: \p{L} matches unicode letters, \p{N} matches unicode numbers + SpaceSeparatedTokens = regexp.MustCompile(`^([\s\p{L}\p{N}_-]+)$`) + + // Number is a double value used on HTML5 meter and progress elements + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-button-element.html#the-meter-element + Number = regexp.MustCompile(`^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$`) + + // NumberOrPercent is used predominantly as units of measurement in width + // and height attributes + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-height + NumberOrPercent = regexp.MustCompile(`^[0-9]+[%]?$`) + + // Paragraph of text in an attribute such as *.'title', img.alt, etc + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-title + // Note that we are not allowing chars that could close tags like '>' + Paragraph = regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$`) + + // dataURIImagePrefix is used by AllowDataURIImages to define the acceptable + // prefix of data URIs that contain common web image formats. + // + // This is not exported as it's not useful by itself, and only has value + // within the AllowDataURIImages func + dataURIImagePrefix = regexp.MustCompile( + `^image/(gif|jpeg|png|webp);base64,`, + ) +) + +// AllowStandardURLs is a convenience function that will enable rel="nofollow" +// on "a", "area" and "link" (if you have allowed those elements) and will +// ensure that the URL values are parseable and either relative or belong to the +// "mailto", "http", or "https" schemes +func (p *Policy) AllowStandardURLs() { + // URLs must be parseable by net/url.Parse() + p.RequireParseableURLs(true) + + // !url.IsAbs() is permitted + p.AllowRelativeURLs(true) + + // Most common URL schemes only + p.AllowURLSchemes("mailto", "http", "https") + + // For all anchors we will add rel="nofollow" if it does not already exist + // This applies to "a" "area" "link" + p.RequireNoFollowOnLinks(true) +} + +// AllowStandardAttributes will enable "id", "title" and the language specific +// attributes "dir" and "lang" on all elements that are whitelisted +func (p *Policy) AllowStandardAttributes() { + // "dir" "lang" are permitted as both language attributes affect charsets + // and direction of text. + p.AllowAttrs("dir").Matching(Direction).Globally() + p.AllowAttrs( + "lang", + ).Matching(regexp.MustCompile(`[a-zA-Z]{2,20}`)).Globally() + + // "id" is permitted. This is pretty much as some HTML elements require this + // to work well ("dfn" is an example of a "id" being value) + // This does create a risk that JavaScript and CSS within your web page + // might identify the wrong elements. Ensure that you select things + // accurately + p.AllowAttrs("id").Matching( + regexp.MustCompile(`[a-zA-Z0-9\:\-_\.]+`), + ).Globally() + + // "title" is permitted as it improves accessibility. + p.AllowAttrs("title").Matching(Paragraph).Globally() +} + +// AllowStyling presently enables the class attribute globally. +// +// Note: When bluemonday ships a CSS parser and we can safely sanitise that, +// this will also allow sanitized styling of elements via the style attribute. +func (p *Policy) AllowStyling() { + + // "class" is permitted globally + p.AllowAttrs("class").Matching(SpaceSeparatedTokens).Globally() +} + +// AllowImages enables the img element and some popular attributes. It will also +// ensure that URL values are parseable. This helper does not enable data URI +// images, for that you should also use the AllowDataURIImages() helper. +func (p *Policy) AllowImages() { + + // "img" is permitted + p.AllowAttrs("align").Matching(ImageAlign).OnElements("img") + p.AllowAttrs("alt").Matching(Paragraph).OnElements("img") + p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("img") + + // Standard URLs enabled + p.AllowStandardURLs() + p.AllowAttrs("src").OnElements("img") +} + +// AllowDataURIImages permits the use of inline images defined in RFC2397 +// http://tools.ietf.org/html/rfc2397 +// http://en.wikipedia.org/wiki/Data_URI_scheme +// +// Images must have a mimetype matching: +// image/gif +// image/jpeg +// image/png +// image/webp +// +// NOTE: There is a potential security risk to allowing data URIs and you should +// only permit them on content you already trust. +// http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/ +// https://capec.mitre.org/data/definitions/244.html +func (p *Policy) AllowDataURIImages() { + + // URLs must be parseable by net/url.Parse() + p.RequireParseableURLs(true) + + // Supply a function to validate images contained within data URI + p.AllowURLSchemeWithCustomPolicy( + "data", + func(url *url.URL) (allowUrl bool) { + if url.RawQuery != "" || url.Fragment != "" { + return false + } + + matched := dataURIImagePrefix.FindString(url.Opaque) + if matched == "" { + return false + } + + _, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) + if err != nil { + return false + } + + return true + }, + ) +} + +// AllowLists will enabled ordered and unordered lists, as well as definition +// lists +func (p *Policy) AllowLists() { + // "ol" "ul" are permitted + p.AllowAttrs("type").Matching(ListType).OnElements("ol", "ul") + + // "li" is permitted + p.AllowAttrs("type").Matching(ListType).OnElements("li") + p.AllowAttrs("value").Matching(Integer).OnElements("li") + + // "dl" "dt" "dd" are permitted + p.AllowElements("dl", "dt", "dd") +} + +// AllowTables will enable a rich set of elements and attributes to describe +// HTML tables +func (p *Policy) AllowTables() { + + // "table" is permitted + p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("table") + p.AllowAttrs("summary").Matching(Paragraph).OnElements("table") + + // "caption" is permitted + p.AllowElements("caption") + + // "col" "colgroup" are permitted + p.AllowAttrs("align").Matching(CellAlign).OnElements("col", "colgroup") + p.AllowAttrs("height", "width").Matching( + NumberOrPercent, + ).OnElements("col", "colgroup") + p.AllowAttrs("span").Matching(Integer).OnElements("colgroup", "col") + p.AllowAttrs("valign").Matching( + CellVerticalAlign, + ).OnElements("col", "colgroup") + + // "thead" "tr" are permitted + p.AllowAttrs("align").Matching(CellAlign).OnElements("thead", "tr") + p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("thead", "tr") + + // "td" "th" are permitted + p.AllowAttrs("abbr").Matching(Paragraph).OnElements("td", "th") + p.AllowAttrs("align").Matching(CellAlign).OnElements("td", "th") + p.AllowAttrs("colspan", "rowspan").Matching(Integer).OnElements("td", "th") + p.AllowAttrs("headers").Matching( + SpaceSeparatedTokens, + ).OnElements("td", "th") + p.AllowAttrs("height", "width").Matching( + NumberOrPercent, + ).OnElements("td", "th") + p.AllowAttrs( + "scope", + ).Matching( + regexp.MustCompile(`(?i)(?:row|col)(?:group)?`), + ).OnElements("td", "th") + p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("td", "th") + p.AllowAttrs("nowrap").Matching( + regexp.MustCompile(`(?i)|nowrap`), + ).OnElements("td", "th") + + // "tbody" "tfoot" + p.AllowAttrs("align").Matching(CellAlign).OnElements("tbody", "tfoot") + p.AllowAttrs("valign").Matching( + CellVerticalAlign, + ).OnElements("tbody", "tfoot") +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policies.go b/vendor/github.com/microcosm-cc/bluemonday/policies.go new file mode 100644 index 000000000..570bba886 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/policies.go @@ -0,0 +1,253 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "regexp" +) + +// StrictPolicy returns an empty policy, which will effectively strip all HTML +// elements and their attributes from a document. +func StrictPolicy() *Policy { + return NewPolicy() +} + +// StripTagsPolicy is DEPRECATED. Use StrictPolicy instead. +func StripTagsPolicy() *Policy { + return StrictPolicy() +} + +// UGCPolicy returns a policy aimed at user generated content that is a result +// of HTML WYSIWYG tools and Markdown conversions. +// +// This is expected to be a fairly rich document where as much markup as +// possible should be retained. Markdown permits raw HTML so we are basically +// providing a policy to sanitise HTML5 documents safely but with the +// least intrusion on the formatting expectations of the user. +func UGCPolicy() *Policy { + + p := NewPolicy() + + /////////////////////// + // Global attributes // + /////////////////////// + + // "class" is not permitted as we are not allowing users to style their own + // content + + p.AllowStandardAttributes() + + ////////////////////////////// + // Global URL format policy // + ////////////////////////////// + + p.AllowStandardURLs() + + //////////////////////////////// + // Declarations and structure // + //////////////////////////////// + + // "xml" "xslt" "DOCTYPE" "html" "head" are not permitted as we are + // expecting user generated content to be a fragment of HTML and not a full + // document. + + ////////////////////////// + // Sectioning root tags // + ////////////////////////// + + // "article" and "aside" are permitted and takes no attributes + p.AllowElements("article", "aside") + + // "body" is not permitted as we are expecting user generated content to be a fragment + // of HTML and not a full document. + + // "details" is permitted, including the "open" attribute which can either + // be blank or the value "open". + p.AllowAttrs( + "open", + ).Matching(regexp.MustCompile(`(?i)^(|open)$`)).OnElements("details") + + // "fieldset" is not permitted as we are not allowing forms to be created. + + // "figure" is permitted and takes no attributes + p.AllowElements("figure") + + // "nav" is not permitted as it is assumed that the site (and not the user) + // has defined navigation elements + + // "section" is permitted and takes no attributes + p.AllowElements("section") + + // "summary" is permitted and takes no attributes + p.AllowElements("summary") + + ////////////////////////// + // Headings and footers // + ////////////////////////// + + // "footer" is not permitted as we expect user content to be a fragment and + // not structural to this extent + + // "h1" through "h6" are permitted and take no attributes + p.AllowElements("h1", "h2", "h3", "h4", "h5", "h6") + + // "header" is not permitted as we expect user content to be a fragment and + // not structural to this extent + + // "hgroup" is permitted and takes no attributes + p.AllowElements("hgroup") + + ///////////////////////////////////// + // Content grouping and separating // + ///////////////////////////////////// + + // "blockquote" is permitted, including the "cite" attribute which must be + // a standard URL. + p.AllowAttrs("cite").OnElements("blockquote") + + // "br" "div" "hr" "p" "span" "wbr" are permitted and take no attributes + p.AllowElements("br", "div", "hr", "p", "span", "wbr") + + /////////// + // Links // + /////////// + + // "a" is permitted + p.AllowAttrs("href").OnElements("a") + + // "area" is permitted along with the attributes that map image maps work + p.AllowAttrs("name").Matching( + regexp.MustCompile(`^([\p{L}\p{N}_-]+)$`), + ).OnElements("map") + p.AllowAttrs("alt").Matching(Paragraph).OnElements("area") + p.AllowAttrs("coords").Matching( + regexp.MustCompile(`^([0-9]+,)+[0-9]+$`), + ).OnElements("area") + p.AllowAttrs("href").OnElements("area") + p.AllowAttrs("rel").Matching(SpaceSeparatedTokens).OnElements("area") + p.AllowAttrs("shape").Matching( + regexp.MustCompile(`(?i)^(default|circle|rect|poly)$`), + ).OnElements("area") + p.AllowAttrs("usemap").Matching( + regexp.MustCompile(`(?i)^#[\p{L}\p{N}_-]+$`), + ).OnElements("img") + + // "link" is not permitted + + ///////////////////// + // Phrase elements // + ///////////////////// + + // The following are all inline phrasing elements + p.AllowElements("abbr", "acronym", "cite", "code", "dfn", "em", + "figcaption", "mark", "s", "samp", "strong", "sub", "sup", "var") + + // "q" is permitted and "cite" is a URL and handled by URL policies + p.AllowAttrs("cite").OnElements("q") + + // "time" is permitted + p.AllowAttrs("datetime").Matching(ISO8601).OnElements("time") + + //////////////////// + // Style elements // + //////////////////// + + // block and inline elements that impart no semantic meaning but style the + // document + p.AllowElements("b", "i", "pre", "small", "strike", "tt", "u") + + // "style" is not permitted as we are not yet sanitising CSS and it is an + // XSS attack vector + + ////////////////////// + // HTML5 Formatting // + ////////////////////// + + // "bdi" "bdo" are permitted + p.AllowAttrs("dir").Matching(Direction).OnElements("bdi", "bdo") + + // "rp" "rt" "ruby" are permitted + p.AllowElements("rp", "rt", "ruby") + + /////////////////////////// + // HTML5 Change tracking // + /////////////////////////// + + // "del" "ins" are permitted + p.AllowAttrs("cite").Matching(Paragraph).OnElements("del", "ins") + p.AllowAttrs("datetime").Matching(ISO8601).OnElements("del", "ins") + + /////////// + // Lists // + /////////// + + p.AllowLists() + + //////////// + // Tables // + //////////// + + p.AllowTables() + + /////////// + // Forms // + /////////// + + // By and large, forms are not permitted. However there are some form + // elements that can be used to present data, and we do permit those + // + // "button" "fieldset" "input" "keygen" "label" "output" "select" "datalist" + // "textarea" "optgroup" "option" are all not permitted + + // "meter" is permitted + p.AllowAttrs( + "value", + "min", + "max", + "low", + "high", + "optimum", + ).Matching(Number).OnElements("meter") + + // "progress" is permitted + p.AllowAttrs("value", "max").Matching(Number).OnElements("progress") + + ////////////////////// + // Embedded content // + ////////////////////// + + // Vast majority not permitted + // "audio" "canvas" "embed" "iframe" "object" "param" "source" "svg" "track" + // "video" are all not permitted + + p.AllowImages() + + return p +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policy.go b/vendor/github.com/microcosm-cc/bluemonday/policy.go new file mode 100644 index 000000000..f61d98f59 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/policy.go @@ -0,0 +1,552 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "net/url" + "regexp" + "strings" +) + +// Policy encapsulates the whitelist of HTML elements and attributes that will +// be applied to the sanitised HTML. +// +// You should use bluemonday.NewPolicy() to create a blank policy as the +// unexported fields contain maps that need to be initialized. +type Policy struct { + + // Declares whether the maps have been initialized, used as a cheap check to + // ensure that those using Policy{} directly won't cause nil pointer + // exceptions + initialized bool + + // If true then we add spaces when stripping tags, specifically the closing + // tag is replaced by a space character. + addSpaces bool + + // When true, add rel="nofollow" to HTML anchors + requireNoFollow bool + + // When true, add rel="nofollow" to HTML anchors + // Will add for href="http://foo" + // Will skip for href="/foo" or href="foo" + requireNoFollowFullyQualifiedLinks bool + + // When true add target="_blank" to fully qualified links + // Will add for href="http://foo" + // Will skip for href="/foo" or href="foo" + addTargetBlankToFullyQualifiedLinks bool + + // When true, URLs must be parseable by "net/url" url.Parse() + requireParseableURLs bool + + // When true, u, _ := url.Parse("url"); !u.IsAbs() is permitted + allowRelativeURLs bool + + // When true, allow data attributes. + allowDataAttributes bool + + // map[htmlElementName]map[htmlAttributeName]attrPolicy + elsAndAttrs map[string]map[string]attrPolicy + + // map[htmlAttributeName]attrPolicy + globalAttrs map[string]attrPolicy + + // If urlPolicy is nil, all URLs with matching schema are allowed. + // Otherwise, only the URLs with matching schema and urlPolicy(url) + // returning true are allowed. + allowURLSchemes map[string]urlPolicy + + // If an element has had all attributes removed as a result of a policy + // being applied, then the element would be removed from the output. + // + // However some elements are valid and have strong layout meaning without + // any attributes, i.e. . To prevent those being removed we maintain + // a list of elements that are allowed to have no attributes and that will + // be maintained in the output HTML. + setOfElementsAllowedWithoutAttrs map[string]struct{} + + setOfElementsToSkipContent map[string]struct{} +} + +type attrPolicy struct { + + // optional pattern to match, when not nil the regexp needs to match + // otherwise the attribute is removed + regexp *regexp.Regexp +} + +type attrPolicyBuilder struct { + p *Policy + + attrNames []string + regexp *regexp.Regexp + allowEmpty bool +} + +type urlPolicy func(url *url.URL) (allowUrl bool) + +// init initializes the maps if this has not been done already +func (p *Policy) init() { + if !p.initialized { + p.elsAndAttrs = make(map[string]map[string]attrPolicy) + p.globalAttrs = make(map[string]attrPolicy) + p.allowURLSchemes = make(map[string]urlPolicy) + p.setOfElementsAllowedWithoutAttrs = make(map[string]struct{}) + p.setOfElementsToSkipContent = make(map[string]struct{}) + p.initialized = true + } +} + +// NewPolicy returns a blank policy with nothing whitelisted or permitted. This +// is the recommended way to start building a policy and you should now use +// AllowAttrs() and/or AllowElements() to construct the whitelist of HTML +// elements and attributes. +func NewPolicy() *Policy { + + p := Policy{} + + p.addDefaultElementsWithoutAttrs() + p.addDefaultSkipElementContent() + + return &p +} + +// AllowAttrs takes a range of HTML attribute names and returns an +// attribute policy builder that allows you to specify the pattern and scope of +// the whitelisted attribute. +// +// The attribute policy is only added to the core policy when either Globally() +// or OnElements(...) are called. +func (p *Policy) AllowAttrs(attrNames ...string) *attrPolicyBuilder { + + p.init() + + abp := attrPolicyBuilder{ + p: p, + allowEmpty: false, + } + + for _, attrName := range attrNames { + abp.attrNames = append(abp.attrNames, strings.ToLower(attrName)) + } + + return &abp +} + +// AllowDataAttributes whitelists all data attributes. We can't specify the name +// of each attribute exactly as they are customized. +// +// NOTE: These values are not sanitized and applications that evaluate or process +// them without checking and verification of the input may be at risk if this option +// is enabled. This is a 'caveat emptor' option and the person enabling this option +// needs to fully understand the potential impact with regards to whatever application +// will be consuming the sanitized HTML afterwards, i.e. if you know you put a link in a +// data attribute and use that to automatically load some new window then you're giving +// the author of a HTML fragment the means to open a malicious destination automatically. +// Use with care! +func (p *Policy) AllowDataAttributes() { + p.allowDataAttributes = true +} + +// AllowNoAttrs says that attributes on element are optional. +// +// The attribute policy is only added to the core policy when OnElements(...) +// are called. +func (p *Policy) AllowNoAttrs() *attrPolicyBuilder { + + p.init() + + abp := attrPolicyBuilder{ + p: p, + allowEmpty: true, + } + return &abp +} + +// AllowNoAttrs says that attributes on element are optional. +// +// The attribute policy is only added to the core policy when OnElements(...) +// are called. +func (abp *attrPolicyBuilder) AllowNoAttrs() *attrPolicyBuilder { + + abp.allowEmpty = true + + return abp +} + +// Matching allows a regular expression to be applied to a nascent attribute +// policy, and returns the attribute policy. Calling this more than once will +// replace the existing regexp. +func (abp *attrPolicyBuilder) Matching(regex *regexp.Regexp) *attrPolicyBuilder { + + abp.regexp = regex + + return abp +} + +// OnElements will bind an attribute policy to a given range of HTML elements +// and return the updated policy +func (abp *attrPolicyBuilder) OnElements(elements ...string) *Policy { + + for _, element := range elements { + element = strings.ToLower(element) + + for _, attr := range abp.attrNames { + + if _, ok := abp.p.elsAndAttrs[element]; !ok { + abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + + ap := attrPolicy{} + if abp.regexp != nil { + ap.regexp = abp.regexp + } + + abp.p.elsAndAttrs[element][attr] = ap + } + + if abp.allowEmpty { + abp.p.setOfElementsAllowedWithoutAttrs[element] = struct{}{} + + if _, ok := abp.p.elsAndAttrs[element]; !ok { + abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + } + } + + return abp.p +} + +// Globally will bind an attribute policy to all HTML elements and return the +// updated policy +func (abp *attrPolicyBuilder) Globally() *Policy { + + for _, attr := range abp.attrNames { + if _, ok := abp.p.globalAttrs[attr]; !ok { + abp.p.globalAttrs[attr] = attrPolicy{} + } + + ap := attrPolicy{} + if abp.regexp != nil { + ap.regexp = abp.regexp + } + + abp.p.globalAttrs[attr] = ap + } + + return abp.p +} + +// AllowElements will append HTML elements to the whitelist without applying an +// attribute policy to those elements (the elements are permitted +// sans-attributes) +func (p *Policy) AllowElements(names ...string) *Policy { + p.init() + + for _, element := range names { + element = strings.ToLower(element) + + if _, ok := p.elsAndAttrs[element]; !ok { + p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + } + + return p +} + +// RequireNoFollowOnLinks will result in all tags having a rel="nofollow" +// added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy { + + p.requireNoFollow = require + p.requireParseableURLs = true + + return p +} + +// RequireNoFollowOnFullyQualifiedLinks will result in all tags that point +// to a non-local destination (i.e. starts with a protocol and has a host) +// having a rel="nofollow" added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy { + + p.requireNoFollowFullyQualifiedLinks = require + p.requireParseableURLs = true + + return p +} + +// AddTargetBlankToFullyQualifiedLinks will result in all tags that point +// to a non-local destination (i.e. starts with a protocol and has a host) +// having a target="_blank" added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) AddTargetBlankToFullyQualifiedLinks(require bool) *Policy { + + p.addTargetBlankToFullyQualifiedLinks = require + p.requireParseableURLs = true + + return p +} + +// RequireParseableURLs will result in all URLs requiring that they be parseable +// by "net/url" url.Parse() +// This applies to: +// - a.href +// - area.href +// - blockquote.cite +// - img.src +// - link.href +// - script.src +func (p *Policy) RequireParseableURLs(require bool) *Policy { + + p.requireParseableURLs = require + + return p +} + +// AllowRelativeURLs enables RequireParseableURLs and then permits URLs that +// are parseable, have no schema information and url.IsAbs() returns false +// This permits local URLs +func (p *Policy) AllowRelativeURLs(require bool) *Policy { + + p.RequireParseableURLs(true) + p.allowRelativeURLs = require + + return p +} + +// AllowURLSchemes will append URL schemes to the whitelist +// Example: p.AllowURLSchemes("mailto", "http", "https") +func (p *Policy) AllowURLSchemes(schemes ...string) *Policy { + p.init() + + p.RequireParseableURLs(true) + + for _, scheme := range schemes { + scheme = strings.ToLower(scheme) + + // Allow all URLs with matching scheme. + p.allowURLSchemes[scheme] = nil + } + + return p +} + +// AllowURLSchemeWithCustomPolicy will append URL schemes with +// a custom URL policy to the whitelist. +// Only the URLs with matching schema and urlPolicy(url) +// returning true will be allowed. +func (p *Policy) AllowURLSchemeWithCustomPolicy( + scheme string, + urlPolicy func(url *url.URL) (allowUrl bool), +) *Policy { + + p.init() + + p.RequireParseableURLs(true) + + scheme = strings.ToLower(scheme) + + p.allowURLSchemes[scheme] = urlPolicy + + return p +} + +// AddSpaceWhenStrippingTag states whether to add a single space " " when +// removing tags that are not whitelisted by the policy. +// +// This is useful if you expect to strip tags in dense markup and may lose the +// value of whitespace. +// +// For example: "

    Hello

    World

    "" would be sanitized to "HelloWorld" +// with the default value of false, but you may wish to sanitize this to +// " Hello World " by setting AddSpaceWhenStrippingTag to true as this would +// retain the intent of the text. +func (p *Policy) AddSpaceWhenStrippingTag(allow bool) *Policy { + + p.addSpaces = allow + + return p +} + +// SkipElementsContent adds the HTML elements whose tags is needed to be removed +// with its content. +func (p *Policy) SkipElementsContent(names ...string) *Policy { + + p.init() + + for _, element := range names { + element = strings.ToLower(element) + + if _, ok := p.setOfElementsToSkipContent[element]; !ok { + p.setOfElementsToSkipContent[element] = struct{}{} + } + } + + return p +} + +// AllowElementsContent marks the HTML elements whose content should be +// retained after removing the tag. +func (p *Policy) AllowElementsContent(names ...string) *Policy { + + p.init() + + for _, element := range names { + delete(p.setOfElementsToSkipContent, strings.ToLower(element)) + } + + return p +} + +// addDefaultElementsWithoutAttrs adds the HTML elements that we know are valid +// without any attributes to an internal map. +// i.e. we know that
    is valid, but isn't valid as the "dir" attr +// is mandatory +func (p *Policy) addDefaultElementsWithoutAttrs() { + p.init() + + p.setOfElementsAllowedWithoutAttrs["abbr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["acronym"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["address"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["article"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["aside"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["audio"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["b"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["bdi"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["blockquote"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["body"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["br"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["button"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["canvas"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["caption"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["center"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["cite"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["code"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["col"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["colgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["datalist"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dd"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["del"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["details"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dfn"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["div"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dl"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["em"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["fieldset"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["figcaption"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["figure"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["footer"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h1"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h2"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h3"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h4"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h5"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h6"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["head"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["header"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["hgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["hr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["html"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["i"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ins"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["kbd"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["li"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["mark"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["marquee"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["nav"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ol"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["optgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["option"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["p"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["pre"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["q"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["rp"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["rt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ruby"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["s"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["samp"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["script"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["section"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["select"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["small"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["span"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["strike"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["strong"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["style"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["sub"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["summary"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["sup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["svg"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["table"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tbody"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["td"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["textarea"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tfoot"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["th"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["thead"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["title"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["time"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["u"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ul"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["var"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["video"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["wbr"] = struct{}{} + +} + +// addDefaultSkipElementContent adds the HTML elements that we should skip +// rendering the character content of, if the element itself is not allowed. +// This is all character data that the end user would not normally see. +// i.e. if we exclude a tag. +func (p *Policy) addDefaultSkipElementContent() { + p.init() + + p.setOfElementsToSkipContent["frame"] = struct{}{} + p.setOfElementsToSkipContent["frameset"] = struct{}{} + p.setOfElementsToSkipContent["iframe"] = struct{}{} + p.setOfElementsToSkipContent["noembed"] = struct{}{} + p.setOfElementsToSkipContent["noframes"] = struct{}{} + p.setOfElementsToSkipContent["noscript"] = struct{}{} + p.setOfElementsToSkipContent["nostyle"] = struct{}{} + p.setOfElementsToSkipContent["object"] = struct{}{} + p.setOfElementsToSkipContent["script"] = struct{}{} + p.setOfElementsToSkipContent["style"] = struct{}{} + p.setOfElementsToSkipContent["title"] = struct{}{} +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/sanitize.go b/vendor/github.com/microcosm-cc/bluemonday/sanitize.go new file mode 100644 index 000000000..65ed89b79 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/sanitize.go @@ -0,0 +1,581 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "bytes" + "io" + "net/url" + "regexp" + "strings" + + "golang.org/x/net/html" +) + +var ( + dataAttribute = regexp.MustCompile("^data-.+") + dataAttributeXMLPrefix = regexp.MustCompile("^xml.+") + dataAttributeInvalidChars = regexp.MustCompile("[A-Z;]+") +) + +// Sanitize takes a string that contains a HTML fragment or document and applies +// the given policy whitelist. +// +// It returns a HTML string that has been sanitized by the policy or an empty +// string if an error has occurred (most likely as a consequence of extremely +// malformed input) +func (p *Policy) Sanitize(s string) string { + if strings.TrimSpace(s) == "" { + return s + } + + return p.sanitize(strings.NewReader(s)).String() +} + +// SanitizeBytes takes a []byte that contains a HTML fragment or document and applies +// the given policy whitelist. +// +// It returns a []byte containing the HTML that has been sanitized by the policy +// or an empty []byte if an error has occurred (most likely as a consequence of +// extremely malformed input) +func (p *Policy) SanitizeBytes(b []byte) []byte { + if len(bytes.TrimSpace(b)) == 0 { + return b + } + + return p.sanitize(bytes.NewReader(b)).Bytes() +} + +// SanitizeReader takes an io.Reader that contains a HTML fragment or document +// and applies the given policy whitelist. +// +// It returns a bytes.Buffer containing the HTML that has been sanitized by the +// policy. Errors during sanitization will merely return an empty result. +func (p *Policy) SanitizeReader(r io.Reader) *bytes.Buffer { + return p.sanitize(r) +} + +// Performs the actual sanitization process. +func (p *Policy) sanitize(r io.Reader) *bytes.Buffer { + + // It is possible that the developer has created the policy via: + // p := bluemonday.Policy{} + // rather than: + // p := bluemonday.NewPolicy() + // If this is the case, and if they haven't yet triggered an action that + // would initiliaze the maps, then we need to do that. + p.init() + + var ( + buff bytes.Buffer + skipElementContent bool + skippingElementsCount int64 + skipClosingTag bool + closingTagToSkipStack []string + mostRecentlyStartedToken string + ) + + tokenizer := html.NewTokenizer(r) + for { + if tokenizer.Next() == html.ErrorToken { + err := tokenizer.Err() + if err == io.EOF { + // End of input means end of processing + return &buff + } + + // Raw tokenizer error + return &bytes.Buffer{} + } + + token := tokenizer.Token() + switch token.Type { + case html.DoctypeToken: + + // DocType is not handled as there is no safe parsing mechanism + // provided by golang.org/x/net/html for the content, and this can + // be misused to insert HTML tags that are not then sanitized + // + // One might wish to recursively sanitize here using the same policy + // but I will need to do some further testing before considering + // this. + + case html.CommentToken: + + // Comments are ignored by default + + case html.StartTagToken: + + mostRecentlyStartedToken = token.Data + + aps, ok := p.elsAndAttrs[token.Data] + if !ok { + if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { + skipElementContent = true + skippingElementsCount++ + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if len(token.Attr) != 0 { + token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) + } + + if len(token.Attr) == 0 { + if !p.allowNoAttrs(token.Data) { + skipClosingTag = true + closingTagToSkipStack = append(closingTagToSkipStack, token.Data) + if p.addSpaces { + buff.WriteString(" ") + } + break + } + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.EndTagToken: + + if mostRecentlyStartedToken == token.Data { + mostRecentlyStartedToken = "" + } + + if skipClosingTag && closingTagToSkipStack[len(closingTagToSkipStack)-1] == token.Data { + closingTagToSkipStack = closingTagToSkipStack[:len(closingTagToSkipStack)-1] + if len(closingTagToSkipStack) == 0 { + skipClosingTag = false + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if _, ok := p.elsAndAttrs[token.Data]; !ok { + if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { + skippingElementsCount-- + if skippingElementsCount == 0 { + skipElementContent = false + } + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.SelfClosingTagToken: + + aps, ok := p.elsAndAttrs[token.Data] + if !ok { + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if len(token.Attr) != 0 { + token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) + } + + if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) { + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.TextToken: + + if !skipElementContent { + switch mostRecentlyStartedToken { + case "script": + // not encouraged, but if a policy allows JavaScript we + // should not HTML escape it as that would break the output + buff.WriteString(token.Data) + case "style": + // not encouraged, but if a policy allows CSS styles we + // should not HTML escape it as that would break the output + buff.WriteString(token.Data) + default: + // HTML escape the text + buff.WriteString(token.String()) + } + } + default: + // A token that didn't exist in the html package when we wrote this + return &bytes.Buffer{} + } + } +} + +// sanitizeAttrs takes a set of element attribute policies and the global +// attribute policies and applies them to the []html.Attribute returning a set +// of html.Attributes that match the policies +func (p *Policy) sanitizeAttrs( + elementName string, + attrs []html.Attribute, + aps map[string]attrPolicy, +) []html.Attribute { + + if len(attrs) == 0 { + return attrs + } + + // Builds a new attribute slice based on the whether the attribute has been + // whitelisted explicitly or globally. + cleanAttrs := []html.Attribute{} + for _, htmlAttr := range attrs { + if p.allowDataAttributes { + // If we see a data attribute, let it through. + if isDataAttribute(htmlAttr.Key) { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } + // Is there an element specific attribute policy that applies? + if ap, ok := aps[htmlAttr.Key]; ok { + if ap.regexp != nil { + if ap.regexp.MatchString(htmlAttr.Val) { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } else { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } + + // Is there a global attribute policy that applies? + if ap, ok := p.globalAttrs[htmlAttr.Key]; ok { + + if ap.regexp != nil { + if ap.regexp.MatchString(htmlAttr.Val) { + cleanAttrs = append(cleanAttrs, htmlAttr) + } + } else { + cleanAttrs = append(cleanAttrs, htmlAttr) + } + } + } + + if len(cleanAttrs) == 0 { + // If nothing was allowed, let's get out of here + return cleanAttrs + } + // cleanAttrs now contains the attributes that are permitted + + if linkable(elementName) { + if p.requireParseableURLs { + // Ensure URLs are parseable: + // - a.href + // - area.href + // - link.href + // - blockquote.cite + // - q.cite + // - img.src + // - script.src + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + switch elementName { + case "a", "area", "link": + if htmlAttr.Key == "href" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + case "blockquote", "q": + if htmlAttr.Key == "cite" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + case "img", "script": + if htmlAttr.Key == "src" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + default: + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + cleanAttrs = tmpAttrs + } + + if (p.requireNoFollow || + p.requireNoFollowFullyQualifiedLinks || + p.addTargetBlankToFullyQualifiedLinks) && + len(cleanAttrs) > 0 { + + // Add rel="nofollow" if a "href" exists + switch elementName { + case "a", "area", "link": + var hrefFound bool + var externalLink bool + for _, htmlAttr := range cleanAttrs { + if htmlAttr.Key == "href" { + hrefFound = true + + u, err := url.Parse(htmlAttr.Val) + if err != nil { + continue + } + if u.Host != "" { + externalLink = true + } + + continue + } + } + + if hrefFound { + var ( + noFollowFound bool + targetBlankFound bool + ) + + addNoFollow := (p.requireNoFollow || + externalLink && p.requireNoFollowFullyQualifiedLinks) + + addTargetBlank := (externalLink && + p.addTargetBlankToFullyQualifiedLinks) + + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + + var appended bool + if htmlAttr.Key == "rel" && addNoFollow { + + if strings.Contains(htmlAttr.Val, "nofollow") { + noFollowFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } else { + htmlAttr.Val += " nofollow" + noFollowFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } + } + + if elementName == "a" && htmlAttr.Key == "target" { + if htmlAttr.Val == "_blank" { + targetBlankFound = true + } + if addTargetBlank && !targetBlankFound { + htmlAttr.Val = "_blank" + targetBlankFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } + } + + if !appended { + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + if noFollowFound || targetBlankFound { + cleanAttrs = tmpAttrs + } + + if addNoFollow && !noFollowFound { + rel := html.Attribute{} + rel.Key = "rel" + rel.Val = "nofollow" + cleanAttrs = append(cleanAttrs, rel) + } + + if elementName == "a" && addTargetBlank && !targetBlankFound { + rel := html.Attribute{} + rel.Key = "target" + rel.Val = "_blank" + targetBlankFound = true + cleanAttrs = append(cleanAttrs, rel) + } + + if targetBlankFound { + // target="_blank" has a security risk that allows the + // opened window/tab to issue JavaScript calls against + // window.opener, which in effect allow the destination + // of the link to control the source: + // https://dev.to/ben/the-targetblank-vulnerability-by-example + // + // To mitigate this risk, we need to add a specific rel + // attribute if it is not already present. + // rel="noopener" + // + // Unfortunately this is processing the rel twice (we + // already looked at it earlier ^^) as we cannot be sure + // of the ordering of the href and rel, and whether we + // have fully satisfied that we need to do this. This + // double processing only happens *if* target="_blank" + // is true. + var noOpenerAdded bool + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + var appended bool + if htmlAttr.Key == "rel" { + if strings.Contains(htmlAttr.Val, "noopener") { + noOpenerAdded = true + tmpAttrs = append(tmpAttrs, htmlAttr) + } else { + htmlAttr.Val += " noopener" + noOpenerAdded = true + tmpAttrs = append(tmpAttrs, htmlAttr) + } + + appended = true + } + if !appended { + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + if noOpenerAdded { + cleanAttrs = tmpAttrs + } else { + // rel attr was not found, or else noopener would + // have been added already + rel := html.Attribute{} + rel.Key = "rel" + rel.Val = "noopener" + cleanAttrs = append(cleanAttrs, rel) + } + + } + } + default: + } + } + } + + return cleanAttrs +} + +func (p *Policy) allowNoAttrs(elementName string) bool { + _, ok := p.setOfElementsAllowedWithoutAttrs[elementName] + return ok +} + +func (p *Policy) validURL(rawurl string) (string, bool) { + if p.requireParseableURLs { + // URLs are valid if when space is trimmed the URL is valid + rawurl = strings.TrimSpace(rawurl) + + // URLs cannot contain whitespace, unless it is a data-uri + if (strings.Contains(rawurl, " ") || + strings.Contains(rawurl, "\t") || + strings.Contains(rawurl, "\n")) && + !strings.HasPrefix(rawurl, `data:`) { + return "", false + } + + // URLs are valid if they parse + u, err := url.Parse(rawurl) + if err != nil { + return "", false + } + + if u.Scheme != "" { + + urlPolicy, ok := p.allowURLSchemes[u.Scheme] + if !ok { + return "", false + + } + + if urlPolicy == nil || urlPolicy(u) == true { + return u.String(), true + } + + return "", false + } + + if p.allowRelativeURLs { + if u.String() != "" { + return u.String(), true + } + } + + return "", false + } + + return rawurl, true +} + +func linkable(elementName string) bool { + switch elementName { + case "a", "area", "blockquote", "img", "link", "script": + return true + default: + return false + } +} + +func isDataAttribute(val string) bool { + if !dataAttribute.MatchString(val) { + return false + } + rest := strings.Split(val, "data-") + if len(rest) == 1 { + return false + } + // data-xml* is invalid. + if dataAttributeXMLPrefix.MatchString(rest[1]) { + return false + } + // no uppercase or semi-colons allowed. + if dataAttributeInvalidChars.MatchString(rest[1]) { + return false + } + return true +} diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml new file mode 100644 index 000000000..93b1fcdb3 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml @@ -0,0 +1,16 @@ +sudo: false +language: go +go: + - 1.x + - master +matrix: + allow_failures: + - go: master + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE new file mode 100644 index 000000000..c35c17af9 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 Dmitri Shuralyov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md new file mode 100644 index 000000000..670bf0fe6 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md @@ -0,0 +1,36 @@ +sanitized_anchor_name +===================== + +[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) + +Package sanitized_anchor_name provides a func to create sanitized anchor names. + +Its logic can be reused by multiple packages to create interoperable anchor names +and links to those anchors. + +At this time, it does not try to ensure that generated anchor names +are unique, that responsibility falls on the caller. + +Installation +------------ + +```bash +go get -u github.com/shurcooL/sanitized_anchor_name +``` + +Example +------- + +```Go +anchorName := sanitized_anchor_name.Create("This is a header") + +fmt.Println(anchorName) + +// Output: +// this-is-a-header +``` + +License +------- + +- [MIT License](LICENSE) diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod new file mode 100644 index 000000000..1e2553475 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod @@ -0,0 +1 @@ +module github.com/shurcooL/sanitized_anchor_name diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go new file mode 100644 index 000000000..6a77d1243 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go @@ -0,0 +1,29 @@ +// Package sanitized_anchor_name provides a func to create sanitized anchor names. +// +// Its logic can be reused by multiple packages to create interoperable anchor names +// and links to those anchors. +// +// At this time, it does not try to ensure that generated anchor names +// are unique, that responsibility falls on the caller. +package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" + +import "unicode" + +// Create returns a sanitized anchor name for the given text. +func Create(text string) string { + var anchorName []rune + var futureDash = false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } + } + return string(anchorName) +} diff --git a/vendor/golang.org/x/net/html/atom/atom.go b/vendor/golang.org/x/net/html/atom/atom.go new file mode 100644 index 000000000..cd0a8ac15 --- /dev/null +++ b/vendor/golang.org/x/net/html/atom/atom.go @@ -0,0 +1,78 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package atom provides integer codes (also known as atoms) for a fixed set of +// frequently occurring HTML strings: tag names and attribute keys such as "p" +// and "id". +// +// Sharing an atom's name between all elements with the same tag can result in +// fewer string allocations when tokenizing and parsing HTML. Integer +// comparisons are also generally faster than string comparisons. +// +// The value of an atom's particular code is not guaranteed to stay the same +// between versions of this package. Neither is any ordering guaranteed: +// whether atom.H1 < atom.H2 may also change. The codes are not guaranteed to +// be dense. The only guarantees are that e.g. looking up "div" will yield +// atom.Div, calling atom.Div.String will return "div", and atom.Div != 0. +package atom // import "golang.org/x/net/html/atom" + +// Atom is an integer code for a string. The zero value maps to "". +type Atom uint32 + +// String returns the atom's name. +func (a Atom) String() string { + start := uint32(a >> 8) + n := uint32(a & 0xff) + if start+n > uint32(len(atomText)) { + return "" + } + return atomText[start : start+n] +} + +func (a Atom) string() string { + return atomText[a>>8 : a>>8+a&0xff] +} + +// fnv computes the FNV hash with an arbitrary starting value h. +func fnv(h uint32, s []byte) uint32 { + for i := range s { + h ^= uint32(s[i]) + h *= 16777619 + } + return h +} + +func match(s string, t []byte) bool { + for i, c := range t { + if s[i] != c { + return false + } + } + return true +} + +// Lookup returns the atom whose name is s. It returns zero if there is no +// such atom. The lookup is case sensitive. +func Lookup(s []byte) Atom { + if len(s) == 0 || len(s) > maxAtomLen { + return 0 + } + h := fnv(hash0, s) + if a := table[h&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { + return a + } + if a := table[(h>>16)&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { + return a + } + return 0 +} + +// String returns a string whose contents are equal to s. In that sense, it is +// equivalent to string(s) but may be more efficient. +func String(s []byte) string { + if a := Lookup(s); a != 0 { + return a.String() + } + return string(s) +} diff --git a/vendor/golang.org/x/net/html/atom/gen.go b/vendor/golang.org/x/net/html/atom/gen.go new file mode 100644 index 000000000..cc5dc5dbc --- /dev/null +++ b/vendor/golang.org/x/net/html/atom/gen.go @@ -0,0 +1,709 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ignore + +//go:generate go run gen.go +//go:generate go run gen.go -test + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "io/ioutil" + "math/rand" + "os" + "sort" + "strings" +) + +// identifier converts s to a Go exported identifier. +// It converts "div" to "Div" and "accept-charset" to "AcceptCharset". +func identifier(s string) string { + b := make([]byte, 0, len(s)) + cap := true + for _, c := range s { + if c == '-' { + cap = true + continue + } + if cap && 'a' <= c && c <= 'z' { + c -= 'a' - 'A' + } + cap = false + b = append(b, byte(c)) + } + return string(b) +} + +var test = flag.Bool("test", false, "generate table_test.go") + +func genFile(name string, buf *bytes.Buffer) { + b, err := format.Source(buf.Bytes()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := ioutil.WriteFile(name, b, 0644); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func main() { + flag.Parse() + + var all []string + all = append(all, elements...) + all = append(all, attributes...) + all = append(all, eventHandlers...) + all = append(all, extra...) + sort.Strings(all) + + // uniq - lists have dups + w := 0 + for _, s := range all { + if w == 0 || all[w-1] != s { + all[w] = s + w++ + } + } + all = all[:w] + + if *test { + var buf bytes.Buffer + fmt.Fprintln(&buf, "// Code generated by go generate gen.go; DO NOT EDIT.\n") + fmt.Fprintln(&buf, "//go:generate go run gen.go -test\n") + fmt.Fprintln(&buf, "package atom\n") + fmt.Fprintln(&buf, "var testAtomList = []string{") + for _, s := range all { + fmt.Fprintf(&buf, "\t%q,\n", s) + } + fmt.Fprintln(&buf, "}") + + genFile("table_test.go", &buf) + return + } + + // Find hash that minimizes table size. + var best *table + for i := 0; i < 1000000; i++ { + if best != nil && 1<<(best.k-1) < len(all) { + break + } + h := rand.Uint32() + for k := uint(0); k <= 16; k++ { + if best != nil && k >= best.k { + break + } + var t table + if t.init(h, k, all) { + best = &t + break + } + } + } + if best == nil { + fmt.Fprintf(os.Stderr, "failed to construct string table\n") + os.Exit(1) + } + + // Lay out strings, using overlaps when possible. + layout := append([]string{}, all...) + + // Remove strings that are substrings of other strings + for changed := true; changed; { + changed = false + for i, s := range layout { + if s == "" { + continue + } + for j, t := range layout { + if i != j && t != "" && strings.Contains(s, t) { + changed = true + layout[j] = "" + } + } + } + } + + // Join strings where one suffix matches another prefix. + for { + // Find best i, j, k such that layout[i][len-k:] == layout[j][:k], + // maximizing overlap length k. + besti := -1 + bestj := -1 + bestk := 0 + for i, s := range layout { + if s == "" { + continue + } + for j, t := range layout { + if i == j { + continue + } + for k := bestk + 1; k <= len(s) && k <= len(t); k++ { + if s[len(s)-k:] == t[:k] { + besti = i + bestj = j + bestk = k + } + } + } + } + if bestk > 0 { + layout[besti] += layout[bestj][bestk:] + layout[bestj] = "" + continue + } + break + } + + text := strings.Join(layout, "") + + atom := map[string]uint32{} + for _, s := range all { + off := strings.Index(text, s) + if off < 0 { + panic("lost string " + s) + } + atom[s] = uint32(off<<8 | len(s)) + } + + var buf bytes.Buffer + // Generate the Go code. + fmt.Fprintln(&buf, "// Code generated by go generate gen.go; DO NOT EDIT.\n") + fmt.Fprintln(&buf, "//go:generate go run gen.go\n") + fmt.Fprintln(&buf, "package atom\n\nconst (") + + // compute max len + maxLen := 0 + for _, s := range all { + if maxLen < len(s) { + maxLen = len(s) + } + fmt.Fprintf(&buf, "\t%s Atom = %#x\n", identifier(s), atom[s]) + } + fmt.Fprintln(&buf, ")\n") + + fmt.Fprintf(&buf, "const hash0 = %#x\n\n", best.h0) + fmt.Fprintf(&buf, "const maxAtomLen = %d\n\n", maxLen) + + fmt.Fprintf(&buf, "var table = [1<<%d]Atom{\n", best.k) + for i, s := range best.tab { + if s == "" { + continue + } + fmt.Fprintf(&buf, "\t%#x: %#x, // %s\n", i, atom[s], s) + } + fmt.Fprintf(&buf, "}\n") + datasize := (1 << best.k) * 4 + + fmt.Fprintln(&buf, "const atomText =") + textsize := len(text) + for len(text) > 60 { + fmt.Fprintf(&buf, "\t%q +\n", text[:60]) + text = text[60:] + } + fmt.Fprintf(&buf, "\t%q\n\n", text) + + genFile("table.go", &buf) + + fmt.Fprintf(os.Stdout, "%d atoms; %d string bytes + %d tables = %d total data\n", len(all), textsize, datasize, textsize+datasize) +} + +type byLen []string + +func (x byLen) Less(i, j int) bool { return len(x[i]) > len(x[j]) } +func (x byLen) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byLen) Len() int { return len(x) } + +// fnv computes the FNV hash with an arbitrary starting value h. +func fnv(h uint32, s string) uint32 { + for i := 0; i < len(s); i++ { + h ^= uint32(s[i]) + h *= 16777619 + } + return h +} + +// A table represents an attempt at constructing the lookup table. +// The lookup table uses cuckoo hashing, meaning that each string +// can be found in one of two positions. +type table struct { + h0 uint32 + k uint + mask uint32 + tab []string +} + +// hash returns the two hashes for s. +func (t *table) hash(s string) (h1, h2 uint32) { + h := fnv(t.h0, s) + h1 = h & t.mask + h2 = (h >> 16) & t.mask + return +} + +// init initializes the table with the given parameters. +// h0 is the initial hash value, +// k is the number of bits of hash value to use, and +// x is the list of strings to store in the table. +// init returns false if the table cannot be constructed. +func (t *table) init(h0 uint32, k uint, x []string) bool { + t.h0 = h0 + t.k = k + t.tab = make([]string, 1< len(t.tab) { + return false + } + s := t.tab[i] + h1, h2 := t.hash(s) + j := h1 + h2 - i + if t.tab[j] != "" && !t.push(j, depth+1) { + return false + } + t.tab[j] = s + return true +} + +// The lists of element names and attribute keys were taken from +// https://html.spec.whatwg.org/multipage/indices.html#index +// as of the "HTML Living Standard - Last Updated 18 September 2017" version. + +// "command", "keygen" and "menuitem" have been removed from the spec, +// but are kept here for backwards compatibility. +var elements = []string{ + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "command", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "slot", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "template", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", +} + +// https://html.spec.whatwg.org/multipage/indices.html#attributes-3 +// +// "challenge", "command", "contextmenu", "dropzone", "icon", "keytype", "mediagroup", +// "radiogroup", "spellcheck", "scoped", "seamless", "sortable" and "sorted" have been removed from the spec, +// but are kept here for backwards compatibility. +var attributes = []string{ + "abbr", + "accept", + "accept-charset", + "accesskey", + "action", + "allowfullscreen", + "allowpaymentrequest", + "allowusermedia", + "alt", + "as", + "async", + "autocomplete", + "autofocus", + "autoplay", + "challenge", + "charset", + "checked", + "cite", + "class", + "color", + "cols", + "colspan", + "command", + "content", + "contenteditable", + "contextmenu", + "controls", + "coords", + "crossorigin", + "data", + "datetime", + "default", + "defer", + "dir", + "dirname", + "disabled", + "download", + "draggable", + "dropzone", + "enctype", + "for", + "form", + "formaction", + "formenctype", + "formmethod", + "formnovalidate", + "formtarget", + "headers", + "height", + "hidden", + "high", + "href", + "hreflang", + "http-equiv", + "icon", + "id", + "inputmode", + "integrity", + "is", + "ismap", + "itemid", + "itemprop", + "itemref", + "itemscope", + "itemtype", + "keytype", + "kind", + "label", + "lang", + "list", + "loop", + "low", + "manifest", + "max", + "maxlength", + "media", + "mediagroup", + "method", + "min", + "minlength", + "multiple", + "muted", + "name", + "nomodule", + "nonce", + "novalidate", + "open", + "optimum", + "pattern", + "ping", + "placeholder", + "playsinline", + "poster", + "preload", + "radiogroup", + "readonly", + "referrerpolicy", + "rel", + "required", + "reversed", + "rows", + "rowspan", + "sandbox", + "spellcheck", + "scope", + "scoped", + "seamless", + "selected", + "shape", + "size", + "sizes", + "sortable", + "sorted", + "slot", + "span", + "spellcheck", + "src", + "srcdoc", + "srclang", + "srcset", + "start", + "step", + "style", + "tabindex", + "target", + "title", + "translate", + "type", + "typemustmatch", + "updateviacache", + "usemap", + "value", + "width", + "workertype", + "wrap", +} + +// "onautocomplete", "onautocompleteerror", "onmousewheel", +// "onshow" and "onsort" have been removed from the spec, +// but are kept here for backwards compatibility. +var eventHandlers = []string{ + "onabort", + "onautocomplete", + "onautocompleteerror", + "onauxclick", + "onafterprint", + "onbeforeprint", + "onbeforeunload", + "onblur", + "oncancel", + "oncanplay", + "oncanplaythrough", + "onchange", + "onclick", + "onclose", + "oncontextmenu", + "oncopy", + "oncuechange", + "oncut", + "ondblclick", + "ondrag", + "ondragend", + "ondragenter", + "ondragexit", + "ondragleave", + "ondragover", + "ondragstart", + "ondrop", + "ondurationchange", + "onemptied", + "onended", + "onerror", + "onfocus", + "onhashchange", + "oninput", + "oninvalid", + "onkeydown", + "onkeypress", + "onkeyup", + "onlanguagechange", + "onload", + "onloadeddata", + "onloadedmetadata", + "onloadend", + "onloadstart", + "onmessage", + "onmessageerror", + "onmousedown", + "onmouseenter", + "onmouseleave", + "onmousemove", + "onmouseout", + "onmouseover", + "onmouseup", + "onmousewheel", + "onwheel", + "onoffline", + "ononline", + "onpagehide", + "onpageshow", + "onpaste", + "onpause", + "onplay", + "onplaying", + "onpopstate", + "onprogress", + "onratechange", + "onreset", + "onresize", + "onrejectionhandled", + "onscroll", + "onsecuritypolicyviolation", + "onseeked", + "onseeking", + "onselect", + "onshow", + "onsort", + "onstalled", + "onstorage", + "onsubmit", + "onsuspend", + "ontimeupdate", + "ontoggle", + "onunhandledrejection", + "onunload", + "onvolumechange", + "onwaiting", +} + +// extra are ad-hoc values not covered by any of the lists above. +var extra = []string{ + "align", + "annotation", + "annotation-xml", + "applet", + "basefont", + "bgsound", + "big", + "blink", + "center", + "color", + "desc", + "face", + "font", + "foreignObject", // HTML is case-insensitive, but SVG-embedded-in-HTML is case-sensitive. + "foreignobject", + "frame", + "frameset", + "image", + "isindex", + "listing", + "malignmark", + "marquee", + "math", + "mglyph", + "mi", + "mn", + "mo", + "ms", + "mtext", + "nobr", + "noembed", + "noframes", + "plaintext", + "prompt", + "public", + "spacer", + "strike", + "svg", + "system", + "tt", + "xmp", +} diff --git a/vendor/golang.org/x/net/html/atom/table.go b/vendor/golang.org/x/net/html/atom/table.go new file mode 100644 index 000000000..f74018ece --- /dev/null +++ b/vendor/golang.org/x/net/html/atom/table.go @@ -0,0 +1,777 @@ +// Code generated by go generate gen.go; DO NOT EDIT. + +//go:generate go run gen.go + +package atom + +const ( + A Atom = 0x1 + Abbr Atom = 0x4 + Accept Atom = 0x1a06 + AcceptCharset Atom = 0x1a0e + Accesskey Atom = 0x2c09 + Action Atom = 0x25a06 + Address Atom = 0x6ed07 + Align Atom = 0x6d405 + Allowfullscreen Atom = 0x1f00f + Allowpaymentrequest Atom = 0x6913 + Allowusermedia Atom = 0x850e + Alt Atom = 0xb003 + Annotation Atom = 0x1b90a + AnnotationXml Atom = 0x1b90e + Applet Atom = 0x30106 + Area Atom = 0x34a04 + Article Atom = 0x3f007 + As Atom = 0xb902 + Aside Atom = 0xc105 + Async Atom = 0xb905 + Audio Atom = 0xcf05 + Autocomplete Atom = 0x2600c + Autofocus Atom = 0xeb09 + Autoplay Atom = 0x10608 + B Atom = 0x101 + Base Atom = 0x11504 + Basefont Atom = 0x11508 + Bdi Atom = 0x16103 + Bdo Atom = 0x13403 + Bgsound Atom = 0x14707 + Big Atom = 0x15903 + Blink Atom = 0x15c05 + Blockquote Atom = 0x1680a + Body Atom = 0x2804 + Br Atom = 0x202 + Button Atom = 0x17206 + Canvas Atom = 0xbd06 + Caption Atom = 0x21907 + Center Atom = 0x20806 + Challenge Atom = 0x28309 + Charset Atom = 0x2107 + Checked Atom = 0x46d07 + Cite Atom = 0x55804 + Class Atom = 0x5b905 + Code Atom = 0x19004 + Col Atom = 0x19703 + Colgroup Atom = 0x19708 + Color Atom = 0x1af05 + Cols Atom = 0x1b404 + Colspan Atom = 0x1b407 + Command Atom = 0x1c707 + Content Atom = 0x57f07 + Contenteditable Atom = 0x57f0f + Contextmenu Atom = 0x3740b + Controls Atom = 0x1ce08 + Coords Atom = 0x1da06 + Crossorigin Atom = 0x1e30b + Data Atom = 0x49904 + Datalist Atom = 0x49908 + Datetime Atom = 0x2a008 + Dd Atom = 0x2bf02 + Default Atom = 0xc407 + Defer Atom = 0x19205 + Del Atom = 0x44603 + Desc Atom = 0x55504 + Details Atom = 0x4607 + Dfn Atom = 0x5f03 + Dialog Atom = 0x16206 + Dir Atom = 0xa303 + Dirname Atom = 0xa307 + Disabled Atom = 0x14d08 + Div Atom = 0x15403 + Dl Atom = 0x5e202 + Download Atom = 0x45708 + Draggable Atom = 0x18309 + Dropzone Atom = 0x3f908 + Dt Atom = 0x64702 + Em Atom = 0x4202 + Embed Atom = 0x4205 + Enctype Atom = 0x27507 + Face Atom = 0x20604 + Fieldset Atom = 0x20e08 + Figcaption Atom = 0x2160a + Figure Atom = 0x23006 + Font Atom = 0x11904 + Footer Atom = 0xb306 + For Atom = 0x23c03 + ForeignObject Atom = 0x23c0d + Foreignobject Atom = 0x2490d + Form Atom = 0x25604 + Formaction Atom = 0x2560a + Formenctype Atom = 0x2710b + Formmethod Atom = 0x28c0a + Formnovalidate Atom = 0x2960e + Formtarget Atom = 0x2a80a + Frame Atom = 0x5705 + Frameset Atom = 0x5708 + H1 Atom = 0x14502 + H2 Atom = 0x2c602 + H3 Atom = 0x2f502 + H4 Atom = 0x33902 + H5 Atom = 0x34302 + H6 Atom = 0x64902 + Head Atom = 0x32504 + Header Atom = 0x32506 + Headers Atom = 0x32507 + Height Atom = 0x12c06 + Hgroup Atom = 0x2b206 + Hidden Atom = 0x2bd06 + High Atom = 0x2c304 + Hr Atom = 0x14002 + Href Atom = 0x2c804 + Hreflang Atom = 0x2c808 + Html Atom = 0x13004 + HttpEquiv Atom = 0x2d00a + I Atom = 0x601 + Icon Atom = 0x57e04 + Id Atom = 0xc302 + Iframe Atom = 0x2e406 + Image Atom = 0x2ea05 + Img Atom = 0x2ef03 + Input Atom = 0x43f05 + Inputmode Atom = 0x43f09 + Ins Atom = 0x1ec03 + Integrity Atom = 0x22709 + Is Atom = 0x14e02 + Isindex Atom = 0x2f707 + Ismap Atom = 0x2fe05 + Itemid Atom = 0x37f06 + Itemprop Atom = 0x55908 + Itemref Atom = 0x3c107 + Itemscope Atom = 0x66d09 + Itemtype Atom = 0x30708 + Kbd Atom = 0x16003 + Keygen Atom = 0x3206 + Keytype Atom = 0x7e07 + Kind Atom = 0x18004 + Label Atom = 0xda05 + Lang Atom = 0x2cc04 + Legend Atom = 0x18a06 + Li Atom = 0x11102 + Link Atom = 0x15d04 + List Atom = 0x49d04 + Listing Atom = 0x49d07 + Loop Atom = 0xde04 + Low Atom = 0x6b03 + Main Atom = 0x1004 + Malignmark Atom = 0x6d30a + Manifest Atom = 0x30f08 + Map Atom = 0x30003 + Mark Atom = 0x6d904 + Marquee Atom = 0x31b07 + Math Atom = 0x32204 + Max Atom = 0x33103 + Maxlength Atom = 0x33109 + Media Atom = 0x8e05 + Mediagroup Atom = 0x8e0a + Menu Atom = 0x37b04 + Menuitem Atom = 0x37b08 + Meta Atom = 0x4ac04 + Meter Atom = 0xa805 + Method Atom = 0x29006 + Mglyph Atom = 0x2f006 + Mi Atom = 0x33b02 + Min Atom = 0x33b03 + Minlength Atom = 0x33b09 + Mn Atom = 0x29902 + Mo Atom = 0x6302 + Ms Atom = 0x67002 + Mtext Atom = 0x34505 + Multiple Atom = 0x35308 + Muted Atom = 0x35b05 + Name Atom = 0xa604 + Nav Atom = 0x1303 + Nobr Atom = 0x3704 + Noembed Atom = 0x4007 + Noframes Atom = 0x5508 + Nomodule Atom = 0x6108 + Nonce Atom = 0x56205 + Noscript Atom = 0x1fe08 + Novalidate Atom = 0x29a0a + Object Atom = 0x25006 + Ol Atom = 0x10102 + Onabort Atom = 0x17607 + Onafterprint Atom = 0x21e0c + Onautocomplete Atom = 0x25e0e + Onautocompleteerror Atom = 0x25e13 + Onauxclick Atom = 0x61b0a + Onbeforeprint Atom = 0x69a0d + Onbeforeunload Atom = 0x6e10e + Onblur Atom = 0x5c206 + Oncancel Atom = 0xd308 + Oncanplay Atom = 0x13609 + Oncanplaythrough Atom = 0x13610 + Onchange Atom = 0x40f08 + Onclick Atom = 0x2dd07 + Onclose Atom = 0x36007 + Oncontextmenu Atom = 0x3720d + Oncopy Atom = 0x38506 + Oncuechange Atom = 0x38b0b + Oncut Atom = 0x39605 + Ondblclick Atom = 0x39b0a + Ondrag Atom = 0x3a506 + Ondragend Atom = 0x3a509 + Ondragenter Atom = 0x3ae0b + Ondragexit Atom = 0x3b90a + Ondragleave Atom = 0x3d30b + Ondragover Atom = 0x3de0a + Ondragstart Atom = 0x3e80b + Ondrop Atom = 0x3f706 + Ondurationchange Atom = 0x40710 + Onemptied Atom = 0x3fe09 + Onended Atom = 0x41707 + Onerror Atom = 0x41e07 + Onfocus Atom = 0x42507 + Onhashchange Atom = 0x4310c + Oninput Atom = 0x43d07 + Oninvalid Atom = 0x44909 + Onkeydown Atom = 0x45209 + Onkeypress Atom = 0x45f0a + Onkeyup Atom = 0x47407 + Onlanguagechange Atom = 0x48110 + Onload Atom = 0x49106 + Onloadeddata Atom = 0x4910c + Onloadedmetadata Atom = 0x4a410 + Onloadend Atom = 0x4ba09 + Onloadstart Atom = 0x4c30b + Onmessage Atom = 0x4ce09 + Onmessageerror Atom = 0x4ce0e + Onmousedown Atom = 0x4dc0b + Onmouseenter Atom = 0x4e70c + Onmouseleave Atom = 0x4f30c + Onmousemove Atom = 0x4ff0b + Onmouseout Atom = 0x50a0a + Onmouseover Atom = 0x5170b + Onmouseup Atom = 0x52209 + Onmousewheel Atom = 0x5300c + Onoffline Atom = 0x53c09 + Ononline Atom = 0x54508 + Onpagehide Atom = 0x54d0a + Onpageshow Atom = 0x5670a + Onpaste Atom = 0x57307 + Onpause Atom = 0x58e07 + Onplay Atom = 0x59806 + Onplaying Atom = 0x59809 + Onpopstate Atom = 0x5a10a + Onprogress Atom = 0x5ab0a + Onratechange Atom = 0x5c80c + Onrejectionhandled Atom = 0x5d412 + Onreset Atom = 0x5e607 + Onresize Atom = 0x5ed08 + Onscroll Atom = 0x5fc08 + Onsecuritypolicyviolation Atom = 0x60419 + Onseeked Atom = 0x62508 + Onseeking Atom = 0x62d09 + Onselect Atom = 0x63608 + Onshow Atom = 0x64006 + Onsort Atom = 0x64b06 + Onstalled Atom = 0x65509 + Onstorage Atom = 0x65e09 + Onsubmit Atom = 0x66708 + Onsuspend Atom = 0x67709 + Ontimeupdate Atom = 0x11a0c + Ontoggle Atom = 0x68008 + Onunhandledrejection Atom = 0x68814 + Onunload Atom = 0x6a708 + Onvolumechange Atom = 0x6af0e + Onwaiting Atom = 0x6bd09 + Onwheel Atom = 0x6c607 + Open Atom = 0x55f04 + Optgroup Atom = 0xe008 + Optimum Atom = 0x6cd07 + Option Atom = 0x6dd06 + Output Atom = 0x51106 + P Atom = 0xc01 + Param Atom = 0xc05 + Pattern Atom = 0x4f07 + Picture Atom = 0x9707 + Ping Atom = 0xe704 + Placeholder Atom = 0xfb0b + Plaintext Atom = 0x19e09 + Playsinline Atom = 0x10a0b + Poster Atom = 0x2b706 + Pre Atom = 0x46403 + Preload Atom = 0x47a07 + Progress Atom = 0x5ad08 + Prompt Atom = 0x52a06 + Public Atom = 0x57a06 + Q Atom = 0x7701 + Radiogroup Atom = 0x30a + Readonly Atom = 0x34b08 + Referrerpolicy Atom = 0x3c50e + Rel Atom = 0x47b03 + Required Atom = 0x23408 + Reversed Atom = 0x9c08 + Rows Atom = 0x3a04 + Rowspan Atom = 0x3a07 + Rp Atom = 0x22402 + Rt Atom = 0x17b02 + Ruby Atom = 0xac04 + S Atom = 0x2501 + Samp Atom = 0x4c04 + Sandbox Atom = 0xf307 + Scope Atom = 0x67105 + Scoped Atom = 0x67106 + Script Atom = 0x20006 + Seamless Atom = 0x36508 + Section Atom = 0x5bd07 + Select Atom = 0x63806 + Selected Atom = 0x63808 + Shape Atom = 0x1d505 + Size Atom = 0x5f104 + Sizes Atom = 0x5f105 + Slot Atom = 0x1df04 + Small Atom = 0x1ee05 + Sortable Atom = 0x64d08 + Sorted Atom = 0x32b06 + Source Atom = 0x36c06 + Spacer Atom = 0x42b06 + Span Atom = 0x3d04 + Spellcheck Atom = 0x4680a + Src Atom = 0x5b403 + Srcdoc Atom = 0x5b406 + Srclang Atom = 0x5f507 + Srcset Atom = 0x6f306 + Start Atom = 0x3ee05 + Step Atom = 0x57704 + Strike Atom = 0x7a06 + Strong Atom = 0x31506 + Style Atom = 0x6f905 + Sub Atom = 0x66903 + Summary Atom = 0x6fe07 + Sup Atom = 0x70503 + Svg Atom = 0x70803 + System Atom = 0x70b06 + Tabindex Atom = 0x4b208 + Table Atom = 0x58905 + Target Atom = 0x2ac06 + Tbody Atom = 0x2705 + Td Atom = 0x5e02 + Template Atom = 0x70e08 + Textarea Atom = 0x34608 + Tfoot Atom = 0xb205 + Th Atom = 0x13f02 + Thead Atom = 0x32405 + Time Atom = 0x11c04 + Title Atom = 0xca05 + Tr Atom = 0x7402 + Track Atom = 0x17c05 + Translate Atom = 0x1a609 + Tt Atom = 0x5102 + Type Atom = 0x8104 + Typemustmatch Atom = 0x2780d + U Atom = 0xb01 + Ul Atom = 0x6602 + Updateviacache Atom = 0x1200e + Usemap Atom = 0x59206 + Value Atom = 0x1505 + Var Atom = 0x15603 + Video Atom = 0x2d905 + Wbr Atom = 0x57003 + Width Atom = 0x64505 + Workertype Atom = 0x7160a + Wrap Atom = 0x72004 + Xmp Atom = 0xf903 +) + +const hash0 = 0x81cdf10e + +const maxAtomLen = 25 + +var table = [1 << 9]Atom{ + 0x1: 0x8e0a, // mediagroup + 0x2: 0x2cc04, // lang + 0x4: 0x2c09, // accesskey + 0x5: 0x5708, // frameset + 0x7: 0x63608, // onselect + 0x8: 0x70b06, // system + 0xa: 0x64505, // width + 0xc: 0x2710b, // formenctype + 0xd: 0x10102, // ol + 0xe: 0x38b0b, // oncuechange + 0x10: 0x13403, // bdo + 0x11: 0xcf05, // audio + 0x12: 0x18309, // draggable + 0x14: 0x2d905, // video + 0x15: 0x29902, // mn + 0x16: 0x37b04, // menu + 0x17: 0x2b706, // poster + 0x19: 0xb306, // footer + 0x1a: 0x29006, // method + 0x1b: 0x2a008, // datetime + 0x1c: 0x17607, // onabort + 0x1d: 0x1200e, // updateviacache + 0x1e: 0xb905, // async + 0x1f: 0x49106, // onload + 0x21: 0xd308, // oncancel + 0x22: 0x62508, // onseeked + 0x23: 0x2ea05, // image + 0x24: 0x5d412, // onrejectionhandled + 0x26: 0x15d04, // link + 0x27: 0x51106, // output + 0x28: 0x32504, // head + 0x29: 0x4f30c, // onmouseleave + 0x2a: 0x57307, // onpaste + 0x2b: 0x59809, // onplaying + 0x2c: 0x1b407, // colspan + 0x2f: 0x1af05, // color + 0x30: 0x5f104, // size + 0x31: 0x2d00a, // http-equiv + 0x33: 0x601, // i + 0x34: 0x54d0a, // onpagehide + 0x35: 0x68814, // onunhandledrejection + 0x37: 0x41e07, // onerror + 0x3a: 0x11508, // basefont + 0x3f: 0x1303, // nav + 0x40: 0x18004, // kind + 0x41: 0x34b08, // readonly + 0x42: 0x2f006, // mglyph + 0x44: 0x11102, // li + 0x46: 0x2bd06, // hidden + 0x47: 0x70803, // svg + 0x48: 0x57704, // step + 0x49: 0x22709, // integrity + 0x4a: 0x57a06, // public + 0x4c: 0x19703, // col + 0x4d: 0x1680a, // blockquote + 0x4e: 0x34302, // h5 + 0x50: 0x5ad08, // progress + 0x51: 0x5f105, // sizes + 0x52: 0x33902, // h4 + 0x56: 0x32405, // thead + 0x57: 0x7e07, // keytype + 0x58: 0x5ab0a, // onprogress + 0x59: 0x43f09, // inputmode + 0x5a: 0x3a509, // ondragend + 0x5d: 0x39605, // oncut + 0x5e: 0x42b06, // spacer + 0x5f: 0x19708, // colgroup + 0x62: 0x14e02, // is + 0x65: 0xb902, // as + 0x66: 0x53c09, // onoffline + 0x67: 0x32b06, // sorted + 0x69: 0x48110, // onlanguagechange + 0x6c: 0x4310c, // onhashchange + 0x6d: 0xa604, // name + 0x6e: 0xb205, // tfoot + 0x6f: 0x55504, // desc + 0x70: 0x33103, // max + 0x72: 0x1da06, // coords + 0x73: 0x2f502, // h3 + 0x74: 0x6e10e, // onbeforeunload + 0x75: 0x3a04, // rows + 0x76: 0x63806, // select + 0x77: 0xa805, // meter + 0x78: 0x37f06, // itemid + 0x79: 0x5300c, // onmousewheel + 0x7a: 0x5b406, // srcdoc + 0x7d: 0x17c05, // track + 0x7f: 0x30708, // itemtype + 0x82: 0x6302, // mo + 0x83: 0x40f08, // onchange + 0x84: 0x32507, // headers + 0x85: 0x5c80c, // onratechange + 0x86: 0x60419, // onsecuritypolicyviolation + 0x88: 0x49908, // datalist + 0x89: 0x4dc0b, // onmousedown + 0x8a: 0x1df04, // slot + 0x8b: 0x4a410, // onloadedmetadata + 0x8c: 0x1a06, // accept + 0x8d: 0x25006, // object + 0x91: 0x6af0e, // onvolumechange + 0x92: 0x2107, // charset + 0x93: 0x25e13, // onautocompleteerror + 0x94: 0x6913, // allowpaymentrequest + 0x95: 0x2804, // body + 0x96: 0xc407, // default + 0x97: 0x63808, // selected + 0x98: 0x20604, // face + 0x99: 0x1d505, // shape + 0x9b: 0x68008, // ontoggle + 0x9e: 0x64702, // dt + 0x9f: 0x6d904, // mark + 0xa1: 0xb01, // u + 0xa4: 0x6a708, // onunload + 0xa5: 0xde04, // loop + 0xa6: 0x14d08, // disabled + 0xaa: 0x41707, // onended + 0xab: 0x6d30a, // malignmark + 0xad: 0x67709, // onsuspend + 0xae: 0x34505, // mtext + 0xaf: 0x64b06, // onsort + 0xb0: 0x55908, // itemprop + 0xb3: 0x66d09, // itemscope + 0xb4: 0x15c05, // blink + 0xb6: 0x3a506, // ondrag + 0xb7: 0x6602, // ul + 0xb8: 0x25604, // form + 0xb9: 0xf307, // sandbox + 0xba: 0x5705, // frame + 0xbb: 0x1505, // value + 0xbc: 0x65e09, // onstorage + 0xc0: 0x17b02, // rt + 0xc2: 0x202, // br + 0xc3: 0x20e08, // fieldset + 0xc4: 0x2780d, // typemustmatch + 0xc5: 0x6108, // nomodule + 0xc6: 0x4007, // noembed + 0xc7: 0x69a0d, // onbeforeprint + 0xc8: 0x17206, // button + 0xc9: 0x2dd07, // onclick + 0xca: 0x6fe07, // summary + 0xcd: 0xac04, // ruby + 0xce: 0x5b905, // class + 0xcf: 0x3e80b, // ondragstart + 0xd0: 0x21907, // caption + 0xd4: 0x850e, // allowusermedia + 0xd5: 0x4c30b, // onloadstart + 0xd9: 0x15403, // div + 0xda: 0x49d04, // list + 0xdb: 0x32204, // math + 0xdc: 0x43f05, // input + 0xdf: 0x3de0a, // ondragover + 0xe0: 0x2c602, // h2 + 0xe2: 0x19e09, // plaintext + 0xe4: 0x4e70c, // onmouseenter + 0xe7: 0x46d07, // checked + 0xe8: 0x46403, // pre + 0xea: 0x35308, // multiple + 0xeb: 0x16103, // bdi + 0xec: 0x33109, // maxlength + 0xed: 0x7701, // q + 0xee: 0x61b0a, // onauxclick + 0xf0: 0x57003, // wbr + 0xf2: 0x11504, // base + 0xf3: 0x6dd06, // option + 0xf5: 0x40710, // ondurationchange + 0xf7: 0x5508, // noframes + 0xf9: 0x3f908, // dropzone + 0xfb: 0x67105, // scope + 0xfc: 0x9c08, // reversed + 0xfd: 0x3ae0b, // ondragenter + 0xfe: 0x3ee05, // start + 0xff: 0xf903, // xmp + 0x100: 0x5f507, // srclang + 0x101: 0x2ef03, // img + 0x104: 0x101, // b + 0x105: 0x23c03, // for + 0x106: 0xc105, // aside + 0x107: 0x43d07, // oninput + 0x108: 0x34a04, // area + 0x109: 0x28c0a, // formmethod + 0x10a: 0x72004, // wrap + 0x10c: 0x22402, // rp + 0x10d: 0x45f0a, // onkeypress + 0x10e: 0x5102, // tt + 0x110: 0x33b02, // mi + 0x111: 0x35b05, // muted + 0x112: 0xb003, // alt + 0x113: 0x19004, // code + 0x114: 0x4202, // em + 0x115: 0x3b90a, // ondragexit + 0x117: 0x3d04, // span + 0x119: 0x30f08, // manifest + 0x11a: 0x37b08, // menuitem + 0x11b: 0x57f07, // content + 0x11d: 0x6bd09, // onwaiting + 0x11f: 0x4ba09, // onloadend + 0x121: 0x3720d, // oncontextmenu + 0x123: 0x5c206, // onblur + 0x124: 0x3f007, // article + 0x125: 0xa303, // dir + 0x126: 0xe704, // ping + 0x127: 0x23408, // required + 0x128: 0x44909, // oninvalid + 0x129: 0x6d405, // align + 0x12b: 0x57e04, // icon + 0x12c: 0x64902, // h6 + 0x12d: 0x1b404, // cols + 0x12e: 0x2160a, // figcaption + 0x12f: 0x45209, // onkeydown + 0x130: 0x66708, // onsubmit + 0x131: 0x13609, // oncanplay + 0x132: 0x70503, // sup + 0x133: 0xc01, // p + 0x135: 0x3fe09, // onemptied + 0x136: 0x38506, // oncopy + 0x137: 0x55804, // cite + 0x138: 0x39b0a, // ondblclick + 0x13a: 0x4ff0b, // onmousemove + 0x13c: 0x66903, // sub + 0x13d: 0x47b03, // rel + 0x13e: 0xe008, // optgroup + 0x142: 0x3a07, // rowspan + 0x143: 0x36c06, // source + 0x144: 0x1fe08, // noscript + 0x145: 0x55f04, // open + 0x146: 0x1ec03, // ins + 0x147: 0x23c0d, // foreignObject + 0x148: 0x5a10a, // onpopstate + 0x14a: 0x27507, // enctype + 0x14b: 0x25e0e, // onautocomplete + 0x14c: 0x34608, // textarea + 0x14e: 0x2600c, // autocomplete + 0x14f: 0x14002, // hr + 0x150: 0x1ce08, // controls + 0x151: 0xc302, // id + 0x153: 0x21e0c, // onafterprint + 0x155: 0x2490d, // foreignobject + 0x156: 0x31b07, // marquee + 0x157: 0x58e07, // onpause + 0x158: 0x5e202, // dl + 0x159: 0x12c06, // height + 0x15a: 0x33b03, // min + 0x15b: 0xa307, // dirname + 0x15c: 0x1a609, // translate + 0x15d: 0x13004, // html + 0x15e: 0x33b09, // minlength + 0x15f: 0x47a07, // preload + 0x160: 0x70e08, // template + 0x161: 0x3d30b, // ondragleave + 0x164: 0x5b403, // src + 0x165: 0x31506, // strong + 0x167: 0x4c04, // samp + 0x168: 0x6ed07, // address + 0x169: 0x54508, // ononline + 0x16b: 0xfb0b, // placeholder + 0x16c: 0x2ac06, // target + 0x16d: 0x1ee05, // small + 0x16e: 0x6c607, // onwheel + 0x16f: 0x1b90a, // annotation + 0x170: 0x4680a, // spellcheck + 0x171: 0x4607, // details + 0x172: 0xbd06, // canvas + 0x173: 0xeb09, // autofocus + 0x174: 0xc05, // param + 0x176: 0x45708, // download + 0x177: 0x44603, // del + 0x178: 0x36007, // onclose + 0x179: 0x16003, // kbd + 0x17a: 0x30106, // applet + 0x17b: 0x2c804, // href + 0x17c: 0x5ed08, // onresize + 0x17e: 0x4910c, // onloadeddata + 0x180: 0x7402, // tr + 0x181: 0x2a80a, // formtarget + 0x182: 0xca05, // title + 0x183: 0x6f905, // style + 0x184: 0x7a06, // strike + 0x185: 0x59206, // usemap + 0x186: 0x2e406, // iframe + 0x187: 0x1004, // main + 0x189: 0x9707, // picture + 0x18c: 0x2fe05, // ismap + 0x18e: 0x49904, // data + 0x18f: 0xda05, // label + 0x191: 0x3c50e, // referrerpolicy + 0x192: 0x13f02, // th + 0x194: 0x52a06, // prompt + 0x195: 0x5bd07, // section + 0x197: 0x6cd07, // optimum + 0x198: 0x2c304, // high + 0x199: 0x14502, // h1 + 0x19a: 0x65509, // onstalled + 0x19b: 0x15603, // var + 0x19c: 0x11c04, // time + 0x19e: 0x67002, // ms + 0x19f: 0x32506, // header + 0x1a0: 0x4ce09, // onmessage + 0x1a1: 0x56205, // nonce + 0x1a2: 0x2560a, // formaction + 0x1a3: 0x20806, // center + 0x1a4: 0x3704, // nobr + 0x1a5: 0x58905, // table + 0x1a6: 0x49d07, // listing + 0x1a7: 0x18a06, // legend + 0x1a9: 0x28309, // challenge + 0x1aa: 0x23006, // figure + 0x1ab: 0x8e05, // media + 0x1ae: 0x8104, // type + 0x1af: 0x11904, // font + 0x1b0: 0x4ce0e, // onmessageerror + 0x1b1: 0x36508, // seamless + 0x1b2: 0x5f03, // dfn + 0x1b3: 0x19205, // defer + 0x1b4: 0x6b03, // low + 0x1b5: 0x62d09, // onseeking + 0x1b6: 0x5170b, // onmouseover + 0x1b7: 0x29a0a, // novalidate + 0x1b8: 0x7160a, // workertype + 0x1ba: 0x3c107, // itemref + 0x1bd: 0x1, // a + 0x1be: 0x30003, // map + 0x1bf: 0x11a0c, // ontimeupdate + 0x1c0: 0x14707, // bgsound + 0x1c1: 0x3206, // keygen + 0x1c2: 0x2705, // tbody + 0x1c5: 0x64006, // onshow + 0x1c7: 0x2501, // s + 0x1c8: 0x4f07, // pattern + 0x1cc: 0x13610, // oncanplaythrough + 0x1ce: 0x2bf02, // dd + 0x1cf: 0x6f306, // srcset + 0x1d0: 0x15903, // big + 0x1d2: 0x64d08, // sortable + 0x1d3: 0x47407, // onkeyup + 0x1d5: 0x59806, // onplay + 0x1d7: 0x4ac04, // meta + 0x1d8: 0x3f706, // ondrop + 0x1da: 0x5fc08, // onscroll + 0x1db: 0x1e30b, // crossorigin + 0x1dc: 0x5670a, // onpageshow + 0x1dd: 0x4, // abbr + 0x1de: 0x5e02, // td + 0x1df: 0x57f0f, // contenteditable + 0x1e0: 0x25a06, // action + 0x1e1: 0x10a0b, // playsinline + 0x1e2: 0x42507, // onfocus + 0x1e3: 0x2c808, // hreflang + 0x1e5: 0x50a0a, // onmouseout + 0x1e6: 0x5e607, // onreset + 0x1e7: 0x10608, // autoplay + 0x1ea: 0x67106, // scoped + 0x1ec: 0x30a, // radiogroup + 0x1ee: 0x3740b, // contextmenu + 0x1ef: 0x52209, // onmouseup + 0x1f1: 0x2b206, // hgroup + 0x1f2: 0x1f00f, // allowfullscreen + 0x1f3: 0x4b208, // tabindex + 0x1f6: 0x2f707, // isindex + 0x1f7: 0x1a0e, // accept-charset + 0x1f8: 0x2960e, // formnovalidate + 0x1fb: 0x1b90e, // annotation-xml + 0x1fc: 0x4205, // embed + 0x1fd: 0x20006, // script + 0x1fe: 0x16206, // dialog + 0x1ff: 0x1c707, // command +} + +const atomText = "abbradiogrouparamainavalueaccept-charsetbodyaccesskeygenobro" + + "wspanoembedetailsampatternoframesetdfnomoduleallowpaymentreq" + + "uestrikeytypeallowusermediagroupictureversedirnameterubyaltf" + + "ooterasyncanvasidefaultitleaudioncancelabelooptgroupingautof" + + "ocusandboxmplaceholderautoplaysinlinebasefontimeupdateviacac" + + "heightmlbdoncanplaythrough1bgsoundisabledivarbigblinkbdialog" + + "blockquotebuttonabortrackindraggablegendcodefercolgrouplaint" + + "extranslatecolorcolspannotation-xmlcommandcontrolshapecoords" + + "lotcrossoriginsmallowfullscreenoscriptfacenterfieldsetfigcap" + + "tionafterprintegrityfigurequiredforeignObjectforeignobjectfo" + + "rmactionautocompleteerrorformenctypemustmatchallengeformmeth" + + "odformnovalidatetimeformtargethgrouposterhiddenhigh2hreflang" + + "http-equivideonclickiframeimageimglyph3isindexismappletitemt" + + "ypemanifestrongmarqueematheadersortedmaxlength4minlength5mte" + + "xtareadonlymultiplemutedoncloseamlessourceoncontextmenuitemi" + + "doncopyoncuechangeoncutondblclickondragendondragenterondrage" + + "xitemreferrerpolicyondragleaveondragoverondragstarticleondro" + + "pzonemptiedondurationchangeonendedonerroronfocuspaceronhashc" + + "hangeoninputmodeloninvalidonkeydownloadonkeypresspellchecked" + + "onkeyupreloadonlanguagechangeonloadeddatalistingonloadedmeta" + + "databindexonloadendonloadstartonmessageerroronmousedownonmou" + + "seenteronmouseleaveonmousemoveonmouseoutputonmouseoveronmous" + + "eupromptonmousewheelonofflineononlineonpagehidescitempropeno" + + "nceonpageshowbronpastepublicontenteditableonpausemaponplayin" + + "gonpopstateonprogressrcdoclassectionbluronratechangeonreject" + + "ionhandledonresetonresizesrclangonscrollonsecuritypolicyviol" + + "ationauxclickonseekedonseekingonselectedonshowidth6onsortabl" + + "eonstalledonstorageonsubmitemscopedonsuspendontoggleonunhand" + + "ledrejectionbeforeprintonunloadonvolumechangeonwaitingonwhee" + + "loptimumalignmarkoptionbeforeunloaddressrcsetstylesummarysup" + + "svgsystemplateworkertypewrap" diff --git a/vendor/golang.org/x/net/html/const.go b/vendor/golang.org/x/net/html/const.go new file mode 100644 index 000000000..b37e62124 --- /dev/null +++ b/vendor/golang.org/x/net/html/const.go @@ -0,0 +1,104 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +// Section 12.2.3.2 of the HTML5 specification says "The following elements +// have varying levels of special parsing rules". +// https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements +var isSpecialElementMap = map[string]bool{ + "address": true, + "applet": true, + "area": true, + "article": true, + "aside": true, + "base": true, + "basefont": true, + "bgsound": true, + "blockquote": true, + "body": true, + "br": true, + "button": true, + "caption": true, + "center": true, + "col": true, + "colgroup": true, + "dd": true, + "details": true, + "dir": true, + "div": true, + "dl": true, + "dt": true, + "embed": true, + "fieldset": true, + "figcaption": true, + "figure": true, + "footer": true, + "form": true, + "frame": true, + "frameset": true, + "h1": true, + "h2": true, + "h3": true, + "h4": true, + "h5": true, + "h6": true, + "head": true, + "header": true, + "hgroup": true, + "hr": true, + "html": true, + "iframe": true, + "img": true, + "input": true, + "isindex": true, // The 'isindex' element has been removed, but keep it for backwards compatibility. + "keygen": true, + "li": true, + "link": true, + "listing": true, + "main": true, + "marquee": true, + "menu": true, + "meta": true, + "nav": true, + "noembed": true, + "noframes": true, + "noscript": true, + "object": true, + "ol": true, + "p": true, + "param": true, + "plaintext": true, + "pre": true, + "script": true, + "section": true, + "select": true, + "source": true, + "style": true, + "summary": true, + "table": true, + "tbody": true, + "td": true, + "template": true, + "textarea": true, + "tfoot": true, + "th": true, + "thead": true, + "title": true, + "tr": true, + "track": true, + "ul": true, + "wbr": true, + "xmp": true, +} + +func isSpecialElement(element *Node) bool { + switch element.Namespace { + case "", "html": + return isSpecialElementMap[element.Data] + case "svg": + return element.Data == "foreignObject" + } + return false +} diff --git a/vendor/golang.org/x/net/html/doc.go b/vendor/golang.org/x/net/html/doc.go new file mode 100644 index 000000000..822ed42a0 --- /dev/null +++ b/vendor/golang.org/x/net/html/doc.go @@ -0,0 +1,106 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package html implements an HTML5-compliant tokenizer and parser. + +Tokenization is done by creating a Tokenizer for an io.Reader r. It is the +caller's responsibility to ensure that r provides UTF-8 encoded HTML. + + z := html.NewTokenizer(r) + +Given a Tokenizer z, the HTML is tokenized by repeatedly calling z.Next(), +which parses the next token and returns its type, or an error: + + for { + tt := z.Next() + if tt == html.ErrorToken { + // ... + return ... + } + // Process the current token. + } + +There are two APIs for retrieving the current token. The high-level API is to +call Token; the low-level API is to call Text or TagName / TagAttr. Both APIs +allow optionally calling Raw after Next but before Token, Text, TagName, or +TagAttr. In EBNF notation, the valid call sequence per token is: + + Next {Raw} [ Token | Text | TagName {TagAttr} ] + +Token returns an independent data structure that completely describes a token. +Entities (such as "<") are unescaped, tag names and attribute keys are +lower-cased, and attributes are collected into a []Attribute. For example: + + for { + if z.Next() == html.ErrorToken { + // Returning io.EOF indicates success. + return z.Err() + } + emitToken(z.Token()) + } + +The low-level API performs fewer allocations and copies, but the contents of +the []byte values returned by Text, TagName and TagAttr may change on the next +call to Next. For example, to extract an HTML page's anchor text: + + depth := 0 + for { + tt := z.Next() + switch tt { + case html.ErrorToken: + return z.Err() + case html.TextToken: + if depth > 0 { + // emitBytes should copy the []byte it receives, + // if it doesn't process it immediately. + emitBytes(z.Text()) + } + case html.StartTagToken, html.EndTagToken: + tn, _ := z.TagName() + if len(tn) == 1 && tn[0] == 'a' { + if tt == html.StartTagToken { + depth++ + } else { + depth-- + } + } + } + } + +Parsing is done by calling Parse with an io.Reader, which returns the root of +the parse tree (the document element) as a *Node. It is the caller's +responsibility to ensure that the Reader provides UTF-8 encoded HTML. For +example, to process each anchor node in depth-first order: + + doc, err := html.Parse(r) + if err != nil { + // ... + } + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + // Do something with n... + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + +The relevant specifications include: +https://html.spec.whatwg.org/multipage/syntax.html and +https://html.spec.whatwg.org/multipage/syntax.html#tokenization +*/ +package html // import "golang.org/x/net/html" + +// The tokenization algorithm implemented by this package is not a line-by-line +// transliteration of the relatively verbose state-machine in the WHATWG +// specification. A more direct approach is used instead, where the program +// counter implies the state, such as whether it is tokenizing a tag or a text +// node. Specification compliance is verified by checking expected and actual +// outputs over a test suite rather than aiming for algorithmic fidelity. + +// TODO(nigeltao): Does a DOM API belong in this package or a separate one? +// TODO(nigeltao): How does parsing interact with a JavaScript engine? diff --git a/vendor/golang.org/x/net/html/doctype.go b/vendor/golang.org/x/net/html/doctype.go new file mode 100644 index 000000000..c484e5a94 --- /dev/null +++ b/vendor/golang.org/x/net/html/doctype.go @@ -0,0 +1,156 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "strings" +) + +// parseDoctype parses the data from a DoctypeToken into a name, +// public identifier, and system identifier. It returns a Node whose Type +// is DoctypeNode, whose Data is the name, and which has attributes +// named "system" and "public" for the two identifiers if they were present. +// quirks is whether the document should be parsed in "quirks mode". +func parseDoctype(s string) (n *Node, quirks bool) { + n = &Node{Type: DoctypeNode} + + // Find the name. + space := strings.IndexAny(s, whitespace) + if space == -1 { + space = len(s) + } + n.Data = s[:space] + // The comparison to "html" is case-sensitive. + if n.Data != "html" { + quirks = true + } + n.Data = strings.ToLower(n.Data) + s = strings.TrimLeft(s[space:], whitespace) + + if len(s) < 6 { + // It can't start with "PUBLIC" or "SYSTEM". + // Ignore the rest of the string. + return n, quirks || s != "" + } + + key := strings.ToLower(s[:6]) + s = s[6:] + for key == "public" || key == "system" { + s = strings.TrimLeft(s, whitespace) + if s == "" { + break + } + quote := s[0] + if quote != '"' && quote != '\'' { + break + } + s = s[1:] + q := strings.IndexRune(s, rune(quote)) + var id string + if q == -1 { + id = s + s = "" + } else { + id = s[:q] + s = s[q+1:] + } + n.Attr = append(n.Attr, Attribute{Key: key, Val: id}) + if key == "public" { + key = "system" + } else { + key = "" + } + } + + if key != "" || s != "" { + quirks = true + } else if len(n.Attr) > 0 { + if n.Attr[0].Key == "public" { + public := strings.ToLower(n.Attr[0].Val) + switch public { + case "-//w3o//dtd w3 html strict 3.0//en//", "-/w3d/dtd html 4.0 transitional/en", "html": + quirks = true + default: + for _, q := range quirkyIDs { + if strings.HasPrefix(public, q) { + quirks = true + break + } + } + } + // The following two public IDs only cause quirks mode if there is no system ID. + if len(n.Attr) == 1 && (strings.HasPrefix(public, "-//w3c//dtd html 4.01 frameset//") || + strings.HasPrefix(public, "-//w3c//dtd html 4.01 transitional//")) { + quirks = true + } + } + if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" && + strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" { + quirks = true + } + } + + return n, quirks +} + +// quirkyIDs is a list of public doctype identifiers that cause a document +// to be interpreted in quirks mode. The identifiers should be in lower case. +var quirkyIDs = []string{ + "+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", + "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//", +} diff --git a/vendor/golang.org/x/net/html/entity.go b/vendor/golang.org/x/net/html/entity.go new file mode 100644 index 000000000..a50c04c60 --- /dev/null +++ b/vendor/golang.org/x/net/html/entity.go @@ -0,0 +1,2253 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +// All entities that do not end with ';' are 6 or fewer bytes long. +const longestEntityWithoutSemicolon = 6 + +// entity is a map from HTML entity names to their values. The semicolon matters: +// https://html.spec.whatwg.org/multipage/syntax.html#named-character-references +// lists both "amp" and "amp;" as two separate entries. +// +// Note that the HTML5 list is larger than the HTML4 list at +// http://www.w3.org/TR/html4/sgml/entities.html +var entity = map[string]rune{ + "AElig;": '\U000000C6', + "AMP;": '\U00000026', + "Aacute;": '\U000000C1', + "Abreve;": '\U00000102', + "Acirc;": '\U000000C2', + "Acy;": '\U00000410', + "Afr;": '\U0001D504', + "Agrave;": '\U000000C0', + "Alpha;": '\U00000391', + "Amacr;": '\U00000100', + "And;": '\U00002A53', + "Aogon;": '\U00000104', + "Aopf;": '\U0001D538', + "ApplyFunction;": '\U00002061', + "Aring;": '\U000000C5', + "Ascr;": '\U0001D49C', + "Assign;": '\U00002254', + "Atilde;": '\U000000C3', + "Auml;": '\U000000C4', + "Backslash;": '\U00002216', + "Barv;": '\U00002AE7', + "Barwed;": '\U00002306', + "Bcy;": '\U00000411', + "Because;": '\U00002235', + "Bernoullis;": '\U0000212C', + "Beta;": '\U00000392', + "Bfr;": '\U0001D505', + "Bopf;": '\U0001D539', + "Breve;": '\U000002D8', + "Bscr;": '\U0000212C', + "Bumpeq;": '\U0000224E', + "CHcy;": '\U00000427', + "COPY;": '\U000000A9', + "Cacute;": '\U00000106', + "Cap;": '\U000022D2', + "CapitalDifferentialD;": '\U00002145', + "Cayleys;": '\U0000212D', + "Ccaron;": '\U0000010C', + "Ccedil;": '\U000000C7', + "Ccirc;": '\U00000108', + "Cconint;": '\U00002230', + "Cdot;": '\U0000010A', + "Cedilla;": '\U000000B8', + "CenterDot;": '\U000000B7', + "Cfr;": '\U0000212D', + "Chi;": '\U000003A7', + "CircleDot;": '\U00002299', + "CircleMinus;": '\U00002296', + "CirclePlus;": '\U00002295', + "CircleTimes;": '\U00002297', + "ClockwiseContourIntegral;": '\U00002232', + "CloseCurlyDoubleQuote;": '\U0000201D', + "CloseCurlyQuote;": '\U00002019', + "Colon;": '\U00002237', + "Colone;": '\U00002A74', + "Congruent;": '\U00002261', + "Conint;": '\U0000222F', + "ContourIntegral;": '\U0000222E', + "Copf;": '\U00002102', + "Coproduct;": '\U00002210', + "CounterClockwiseContourIntegral;": '\U00002233', + "Cross;": '\U00002A2F', + "Cscr;": '\U0001D49E', + "Cup;": '\U000022D3', + "CupCap;": '\U0000224D', + "DD;": '\U00002145', + "DDotrahd;": '\U00002911', + "DJcy;": '\U00000402', + "DScy;": '\U00000405', + "DZcy;": '\U0000040F', + "Dagger;": '\U00002021', + "Darr;": '\U000021A1', + "Dashv;": '\U00002AE4', + "Dcaron;": '\U0000010E', + "Dcy;": '\U00000414', + "Del;": '\U00002207', + "Delta;": '\U00000394', + "Dfr;": '\U0001D507', + "DiacriticalAcute;": '\U000000B4', + "DiacriticalDot;": '\U000002D9', + "DiacriticalDoubleAcute;": '\U000002DD', + "DiacriticalGrave;": '\U00000060', + "DiacriticalTilde;": '\U000002DC', + "Diamond;": '\U000022C4', + "DifferentialD;": '\U00002146', + "Dopf;": '\U0001D53B', + "Dot;": '\U000000A8', + "DotDot;": '\U000020DC', + "DotEqual;": '\U00002250', + "DoubleContourIntegral;": '\U0000222F', + "DoubleDot;": '\U000000A8', + "DoubleDownArrow;": '\U000021D3', + "DoubleLeftArrow;": '\U000021D0', + "DoubleLeftRightArrow;": '\U000021D4', + "DoubleLeftTee;": '\U00002AE4', + "DoubleLongLeftArrow;": '\U000027F8', + "DoubleLongLeftRightArrow;": '\U000027FA', + "DoubleLongRightArrow;": '\U000027F9', + "DoubleRightArrow;": '\U000021D2', + "DoubleRightTee;": '\U000022A8', + "DoubleUpArrow;": '\U000021D1', + "DoubleUpDownArrow;": '\U000021D5', + "DoubleVerticalBar;": '\U00002225', + "DownArrow;": '\U00002193', + "DownArrowBar;": '\U00002913', + "DownArrowUpArrow;": '\U000021F5', + "DownBreve;": '\U00000311', + "DownLeftRightVector;": '\U00002950', + "DownLeftTeeVector;": '\U0000295E', + "DownLeftVector;": '\U000021BD', + "DownLeftVectorBar;": '\U00002956', + "DownRightTeeVector;": '\U0000295F', + "DownRightVector;": '\U000021C1', + "DownRightVectorBar;": '\U00002957', + "DownTee;": '\U000022A4', + "DownTeeArrow;": '\U000021A7', + "Downarrow;": '\U000021D3', + "Dscr;": '\U0001D49F', + "Dstrok;": '\U00000110', + "ENG;": '\U0000014A', + "ETH;": '\U000000D0', + "Eacute;": '\U000000C9', + "Ecaron;": '\U0000011A', + "Ecirc;": '\U000000CA', + "Ecy;": '\U0000042D', + "Edot;": '\U00000116', + "Efr;": '\U0001D508', + "Egrave;": '\U000000C8', + "Element;": '\U00002208', + "Emacr;": '\U00000112', + "EmptySmallSquare;": '\U000025FB', + "EmptyVerySmallSquare;": '\U000025AB', + "Eogon;": '\U00000118', + "Eopf;": '\U0001D53C', + "Epsilon;": '\U00000395', + "Equal;": '\U00002A75', + "EqualTilde;": '\U00002242', + "Equilibrium;": '\U000021CC', + "Escr;": '\U00002130', + "Esim;": '\U00002A73', + "Eta;": '\U00000397', + "Euml;": '\U000000CB', + "Exists;": '\U00002203', + "ExponentialE;": '\U00002147', + "Fcy;": '\U00000424', + "Ffr;": '\U0001D509', + "FilledSmallSquare;": '\U000025FC', + "FilledVerySmallSquare;": '\U000025AA', + "Fopf;": '\U0001D53D', + "ForAll;": '\U00002200', + "Fouriertrf;": '\U00002131', + "Fscr;": '\U00002131', + "GJcy;": '\U00000403', + "GT;": '\U0000003E', + "Gamma;": '\U00000393', + "Gammad;": '\U000003DC', + "Gbreve;": '\U0000011E', + "Gcedil;": '\U00000122', + "Gcirc;": '\U0000011C', + "Gcy;": '\U00000413', + "Gdot;": '\U00000120', + "Gfr;": '\U0001D50A', + "Gg;": '\U000022D9', + "Gopf;": '\U0001D53E', + "GreaterEqual;": '\U00002265', + "GreaterEqualLess;": '\U000022DB', + "GreaterFullEqual;": '\U00002267', + "GreaterGreater;": '\U00002AA2', + "GreaterLess;": '\U00002277', + "GreaterSlantEqual;": '\U00002A7E', + "GreaterTilde;": '\U00002273', + "Gscr;": '\U0001D4A2', + "Gt;": '\U0000226B', + "HARDcy;": '\U0000042A', + "Hacek;": '\U000002C7', + "Hat;": '\U0000005E', + "Hcirc;": '\U00000124', + "Hfr;": '\U0000210C', + "HilbertSpace;": '\U0000210B', + "Hopf;": '\U0000210D', + "HorizontalLine;": '\U00002500', + "Hscr;": '\U0000210B', + "Hstrok;": '\U00000126', + "HumpDownHump;": '\U0000224E', + "HumpEqual;": '\U0000224F', + "IEcy;": '\U00000415', + "IJlig;": '\U00000132', + "IOcy;": '\U00000401', + "Iacute;": '\U000000CD', + "Icirc;": '\U000000CE', + "Icy;": '\U00000418', + "Idot;": '\U00000130', + "Ifr;": '\U00002111', + "Igrave;": '\U000000CC', + "Im;": '\U00002111', + "Imacr;": '\U0000012A', + "ImaginaryI;": '\U00002148', + "Implies;": '\U000021D2', + "Int;": '\U0000222C', + "Integral;": '\U0000222B', + "Intersection;": '\U000022C2', + "InvisibleComma;": '\U00002063', + "InvisibleTimes;": '\U00002062', + "Iogon;": '\U0000012E', + "Iopf;": '\U0001D540', + "Iota;": '\U00000399', + "Iscr;": '\U00002110', + "Itilde;": '\U00000128', + "Iukcy;": '\U00000406', + "Iuml;": '\U000000CF', + "Jcirc;": '\U00000134', + "Jcy;": '\U00000419', + "Jfr;": '\U0001D50D', + "Jopf;": '\U0001D541', + "Jscr;": '\U0001D4A5', + "Jsercy;": '\U00000408', + "Jukcy;": '\U00000404', + "KHcy;": '\U00000425', + "KJcy;": '\U0000040C', + "Kappa;": '\U0000039A', + "Kcedil;": '\U00000136', + "Kcy;": '\U0000041A', + "Kfr;": '\U0001D50E', + "Kopf;": '\U0001D542', + "Kscr;": '\U0001D4A6', + "LJcy;": '\U00000409', + "LT;": '\U0000003C', + "Lacute;": '\U00000139', + "Lambda;": '\U0000039B', + "Lang;": '\U000027EA', + "Laplacetrf;": '\U00002112', + "Larr;": '\U0000219E', + "Lcaron;": '\U0000013D', + "Lcedil;": '\U0000013B', + "Lcy;": '\U0000041B', + "LeftAngleBracket;": '\U000027E8', + "LeftArrow;": '\U00002190', + "LeftArrowBar;": '\U000021E4', + "LeftArrowRightArrow;": '\U000021C6', + "LeftCeiling;": '\U00002308', + "LeftDoubleBracket;": '\U000027E6', + "LeftDownTeeVector;": '\U00002961', + "LeftDownVector;": '\U000021C3', + "LeftDownVectorBar;": '\U00002959', + "LeftFloor;": '\U0000230A', + "LeftRightArrow;": '\U00002194', + "LeftRightVector;": '\U0000294E', + "LeftTee;": '\U000022A3', + "LeftTeeArrow;": '\U000021A4', + "LeftTeeVector;": '\U0000295A', + "LeftTriangle;": '\U000022B2', + "LeftTriangleBar;": '\U000029CF', + "LeftTriangleEqual;": '\U000022B4', + "LeftUpDownVector;": '\U00002951', + "LeftUpTeeVector;": '\U00002960', + "LeftUpVector;": '\U000021BF', + "LeftUpVectorBar;": '\U00002958', + "LeftVector;": '\U000021BC', + "LeftVectorBar;": '\U00002952', + "Leftarrow;": '\U000021D0', + "Leftrightarrow;": '\U000021D4', + "LessEqualGreater;": '\U000022DA', + "LessFullEqual;": '\U00002266', + "LessGreater;": '\U00002276', + "LessLess;": '\U00002AA1', + "LessSlantEqual;": '\U00002A7D', + "LessTilde;": '\U00002272', + "Lfr;": '\U0001D50F', + "Ll;": '\U000022D8', + "Lleftarrow;": '\U000021DA', + "Lmidot;": '\U0000013F', + "LongLeftArrow;": '\U000027F5', + "LongLeftRightArrow;": '\U000027F7', + "LongRightArrow;": '\U000027F6', + "Longleftarrow;": '\U000027F8', + "Longleftrightarrow;": '\U000027FA', + "Longrightarrow;": '\U000027F9', + "Lopf;": '\U0001D543', + "LowerLeftArrow;": '\U00002199', + "LowerRightArrow;": '\U00002198', + "Lscr;": '\U00002112', + "Lsh;": '\U000021B0', + "Lstrok;": '\U00000141', + "Lt;": '\U0000226A', + "Map;": '\U00002905', + "Mcy;": '\U0000041C', + "MediumSpace;": '\U0000205F', + "Mellintrf;": '\U00002133', + "Mfr;": '\U0001D510', + "MinusPlus;": '\U00002213', + "Mopf;": '\U0001D544', + "Mscr;": '\U00002133', + "Mu;": '\U0000039C', + "NJcy;": '\U0000040A', + "Nacute;": '\U00000143', + "Ncaron;": '\U00000147', + "Ncedil;": '\U00000145', + "Ncy;": '\U0000041D', + "NegativeMediumSpace;": '\U0000200B', + "NegativeThickSpace;": '\U0000200B', + "NegativeThinSpace;": '\U0000200B', + "NegativeVeryThinSpace;": '\U0000200B', + "NestedGreaterGreater;": '\U0000226B', + "NestedLessLess;": '\U0000226A', + "NewLine;": '\U0000000A', + "Nfr;": '\U0001D511', + "NoBreak;": '\U00002060', + "NonBreakingSpace;": '\U000000A0', + "Nopf;": '\U00002115', + "Not;": '\U00002AEC', + "NotCongruent;": '\U00002262', + "NotCupCap;": '\U0000226D', + "NotDoubleVerticalBar;": '\U00002226', + "NotElement;": '\U00002209', + "NotEqual;": '\U00002260', + "NotExists;": '\U00002204', + "NotGreater;": '\U0000226F', + "NotGreaterEqual;": '\U00002271', + "NotGreaterLess;": '\U00002279', + "NotGreaterTilde;": '\U00002275', + "NotLeftTriangle;": '\U000022EA', + "NotLeftTriangleEqual;": '\U000022EC', + "NotLess;": '\U0000226E', + "NotLessEqual;": '\U00002270', + "NotLessGreater;": '\U00002278', + "NotLessTilde;": '\U00002274', + "NotPrecedes;": '\U00002280', + "NotPrecedesSlantEqual;": '\U000022E0', + "NotReverseElement;": '\U0000220C', + "NotRightTriangle;": '\U000022EB', + "NotRightTriangleEqual;": '\U000022ED', + "NotSquareSubsetEqual;": '\U000022E2', + "NotSquareSupersetEqual;": '\U000022E3', + "NotSubsetEqual;": '\U00002288', + "NotSucceeds;": '\U00002281', + "NotSucceedsSlantEqual;": '\U000022E1', + "NotSupersetEqual;": '\U00002289', + "NotTilde;": '\U00002241', + "NotTildeEqual;": '\U00002244', + "NotTildeFullEqual;": '\U00002247', + "NotTildeTilde;": '\U00002249', + "NotVerticalBar;": '\U00002224', + "Nscr;": '\U0001D4A9', + "Ntilde;": '\U000000D1', + "Nu;": '\U0000039D', + "OElig;": '\U00000152', + "Oacute;": '\U000000D3', + "Ocirc;": '\U000000D4', + "Ocy;": '\U0000041E', + "Odblac;": '\U00000150', + "Ofr;": '\U0001D512', + "Ograve;": '\U000000D2', + "Omacr;": '\U0000014C', + "Omega;": '\U000003A9', + "Omicron;": '\U0000039F', + "Oopf;": '\U0001D546', + "OpenCurlyDoubleQuote;": '\U0000201C', + "OpenCurlyQuote;": '\U00002018', + "Or;": '\U00002A54', + "Oscr;": '\U0001D4AA', + "Oslash;": '\U000000D8', + "Otilde;": '\U000000D5', + "Otimes;": '\U00002A37', + "Ouml;": '\U000000D6', + "OverBar;": '\U0000203E', + "OverBrace;": '\U000023DE', + "OverBracket;": '\U000023B4', + "OverParenthesis;": '\U000023DC', + "PartialD;": '\U00002202', + "Pcy;": '\U0000041F', + "Pfr;": '\U0001D513', + "Phi;": '\U000003A6', + "Pi;": '\U000003A0', + "PlusMinus;": '\U000000B1', + "Poincareplane;": '\U0000210C', + "Popf;": '\U00002119', + "Pr;": '\U00002ABB', + "Precedes;": '\U0000227A', + "PrecedesEqual;": '\U00002AAF', + "PrecedesSlantEqual;": '\U0000227C', + "PrecedesTilde;": '\U0000227E', + "Prime;": '\U00002033', + "Product;": '\U0000220F', + "Proportion;": '\U00002237', + "Proportional;": '\U0000221D', + "Pscr;": '\U0001D4AB', + "Psi;": '\U000003A8', + "QUOT;": '\U00000022', + "Qfr;": '\U0001D514', + "Qopf;": '\U0000211A', + "Qscr;": '\U0001D4AC', + "RBarr;": '\U00002910', + "REG;": '\U000000AE', + "Racute;": '\U00000154', + "Rang;": '\U000027EB', + "Rarr;": '\U000021A0', + "Rarrtl;": '\U00002916', + "Rcaron;": '\U00000158', + "Rcedil;": '\U00000156', + "Rcy;": '\U00000420', + "Re;": '\U0000211C', + "ReverseElement;": '\U0000220B', + "ReverseEquilibrium;": '\U000021CB', + "ReverseUpEquilibrium;": '\U0000296F', + "Rfr;": '\U0000211C', + "Rho;": '\U000003A1', + "RightAngleBracket;": '\U000027E9', + "RightArrow;": '\U00002192', + "RightArrowBar;": '\U000021E5', + "RightArrowLeftArrow;": '\U000021C4', + "RightCeiling;": '\U00002309', + "RightDoubleBracket;": '\U000027E7', + "RightDownTeeVector;": '\U0000295D', + "RightDownVector;": '\U000021C2', + "RightDownVectorBar;": '\U00002955', + "RightFloor;": '\U0000230B', + "RightTee;": '\U000022A2', + "RightTeeArrow;": '\U000021A6', + "RightTeeVector;": '\U0000295B', + "RightTriangle;": '\U000022B3', + "RightTriangleBar;": '\U000029D0', + "RightTriangleEqual;": '\U000022B5', + "RightUpDownVector;": '\U0000294F', + "RightUpTeeVector;": '\U0000295C', + "RightUpVector;": '\U000021BE', + "RightUpVectorBar;": '\U00002954', + "RightVector;": '\U000021C0', + "RightVectorBar;": '\U00002953', + "Rightarrow;": '\U000021D2', + "Ropf;": '\U0000211D', + "RoundImplies;": '\U00002970', + "Rrightarrow;": '\U000021DB', + "Rscr;": '\U0000211B', + "Rsh;": '\U000021B1', + "RuleDelayed;": '\U000029F4', + "SHCHcy;": '\U00000429', + "SHcy;": '\U00000428', + "SOFTcy;": '\U0000042C', + "Sacute;": '\U0000015A', + "Sc;": '\U00002ABC', + "Scaron;": '\U00000160', + "Scedil;": '\U0000015E', + "Scirc;": '\U0000015C', + "Scy;": '\U00000421', + "Sfr;": '\U0001D516', + "ShortDownArrow;": '\U00002193', + "ShortLeftArrow;": '\U00002190', + "ShortRightArrow;": '\U00002192', + "ShortUpArrow;": '\U00002191', + "Sigma;": '\U000003A3', + "SmallCircle;": '\U00002218', + "Sopf;": '\U0001D54A', + "Sqrt;": '\U0000221A', + "Square;": '\U000025A1', + "SquareIntersection;": '\U00002293', + "SquareSubset;": '\U0000228F', + "SquareSubsetEqual;": '\U00002291', + "SquareSuperset;": '\U00002290', + "SquareSupersetEqual;": '\U00002292', + "SquareUnion;": '\U00002294', + "Sscr;": '\U0001D4AE', + "Star;": '\U000022C6', + "Sub;": '\U000022D0', + "Subset;": '\U000022D0', + "SubsetEqual;": '\U00002286', + "Succeeds;": '\U0000227B', + "SucceedsEqual;": '\U00002AB0', + "SucceedsSlantEqual;": '\U0000227D', + "SucceedsTilde;": '\U0000227F', + "SuchThat;": '\U0000220B', + "Sum;": '\U00002211', + "Sup;": '\U000022D1', + "Superset;": '\U00002283', + "SupersetEqual;": '\U00002287', + "Supset;": '\U000022D1', + "THORN;": '\U000000DE', + "TRADE;": '\U00002122', + "TSHcy;": '\U0000040B', + "TScy;": '\U00000426', + "Tab;": '\U00000009', + "Tau;": '\U000003A4', + "Tcaron;": '\U00000164', + "Tcedil;": '\U00000162', + "Tcy;": '\U00000422', + "Tfr;": '\U0001D517', + "Therefore;": '\U00002234', + "Theta;": '\U00000398', + "ThinSpace;": '\U00002009', + "Tilde;": '\U0000223C', + "TildeEqual;": '\U00002243', + "TildeFullEqual;": '\U00002245', + "TildeTilde;": '\U00002248', + "Topf;": '\U0001D54B', + "TripleDot;": '\U000020DB', + "Tscr;": '\U0001D4AF', + "Tstrok;": '\U00000166', + "Uacute;": '\U000000DA', + "Uarr;": '\U0000219F', + "Uarrocir;": '\U00002949', + "Ubrcy;": '\U0000040E', + "Ubreve;": '\U0000016C', + "Ucirc;": '\U000000DB', + "Ucy;": '\U00000423', + "Udblac;": '\U00000170', + "Ufr;": '\U0001D518', + "Ugrave;": '\U000000D9', + "Umacr;": '\U0000016A', + "UnderBar;": '\U0000005F', + "UnderBrace;": '\U000023DF', + "UnderBracket;": '\U000023B5', + "UnderParenthesis;": '\U000023DD', + "Union;": '\U000022C3', + "UnionPlus;": '\U0000228E', + "Uogon;": '\U00000172', + "Uopf;": '\U0001D54C', + "UpArrow;": '\U00002191', + "UpArrowBar;": '\U00002912', + "UpArrowDownArrow;": '\U000021C5', + "UpDownArrow;": '\U00002195', + "UpEquilibrium;": '\U0000296E', + "UpTee;": '\U000022A5', + "UpTeeArrow;": '\U000021A5', + "Uparrow;": '\U000021D1', + "Updownarrow;": '\U000021D5', + "UpperLeftArrow;": '\U00002196', + "UpperRightArrow;": '\U00002197', + "Upsi;": '\U000003D2', + "Upsilon;": '\U000003A5', + "Uring;": '\U0000016E', + "Uscr;": '\U0001D4B0', + "Utilde;": '\U00000168', + "Uuml;": '\U000000DC', + "VDash;": '\U000022AB', + "Vbar;": '\U00002AEB', + "Vcy;": '\U00000412', + "Vdash;": '\U000022A9', + "Vdashl;": '\U00002AE6', + "Vee;": '\U000022C1', + "Verbar;": '\U00002016', + "Vert;": '\U00002016', + "VerticalBar;": '\U00002223', + "VerticalLine;": '\U0000007C', + "VerticalSeparator;": '\U00002758', + "VerticalTilde;": '\U00002240', + "VeryThinSpace;": '\U0000200A', + "Vfr;": '\U0001D519', + "Vopf;": '\U0001D54D', + "Vscr;": '\U0001D4B1', + "Vvdash;": '\U000022AA', + "Wcirc;": '\U00000174', + "Wedge;": '\U000022C0', + "Wfr;": '\U0001D51A', + "Wopf;": '\U0001D54E', + "Wscr;": '\U0001D4B2', + "Xfr;": '\U0001D51B', + "Xi;": '\U0000039E', + "Xopf;": '\U0001D54F', + "Xscr;": '\U0001D4B3', + "YAcy;": '\U0000042F', + "YIcy;": '\U00000407', + "YUcy;": '\U0000042E', + "Yacute;": '\U000000DD', + "Ycirc;": '\U00000176', + "Ycy;": '\U0000042B', + "Yfr;": '\U0001D51C', + "Yopf;": '\U0001D550', + "Yscr;": '\U0001D4B4', + "Yuml;": '\U00000178', + "ZHcy;": '\U00000416', + "Zacute;": '\U00000179', + "Zcaron;": '\U0000017D', + "Zcy;": '\U00000417', + "Zdot;": '\U0000017B', + "ZeroWidthSpace;": '\U0000200B', + "Zeta;": '\U00000396', + "Zfr;": '\U00002128', + "Zopf;": '\U00002124', + "Zscr;": '\U0001D4B5', + "aacute;": '\U000000E1', + "abreve;": '\U00000103', + "ac;": '\U0000223E', + "acd;": '\U0000223F', + "acirc;": '\U000000E2', + "acute;": '\U000000B4', + "acy;": '\U00000430', + "aelig;": '\U000000E6', + "af;": '\U00002061', + "afr;": '\U0001D51E', + "agrave;": '\U000000E0', + "alefsym;": '\U00002135', + "aleph;": '\U00002135', + "alpha;": '\U000003B1', + "amacr;": '\U00000101', + "amalg;": '\U00002A3F', + "amp;": '\U00000026', + "and;": '\U00002227', + "andand;": '\U00002A55', + "andd;": '\U00002A5C', + "andslope;": '\U00002A58', + "andv;": '\U00002A5A', + "ang;": '\U00002220', + "ange;": '\U000029A4', + "angle;": '\U00002220', + "angmsd;": '\U00002221', + "angmsdaa;": '\U000029A8', + "angmsdab;": '\U000029A9', + "angmsdac;": '\U000029AA', + "angmsdad;": '\U000029AB', + "angmsdae;": '\U000029AC', + "angmsdaf;": '\U000029AD', + "angmsdag;": '\U000029AE', + "angmsdah;": '\U000029AF', + "angrt;": '\U0000221F', + "angrtvb;": '\U000022BE', + "angrtvbd;": '\U0000299D', + "angsph;": '\U00002222', + "angst;": '\U000000C5', + "angzarr;": '\U0000237C', + "aogon;": '\U00000105', + "aopf;": '\U0001D552', + "ap;": '\U00002248', + "apE;": '\U00002A70', + "apacir;": '\U00002A6F', + "ape;": '\U0000224A', + "apid;": '\U0000224B', + "apos;": '\U00000027', + "approx;": '\U00002248', + "approxeq;": '\U0000224A', + "aring;": '\U000000E5', + "ascr;": '\U0001D4B6', + "ast;": '\U0000002A', + "asymp;": '\U00002248', + "asympeq;": '\U0000224D', + "atilde;": '\U000000E3', + "auml;": '\U000000E4', + "awconint;": '\U00002233', + "awint;": '\U00002A11', + "bNot;": '\U00002AED', + "backcong;": '\U0000224C', + "backepsilon;": '\U000003F6', + "backprime;": '\U00002035', + "backsim;": '\U0000223D', + "backsimeq;": '\U000022CD', + "barvee;": '\U000022BD', + "barwed;": '\U00002305', + "barwedge;": '\U00002305', + "bbrk;": '\U000023B5', + "bbrktbrk;": '\U000023B6', + "bcong;": '\U0000224C', + "bcy;": '\U00000431', + "bdquo;": '\U0000201E', + "becaus;": '\U00002235', + "because;": '\U00002235', + "bemptyv;": '\U000029B0', + "bepsi;": '\U000003F6', + "bernou;": '\U0000212C', + "beta;": '\U000003B2', + "beth;": '\U00002136', + "between;": '\U0000226C', + "bfr;": '\U0001D51F', + "bigcap;": '\U000022C2', + "bigcirc;": '\U000025EF', + "bigcup;": '\U000022C3', + "bigodot;": '\U00002A00', + "bigoplus;": '\U00002A01', + "bigotimes;": '\U00002A02', + "bigsqcup;": '\U00002A06', + "bigstar;": '\U00002605', + "bigtriangledown;": '\U000025BD', + "bigtriangleup;": '\U000025B3', + "biguplus;": '\U00002A04', + "bigvee;": '\U000022C1', + "bigwedge;": '\U000022C0', + "bkarow;": '\U0000290D', + "blacklozenge;": '\U000029EB', + "blacksquare;": '\U000025AA', + "blacktriangle;": '\U000025B4', + "blacktriangledown;": '\U000025BE', + "blacktriangleleft;": '\U000025C2', + "blacktriangleright;": '\U000025B8', + "blank;": '\U00002423', + "blk12;": '\U00002592', + "blk14;": '\U00002591', + "blk34;": '\U00002593', + "block;": '\U00002588', + "bnot;": '\U00002310', + "bopf;": '\U0001D553', + "bot;": '\U000022A5', + "bottom;": '\U000022A5', + "bowtie;": '\U000022C8', + "boxDL;": '\U00002557', + "boxDR;": '\U00002554', + "boxDl;": '\U00002556', + "boxDr;": '\U00002553', + "boxH;": '\U00002550', + "boxHD;": '\U00002566', + "boxHU;": '\U00002569', + "boxHd;": '\U00002564', + "boxHu;": '\U00002567', + "boxUL;": '\U0000255D', + "boxUR;": '\U0000255A', + "boxUl;": '\U0000255C', + "boxUr;": '\U00002559', + "boxV;": '\U00002551', + "boxVH;": '\U0000256C', + "boxVL;": '\U00002563', + "boxVR;": '\U00002560', + "boxVh;": '\U0000256B', + "boxVl;": '\U00002562', + "boxVr;": '\U0000255F', + "boxbox;": '\U000029C9', + "boxdL;": '\U00002555', + "boxdR;": '\U00002552', + "boxdl;": '\U00002510', + "boxdr;": '\U0000250C', + "boxh;": '\U00002500', + "boxhD;": '\U00002565', + "boxhU;": '\U00002568', + "boxhd;": '\U0000252C', + "boxhu;": '\U00002534', + "boxminus;": '\U0000229F', + "boxplus;": '\U0000229E', + "boxtimes;": '\U000022A0', + "boxuL;": '\U0000255B', + "boxuR;": '\U00002558', + "boxul;": '\U00002518', + "boxur;": '\U00002514', + "boxv;": '\U00002502', + "boxvH;": '\U0000256A', + "boxvL;": '\U00002561', + "boxvR;": '\U0000255E', + "boxvh;": '\U0000253C', + "boxvl;": '\U00002524', + "boxvr;": '\U0000251C', + "bprime;": '\U00002035', + "breve;": '\U000002D8', + "brvbar;": '\U000000A6', + "bscr;": '\U0001D4B7', + "bsemi;": '\U0000204F', + "bsim;": '\U0000223D', + "bsime;": '\U000022CD', + "bsol;": '\U0000005C', + "bsolb;": '\U000029C5', + "bsolhsub;": '\U000027C8', + "bull;": '\U00002022', + "bullet;": '\U00002022', + "bump;": '\U0000224E', + "bumpE;": '\U00002AAE', + "bumpe;": '\U0000224F', + "bumpeq;": '\U0000224F', + "cacute;": '\U00000107', + "cap;": '\U00002229', + "capand;": '\U00002A44', + "capbrcup;": '\U00002A49', + "capcap;": '\U00002A4B', + "capcup;": '\U00002A47', + "capdot;": '\U00002A40', + "caret;": '\U00002041', + "caron;": '\U000002C7', + "ccaps;": '\U00002A4D', + "ccaron;": '\U0000010D', + "ccedil;": '\U000000E7', + "ccirc;": '\U00000109', + "ccups;": '\U00002A4C', + "ccupssm;": '\U00002A50', + "cdot;": '\U0000010B', + "cedil;": '\U000000B8', + "cemptyv;": '\U000029B2', + "cent;": '\U000000A2', + "centerdot;": '\U000000B7', + "cfr;": '\U0001D520', + "chcy;": '\U00000447', + "check;": '\U00002713', + "checkmark;": '\U00002713', + "chi;": '\U000003C7', + "cir;": '\U000025CB', + "cirE;": '\U000029C3', + "circ;": '\U000002C6', + "circeq;": '\U00002257', + "circlearrowleft;": '\U000021BA', + "circlearrowright;": '\U000021BB', + "circledR;": '\U000000AE', + "circledS;": '\U000024C8', + "circledast;": '\U0000229B', + "circledcirc;": '\U0000229A', + "circleddash;": '\U0000229D', + "cire;": '\U00002257', + "cirfnint;": '\U00002A10', + "cirmid;": '\U00002AEF', + "cirscir;": '\U000029C2', + "clubs;": '\U00002663', + "clubsuit;": '\U00002663', + "colon;": '\U0000003A', + "colone;": '\U00002254', + "coloneq;": '\U00002254', + "comma;": '\U0000002C', + "commat;": '\U00000040', + "comp;": '\U00002201', + "compfn;": '\U00002218', + "complement;": '\U00002201', + "complexes;": '\U00002102', + "cong;": '\U00002245', + "congdot;": '\U00002A6D', + "conint;": '\U0000222E', + "copf;": '\U0001D554', + "coprod;": '\U00002210', + "copy;": '\U000000A9', + "copysr;": '\U00002117', + "crarr;": '\U000021B5', + "cross;": '\U00002717', + "cscr;": '\U0001D4B8', + "csub;": '\U00002ACF', + "csube;": '\U00002AD1', + "csup;": '\U00002AD0', + "csupe;": '\U00002AD2', + "ctdot;": '\U000022EF', + "cudarrl;": '\U00002938', + "cudarrr;": '\U00002935', + "cuepr;": '\U000022DE', + "cuesc;": '\U000022DF', + "cularr;": '\U000021B6', + "cularrp;": '\U0000293D', + "cup;": '\U0000222A', + "cupbrcap;": '\U00002A48', + "cupcap;": '\U00002A46', + "cupcup;": '\U00002A4A', + "cupdot;": '\U0000228D', + "cupor;": '\U00002A45', + "curarr;": '\U000021B7', + "curarrm;": '\U0000293C', + "curlyeqprec;": '\U000022DE', + "curlyeqsucc;": '\U000022DF', + "curlyvee;": '\U000022CE', + "curlywedge;": '\U000022CF', + "curren;": '\U000000A4', + "curvearrowleft;": '\U000021B6', + "curvearrowright;": '\U000021B7', + "cuvee;": '\U000022CE', + "cuwed;": '\U000022CF', + "cwconint;": '\U00002232', + "cwint;": '\U00002231', + "cylcty;": '\U0000232D', + "dArr;": '\U000021D3', + "dHar;": '\U00002965', + "dagger;": '\U00002020', + "daleth;": '\U00002138', + "darr;": '\U00002193', + "dash;": '\U00002010', + "dashv;": '\U000022A3', + "dbkarow;": '\U0000290F', + "dblac;": '\U000002DD', + "dcaron;": '\U0000010F', + "dcy;": '\U00000434', + "dd;": '\U00002146', + "ddagger;": '\U00002021', + "ddarr;": '\U000021CA', + "ddotseq;": '\U00002A77', + "deg;": '\U000000B0', + "delta;": '\U000003B4', + "demptyv;": '\U000029B1', + "dfisht;": '\U0000297F', + "dfr;": '\U0001D521', + "dharl;": '\U000021C3', + "dharr;": '\U000021C2', + "diam;": '\U000022C4', + "diamond;": '\U000022C4', + "diamondsuit;": '\U00002666', + "diams;": '\U00002666', + "die;": '\U000000A8', + "digamma;": '\U000003DD', + "disin;": '\U000022F2', + "div;": '\U000000F7', + "divide;": '\U000000F7', + "divideontimes;": '\U000022C7', + "divonx;": '\U000022C7', + "djcy;": '\U00000452', + "dlcorn;": '\U0000231E', + "dlcrop;": '\U0000230D', + "dollar;": '\U00000024', + "dopf;": '\U0001D555', + "dot;": '\U000002D9', + "doteq;": '\U00002250', + "doteqdot;": '\U00002251', + "dotminus;": '\U00002238', + "dotplus;": '\U00002214', + "dotsquare;": '\U000022A1', + "doublebarwedge;": '\U00002306', + "downarrow;": '\U00002193', + "downdownarrows;": '\U000021CA', + "downharpoonleft;": '\U000021C3', + "downharpoonright;": '\U000021C2', + "drbkarow;": '\U00002910', + "drcorn;": '\U0000231F', + "drcrop;": '\U0000230C', + "dscr;": '\U0001D4B9', + "dscy;": '\U00000455', + "dsol;": '\U000029F6', + "dstrok;": '\U00000111', + "dtdot;": '\U000022F1', + "dtri;": '\U000025BF', + "dtrif;": '\U000025BE', + "duarr;": '\U000021F5', + "duhar;": '\U0000296F', + "dwangle;": '\U000029A6', + "dzcy;": '\U0000045F', + "dzigrarr;": '\U000027FF', + "eDDot;": '\U00002A77', + "eDot;": '\U00002251', + "eacute;": '\U000000E9', + "easter;": '\U00002A6E', + "ecaron;": '\U0000011B', + "ecir;": '\U00002256', + "ecirc;": '\U000000EA', + "ecolon;": '\U00002255', + "ecy;": '\U0000044D', + "edot;": '\U00000117', + "ee;": '\U00002147', + "efDot;": '\U00002252', + "efr;": '\U0001D522', + "eg;": '\U00002A9A', + "egrave;": '\U000000E8', + "egs;": '\U00002A96', + "egsdot;": '\U00002A98', + "el;": '\U00002A99', + "elinters;": '\U000023E7', + "ell;": '\U00002113', + "els;": '\U00002A95', + "elsdot;": '\U00002A97', + "emacr;": '\U00000113', + "empty;": '\U00002205', + "emptyset;": '\U00002205', + "emptyv;": '\U00002205', + "emsp;": '\U00002003', + "emsp13;": '\U00002004', + "emsp14;": '\U00002005', + "eng;": '\U0000014B', + "ensp;": '\U00002002', + "eogon;": '\U00000119', + "eopf;": '\U0001D556', + "epar;": '\U000022D5', + "eparsl;": '\U000029E3', + "eplus;": '\U00002A71', + "epsi;": '\U000003B5', + "epsilon;": '\U000003B5', + "epsiv;": '\U000003F5', + "eqcirc;": '\U00002256', + "eqcolon;": '\U00002255', + "eqsim;": '\U00002242', + "eqslantgtr;": '\U00002A96', + "eqslantless;": '\U00002A95', + "equals;": '\U0000003D', + "equest;": '\U0000225F', + "equiv;": '\U00002261', + "equivDD;": '\U00002A78', + "eqvparsl;": '\U000029E5', + "erDot;": '\U00002253', + "erarr;": '\U00002971', + "escr;": '\U0000212F', + "esdot;": '\U00002250', + "esim;": '\U00002242', + "eta;": '\U000003B7', + "eth;": '\U000000F0', + "euml;": '\U000000EB', + "euro;": '\U000020AC', + "excl;": '\U00000021', + "exist;": '\U00002203', + "expectation;": '\U00002130', + "exponentiale;": '\U00002147', + "fallingdotseq;": '\U00002252', + "fcy;": '\U00000444', + "female;": '\U00002640', + "ffilig;": '\U0000FB03', + "fflig;": '\U0000FB00', + "ffllig;": '\U0000FB04', + "ffr;": '\U0001D523', + "filig;": '\U0000FB01', + "flat;": '\U0000266D', + "fllig;": '\U0000FB02', + "fltns;": '\U000025B1', + "fnof;": '\U00000192', + "fopf;": '\U0001D557', + "forall;": '\U00002200', + "fork;": '\U000022D4', + "forkv;": '\U00002AD9', + "fpartint;": '\U00002A0D', + "frac12;": '\U000000BD', + "frac13;": '\U00002153', + "frac14;": '\U000000BC', + "frac15;": '\U00002155', + "frac16;": '\U00002159', + "frac18;": '\U0000215B', + "frac23;": '\U00002154', + "frac25;": '\U00002156', + "frac34;": '\U000000BE', + "frac35;": '\U00002157', + "frac38;": '\U0000215C', + "frac45;": '\U00002158', + "frac56;": '\U0000215A', + "frac58;": '\U0000215D', + "frac78;": '\U0000215E', + "frasl;": '\U00002044', + "frown;": '\U00002322', + "fscr;": '\U0001D4BB', + "gE;": '\U00002267', + "gEl;": '\U00002A8C', + "gacute;": '\U000001F5', + "gamma;": '\U000003B3', + "gammad;": '\U000003DD', + "gap;": '\U00002A86', + "gbreve;": '\U0000011F', + "gcirc;": '\U0000011D', + "gcy;": '\U00000433', + "gdot;": '\U00000121', + "ge;": '\U00002265', + "gel;": '\U000022DB', + "geq;": '\U00002265', + "geqq;": '\U00002267', + "geqslant;": '\U00002A7E', + "ges;": '\U00002A7E', + "gescc;": '\U00002AA9', + "gesdot;": '\U00002A80', + "gesdoto;": '\U00002A82', + "gesdotol;": '\U00002A84', + "gesles;": '\U00002A94', + "gfr;": '\U0001D524', + "gg;": '\U0000226B', + "ggg;": '\U000022D9', + "gimel;": '\U00002137', + "gjcy;": '\U00000453', + "gl;": '\U00002277', + "glE;": '\U00002A92', + "gla;": '\U00002AA5', + "glj;": '\U00002AA4', + "gnE;": '\U00002269', + "gnap;": '\U00002A8A', + "gnapprox;": '\U00002A8A', + "gne;": '\U00002A88', + "gneq;": '\U00002A88', + "gneqq;": '\U00002269', + "gnsim;": '\U000022E7', + "gopf;": '\U0001D558', + "grave;": '\U00000060', + "gscr;": '\U0000210A', + "gsim;": '\U00002273', + "gsime;": '\U00002A8E', + "gsiml;": '\U00002A90', + "gt;": '\U0000003E', + "gtcc;": '\U00002AA7', + "gtcir;": '\U00002A7A', + "gtdot;": '\U000022D7', + "gtlPar;": '\U00002995', + "gtquest;": '\U00002A7C', + "gtrapprox;": '\U00002A86', + "gtrarr;": '\U00002978', + "gtrdot;": '\U000022D7', + "gtreqless;": '\U000022DB', + "gtreqqless;": '\U00002A8C', + "gtrless;": '\U00002277', + "gtrsim;": '\U00002273', + "hArr;": '\U000021D4', + "hairsp;": '\U0000200A', + "half;": '\U000000BD', + "hamilt;": '\U0000210B', + "hardcy;": '\U0000044A', + "harr;": '\U00002194', + "harrcir;": '\U00002948', + "harrw;": '\U000021AD', + "hbar;": '\U0000210F', + "hcirc;": '\U00000125', + "hearts;": '\U00002665', + "heartsuit;": '\U00002665', + "hellip;": '\U00002026', + "hercon;": '\U000022B9', + "hfr;": '\U0001D525', + "hksearow;": '\U00002925', + "hkswarow;": '\U00002926', + "hoarr;": '\U000021FF', + "homtht;": '\U0000223B', + "hookleftarrow;": '\U000021A9', + "hookrightarrow;": '\U000021AA', + "hopf;": '\U0001D559', + "horbar;": '\U00002015', + "hscr;": '\U0001D4BD', + "hslash;": '\U0000210F', + "hstrok;": '\U00000127', + "hybull;": '\U00002043', + "hyphen;": '\U00002010', + "iacute;": '\U000000ED', + "ic;": '\U00002063', + "icirc;": '\U000000EE', + "icy;": '\U00000438', + "iecy;": '\U00000435', + "iexcl;": '\U000000A1', + "iff;": '\U000021D4', + "ifr;": '\U0001D526', + "igrave;": '\U000000EC', + "ii;": '\U00002148', + "iiiint;": '\U00002A0C', + "iiint;": '\U0000222D', + "iinfin;": '\U000029DC', + "iiota;": '\U00002129', + "ijlig;": '\U00000133', + "imacr;": '\U0000012B', + "image;": '\U00002111', + "imagline;": '\U00002110', + "imagpart;": '\U00002111', + "imath;": '\U00000131', + "imof;": '\U000022B7', + "imped;": '\U000001B5', + "in;": '\U00002208', + "incare;": '\U00002105', + "infin;": '\U0000221E', + "infintie;": '\U000029DD', + "inodot;": '\U00000131', + "int;": '\U0000222B', + "intcal;": '\U000022BA', + "integers;": '\U00002124', + "intercal;": '\U000022BA', + "intlarhk;": '\U00002A17', + "intprod;": '\U00002A3C', + "iocy;": '\U00000451', + "iogon;": '\U0000012F', + "iopf;": '\U0001D55A', + "iota;": '\U000003B9', + "iprod;": '\U00002A3C', + "iquest;": '\U000000BF', + "iscr;": '\U0001D4BE', + "isin;": '\U00002208', + "isinE;": '\U000022F9', + "isindot;": '\U000022F5', + "isins;": '\U000022F4', + "isinsv;": '\U000022F3', + "isinv;": '\U00002208', + "it;": '\U00002062', + "itilde;": '\U00000129', + "iukcy;": '\U00000456', + "iuml;": '\U000000EF', + "jcirc;": '\U00000135', + "jcy;": '\U00000439', + "jfr;": '\U0001D527', + "jmath;": '\U00000237', + "jopf;": '\U0001D55B', + "jscr;": '\U0001D4BF', + "jsercy;": '\U00000458', + "jukcy;": '\U00000454', + "kappa;": '\U000003BA', + "kappav;": '\U000003F0', + "kcedil;": '\U00000137', + "kcy;": '\U0000043A', + "kfr;": '\U0001D528', + "kgreen;": '\U00000138', + "khcy;": '\U00000445', + "kjcy;": '\U0000045C', + "kopf;": '\U0001D55C', + "kscr;": '\U0001D4C0', + "lAarr;": '\U000021DA', + "lArr;": '\U000021D0', + "lAtail;": '\U0000291B', + "lBarr;": '\U0000290E', + "lE;": '\U00002266', + "lEg;": '\U00002A8B', + "lHar;": '\U00002962', + "lacute;": '\U0000013A', + "laemptyv;": '\U000029B4', + "lagran;": '\U00002112', + "lambda;": '\U000003BB', + "lang;": '\U000027E8', + "langd;": '\U00002991', + "langle;": '\U000027E8', + "lap;": '\U00002A85', + "laquo;": '\U000000AB', + "larr;": '\U00002190', + "larrb;": '\U000021E4', + "larrbfs;": '\U0000291F', + "larrfs;": '\U0000291D', + "larrhk;": '\U000021A9', + "larrlp;": '\U000021AB', + "larrpl;": '\U00002939', + "larrsim;": '\U00002973', + "larrtl;": '\U000021A2', + "lat;": '\U00002AAB', + "latail;": '\U00002919', + "late;": '\U00002AAD', + "lbarr;": '\U0000290C', + "lbbrk;": '\U00002772', + "lbrace;": '\U0000007B', + "lbrack;": '\U0000005B', + "lbrke;": '\U0000298B', + "lbrksld;": '\U0000298F', + "lbrkslu;": '\U0000298D', + "lcaron;": '\U0000013E', + "lcedil;": '\U0000013C', + "lceil;": '\U00002308', + "lcub;": '\U0000007B', + "lcy;": '\U0000043B', + "ldca;": '\U00002936', + "ldquo;": '\U0000201C', + "ldquor;": '\U0000201E', + "ldrdhar;": '\U00002967', + "ldrushar;": '\U0000294B', + "ldsh;": '\U000021B2', + "le;": '\U00002264', + "leftarrow;": '\U00002190', + "leftarrowtail;": '\U000021A2', + "leftharpoondown;": '\U000021BD', + "leftharpoonup;": '\U000021BC', + "leftleftarrows;": '\U000021C7', + "leftrightarrow;": '\U00002194', + "leftrightarrows;": '\U000021C6', + "leftrightharpoons;": '\U000021CB', + "leftrightsquigarrow;": '\U000021AD', + "leftthreetimes;": '\U000022CB', + "leg;": '\U000022DA', + "leq;": '\U00002264', + "leqq;": '\U00002266', + "leqslant;": '\U00002A7D', + "les;": '\U00002A7D', + "lescc;": '\U00002AA8', + "lesdot;": '\U00002A7F', + "lesdoto;": '\U00002A81', + "lesdotor;": '\U00002A83', + "lesges;": '\U00002A93', + "lessapprox;": '\U00002A85', + "lessdot;": '\U000022D6', + "lesseqgtr;": '\U000022DA', + "lesseqqgtr;": '\U00002A8B', + "lessgtr;": '\U00002276', + "lesssim;": '\U00002272', + "lfisht;": '\U0000297C', + "lfloor;": '\U0000230A', + "lfr;": '\U0001D529', + "lg;": '\U00002276', + "lgE;": '\U00002A91', + "lhard;": '\U000021BD', + "lharu;": '\U000021BC', + "lharul;": '\U0000296A', + "lhblk;": '\U00002584', + "ljcy;": '\U00000459', + "ll;": '\U0000226A', + "llarr;": '\U000021C7', + "llcorner;": '\U0000231E', + "llhard;": '\U0000296B', + "lltri;": '\U000025FA', + "lmidot;": '\U00000140', + "lmoust;": '\U000023B0', + "lmoustache;": '\U000023B0', + "lnE;": '\U00002268', + "lnap;": '\U00002A89', + "lnapprox;": '\U00002A89', + "lne;": '\U00002A87', + "lneq;": '\U00002A87', + "lneqq;": '\U00002268', + "lnsim;": '\U000022E6', + "loang;": '\U000027EC', + "loarr;": '\U000021FD', + "lobrk;": '\U000027E6', + "longleftarrow;": '\U000027F5', + "longleftrightarrow;": '\U000027F7', + "longmapsto;": '\U000027FC', + "longrightarrow;": '\U000027F6', + "looparrowleft;": '\U000021AB', + "looparrowright;": '\U000021AC', + "lopar;": '\U00002985', + "lopf;": '\U0001D55D', + "loplus;": '\U00002A2D', + "lotimes;": '\U00002A34', + "lowast;": '\U00002217', + "lowbar;": '\U0000005F', + "loz;": '\U000025CA', + "lozenge;": '\U000025CA', + "lozf;": '\U000029EB', + "lpar;": '\U00000028', + "lparlt;": '\U00002993', + "lrarr;": '\U000021C6', + "lrcorner;": '\U0000231F', + "lrhar;": '\U000021CB', + "lrhard;": '\U0000296D', + "lrm;": '\U0000200E', + "lrtri;": '\U000022BF', + "lsaquo;": '\U00002039', + "lscr;": '\U0001D4C1', + "lsh;": '\U000021B0', + "lsim;": '\U00002272', + "lsime;": '\U00002A8D', + "lsimg;": '\U00002A8F', + "lsqb;": '\U0000005B', + "lsquo;": '\U00002018', + "lsquor;": '\U0000201A', + "lstrok;": '\U00000142', + "lt;": '\U0000003C', + "ltcc;": '\U00002AA6', + "ltcir;": '\U00002A79', + "ltdot;": '\U000022D6', + "lthree;": '\U000022CB', + "ltimes;": '\U000022C9', + "ltlarr;": '\U00002976', + "ltquest;": '\U00002A7B', + "ltrPar;": '\U00002996', + "ltri;": '\U000025C3', + "ltrie;": '\U000022B4', + "ltrif;": '\U000025C2', + "lurdshar;": '\U0000294A', + "luruhar;": '\U00002966', + "mDDot;": '\U0000223A', + "macr;": '\U000000AF', + "male;": '\U00002642', + "malt;": '\U00002720', + "maltese;": '\U00002720', + "map;": '\U000021A6', + "mapsto;": '\U000021A6', + "mapstodown;": '\U000021A7', + "mapstoleft;": '\U000021A4', + "mapstoup;": '\U000021A5', + "marker;": '\U000025AE', + "mcomma;": '\U00002A29', + "mcy;": '\U0000043C', + "mdash;": '\U00002014', + "measuredangle;": '\U00002221', + "mfr;": '\U0001D52A', + "mho;": '\U00002127', + "micro;": '\U000000B5', + "mid;": '\U00002223', + "midast;": '\U0000002A', + "midcir;": '\U00002AF0', + "middot;": '\U000000B7', + "minus;": '\U00002212', + "minusb;": '\U0000229F', + "minusd;": '\U00002238', + "minusdu;": '\U00002A2A', + "mlcp;": '\U00002ADB', + "mldr;": '\U00002026', + "mnplus;": '\U00002213', + "models;": '\U000022A7', + "mopf;": '\U0001D55E', + "mp;": '\U00002213', + "mscr;": '\U0001D4C2', + "mstpos;": '\U0000223E', + "mu;": '\U000003BC', + "multimap;": '\U000022B8', + "mumap;": '\U000022B8', + "nLeftarrow;": '\U000021CD', + "nLeftrightarrow;": '\U000021CE', + "nRightarrow;": '\U000021CF', + "nVDash;": '\U000022AF', + "nVdash;": '\U000022AE', + "nabla;": '\U00002207', + "nacute;": '\U00000144', + "nap;": '\U00002249', + "napos;": '\U00000149', + "napprox;": '\U00002249', + "natur;": '\U0000266E', + "natural;": '\U0000266E', + "naturals;": '\U00002115', + "nbsp;": '\U000000A0', + "ncap;": '\U00002A43', + "ncaron;": '\U00000148', + "ncedil;": '\U00000146', + "ncong;": '\U00002247', + "ncup;": '\U00002A42', + "ncy;": '\U0000043D', + "ndash;": '\U00002013', + "ne;": '\U00002260', + "neArr;": '\U000021D7', + "nearhk;": '\U00002924', + "nearr;": '\U00002197', + "nearrow;": '\U00002197', + "nequiv;": '\U00002262', + "nesear;": '\U00002928', + "nexist;": '\U00002204', + "nexists;": '\U00002204', + "nfr;": '\U0001D52B', + "nge;": '\U00002271', + "ngeq;": '\U00002271', + "ngsim;": '\U00002275', + "ngt;": '\U0000226F', + "ngtr;": '\U0000226F', + "nhArr;": '\U000021CE', + "nharr;": '\U000021AE', + "nhpar;": '\U00002AF2', + "ni;": '\U0000220B', + "nis;": '\U000022FC', + "nisd;": '\U000022FA', + "niv;": '\U0000220B', + "njcy;": '\U0000045A', + "nlArr;": '\U000021CD', + "nlarr;": '\U0000219A', + "nldr;": '\U00002025', + "nle;": '\U00002270', + "nleftarrow;": '\U0000219A', + "nleftrightarrow;": '\U000021AE', + "nleq;": '\U00002270', + "nless;": '\U0000226E', + "nlsim;": '\U00002274', + "nlt;": '\U0000226E', + "nltri;": '\U000022EA', + "nltrie;": '\U000022EC', + "nmid;": '\U00002224', + "nopf;": '\U0001D55F', + "not;": '\U000000AC', + "notin;": '\U00002209', + "notinva;": '\U00002209', + "notinvb;": '\U000022F7', + "notinvc;": '\U000022F6', + "notni;": '\U0000220C', + "notniva;": '\U0000220C', + "notnivb;": '\U000022FE', + "notnivc;": '\U000022FD', + "npar;": '\U00002226', + "nparallel;": '\U00002226', + "npolint;": '\U00002A14', + "npr;": '\U00002280', + "nprcue;": '\U000022E0', + "nprec;": '\U00002280', + "nrArr;": '\U000021CF', + "nrarr;": '\U0000219B', + "nrightarrow;": '\U0000219B', + "nrtri;": '\U000022EB', + "nrtrie;": '\U000022ED', + "nsc;": '\U00002281', + "nsccue;": '\U000022E1', + "nscr;": '\U0001D4C3', + "nshortmid;": '\U00002224', + "nshortparallel;": '\U00002226', + "nsim;": '\U00002241', + "nsime;": '\U00002244', + "nsimeq;": '\U00002244', + "nsmid;": '\U00002224', + "nspar;": '\U00002226', + "nsqsube;": '\U000022E2', + "nsqsupe;": '\U000022E3', + "nsub;": '\U00002284', + "nsube;": '\U00002288', + "nsubseteq;": '\U00002288', + "nsucc;": '\U00002281', + "nsup;": '\U00002285', + "nsupe;": '\U00002289', + "nsupseteq;": '\U00002289', + "ntgl;": '\U00002279', + "ntilde;": '\U000000F1', + "ntlg;": '\U00002278', + "ntriangleleft;": '\U000022EA', + "ntrianglelefteq;": '\U000022EC', + "ntriangleright;": '\U000022EB', + "ntrianglerighteq;": '\U000022ED', + "nu;": '\U000003BD', + "num;": '\U00000023', + "numero;": '\U00002116', + "numsp;": '\U00002007', + "nvDash;": '\U000022AD', + "nvHarr;": '\U00002904', + "nvdash;": '\U000022AC', + "nvinfin;": '\U000029DE', + "nvlArr;": '\U00002902', + "nvrArr;": '\U00002903', + "nwArr;": '\U000021D6', + "nwarhk;": '\U00002923', + "nwarr;": '\U00002196', + "nwarrow;": '\U00002196', + "nwnear;": '\U00002927', + "oS;": '\U000024C8', + "oacute;": '\U000000F3', + "oast;": '\U0000229B', + "ocir;": '\U0000229A', + "ocirc;": '\U000000F4', + "ocy;": '\U0000043E', + "odash;": '\U0000229D', + "odblac;": '\U00000151', + "odiv;": '\U00002A38', + "odot;": '\U00002299', + "odsold;": '\U000029BC', + "oelig;": '\U00000153', + "ofcir;": '\U000029BF', + "ofr;": '\U0001D52C', + "ogon;": '\U000002DB', + "ograve;": '\U000000F2', + "ogt;": '\U000029C1', + "ohbar;": '\U000029B5', + "ohm;": '\U000003A9', + "oint;": '\U0000222E', + "olarr;": '\U000021BA', + "olcir;": '\U000029BE', + "olcross;": '\U000029BB', + "oline;": '\U0000203E', + "olt;": '\U000029C0', + "omacr;": '\U0000014D', + "omega;": '\U000003C9', + "omicron;": '\U000003BF', + "omid;": '\U000029B6', + "ominus;": '\U00002296', + "oopf;": '\U0001D560', + "opar;": '\U000029B7', + "operp;": '\U000029B9', + "oplus;": '\U00002295', + "or;": '\U00002228', + "orarr;": '\U000021BB', + "ord;": '\U00002A5D', + "order;": '\U00002134', + "orderof;": '\U00002134', + "ordf;": '\U000000AA', + "ordm;": '\U000000BA', + "origof;": '\U000022B6', + "oror;": '\U00002A56', + "orslope;": '\U00002A57', + "orv;": '\U00002A5B', + "oscr;": '\U00002134', + "oslash;": '\U000000F8', + "osol;": '\U00002298', + "otilde;": '\U000000F5', + "otimes;": '\U00002297', + "otimesas;": '\U00002A36', + "ouml;": '\U000000F6', + "ovbar;": '\U0000233D', + "par;": '\U00002225', + "para;": '\U000000B6', + "parallel;": '\U00002225', + "parsim;": '\U00002AF3', + "parsl;": '\U00002AFD', + "part;": '\U00002202', + "pcy;": '\U0000043F', + "percnt;": '\U00000025', + "period;": '\U0000002E', + "permil;": '\U00002030', + "perp;": '\U000022A5', + "pertenk;": '\U00002031', + "pfr;": '\U0001D52D', + "phi;": '\U000003C6', + "phiv;": '\U000003D5', + "phmmat;": '\U00002133', + "phone;": '\U0000260E', + "pi;": '\U000003C0', + "pitchfork;": '\U000022D4', + "piv;": '\U000003D6', + "planck;": '\U0000210F', + "planckh;": '\U0000210E', + "plankv;": '\U0000210F', + "plus;": '\U0000002B', + "plusacir;": '\U00002A23', + "plusb;": '\U0000229E', + "pluscir;": '\U00002A22', + "plusdo;": '\U00002214', + "plusdu;": '\U00002A25', + "pluse;": '\U00002A72', + "plusmn;": '\U000000B1', + "plussim;": '\U00002A26', + "plustwo;": '\U00002A27', + "pm;": '\U000000B1', + "pointint;": '\U00002A15', + "popf;": '\U0001D561', + "pound;": '\U000000A3', + "pr;": '\U0000227A', + "prE;": '\U00002AB3', + "prap;": '\U00002AB7', + "prcue;": '\U0000227C', + "pre;": '\U00002AAF', + "prec;": '\U0000227A', + "precapprox;": '\U00002AB7', + "preccurlyeq;": '\U0000227C', + "preceq;": '\U00002AAF', + "precnapprox;": '\U00002AB9', + "precneqq;": '\U00002AB5', + "precnsim;": '\U000022E8', + "precsim;": '\U0000227E', + "prime;": '\U00002032', + "primes;": '\U00002119', + "prnE;": '\U00002AB5', + "prnap;": '\U00002AB9', + "prnsim;": '\U000022E8', + "prod;": '\U0000220F', + "profalar;": '\U0000232E', + "profline;": '\U00002312', + "profsurf;": '\U00002313', + "prop;": '\U0000221D', + "propto;": '\U0000221D', + "prsim;": '\U0000227E', + "prurel;": '\U000022B0', + "pscr;": '\U0001D4C5', + "psi;": '\U000003C8', + "puncsp;": '\U00002008', + "qfr;": '\U0001D52E', + "qint;": '\U00002A0C', + "qopf;": '\U0001D562', + "qprime;": '\U00002057', + "qscr;": '\U0001D4C6', + "quaternions;": '\U0000210D', + "quatint;": '\U00002A16', + "quest;": '\U0000003F', + "questeq;": '\U0000225F', + "quot;": '\U00000022', + "rAarr;": '\U000021DB', + "rArr;": '\U000021D2', + "rAtail;": '\U0000291C', + "rBarr;": '\U0000290F', + "rHar;": '\U00002964', + "racute;": '\U00000155', + "radic;": '\U0000221A', + "raemptyv;": '\U000029B3', + "rang;": '\U000027E9', + "rangd;": '\U00002992', + "range;": '\U000029A5', + "rangle;": '\U000027E9', + "raquo;": '\U000000BB', + "rarr;": '\U00002192', + "rarrap;": '\U00002975', + "rarrb;": '\U000021E5', + "rarrbfs;": '\U00002920', + "rarrc;": '\U00002933', + "rarrfs;": '\U0000291E', + "rarrhk;": '\U000021AA', + "rarrlp;": '\U000021AC', + "rarrpl;": '\U00002945', + "rarrsim;": '\U00002974', + "rarrtl;": '\U000021A3', + "rarrw;": '\U0000219D', + "ratail;": '\U0000291A', + "ratio;": '\U00002236', + "rationals;": '\U0000211A', + "rbarr;": '\U0000290D', + "rbbrk;": '\U00002773', + "rbrace;": '\U0000007D', + "rbrack;": '\U0000005D', + "rbrke;": '\U0000298C', + "rbrksld;": '\U0000298E', + "rbrkslu;": '\U00002990', + "rcaron;": '\U00000159', + "rcedil;": '\U00000157', + "rceil;": '\U00002309', + "rcub;": '\U0000007D', + "rcy;": '\U00000440', + "rdca;": '\U00002937', + "rdldhar;": '\U00002969', + "rdquo;": '\U0000201D', + "rdquor;": '\U0000201D', + "rdsh;": '\U000021B3', + "real;": '\U0000211C', + "realine;": '\U0000211B', + "realpart;": '\U0000211C', + "reals;": '\U0000211D', + "rect;": '\U000025AD', + "reg;": '\U000000AE', + "rfisht;": '\U0000297D', + "rfloor;": '\U0000230B', + "rfr;": '\U0001D52F', + "rhard;": '\U000021C1', + "rharu;": '\U000021C0', + "rharul;": '\U0000296C', + "rho;": '\U000003C1', + "rhov;": '\U000003F1', + "rightarrow;": '\U00002192', + "rightarrowtail;": '\U000021A3', + "rightharpoondown;": '\U000021C1', + "rightharpoonup;": '\U000021C0', + "rightleftarrows;": '\U000021C4', + "rightleftharpoons;": '\U000021CC', + "rightrightarrows;": '\U000021C9', + "rightsquigarrow;": '\U0000219D', + "rightthreetimes;": '\U000022CC', + "ring;": '\U000002DA', + "risingdotseq;": '\U00002253', + "rlarr;": '\U000021C4', + "rlhar;": '\U000021CC', + "rlm;": '\U0000200F', + "rmoust;": '\U000023B1', + "rmoustache;": '\U000023B1', + "rnmid;": '\U00002AEE', + "roang;": '\U000027ED', + "roarr;": '\U000021FE', + "robrk;": '\U000027E7', + "ropar;": '\U00002986', + "ropf;": '\U0001D563', + "roplus;": '\U00002A2E', + "rotimes;": '\U00002A35', + "rpar;": '\U00000029', + "rpargt;": '\U00002994', + "rppolint;": '\U00002A12', + "rrarr;": '\U000021C9', + "rsaquo;": '\U0000203A', + "rscr;": '\U0001D4C7', + "rsh;": '\U000021B1', + "rsqb;": '\U0000005D', + "rsquo;": '\U00002019', + "rsquor;": '\U00002019', + "rthree;": '\U000022CC', + "rtimes;": '\U000022CA', + "rtri;": '\U000025B9', + "rtrie;": '\U000022B5', + "rtrif;": '\U000025B8', + "rtriltri;": '\U000029CE', + "ruluhar;": '\U00002968', + "rx;": '\U0000211E', + "sacute;": '\U0000015B', + "sbquo;": '\U0000201A', + "sc;": '\U0000227B', + "scE;": '\U00002AB4', + "scap;": '\U00002AB8', + "scaron;": '\U00000161', + "sccue;": '\U0000227D', + "sce;": '\U00002AB0', + "scedil;": '\U0000015F', + "scirc;": '\U0000015D', + "scnE;": '\U00002AB6', + "scnap;": '\U00002ABA', + "scnsim;": '\U000022E9', + "scpolint;": '\U00002A13', + "scsim;": '\U0000227F', + "scy;": '\U00000441', + "sdot;": '\U000022C5', + "sdotb;": '\U000022A1', + "sdote;": '\U00002A66', + "seArr;": '\U000021D8', + "searhk;": '\U00002925', + "searr;": '\U00002198', + "searrow;": '\U00002198', + "sect;": '\U000000A7', + "semi;": '\U0000003B', + "seswar;": '\U00002929', + "setminus;": '\U00002216', + "setmn;": '\U00002216', + "sext;": '\U00002736', + "sfr;": '\U0001D530', + "sfrown;": '\U00002322', + "sharp;": '\U0000266F', + "shchcy;": '\U00000449', + "shcy;": '\U00000448', + "shortmid;": '\U00002223', + "shortparallel;": '\U00002225', + "shy;": '\U000000AD', + "sigma;": '\U000003C3', + "sigmaf;": '\U000003C2', + "sigmav;": '\U000003C2', + "sim;": '\U0000223C', + "simdot;": '\U00002A6A', + "sime;": '\U00002243', + "simeq;": '\U00002243', + "simg;": '\U00002A9E', + "simgE;": '\U00002AA0', + "siml;": '\U00002A9D', + "simlE;": '\U00002A9F', + "simne;": '\U00002246', + "simplus;": '\U00002A24', + "simrarr;": '\U00002972', + "slarr;": '\U00002190', + "smallsetminus;": '\U00002216', + "smashp;": '\U00002A33', + "smeparsl;": '\U000029E4', + "smid;": '\U00002223', + "smile;": '\U00002323', + "smt;": '\U00002AAA', + "smte;": '\U00002AAC', + "softcy;": '\U0000044C', + "sol;": '\U0000002F', + "solb;": '\U000029C4', + "solbar;": '\U0000233F', + "sopf;": '\U0001D564', + "spades;": '\U00002660', + "spadesuit;": '\U00002660', + "spar;": '\U00002225', + "sqcap;": '\U00002293', + "sqcup;": '\U00002294', + "sqsub;": '\U0000228F', + "sqsube;": '\U00002291', + "sqsubset;": '\U0000228F', + "sqsubseteq;": '\U00002291', + "sqsup;": '\U00002290', + "sqsupe;": '\U00002292', + "sqsupset;": '\U00002290', + "sqsupseteq;": '\U00002292', + "squ;": '\U000025A1', + "square;": '\U000025A1', + "squarf;": '\U000025AA', + "squf;": '\U000025AA', + "srarr;": '\U00002192', + "sscr;": '\U0001D4C8', + "ssetmn;": '\U00002216', + "ssmile;": '\U00002323', + "sstarf;": '\U000022C6', + "star;": '\U00002606', + "starf;": '\U00002605', + "straightepsilon;": '\U000003F5', + "straightphi;": '\U000003D5', + "strns;": '\U000000AF', + "sub;": '\U00002282', + "subE;": '\U00002AC5', + "subdot;": '\U00002ABD', + "sube;": '\U00002286', + "subedot;": '\U00002AC3', + "submult;": '\U00002AC1', + "subnE;": '\U00002ACB', + "subne;": '\U0000228A', + "subplus;": '\U00002ABF', + "subrarr;": '\U00002979', + "subset;": '\U00002282', + "subseteq;": '\U00002286', + "subseteqq;": '\U00002AC5', + "subsetneq;": '\U0000228A', + "subsetneqq;": '\U00002ACB', + "subsim;": '\U00002AC7', + "subsub;": '\U00002AD5', + "subsup;": '\U00002AD3', + "succ;": '\U0000227B', + "succapprox;": '\U00002AB8', + "succcurlyeq;": '\U0000227D', + "succeq;": '\U00002AB0', + "succnapprox;": '\U00002ABA', + "succneqq;": '\U00002AB6', + "succnsim;": '\U000022E9', + "succsim;": '\U0000227F', + "sum;": '\U00002211', + "sung;": '\U0000266A', + "sup;": '\U00002283', + "sup1;": '\U000000B9', + "sup2;": '\U000000B2', + "sup3;": '\U000000B3', + "supE;": '\U00002AC6', + "supdot;": '\U00002ABE', + "supdsub;": '\U00002AD8', + "supe;": '\U00002287', + "supedot;": '\U00002AC4', + "suphsol;": '\U000027C9', + "suphsub;": '\U00002AD7', + "suplarr;": '\U0000297B', + "supmult;": '\U00002AC2', + "supnE;": '\U00002ACC', + "supne;": '\U0000228B', + "supplus;": '\U00002AC0', + "supset;": '\U00002283', + "supseteq;": '\U00002287', + "supseteqq;": '\U00002AC6', + "supsetneq;": '\U0000228B', + "supsetneqq;": '\U00002ACC', + "supsim;": '\U00002AC8', + "supsub;": '\U00002AD4', + "supsup;": '\U00002AD6', + "swArr;": '\U000021D9', + "swarhk;": '\U00002926', + "swarr;": '\U00002199', + "swarrow;": '\U00002199', + "swnwar;": '\U0000292A', + "szlig;": '\U000000DF', + "target;": '\U00002316', + "tau;": '\U000003C4', + "tbrk;": '\U000023B4', + "tcaron;": '\U00000165', + "tcedil;": '\U00000163', + "tcy;": '\U00000442', + "tdot;": '\U000020DB', + "telrec;": '\U00002315', + "tfr;": '\U0001D531', + "there4;": '\U00002234', + "therefore;": '\U00002234', + "theta;": '\U000003B8', + "thetasym;": '\U000003D1', + "thetav;": '\U000003D1', + "thickapprox;": '\U00002248', + "thicksim;": '\U0000223C', + "thinsp;": '\U00002009', + "thkap;": '\U00002248', + "thksim;": '\U0000223C', + "thorn;": '\U000000FE', + "tilde;": '\U000002DC', + "times;": '\U000000D7', + "timesb;": '\U000022A0', + "timesbar;": '\U00002A31', + "timesd;": '\U00002A30', + "tint;": '\U0000222D', + "toea;": '\U00002928', + "top;": '\U000022A4', + "topbot;": '\U00002336', + "topcir;": '\U00002AF1', + "topf;": '\U0001D565', + "topfork;": '\U00002ADA', + "tosa;": '\U00002929', + "tprime;": '\U00002034', + "trade;": '\U00002122', + "triangle;": '\U000025B5', + "triangledown;": '\U000025BF', + "triangleleft;": '\U000025C3', + "trianglelefteq;": '\U000022B4', + "triangleq;": '\U0000225C', + "triangleright;": '\U000025B9', + "trianglerighteq;": '\U000022B5', + "tridot;": '\U000025EC', + "trie;": '\U0000225C', + "triminus;": '\U00002A3A', + "triplus;": '\U00002A39', + "trisb;": '\U000029CD', + "tritime;": '\U00002A3B', + "trpezium;": '\U000023E2', + "tscr;": '\U0001D4C9', + "tscy;": '\U00000446', + "tshcy;": '\U0000045B', + "tstrok;": '\U00000167', + "twixt;": '\U0000226C', + "twoheadleftarrow;": '\U0000219E', + "twoheadrightarrow;": '\U000021A0', + "uArr;": '\U000021D1', + "uHar;": '\U00002963', + "uacute;": '\U000000FA', + "uarr;": '\U00002191', + "ubrcy;": '\U0000045E', + "ubreve;": '\U0000016D', + "ucirc;": '\U000000FB', + "ucy;": '\U00000443', + "udarr;": '\U000021C5', + "udblac;": '\U00000171', + "udhar;": '\U0000296E', + "ufisht;": '\U0000297E', + "ufr;": '\U0001D532', + "ugrave;": '\U000000F9', + "uharl;": '\U000021BF', + "uharr;": '\U000021BE', + "uhblk;": '\U00002580', + "ulcorn;": '\U0000231C', + "ulcorner;": '\U0000231C', + "ulcrop;": '\U0000230F', + "ultri;": '\U000025F8', + "umacr;": '\U0000016B', + "uml;": '\U000000A8', + "uogon;": '\U00000173', + "uopf;": '\U0001D566', + "uparrow;": '\U00002191', + "updownarrow;": '\U00002195', + "upharpoonleft;": '\U000021BF', + "upharpoonright;": '\U000021BE', + "uplus;": '\U0000228E', + "upsi;": '\U000003C5', + "upsih;": '\U000003D2', + "upsilon;": '\U000003C5', + "upuparrows;": '\U000021C8', + "urcorn;": '\U0000231D', + "urcorner;": '\U0000231D', + "urcrop;": '\U0000230E', + "uring;": '\U0000016F', + "urtri;": '\U000025F9', + "uscr;": '\U0001D4CA', + "utdot;": '\U000022F0', + "utilde;": '\U00000169', + "utri;": '\U000025B5', + "utrif;": '\U000025B4', + "uuarr;": '\U000021C8', + "uuml;": '\U000000FC', + "uwangle;": '\U000029A7', + "vArr;": '\U000021D5', + "vBar;": '\U00002AE8', + "vBarv;": '\U00002AE9', + "vDash;": '\U000022A8', + "vangrt;": '\U0000299C', + "varepsilon;": '\U000003F5', + "varkappa;": '\U000003F0', + "varnothing;": '\U00002205', + "varphi;": '\U000003D5', + "varpi;": '\U000003D6', + "varpropto;": '\U0000221D', + "varr;": '\U00002195', + "varrho;": '\U000003F1', + "varsigma;": '\U000003C2', + "vartheta;": '\U000003D1', + "vartriangleleft;": '\U000022B2', + "vartriangleright;": '\U000022B3', + "vcy;": '\U00000432', + "vdash;": '\U000022A2', + "vee;": '\U00002228', + "veebar;": '\U000022BB', + "veeeq;": '\U0000225A', + "vellip;": '\U000022EE', + "verbar;": '\U0000007C', + "vert;": '\U0000007C', + "vfr;": '\U0001D533', + "vltri;": '\U000022B2', + "vopf;": '\U0001D567', + "vprop;": '\U0000221D', + "vrtri;": '\U000022B3', + "vscr;": '\U0001D4CB', + "vzigzag;": '\U0000299A', + "wcirc;": '\U00000175', + "wedbar;": '\U00002A5F', + "wedge;": '\U00002227', + "wedgeq;": '\U00002259', + "weierp;": '\U00002118', + "wfr;": '\U0001D534', + "wopf;": '\U0001D568', + "wp;": '\U00002118', + "wr;": '\U00002240', + "wreath;": '\U00002240', + "wscr;": '\U0001D4CC', + "xcap;": '\U000022C2', + "xcirc;": '\U000025EF', + "xcup;": '\U000022C3', + "xdtri;": '\U000025BD', + "xfr;": '\U0001D535', + "xhArr;": '\U000027FA', + "xharr;": '\U000027F7', + "xi;": '\U000003BE', + "xlArr;": '\U000027F8', + "xlarr;": '\U000027F5', + "xmap;": '\U000027FC', + "xnis;": '\U000022FB', + "xodot;": '\U00002A00', + "xopf;": '\U0001D569', + "xoplus;": '\U00002A01', + "xotime;": '\U00002A02', + "xrArr;": '\U000027F9', + "xrarr;": '\U000027F6', + "xscr;": '\U0001D4CD', + "xsqcup;": '\U00002A06', + "xuplus;": '\U00002A04', + "xutri;": '\U000025B3', + "xvee;": '\U000022C1', + "xwedge;": '\U000022C0', + "yacute;": '\U000000FD', + "yacy;": '\U0000044F', + "ycirc;": '\U00000177', + "ycy;": '\U0000044B', + "yen;": '\U000000A5', + "yfr;": '\U0001D536', + "yicy;": '\U00000457', + "yopf;": '\U0001D56A', + "yscr;": '\U0001D4CE', + "yucy;": '\U0000044E', + "yuml;": '\U000000FF', + "zacute;": '\U0000017A', + "zcaron;": '\U0000017E', + "zcy;": '\U00000437', + "zdot;": '\U0000017C', + "zeetrf;": '\U00002128', + "zeta;": '\U000003B6', + "zfr;": '\U0001D537', + "zhcy;": '\U00000436', + "zigrarr;": '\U000021DD', + "zopf;": '\U0001D56B', + "zscr;": '\U0001D4CF', + "zwj;": '\U0000200D', + "zwnj;": '\U0000200C', + "AElig": '\U000000C6', + "AMP": '\U00000026', + "Aacute": '\U000000C1', + "Acirc": '\U000000C2', + "Agrave": '\U000000C0', + "Aring": '\U000000C5', + "Atilde": '\U000000C3', + "Auml": '\U000000C4', + "COPY": '\U000000A9', + "Ccedil": '\U000000C7', + "ETH": '\U000000D0', + "Eacute": '\U000000C9', + "Ecirc": '\U000000CA', + "Egrave": '\U000000C8', + "Euml": '\U000000CB', + "GT": '\U0000003E', + "Iacute": '\U000000CD', + "Icirc": '\U000000CE', + "Igrave": '\U000000CC', + "Iuml": '\U000000CF', + "LT": '\U0000003C', + "Ntilde": '\U000000D1', + "Oacute": '\U000000D3', + "Ocirc": '\U000000D4', + "Ograve": '\U000000D2', + "Oslash": '\U000000D8', + "Otilde": '\U000000D5', + "Ouml": '\U000000D6', + "QUOT": '\U00000022', + "REG": '\U000000AE', + "THORN": '\U000000DE', + "Uacute": '\U000000DA', + "Ucirc": '\U000000DB', + "Ugrave": '\U000000D9', + "Uuml": '\U000000DC', + "Yacute": '\U000000DD', + "aacute": '\U000000E1', + "acirc": '\U000000E2', + "acute": '\U000000B4', + "aelig": '\U000000E6', + "agrave": '\U000000E0', + "amp": '\U00000026', + "aring": '\U000000E5', + "atilde": '\U000000E3', + "auml": '\U000000E4', + "brvbar": '\U000000A6', + "ccedil": '\U000000E7', + "cedil": '\U000000B8', + "cent": '\U000000A2', + "copy": '\U000000A9', + "curren": '\U000000A4', + "deg": '\U000000B0', + "divide": '\U000000F7', + "eacute": '\U000000E9', + "ecirc": '\U000000EA', + "egrave": '\U000000E8', + "eth": '\U000000F0', + "euml": '\U000000EB', + "frac12": '\U000000BD', + "frac14": '\U000000BC', + "frac34": '\U000000BE', + "gt": '\U0000003E', + "iacute": '\U000000ED', + "icirc": '\U000000EE', + "iexcl": '\U000000A1', + "igrave": '\U000000EC', + "iquest": '\U000000BF', + "iuml": '\U000000EF', + "laquo": '\U000000AB', + "lt": '\U0000003C', + "macr": '\U000000AF', + "micro": '\U000000B5', + "middot": '\U000000B7', + "nbsp": '\U000000A0', + "not": '\U000000AC', + "ntilde": '\U000000F1', + "oacute": '\U000000F3', + "ocirc": '\U000000F4', + "ograve": '\U000000F2', + "ordf": '\U000000AA', + "ordm": '\U000000BA', + "oslash": '\U000000F8', + "otilde": '\U000000F5', + "ouml": '\U000000F6', + "para": '\U000000B6', + "plusmn": '\U000000B1', + "pound": '\U000000A3', + "quot": '\U00000022', + "raquo": '\U000000BB', + "reg": '\U000000AE', + "sect": '\U000000A7', + "shy": '\U000000AD', + "sup1": '\U000000B9', + "sup2": '\U000000B2', + "sup3": '\U000000B3', + "szlig": '\U000000DF', + "thorn": '\U000000FE', + "times": '\U000000D7', + "uacute": '\U000000FA', + "ucirc": '\U000000FB', + "ugrave": '\U000000F9', + "uml": '\U000000A8', + "uuml": '\U000000FC', + "yacute": '\U000000FD', + "yen": '\U000000A5', + "yuml": '\U000000FF', +} + +// HTML entities that are two unicode codepoints. +var entity2 = map[string][2]rune{ + // TODO(nigeltao): Handle replacements that are wider than their names. + // "nLt;": {'\u226A', '\u20D2'}, + // "nGt;": {'\u226B', '\u20D2'}, + "NotEqualTilde;": {'\u2242', '\u0338'}, + "NotGreaterFullEqual;": {'\u2267', '\u0338'}, + "NotGreaterGreater;": {'\u226B', '\u0338'}, + "NotGreaterSlantEqual;": {'\u2A7E', '\u0338'}, + "NotHumpDownHump;": {'\u224E', '\u0338'}, + "NotHumpEqual;": {'\u224F', '\u0338'}, + "NotLeftTriangleBar;": {'\u29CF', '\u0338'}, + "NotLessLess;": {'\u226A', '\u0338'}, + "NotLessSlantEqual;": {'\u2A7D', '\u0338'}, + "NotNestedGreaterGreater;": {'\u2AA2', '\u0338'}, + "NotNestedLessLess;": {'\u2AA1', '\u0338'}, + "NotPrecedesEqual;": {'\u2AAF', '\u0338'}, + "NotRightTriangleBar;": {'\u29D0', '\u0338'}, + "NotSquareSubset;": {'\u228F', '\u0338'}, + "NotSquareSuperset;": {'\u2290', '\u0338'}, + "NotSubset;": {'\u2282', '\u20D2'}, + "NotSucceedsEqual;": {'\u2AB0', '\u0338'}, + "NotSucceedsTilde;": {'\u227F', '\u0338'}, + "NotSuperset;": {'\u2283', '\u20D2'}, + "ThickSpace;": {'\u205F', '\u200A'}, + "acE;": {'\u223E', '\u0333'}, + "bne;": {'\u003D', '\u20E5'}, + "bnequiv;": {'\u2261', '\u20E5'}, + "caps;": {'\u2229', '\uFE00'}, + "cups;": {'\u222A', '\uFE00'}, + "fjlig;": {'\u0066', '\u006A'}, + "gesl;": {'\u22DB', '\uFE00'}, + "gvertneqq;": {'\u2269', '\uFE00'}, + "gvnE;": {'\u2269', '\uFE00'}, + "lates;": {'\u2AAD', '\uFE00'}, + "lesg;": {'\u22DA', '\uFE00'}, + "lvertneqq;": {'\u2268', '\uFE00'}, + "lvnE;": {'\u2268', '\uFE00'}, + "nGg;": {'\u22D9', '\u0338'}, + "nGtv;": {'\u226B', '\u0338'}, + "nLl;": {'\u22D8', '\u0338'}, + "nLtv;": {'\u226A', '\u0338'}, + "nang;": {'\u2220', '\u20D2'}, + "napE;": {'\u2A70', '\u0338'}, + "napid;": {'\u224B', '\u0338'}, + "nbump;": {'\u224E', '\u0338'}, + "nbumpe;": {'\u224F', '\u0338'}, + "ncongdot;": {'\u2A6D', '\u0338'}, + "nedot;": {'\u2250', '\u0338'}, + "nesim;": {'\u2242', '\u0338'}, + "ngE;": {'\u2267', '\u0338'}, + "ngeqq;": {'\u2267', '\u0338'}, + "ngeqslant;": {'\u2A7E', '\u0338'}, + "nges;": {'\u2A7E', '\u0338'}, + "nlE;": {'\u2266', '\u0338'}, + "nleqq;": {'\u2266', '\u0338'}, + "nleqslant;": {'\u2A7D', '\u0338'}, + "nles;": {'\u2A7D', '\u0338'}, + "notinE;": {'\u22F9', '\u0338'}, + "notindot;": {'\u22F5', '\u0338'}, + "nparsl;": {'\u2AFD', '\u20E5'}, + "npart;": {'\u2202', '\u0338'}, + "npre;": {'\u2AAF', '\u0338'}, + "npreceq;": {'\u2AAF', '\u0338'}, + "nrarrc;": {'\u2933', '\u0338'}, + "nrarrw;": {'\u219D', '\u0338'}, + "nsce;": {'\u2AB0', '\u0338'}, + "nsubE;": {'\u2AC5', '\u0338'}, + "nsubset;": {'\u2282', '\u20D2'}, + "nsubseteqq;": {'\u2AC5', '\u0338'}, + "nsucceq;": {'\u2AB0', '\u0338'}, + "nsupE;": {'\u2AC6', '\u0338'}, + "nsupset;": {'\u2283', '\u20D2'}, + "nsupseteqq;": {'\u2AC6', '\u0338'}, + "nvap;": {'\u224D', '\u20D2'}, + "nvge;": {'\u2265', '\u20D2'}, + "nvgt;": {'\u003E', '\u20D2'}, + "nvle;": {'\u2264', '\u20D2'}, + "nvlt;": {'\u003C', '\u20D2'}, + "nvltrie;": {'\u22B4', '\u20D2'}, + "nvrtrie;": {'\u22B5', '\u20D2'}, + "nvsim;": {'\u223C', '\u20D2'}, + "race;": {'\u223D', '\u0331'}, + "smtes;": {'\u2AAC', '\uFE00'}, + "sqcaps;": {'\u2293', '\uFE00'}, + "sqcups;": {'\u2294', '\uFE00'}, + "varsubsetneq;": {'\u228A', '\uFE00'}, + "varsubsetneqq;": {'\u2ACB', '\uFE00'}, + "varsupsetneq;": {'\u228B', '\uFE00'}, + "varsupsetneqq;": {'\u2ACC', '\uFE00'}, + "vnsub;": {'\u2282', '\u20D2'}, + "vnsup;": {'\u2283', '\u20D2'}, + "vsubnE;": {'\u2ACB', '\uFE00'}, + "vsubne;": {'\u228A', '\uFE00'}, + "vsupnE;": {'\u2ACC', '\uFE00'}, + "vsupne;": {'\u228B', '\uFE00'}, +} diff --git a/vendor/golang.org/x/net/html/escape.go b/vendor/golang.org/x/net/html/escape.go new file mode 100644 index 000000000..d85613962 --- /dev/null +++ b/vendor/golang.org/x/net/html/escape.go @@ -0,0 +1,258 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "bytes" + "strings" + "unicode/utf8" +) + +// These replacements permit compatibility with old numeric entities that +// assumed Windows-1252 encoding. +// https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference +var replacementTable = [...]rune{ + '\u20AC', // First entry is what 0x80 should be replaced with. + '\u0081', + '\u201A', + '\u0192', + '\u201E', + '\u2026', + '\u2020', + '\u2021', + '\u02C6', + '\u2030', + '\u0160', + '\u2039', + '\u0152', + '\u008D', + '\u017D', + '\u008F', + '\u0090', + '\u2018', + '\u2019', + '\u201C', + '\u201D', + '\u2022', + '\u2013', + '\u2014', + '\u02DC', + '\u2122', + '\u0161', + '\u203A', + '\u0153', + '\u009D', + '\u017E', + '\u0178', // Last entry is 0x9F. + // 0x00->'\uFFFD' is handled programmatically. + // 0x0D->'\u000D' is a no-op. +} + +// unescapeEntity reads an entity like "<" from b[src:] and writes the +// corresponding "<" to b[dst:], returning the incremented dst and src cursors. +// Precondition: b[src] == '&' && dst <= src. +// attribute should be true if parsing an attribute value. +func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { + // https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference + + // i starts at 1 because we already know that s[0] == '&'. + i, s := 1, b[src:] + + if len(s) <= 1 { + b[dst] = b[src] + return dst + 1, src + 1 + } + + if s[i] == '#' { + if len(s) <= 3 { // We need to have at least "&#.". + b[dst] = b[src] + return dst + 1, src + 1 + } + i++ + c := s[i] + hex := false + if c == 'x' || c == 'X' { + hex = true + i++ + } + + x := '\x00' + for i < len(s) { + c = s[i] + i++ + if hex { + if '0' <= c && c <= '9' { + x = 16*x + rune(c) - '0' + continue + } else if 'a' <= c && c <= 'f' { + x = 16*x + rune(c) - 'a' + 10 + continue + } else if 'A' <= c && c <= 'F' { + x = 16*x + rune(c) - 'A' + 10 + continue + } + } else if '0' <= c && c <= '9' { + x = 10*x + rune(c) - '0' + continue + } + if c != ';' { + i-- + } + break + } + + if i <= 3 { // No characters matched. + b[dst] = b[src] + return dst + 1, src + 1 + } + + if 0x80 <= x && x <= 0x9F { + // Replace characters from Windows-1252 with UTF-8 equivalents. + x = replacementTable[x-0x80] + } else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF { + // Replace invalid characters with the replacement character. + x = '\uFFFD' + } + + return dst + utf8.EncodeRune(b[dst:], x), src + i + } + + // Consume the maximum number of characters possible, with the + // consumed characters matching one of the named references. + + for i < len(s) { + c := s[i] + i++ + // Lower-cased characters are more common in entities, so we check for them first. + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { + continue + } + if c != ';' { + i-- + } + break + } + + entityName := string(s[1:i]) + if entityName == "" { + // No-op. + } else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' { + // No-op. + } else if x := entity[entityName]; x != 0 { + return dst + utf8.EncodeRune(b[dst:], x), src + i + } else if x := entity2[entityName]; x[0] != 0 { + dst1 := dst + utf8.EncodeRune(b[dst:], x[0]) + return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i + } else if !attribute { + maxLen := len(entityName) - 1 + if maxLen > longestEntityWithoutSemicolon { + maxLen = longestEntityWithoutSemicolon + } + for j := maxLen; j > 1; j-- { + if x := entity[entityName[:j]]; x != 0 { + return dst + utf8.EncodeRune(b[dst:], x), src + j + 1 + } + } + } + + dst1, src1 = dst+i, src+i + copy(b[dst:dst1], b[src:src1]) + return dst1, src1 +} + +// unescape unescapes b's entities in-place, so that "a<b" becomes "a': + esc = ">" + case '"': + // """ is shorter than """. + esc = """ + case '\r': + esc = " " + default: + panic("unrecognized escape character") + } + s = s[i+1:] + if _, err := w.WriteString(esc); err != nil { + return err + } + i = strings.IndexAny(s, escapedChars) + } + _, err := w.WriteString(s) + return err +} + +// EscapeString escapes special characters like "<" to become "<". It +// escapes only five such characters: <, >, &, ' and ". +// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't +// always true. +func EscapeString(s string) string { + if strings.IndexAny(s, escapedChars) == -1 { + return s + } + var buf bytes.Buffer + escape(&buf, s) + return buf.String() +} + +// UnescapeString unescapes entities like "<" to become "<". It unescapes a +// larger range of entities than EscapeString escapes. For example, "á" +// unescapes to "á", as does "á" and "&xE1;". +// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't +// always true. +func UnescapeString(s string) string { + for _, c := range s { + if c == '&' { + return string(unescape([]byte(s), false)) + } + } + return s +} diff --git a/vendor/golang.org/x/net/html/foreign.go b/vendor/golang.org/x/net/html/foreign.go new file mode 100644 index 000000000..d3b384409 --- /dev/null +++ b/vendor/golang.org/x/net/html/foreign.go @@ -0,0 +1,226 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "strings" +) + +func adjustAttributeNames(aa []Attribute, nameMap map[string]string) { + for i := range aa { + if newName, ok := nameMap[aa[i].Key]; ok { + aa[i].Key = newName + } + } +} + +func adjustForeignAttributes(aa []Attribute) { + for i, a := range aa { + if a.Key == "" || a.Key[0] != 'x' { + continue + } + switch a.Key { + case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", + "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink": + j := strings.Index(a.Key, ":") + aa[i].Namespace = a.Key[:j] + aa[i].Key = a.Key[j+1:] + } + } +} + +func htmlIntegrationPoint(n *Node) bool { + if n.Type != ElementNode { + return false + } + switch n.Namespace { + case "math": + if n.Data == "annotation-xml" { + for _, a := range n.Attr { + if a.Key == "encoding" { + val := strings.ToLower(a.Val) + if val == "text/html" || val == "application/xhtml+xml" { + return true + } + } + } + } + case "svg": + switch n.Data { + case "desc", "foreignObject", "title": + return true + } + } + return false +} + +func mathMLTextIntegrationPoint(n *Node) bool { + if n.Namespace != "math" { + return false + } + switch n.Data { + case "mi", "mo", "mn", "ms", "mtext": + return true + } + return false +} + +// Section 12.2.5.5. +var breakout = map[string]bool{ + "b": true, + "big": true, + "blockquote": true, + "body": true, + "br": true, + "center": true, + "code": true, + "dd": true, + "div": true, + "dl": true, + "dt": true, + "em": true, + "embed": true, + "h1": true, + "h2": true, + "h3": true, + "h4": true, + "h5": true, + "h6": true, + "head": true, + "hr": true, + "i": true, + "img": true, + "li": true, + "listing": true, + "menu": true, + "meta": true, + "nobr": true, + "ol": true, + "p": true, + "pre": true, + "ruby": true, + "s": true, + "small": true, + "span": true, + "strong": true, + "strike": true, + "sub": true, + "sup": true, + "table": true, + "tt": true, + "u": true, + "ul": true, + "var": true, +} + +// Section 12.2.5.5. +var svgTagNameAdjustments = map[string]string{ + "altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath", +} + +// Section 12.2.5.1 +var mathMLAttributeAdjustments = map[string]string{ + "definitionurl": "definitionURL", +} + +var svgAttributeAdjustments = map[string]string{ + "attributename": "attributeName", + "attributetype": "attributeType", + "basefrequency": "baseFrequency", + "baseprofile": "baseProfile", + "calcmode": "calcMode", + "clippathunits": "clipPathUnits", + "contentscripttype": "contentScriptType", + "contentstyletype": "contentStyleType", + "diffuseconstant": "diffuseConstant", + "edgemode": "edgeMode", + "externalresourcesrequired": "externalResourcesRequired", + "filterres": "filterRes", + "filterunits": "filterUnits", + "glyphref": "glyphRef", + "gradienttransform": "gradientTransform", + "gradientunits": "gradientUnits", + "kernelmatrix": "kernelMatrix", + "kernelunitlength": "kernelUnitLength", + "keypoints": "keyPoints", + "keysplines": "keySplines", + "keytimes": "keyTimes", + "lengthadjust": "lengthAdjust", + "limitingconeangle": "limitingConeAngle", + "markerheight": "markerHeight", + "markerunits": "markerUnits", + "markerwidth": "markerWidth", + "maskcontentunits": "maskContentUnits", + "maskunits": "maskUnits", + "numoctaves": "numOctaves", + "pathlength": "pathLength", + "patterncontentunits": "patternContentUnits", + "patterntransform": "patternTransform", + "patternunits": "patternUnits", + "pointsatx": "pointsAtX", + "pointsaty": "pointsAtY", + "pointsatz": "pointsAtZ", + "preservealpha": "preserveAlpha", + "preserveaspectratio": "preserveAspectRatio", + "primitiveunits": "primitiveUnits", + "refx": "refX", + "refy": "refY", + "repeatcount": "repeatCount", + "repeatdur": "repeatDur", + "requiredextensions": "requiredExtensions", + "requiredfeatures": "requiredFeatures", + "specularconstant": "specularConstant", + "specularexponent": "specularExponent", + "spreadmethod": "spreadMethod", + "startoffset": "startOffset", + "stddeviation": "stdDeviation", + "stitchtiles": "stitchTiles", + "surfacescale": "surfaceScale", + "systemlanguage": "systemLanguage", + "tablevalues": "tableValues", + "targetx": "targetX", + "targety": "targetY", + "textlength": "textLength", + "viewbox": "viewBox", + "viewtarget": "viewTarget", + "xchannelselector": "xChannelSelector", + "ychannelselector": "yChannelSelector", + "zoomandpan": "zoomAndPan", +} diff --git a/vendor/golang.org/x/net/html/node.go b/vendor/golang.org/x/net/html/node.go new file mode 100644 index 000000000..26b657aec --- /dev/null +++ b/vendor/golang.org/x/net/html/node.go @@ -0,0 +1,193 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "golang.org/x/net/html/atom" +) + +// A NodeType is the type of a Node. +type NodeType uint32 + +const ( + ErrorNode NodeType = iota + TextNode + DocumentNode + ElementNode + CommentNode + DoctypeNode + scopeMarkerNode +) + +// Section 12.2.3.3 says "scope markers are inserted when entering applet +// elements, buttons, object elements, marquees, table cells, and table +// captions, and are used to prevent formatting from 'leaking'". +var scopeMarker = Node{Type: scopeMarkerNode} + +// A Node consists of a NodeType and some Data (tag name for element nodes, +// content for text) and are part of a tree of Nodes. Element nodes may also +// have a Namespace and contain a slice of Attributes. Data is unescaped, so +// that it looks like "a 0 { + return (*s)[i-1] + } + return nil +} + +// index returns the index of the top-most occurrence of n in the stack, or -1 +// if n is not present. +func (s *nodeStack) index(n *Node) int { + for i := len(*s) - 1; i >= 0; i-- { + if (*s)[i] == n { + return i + } + } + return -1 +} + +// insert inserts a node at the given index. +func (s *nodeStack) insert(i int, n *Node) { + (*s) = append(*s, nil) + copy((*s)[i+1:], (*s)[i:]) + (*s)[i] = n +} + +// remove removes a node from the stack. It is a no-op if n is not present. +func (s *nodeStack) remove(n *Node) { + i := s.index(n) + if i == -1 { + return + } + copy((*s)[i:], (*s)[i+1:]) + j := len(*s) - 1 + (*s)[j] = nil + *s = (*s)[:j] +} diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go new file mode 100644 index 000000000..be4b2bf5a --- /dev/null +++ b/vendor/golang.org/x/net/html/parse.go @@ -0,0 +1,2094 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "errors" + "fmt" + "io" + "strings" + + a "golang.org/x/net/html/atom" +) + +// A parser implements the HTML5 parsing algorithm: +// https://html.spec.whatwg.org/multipage/syntax.html#tree-construction +type parser struct { + // tokenizer provides the tokens for the parser. + tokenizer *Tokenizer + // tok is the most recently read token. + tok Token + // Self-closing tags like
    are treated as start tags, except that + // hasSelfClosingToken is set while they are being processed. + hasSelfClosingToken bool + // doc is the document root element. + doc *Node + // The stack of open elements (section 12.2.3.2) and active formatting + // elements (section 12.2.3.3). + oe, afe nodeStack + // Element pointers (section 12.2.3.4). + head, form *Node + // Other parsing state flags (section 12.2.3.5). + scripting, framesetOK bool + // im is the current insertion mode. + im insertionMode + // originalIM is the insertion mode to go back to after completing a text + // or inTableText insertion mode. + originalIM insertionMode + // fosterParenting is whether new elements should be inserted according to + // the foster parenting rules (section 12.2.5.3). + fosterParenting bool + // quirks is whether the parser is operating in "quirks mode." + quirks bool + // fragment is whether the parser is parsing an HTML fragment. + fragment bool + // context is the context element when parsing an HTML fragment + // (section 12.4). + context *Node +} + +func (p *parser) top() *Node { + if n := p.oe.top(); n != nil { + return n + } + return p.doc +} + +// Stop tags for use in popUntil. These come from section 12.2.3.2. +var ( + defaultScopeStopTags = map[string][]a.Atom{ + "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template}, + "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, + "svg": {a.Desc, a.ForeignObject, a.Title}, + } +) + +type scope int + +const ( + defaultScope scope = iota + listItemScope + buttonScope + tableScope + tableRowScope + tableBodyScope + selectScope +) + +// popUntil pops the stack of open elements at the highest element whose tag +// is in matchTags, provided there is no higher element in the scope's stop +// tags (as defined in section 12.2.3.2). It returns whether or not there was +// such an element. If there was not, popUntil leaves the stack unchanged. +// +// For example, the set of stop tags for table scope is: "html", "table". If +// the stack was: +// ["html", "body", "font", "table", "b", "i", "u"] +// then popUntil(tableScope, "font") would return false, but +// popUntil(tableScope, "i") would return true and the stack would become: +// ["html", "body", "font", "table", "b"] +// +// If an element's tag is in both the stop tags and matchTags, then the stack +// will be popped and the function returns true (provided, of course, there was +// no higher element in the stack that was also in the stop tags). For example, +// popUntil(tableScope, "table") returns true and leaves: +// ["html", "body", "font"] +func (p *parser) popUntil(s scope, matchTags ...a.Atom) bool { + if i := p.indexOfElementInScope(s, matchTags...); i != -1 { + p.oe = p.oe[:i] + return true + } + return false +} + +// indexOfElementInScope returns the index in p.oe of the highest element whose +// tag is in matchTags that is in scope. If no matching element is in scope, it +// returns -1. +func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int { + for i := len(p.oe) - 1; i >= 0; i-- { + tagAtom := p.oe[i].DataAtom + if p.oe[i].Namespace == "" { + for _, t := range matchTags { + if t == tagAtom { + return i + } + } + switch s { + case defaultScope: + // No-op. + case listItemScope: + if tagAtom == a.Ol || tagAtom == a.Ul { + return -1 + } + case buttonScope: + if tagAtom == a.Button { + return -1 + } + case tableScope: + if tagAtom == a.Html || tagAtom == a.Table { + return -1 + } + case selectScope: + if tagAtom != a.Optgroup && tagAtom != a.Option { + return -1 + } + default: + panic("unreachable") + } + } + switch s { + case defaultScope, listItemScope, buttonScope: + for _, t := range defaultScopeStopTags[p.oe[i].Namespace] { + if t == tagAtom { + return -1 + } + } + } + } + return -1 +} + +// elementInScope is like popUntil, except that it doesn't modify the stack of +// open elements. +func (p *parser) elementInScope(s scope, matchTags ...a.Atom) bool { + return p.indexOfElementInScope(s, matchTags...) != -1 +} + +// clearStackToContext pops elements off the stack of open elements until a +// scope-defined element is found. +func (p *parser) clearStackToContext(s scope) { + for i := len(p.oe) - 1; i >= 0; i-- { + tagAtom := p.oe[i].DataAtom + switch s { + case tableScope: + if tagAtom == a.Html || tagAtom == a.Table { + p.oe = p.oe[:i+1] + return + } + case tableRowScope: + if tagAtom == a.Html || tagAtom == a.Tr { + p.oe = p.oe[:i+1] + return + } + case tableBodyScope: + if tagAtom == a.Html || tagAtom == a.Tbody || tagAtom == a.Tfoot || tagAtom == a.Thead { + p.oe = p.oe[:i+1] + return + } + default: + panic("unreachable") + } + } +} + +// generateImpliedEndTags pops nodes off the stack of open elements as long as +// the top node has a tag name of dd, dt, li, option, optgroup, p, rp, or rt. +// If exceptions are specified, nodes with that name will not be popped off. +func (p *parser) generateImpliedEndTags(exceptions ...string) { + var i int +loop: + for i = len(p.oe) - 1; i >= 0; i-- { + n := p.oe[i] + if n.Type == ElementNode { + switch n.DataAtom { + case a.Dd, a.Dt, a.Li, a.Option, a.Optgroup, a.P, a.Rp, a.Rt: + for _, except := range exceptions { + if n.Data == except { + break loop + } + } + continue + } + } + break + } + + p.oe = p.oe[:i+1] +} + +// addChild adds a child node n to the top element, and pushes n onto the stack +// of open elements if it is an element node. +func (p *parser) addChild(n *Node) { + if p.shouldFosterParent() { + p.fosterParent(n) + } else { + p.top().AppendChild(n) + } + + if n.Type == ElementNode { + p.oe = append(p.oe, n) + } +} + +// shouldFosterParent returns whether the next node to be added should be +// foster parented. +func (p *parser) shouldFosterParent() bool { + if p.fosterParenting { + switch p.top().DataAtom { + case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: + return true + } + } + return false +} + +// fosterParent adds a child node according to the foster parenting rules. +// Section 12.2.5.3, "foster parenting". +func (p *parser) fosterParent(n *Node) { + var table, parent, prev *Node + var i int + for i = len(p.oe) - 1; i >= 0; i-- { + if p.oe[i].DataAtom == a.Table { + table = p.oe[i] + break + } + } + + if table == nil { + // The foster parent is the html element. + parent = p.oe[0] + } else { + parent = table.Parent + } + if parent == nil { + parent = p.oe[i-1] + } + + if table != nil { + prev = table.PrevSibling + } else { + prev = parent.LastChild + } + if prev != nil && prev.Type == TextNode && n.Type == TextNode { + prev.Data += n.Data + return + } + + parent.InsertBefore(n, table) +} + +// addText adds text to the preceding node if it is a text node, or else it +// calls addChild with a new text node. +func (p *parser) addText(text string) { + if text == "" { + return + } + + if p.shouldFosterParent() { + p.fosterParent(&Node{ + Type: TextNode, + Data: text, + }) + return + } + + t := p.top() + if n := t.LastChild; n != nil && n.Type == TextNode { + n.Data += text + return + } + p.addChild(&Node{ + Type: TextNode, + Data: text, + }) +} + +// addElement adds a child element based on the current token. +func (p *parser) addElement() { + p.addChild(&Node{ + Type: ElementNode, + DataAtom: p.tok.DataAtom, + Data: p.tok.Data, + Attr: p.tok.Attr, + }) +} + +// Section 12.2.3.3. +func (p *parser) addFormattingElement() { + tagAtom, attr := p.tok.DataAtom, p.tok.Attr + p.addElement() + + // Implement the Noah's Ark clause, but with three per family instead of two. + identicalElements := 0 +findIdenticalElements: + for i := len(p.afe) - 1; i >= 0; i-- { + n := p.afe[i] + if n.Type == scopeMarkerNode { + break + } + if n.Type != ElementNode { + continue + } + if n.Namespace != "" { + continue + } + if n.DataAtom != tagAtom { + continue + } + if len(n.Attr) != len(attr) { + continue + } + compareAttributes: + for _, t0 := range n.Attr { + for _, t1 := range attr { + if t0.Key == t1.Key && t0.Namespace == t1.Namespace && t0.Val == t1.Val { + // Found a match for this attribute, continue with the next attribute. + continue compareAttributes + } + } + // If we get here, there is no attribute that matches a. + // Therefore the element is not identical to the new one. + continue findIdenticalElements + } + + identicalElements++ + if identicalElements >= 3 { + p.afe.remove(n) + } + } + + p.afe = append(p.afe, p.top()) +} + +// Section 12.2.3.3. +func (p *parser) clearActiveFormattingElements() { + for { + n := p.afe.pop() + if len(p.afe) == 0 || n.Type == scopeMarkerNode { + return + } + } +} + +// Section 12.2.3.3. +func (p *parser) reconstructActiveFormattingElements() { + n := p.afe.top() + if n == nil { + return + } + if n.Type == scopeMarkerNode || p.oe.index(n) != -1 { + return + } + i := len(p.afe) - 1 + for n.Type != scopeMarkerNode && p.oe.index(n) == -1 { + if i == 0 { + i = -1 + break + } + i-- + n = p.afe[i] + } + for { + i++ + clone := p.afe[i].clone() + p.addChild(clone) + p.afe[i] = clone + if i == len(p.afe)-1 { + break + } + } +} + +// Section 12.2.4. +func (p *parser) acknowledgeSelfClosingTag() { + p.hasSelfClosingToken = false +} + +// An insertion mode (section 12.2.3.1) is the state transition function from +// a particular state in the HTML5 parser's state machine. It updates the +// parser's fields depending on parser.tok (where ErrorToken means EOF). +// It returns whether the token was consumed. +type insertionMode func(*parser) bool + +// setOriginalIM sets the insertion mode to return to after completing a text or +// inTableText insertion mode. +// Section 12.2.3.1, "using the rules for". +func (p *parser) setOriginalIM() { + if p.originalIM != nil { + panic("html: bad parser state: originalIM was set twice") + } + p.originalIM = p.im +} + +// Section 12.2.3.1, "reset the insertion mode". +func (p *parser) resetInsertionMode() { + for i := len(p.oe) - 1; i >= 0; i-- { + n := p.oe[i] + if i == 0 && p.context != nil { + n = p.context + } + + switch n.DataAtom { + case a.Select: + p.im = inSelectIM + case a.Td, a.Th: + p.im = inCellIM + case a.Tr: + p.im = inRowIM + case a.Tbody, a.Thead, a.Tfoot: + p.im = inTableBodyIM + case a.Caption: + p.im = inCaptionIM + case a.Colgroup: + p.im = inColumnGroupIM + case a.Table: + p.im = inTableIM + case a.Head: + p.im = inBodyIM + case a.Body: + p.im = inBodyIM + case a.Frameset: + p.im = inFramesetIM + case a.Html: + p.im = beforeHeadIM + default: + continue + } + return + } + p.im = inBodyIM +} + +const whitespace = " \t\r\n\f" + +// Section 12.2.5.4.1. +func initialIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case CommentToken: + p.doc.AppendChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + n, quirks := parseDoctype(p.tok.Data) + p.doc.AppendChild(n) + p.quirks = quirks + p.im = beforeHTMLIM + return true + } + p.quirks = true + p.im = beforeHTMLIM + return false +} + +// Section 12.2.5.4.2. +func beforeHTMLIM(p *parser) bool { + switch p.tok.Type { + case DoctypeToken: + // Ignore the token. + return true + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case StartTagToken: + if p.tok.DataAtom == a.Html { + p.addElement() + p.im = beforeHeadIM + return true + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Head, a.Body, a.Html, a.Br: + p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) + return false + default: + // Ignore the token. + return true + } + case CommentToken: + p.doc.AppendChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + } + p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) + return false +} + +// Section 12.2.5.4.3. +func beforeHeadIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Head: + p.addElement() + p.head = p.top() + p.im = inHeadIM + return true + case a.Html: + return inBodyIM(p) + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Head, a.Body, a.Html, a.Br: + p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) + return false + default: + // Ignore the token. + return true + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + // Ignore the token. + return true + } + + p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) + return false +} + +// Section 12.2.5.4.4. +func inHeadIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + s := strings.TrimLeft(p.tok.Data, whitespace) + if len(s) < len(p.tok.Data) { + // Add the initial whitespace to the current node. + p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) + if s == "" { + return true + } + p.tok.Data = s + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Html: + return inBodyIM(p) + case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta: + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + return true + case a.Script, a.Title, a.Noscript, a.Noframes, a.Style: + p.addElement() + p.setOriginalIM() + p.im = textIM + return true + case a.Head: + // Ignore the token. + return true + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Head: + n := p.oe.pop() + if n.DataAtom != a.Head { + panic("html: bad parser state: element not found, in the in-head insertion mode") + } + p.im = afterHeadIM + return true + case a.Body, a.Html, a.Br: + p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) + return false + default: + // Ignore the token. + return true + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + // Ignore the token. + return true + } + + p.parseImpliedToken(EndTagToken, a.Head, a.Head.String()) + return false +} + +// Section 12.2.5.4.6. +func afterHeadIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + s := strings.TrimLeft(p.tok.Data, whitespace) + if len(s) < len(p.tok.Data) { + // Add the initial whitespace to the current node. + p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) + if s == "" { + return true + } + p.tok.Data = s + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Html: + return inBodyIM(p) + case a.Body: + p.addElement() + p.framesetOK = false + p.im = inBodyIM + return true + case a.Frameset: + p.addElement() + p.im = inFramesetIM + return true + case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title: + p.oe = append(p.oe, p.head) + defer p.oe.remove(p.head) + return inHeadIM(p) + case a.Head: + // Ignore the token. + return true + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Body, a.Html, a.Br: + // Drop down to creating an implied tag. + default: + // Ignore the token. + return true + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + // Ignore the token. + return true + } + + p.parseImpliedToken(StartTagToken, a.Body, a.Body.String()) + p.framesetOK = true + return false +} + +// copyAttributes copies attributes of src not found on dst to dst. +func copyAttributes(dst *Node, src Token) { + if len(src.Attr) == 0 { + return + } + attr := map[string]string{} + for _, t := range dst.Attr { + attr[t.Key] = t.Val + } + for _, t := range src.Attr { + if _, ok := attr[t.Key]; !ok { + dst.Attr = append(dst.Attr, t) + attr[t.Key] = t.Val + } + } +} + +// Section 12.2.5.4.7. +func inBodyIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + d := p.tok.Data + switch n := p.oe.top(); n.DataAtom { + case a.Pre, a.Listing: + if n.FirstChild == nil { + // Ignore a newline at the start of a
     block.
    +				if d != "" && d[0] == '\r' {
    +					d = d[1:]
    +				}
    +				if d != "" && d[0] == '\n' {
    +					d = d[1:]
    +				}
    +			}
    +		}
    +		d = strings.Replace(d, "\x00", "", -1)
    +		if d == "" {
    +			return true
    +		}
    +		p.reconstructActiveFormattingElements()
    +		p.addText(d)
    +		if p.framesetOK && strings.TrimLeft(d, whitespace) != "" {
    +			// There were non-whitespace characters inserted.
    +			p.framesetOK = false
    +		}
    +	case StartTagToken:
    +		switch p.tok.DataAtom {
    +		case a.Html:
    +			copyAttributes(p.oe[0], p.tok)
    +		case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title:
    +			return inHeadIM(p)
    +		case a.Body:
    +			if len(p.oe) >= 2 {
    +				body := p.oe[1]
    +				if body.Type == ElementNode && body.DataAtom == a.Body {
    +					p.framesetOK = false
    +					copyAttributes(body, p.tok)
    +				}
    +			}
    +		case a.Frameset:
    +			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body {
    +				// Ignore the token.
    +				return true
    +			}
    +			body := p.oe[1]
    +			if body.Parent != nil {
    +				body.Parent.RemoveChild(body)
    +			}
    +			p.oe = p.oe[:1]
    +			p.addElement()
    +			p.im = inFramesetIM
    +			return true
    +		case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
    +			p.popUntil(buttonScope, a.P)
    +			switch n := p.top(); n.DataAtom {
    +			case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
    +				p.oe.pop()
    +			}
    +			p.addElement()
    +		case a.Pre, a.Listing:
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +			// The newline, if any, will be dealt with by the TextToken case.
    +			p.framesetOK = false
    +		case a.Form:
    +			if p.form == nil {
    +				p.popUntil(buttonScope, a.P)
    +				p.addElement()
    +				p.form = p.top()
    +			}
    +		case a.Li:
    +			p.framesetOK = false
    +			for i := len(p.oe) - 1; i >= 0; i-- {
    +				node := p.oe[i]
    +				switch node.DataAtom {
    +				case a.Li:
    +					p.oe = p.oe[:i]
    +				case a.Address, a.Div, a.P:
    +					continue
    +				default:
    +					if !isSpecialElement(node) {
    +						continue
    +					}
    +				}
    +				break
    +			}
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +		case a.Dd, a.Dt:
    +			p.framesetOK = false
    +			for i := len(p.oe) - 1; i >= 0; i-- {
    +				node := p.oe[i]
    +				switch node.DataAtom {
    +				case a.Dd, a.Dt:
    +					p.oe = p.oe[:i]
    +				case a.Address, a.Div, a.P:
    +					continue
    +				default:
    +					if !isSpecialElement(node) {
    +						continue
    +					}
    +				}
    +				break
    +			}
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +		case a.Plaintext:
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +		case a.Button:
    +			p.popUntil(defaultScope, a.Button)
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +			p.framesetOK = false
    +		case a.A:
    +			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
    +				if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A {
    +					p.inBodyEndTagFormatting(a.A)
    +					p.oe.remove(n)
    +					p.afe.remove(n)
    +					break
    +				}
    +			}
    +			p.reconstructActiveFormattingElements()
    +			p.addFormattingElement()
    +		case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
    +			p.reconstructActiveFormattingElements()
    +			p.addFormattingElement()
    +		case a.Nobr:
    +			p.reconstructActiveFormattingElements()
    +			if p.elementInScope(defaultScope, a.Nobr) {
    +				p.inBodyEndTagFormatting(a.Nobr)
    +				p.reconstructActiveFormattingElements()
    +			}
    +			p.addFormattingElement()
    +		case a.Applet, a.Marquee, a.Object:
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +			p.afe = append(p.afe, &scopeMarker)
    +			p.framesetOK = false
    +		case a.Table:
    +			if !p.quirks {
    +				p.popUntil(buttonScope, a.P)
    +			}
    +			p.addElement()
    +			p.framesetOK = false
    +			p.im = inTableIM
    +			return true
    +		case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +			p.oe.pop()
    +			p.acknowledgeSelfClosingTag()
    +			if p.tok.DataAtom == a.Input {
    +				for _, t := range p.tok.Attr {
    +					if t.Key == "type" {
    +						if strings.ToLower(t.Val) == "hidden" {
    +							// Skip setting framesetOK = false
    +							return true
    +						}
    +					}
    +				}
    +			}
    +			p.framesetOK = false
    +		case a.Param, a.Source, a.Track:
    +			p.addElement()
    +			p.oe.pop()
    +			p.acknowledgeSelfClosingTag()
    +		case a.Hr:
    +			p.popUntil(buttonScope, a.P)
    +			p.addElement()
    +			p.oe.pop()
    +			p.acknowledgeSelfClosingTag()
    +			p.framesetOK = false
    +		case a.Image:
    +			p.tok.DataAtom = a.Img
    +			p.tok.Data = a.Img.String()
    +			return false
    +		case a.Isindex:
    +			if p.form != nil {
    +				// Ignore the token.
    +				return true
    +			}
    +			action := ""
    +			prompt := "This is a searchable index. Enter search keywords: "
    +			attr := []Attribute{{Key: "name", Val: "isindex"}}
    +			for _, t := range p.tok.Attr {
    +				switch t.Key {
    +				case "action":
    +					action = t.Val
    +				case "name":
    +					// Ignore the attribute.
    +				case "prompt":
    +					prompt = t.Val
    +				default:
    +					attr = append(attr, t)
    +				}
    +			}
    +			p.acknowledgeSelfClosingTag()
    +			p.popUntil(buttonScope, a.P)
    +			p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
    +			if action != "" {
    +				p.form.Attr = []Attribute{{Key: "action", Val: action}}
    +			}
    +			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
    +			p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
    +			p.addText(prompt)
    +			p.addChild(&Node{
    +				Type:     ElementNode,
    +				DataAtom: a.Input,
    +				Data:     a.Input.String(),
    +				Attr:     attr,
    +			})
    +			p.oe.pop()
    +			p.parseImpliedToken(EndTagToken, a.Label, a.Label.String())
    +			p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
    +			p.parseImpliedToken(EndTagToken, a.Form, a.Form.String())
    +		case a.Textarea:
    +			p.addElement()
    +			p.setOriginalIM()
    +			p.framesetOK = false
    +			p.im = textIM
    +		case a.Xmp:
    +			p.popUntil(buttonScope, a.P)
    +			p.reconstructActiveFormattingElements()
    +			p.framesetOK = false
    +			p.addElement()
    +			p.setOriginalIM()
    +			p.im = textIM
    +		case a.Iframe:
    +			p.framesetOK = false
    +			p.addElement()
    +			p.setOriginalIM()
    +			p.im = textIM
    +		case a.Noembed, a.Noscript:
    +			p.addElement()
    +			p.setOriginalIM()
    +			p.im = textIM
    +		case a.Select:
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +			p.framesetOK = false
    +			p.im = inSelectIM
    +			return true
    +		case a.Optgroup, a.Option:
    +			if p.top().DataAtom == a.Option {
    +				p.oe.pop()
    +			}
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +		case a.Rp, a.Rt:
    +			if p.elementInScope(defaultScope, a.Ruby) {
    +				p.generateImpliedEndTags()
    +			}
    +			p.addElement()
    +		case a.Math, a.Svg:
    +			p.reconstructActiveFormattingElements()
    +			if p.tok.DataAtom == a.Math {
    +				adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
    +			} else {
    +				adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
    +			}
    +			adjustForeignAttributes(p.tok.Attr)
    +			p.addElement()
    +			p.top().Namespace = p.tok.Data
    +			if p.hasSelfClosingToken {
    +				p.oe.pop()
    +				p.acknowledgeSelfClosingTag()
    +			}
    +			return true
    +		case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
    +			// Ignore the token.
    +		default:
    +			p.reconstructActiveFormattingElements()
    +			p.addElement()
    +		}
    +	case EndTagToken:
    +		switch p.tok.DataAtom {
    +		case a.Body:
    +			if p.elementInScope(defaultScope, a.Body) {
    +				p.im = afterBodyIM
    +			}
    +		case a.Html:
    +			if p.elementInScope(defaultScope, a.Body) {
    +				p.parseImpliedToken(EndTagToken, a.Body, a.Body.String())
    +				return false
    +			}
    +			return true
    +		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
    +			p.popUntil(defaultScope, p.tok.DataAtom)
    +		case a.Form:
    +			node := p.form
    +			p.form = nil
    +			i := p.indexOfElementInScope(defaultScope, a.Form)
    +			if node == nil || i == -1 || p.oe[i] != node {
    +				// Ignore the token.
    +				return true
    +			}
    +			p.generateImpliedEndTags()
    +			p.oe.remove(node)
    +		case a.P:
    +			if !p.elementInScope(buttonScope, a.P) {
    +				p.parseImpliedToken(StartTagToken, a.P, a.P.String())
    +			}
    +			p.popUntil(buttonScope, a.P)
    +		case a.Li:
    +			p.popUntil(listItemScope, a.Li)
    +		case a.Dd, a.Dt:
    +			p.popUntil(defaultScope, p.tok.DataAtom)
    +		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
    +			p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6)
    +		case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
    +			p.inBodyEndTagFormatting(p.tok.DataAtom)
    +		case a.Applet, a.Marquee, a.Object:
    +			if p.popUntil(defaultScope, p.tok.DataAtom) {
    +				p.clearActiveFormattingElements()
    +			}
    +		case a.Br:
    +			p.tok.Type = StartTagToken
    +			return false
    +		default:
    +			p.inBodyEndTagOther(p.tok.DataAtom)
    +		}
    +	case CommentToken:
    +		p.addChild(&Node{
    +			Type: CommentNode,
    +			Data: p.tok.Data,
    +		})
    +	}
    +
    +	return true
    +}
    +
    +func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
    +	// This is the "adoption agency" algorithm, described at
    +	// https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency
    +
    +	// TODO: this is a fairly literal line-by-line translation of that algorithm.
    +	// Once the code successfully parses the comprehensive test suite, we should
    +	// refactor this code to be more idiomatic.
    +
    +	// Steps 1-4. The outer loop.
    +	for i := 0; i < 8; i++ {
    +		// Step 5. Find the formatting element.
    +		var formattingElement *Node
    +		for j := len(p.afe) - 1; j >= 0; j-- {
    +			if p.afe[j].Type == scopeMarkerNode {
    +				break
    +			}
    +			if p.afe[j].DataAtom == tagAtom {
    +				formattingElement = p.afe[j]
    +				break
    +			}
    +		}
    +		if formattingElement == nil {
    +			p.inBodyEndTagOther(tagAtom)
    +			return
    +		}
    +		feIndex := p.oe.index(formattingElement)
    +		if feIndex == -1 {
    +			p.afe.remove(formattingElement)
    +			return
    +		}
    +		if !p.elementInScope(defaultScope, tagAtom) {
    +			// Ignore the tag.
    +			return
    +		}
    +
    +		// Steps 9-10. Find the furthest block.
    +		var furthestBlock *Node
    +		for _, e := range p.oe[feIndex:] {
    +			if isSpecialElement(e) {
    +				furthestBlock = e
    +				break
    +			}
    +		}
    +		if furthestBlock == nil {
    +			e := p.oe.pop()
    +			for e != formattingElement {
    +				e = p.oe.pop()
    +			}
    +			p.afe.remove(e)
    +			return
    +		}
    +
    +		// Steps 11-12. Find the common ancestor and bookmark node.
    +		commonAncestor := p.oe[feIndex-1]
    +		bookmark := p.afe.index(formattingElement)
    +
    +		// Step 13. The inner loop. Find the lastNode to reparent.
    +		lastNode := furthestBlock
    +		node := furthestBlock
    +		x := p.oe.index(node)
    +		// Steps 13.1-13.2
    +		for j := 0; j < 3; j++ {
    +			// Step 13.3.
    +			x--
    +			node = p.oe[x]
    +			// Step 13.4 - 13.5.
    +			if p.afe.index(node) == -1 {
    +				p.oe.remove(node)
    +				continue
    +			}
    +			// Step 13.6.
    +			if node == formattingElement {
    +				break
    +			}
    +			// Step 13.7.
    +			clone := node.clone()
    +			p.afe[p.afe.index(node)] = clone
    +			p.oe[p.oe.index(node)] = clone
    +			node = clone
    +			// Step 13.8.
    +			if lastNode == furthestBlock {
    +				bookmark = p.afe.index(node) + 1
    +			}
    +			// Step 13.9.
    +			if lastNode.Parent != nil {
    +				lastNode.Parent.RemoveChild(lastNode)
    +			}
    +			node.AppendChild(lastNode)
    +			// Step 13.10.
    +			lastNode = node
    +		}
    +
    +		// Step 14. Reparent lastNode to the common ancestor,
    +		// or for misnested table nodes, to the foster parent.
    +		if lastNode.Parent != nil {
    +			lastNode.Parent.RemoveChild(lastNode)
    +		}
    +		switch commonAncestor.DataAtom {
    +		case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
    +			p.fosterParent(lastNode)
    +		default:
    +			commonAncestor.AppendChild(lastNode)
    +		}
    +
    +		// Steps 15-17. Reparent nodes from the furthest block's children
    +		// to a clone of the formatting element.
    +		clone := formattingElement.clone()
    +		reparentChildren(clone, furthestBlock)
    +		furthestBlock.AppendChild(clone)
    +
    +		// Step 18. Fix up the list of active formatting elements.
    +		if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark {
    +			// Move the bookmark with the rest of the list.
    +			bookmark--
    +		}
    +		p.afe.remove(formattingElement)
    +		p.afe.insert(bookmark, clone)
    +
    +		// Step 19. Fix up the stack of open elements.
    +		p.oe.remove(formattingElement)
    +		p.oe.insert(p.oe.index(furthestBlock)+1, clone)
    +	}
    +}
    +
    +// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
    +// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content
    +// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign
    +func (p *parser) inBodyEndTagOther(tagAtom a.Atom) {
    +	for i := len(p.oe) - 1; i >= 0; i-- {
    +		if p.oe[i].DataAtom == tagAtom {
    +			p.oe = p.oe[:i]
    +			break
    +		}
    +		if isSpecialElement(p.oe[i]) {
    +			break
    +		}
    +	}
    +}
    +
    +// Section 12.2.5.4.8.
    +func textIM(p *parser) bool {
    +	switch p.tok.Type {
    +	case ErrorToken:
    +		p.oe.pop()
    +	case TextToken:
    +		d := p.tok.Data
    +		if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil {
    +			// Ignore a newline at the start of a