Merge pull request #719 from mcdafydd/azure-devops

Azure Devops support
This commit is contained in:
Luke Kysow
2019-10-25 18:02:28 -07:00
committed by GitHub
114 changed files with 26640 additions and 95 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -129,7 +129,7 @@
Fargate, etc.
</li>
<li><img class="checkmark" src="/checkmark.svg">Listens for
webhooks from GitHub/GitLab/Bitbucket.
webhooks from GitHub/GitLab/Bitbucket/Azure Devops.
</li>
<li><img class="checkmark" src="/checkmark.svg">Runs terraform
commands remotely and comments back with their output.

View File

@@ -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).

View File

@@ -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)

View File

@@ -85,7 +85,40 @@ If you're using GitLab, navigate to your project's home page in GitLab
- Click **Save**<img src="../guide/images/bitbucket-server-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px;">
- 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/<organization>/_settings/groups`. Now click on the **<organization>/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/<organization>/<project>/_settings/permissions`. Now click on the **[<project>]/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)

View File

@@ -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 `<YOUR_GITHUB_USER>` 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 `<YOUR_GITLAB_USER>` 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 `<YOUR_BITBUCKET_USER>` 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 `<YOUR_AZUREDEVOPS_USER>` 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
<details>
@@ -181,6 +184,26 @@ spec:
key: token
### End Bitbucket Config ###
### Azure Devops Config ###
- name: ATLANTIS_AZUREDEVOPS_USER
value: <YOUR_AZUREDEVOPS_USER> # 6i. If you're using Azure Devops replace <YOUR_AZUREDEVOPS_USER> 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: <YOUR_AZUREDEVOPS_USER> # 6i. If you're using Azure Devops replace <YOUR_AZUREDEVOPS_USER> 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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`

View File

@@ -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='*'`

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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 nilthis is a bug")
}
pull = *maybePull
case models.AzureDevops:
pull, headRepo, err = c.getAzureDevopsData(baseRepo, pullNum)
default:
err = errors.New("Unknown VCS typethis 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())

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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:<redacted>@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:<redacted>@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:<redacted>@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:<redacted>@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)
}

View File

@@ -3,7 +3,6 @@ package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
)

View File

@@ -3,7 +3,6 @@ package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
)

View File

@@ -3,7 +3,6 @@ package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
logging "github.com/runatlantis/atlantis/server/logging"
)

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 "<missing String() implementation>"
}
@@ -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:]
}

View File

@@ -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

View File

@@ -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</details>" +
"\n<br>\n\n**Warning**: Output length greater than max comment size. Continued in next comment."
sepStart := "Continued from previous comment.\n<details><summary>Show Output</summary>\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:]
}

View File

@@ -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
}
}`

View File

@@ -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"
}
]
}

View File

@@ -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"
}`

View File

@@ -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,
},
}
}

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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"`

View File

@@ -116,6 +116,7 @@ type LockDetailData struct {
LockKeyEncoded string
LockKey string
RepoOwner string
RepoProject string
RepoName string
PullRequestLink string
LockedBy string

21
vendor/github.com/mcdafydd/go-azuredevops/LICENSE generated vendored Normal file
View File

@@ -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.

View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -0,0 +1,5 @@
/*
Package azuredevops is a Go client library for accessing the Azure DevOps API.
Installation
*/
package azuredevops

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}}
`

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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 <credentials>
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
}

View File

@@ -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
}

View File

@@ -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) {}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@
repo_token: x2wlA1x0X8CK45ybWpZRCVRB4g7vtkhaw

22
vendor/github.com/microcosm-cc/bluemonday/.travis.yml generated vendored Normal file
View File

@@ -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 ./...

View File

@@ -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.

6
vendor/github.com/microcosm-cc/bluemonday/CREDITS.md generated vendored Normal file
View File

@@ -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

28
vendor/github.com/microcosm-cc/bluemonday/LICENSE.md generated vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2014, David Kitchen <david@buro9.com>
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.

42
vendor/github.com/microcosm-cc/bluemonday/Makefile generated vendored Normal file
View File

@@ -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 ./...

350
vendor/github.com/microcosm-cc/bluemonday/README.md generated vendored Normal file
View File

@@ -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 <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World
```
Into a harmless:
```html
Hello World
```
And it turns this:
```html
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>
```
Into this:
```html
XSS
```
Whilst still allowing this:
```html
<a href="http://www.google.com/">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
```
To pass through mostly unaltered (it gained a rel="nofollow" which is a good thing for user generated content):
```html
<a href="http://www.google.com/" rel="nofollow">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
```
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(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com" rel="nofollow">Google</a>
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 <p> and <a href="">
p.AllowAttrs("href").OnElements("a")
p.AllowElements("p")
html := p.Sanitize(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com">Google</a>
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
<img src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=">
```
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 `<a href="" />` 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 `<a href="//host/path"/>` and will end up as `<a href="//host/path" rel="noopener" target="_blank">`. 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

104
vendor/github.com/microcosm-cc/bluemonday/doc.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
// Copyright (c) 2014, David Kitchen <david@buro9.com>
//
// 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 <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World
Into the more harmless:
Hello World
And it turns this:
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>
Into this:
XSS
Whilst still allowing this:
<a href="http://www.google.com/">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
To pass through mostly unaltered (it gained a rel="nofollow"):
<a href="http://www.google.com/" rel="nofollow">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
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

5
vendor/github.com/microcosm-cc/bluemonday/go.mod generated vendored Normal file
View File

@@ -0,0 +1,5 @@
module github.com/microcosm-cc/bluemonday
go 1.9
require golang.org/x/net v0.0.0-20181220203305-927f97764cc3

2
vendor/github.com/microcosm-cc/bluemonday/go.sum generated vendored Normal file
View File

@@ -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=

297
vendor/github.com/microcosm-cc/bluemonday/helpers.go generated vendored Normal file
View File

@@ -0,0 +1,297 @@
// Copyright (c) 2014, David Kitchen <david@buro9.com>
//
// 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")
}

253
vendor/github.com/microcosm-cc/bluemonday/policies.go generated vendored Normal file
View File

@@ -0,0 +1,253 @@
// Copyright (c) 2014, David Kitchen <david@buro9.com>
//
// 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
}

552
vendor/github.com/microcosm-cc/bluemonday/policy.go generated vendored Normal file
View File

@@ -0,0 +1,552 @@
// Copyright (c) 2014, David Kitchen <david@buro9.com>
//
// 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. <table>. 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 <a> 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 <a> 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 <a> 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: "<p>Hello</p><p>World</p>"" 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 <table> is valid, but <bdo> 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 <script> tag then we shouldn't render the JavaScript or
// anything else until we encounter the closing </script> 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{}{}
}

581
vendor/github.com/microcosm-cc/bluemonday/sanitize.go generated vendored Normal file
View File

@@ -0,0 +1,581 @@
// Copyright (c) 2014, David Kitchen <david@buro9.com>
//
// 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
}

View File

@@ -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 ./...

View File

@@ -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.

View File

@@ -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)

View File

@@ -0,0 +1 @@
module github.com/shurcooL/sanitized_anchor_name

View File

@@ -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)
}

78
vendor/golang.org/x/net/html/atom/atom.go generated vendored Normal file
View File

@@ -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)
}

709
vendor/golang.org/x/net/html/atom/gen.go generated vendored Normal file
View File

@@ -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<<k)
t.mask = 1<<k - 1
for _, s := range x {
if !t.insert(s) {
return false
}
}
return true
}
// insert inserts s in the table.
func (t *table) insert(s string) bool {
h1, h2 := t.hash(s)
if t.tab[h1] == "" {
t.tab[h1] = s
return true
}
if t.tab[h2] == "" {
t.tab[h2] = s
return true
}
if t.push(h1, 0) {
t.tab[h1] = s
return true
}
if t.push(h2, 0) {
t.tab[h2] = s
return true
}
return false
}
// push attempts to push aside the entry in slot i.
func (t *table) push(i uint32, depth int) bool {
if depth > 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",
}

777
vendor/golang.org/x/net/html/atom/table.go generated vendored Normal file
View File

@@ -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"

104
vendor/golang.org/x/net/html/const.go generated vendored Normal file
View File

@@ -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
}

106
vendor/golang.org/x/net/html/doc.go generated vendored Normal file
View File

@@ -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 "&lt;") 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?

156
vendor/golang.org/x/net/html/doctype.go generated vendored Normal file
View File

@@ -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//",
}

2253
vendor/golang.org/x/net/html/entity.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

258
vendor/golang.org/x/net/html/escape.go generated vendored Normal file
View File

@@ -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 "&lt;" 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&lt;b" becomes "a<b".
// attribute should be true if parsing an attribute value.
func unescape(b []byte, attribute bool) []byte {
for i, c := range b {
if c == '&' {
dst, src := unescapeEntity(b, i, i, attribute)
for src < len(b) {
c := b[src]
if c == '&' {
dst, src = unescapeEntity(b, dst, src, attribute)
} else {
b[dst] = c
dst, src = dst+1, src+1
}
}
return b[0:dst]
}
}
return b
}
// lower lower-cases the A-Z bytes in b in-place, so that "aBc" becomes "abc".
func lower(b []byte) []byte {
for i, c := range b {
if 'A' <= c && c <= 'Z' {
b[i] = c + 'a' - 'A'
}
}
return b
}
const escapedChars = "&'<>\"\r"
func escape(w writer, s string) error {
i := strings.IndexAny(s, escapedChars)
for i != -1 {
if _, err := w.WriteString(s[:i]); err != nil {
return err
}
var esc string
switch s[i] {
case '&':
esc = "&amp;"
case '\'':
// "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
esc = "&#39;"
case '<':
esc = "&lt;"
case '>':
esc = "&gt;"
case '"':
// "&#34;" is shorter than "&quot;".
esc = "&#34;"
case '\r':
esc = "&#13;"
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 "&lt;". 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 "&lt;" to become "<". It unescapes a
// larger range of entities than EscapeString escapes. For example, "&aacute;"
// unescapes to "á", as does "&#225;" 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
}

226
vendor/golang.org/x/net/html/foreign.go generated vendored Normal file
View File

@@ -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",
}

193
vendor/golang.org/x/net/html/node.go generated vendored Normal file
View File

@@ -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<b" rather than "a&lt;b". For element nodes, DataAtom
// is the atom for Data, or zero if Data is not a known tag name.
//
// An empty Namespace implies a "http://www.w3.org/1999/xhtml" namespace.
// Similarly, "math" is short for "http://www.w3.org/1998/Math/MathML", and
// "svg" is short for "http://www.w3.org/2000/svg".
type Node struct {
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node
Type NodeType
DataAtom atom.Atom
Data string
Namespace string
Attr []Attribute
}
// InsertBefore inserts newChild as a child of n, immediately before oldChild
// in the sequence of n's children. oldChild may be nil, in which case newChild
// is appended to the end of n's children.
//
// It will panic if newChild already has a parent or siblings.
func (n *Node) InsertBefore(newChild, oldChild *Node) {
if newChild.Parent != nil || newChild.PrevSibling != nil || newChild.NextSibling != nil {
panic("html: InsertBefore called for an attached child Node")
}
var prev, next *Node
if oldChild != nil {
prev, next = oldChild.PrevSibling, oldChild
} else {
prev = n.LastChild
}
if prev != nil {
prev.NextSibling = newChild
} else {
n.FirstChild = newChild
}
if next != nil {
next.PrevSibling = newChild
} else {
n.LastChild = newChild
}
newChild.Parent = n
newChild.PrevSibling = prev
newChild.NextSibling = next
}
// AppendChild adds a node c as a child of n.
//
// It will panic if c already has a parent or siblings.
func (n *Node) AppendChild(c *Node) {
if c.Parent != nil || c.PrevSibling != nil || c.NextSibling != nil {
panic("html: AppendChild called for an attached child Node")
}
last := n.LastChild
if last != nil {
last.NextSibling = c
} else {
n.FirstChild = c
}
n.LastChild = c
c.Parent = n
c.PrevSibling = last
}
// RemoveChild removes a node c that is a child of n. Afterwards, c will have
// no parent and no siblings.
//
// It will panic if c's parent is not n.
func (n *Node) RemoveChild(c *Node) {
if c.Parent != n {
panic("html: RemoveChild called for a non-child Node")
}
if n.FirstChild == c {
n.FirstChild = c.NextSibling
}
if c.NextSibling != nil {
c.NextSibling.PrevSibling = c.PrevSibling
}
if n.LastChild == c {
n.LastChild = c.PrevSibling
}
if c.PrevSibling != nil {
c.PrevSibling.NextSibling = c.NextSibling
}
c.Parent = nil
c.PrevSibling = nil
c.NextSibling = nil
}
// reparentChildren reparents all of src's child nodes to dst.
func reparentChildren(dst, src *Node) {
for {
child := src.FirstChild
if child == nil {
break
}
src.RemoveChild(child)
dst.AppendChild(child)
}
}
// clone returns a new node with the same type, data and attributes.
// The clone has no parent, no siblings and no children.
func (n *Node) clone() *Node {
m := &Node{
Type: n.Type,
DataAtom: n.DataAtom,
Data: n.Data,
Attr: make([]Attribute, len(n.Attr)),
}
copy(m.Attr, n.Attr)
return m
}
// nodeStack is a stack of nodes.
type nodeStack []*Node
// pop pops the stack. It will panic if s is empty.
func (s *nodeStack) pop() *Node {
i := len(*s)
n := (*s)[i-1]
*s = (*s)[:i-1]
return n
}
// top returns the most recently pushed node, or nil if s is empty.
func (s *nodeStack) top() *Node {
if i := len(*s); i > 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]
}

2094
vendor/golang.org/x/net/html/parse.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

271
vendor/golang.org/x/net/html/render.go generated vendored Normal file
View File

@@ -0,0 +1,271 @@
// 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 (
"bufio"
"errors"
"fmt"
"io"
"strings"
)
type writer interface {
io.Writer
io.ByteWriter
WriteString(string) (int, error)
}
// Render renders the parse tree n to the given writer.
//
// Rendering is done on a 'best effort' basis: calling Parse on the output of
// Render will always result in something similar to the original tree, but it
// is not necessarily an exact clone unless the original tree was 'well-formed'.
// 'Well-formed' is not easily specified; the HTML5 specification is
// complicated.
//
// Calling Parse on arbitrary input typically results in a 'well-formed' parse
// tree. However, it is possible for Parse to yield a 'badly-formed' parse tree.
// For example, in a 'well-formed' parse tree, no <a> element is a child of
// another <a> element: parsing "<a><a>" results in two sibling elements.
// Similarly, in a 'well-formed' parse tree, no <a> element is a child of a
// <table> element: parsing "<p><table><a>" results in a <p> with two sibling
// children; the <a> is reparented to the <table>'s parent. However, calling
// Parse on "<a><table><a>" does not return an error, but the result has an <a>
// element with an <a> child, and is therefore not 'well-formed'.
//
// Programmatically constructed trees are typically also 'well-formed', but it
// is possible to construct a tree that looks innocuous but, when rendered and
// re-parsed, results in a different tree. A simple example is that a solitary
// text node would become a tree containing <html>, <head> and <body> elements.
// Another example is that the programmatic equivalent of "a<head>b</head>c"
// becomes "<html><head><head/><body>abc</body></html>".
func Render(w io.Writer, n *Node) error {
if x, ok := w.(writer); ok {
return render(x, n)
}
buf := bufio.NewWriter(w)
if err := render(buf, n); err != nil {
return err
}
return buf.Flush()
}
// plaintextAbort is returned from render1 when a <plaintext> element
// has been rendered. No more end tags should be rendered after that.
var plaintextAbort = errors.New("html: internal error (plaintext abort)")
func render(w writer, n *Node) error {
err := render1(w, n)
if err == plaintextAbort {
err = nil
}
return err
}
func render1(w writer, n *Node) error {
// Render non-element nodes; these are the easy cases.
switch n.Type {
case ErrorNode:
return errors.New("html: cannot render an ErrorNode node")
case TextNode:
return escape(w, n.Data)
case DocumentNode:
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err := render1(w, c); err != nil {
return err
}
}
return nil
case ElementNode:
// No-op.
case CommentNode:
if _, err := w.WriteString("<!--"); err != nil {
return err
}
if _, err := w.WriteString(n.Data); err != nil {
return err
}
if _, err := w.WriteString("-->"); err != nil {
return err
}
return nil
case DoctypeNode:
if _, err := w.WriteString("<!DOCTYPE "); err != nil {
return err
}
if _, err := w.WriteString(n.Data); err != nil {
return err
}
if n.Attr != nil {
var p, s string
for _, a := range n.Attr {
switch a.Key {
case "public":
p = a.Val
case "system":
s = a.Val
}
}
if p != "" {
if _, err := w.WriteString(" PUBLIC "); err != nil {
return err
}
if err := writeQuoted(w, p); err != nil {
return err
}
if s != "" {
if err := w.WriteByte(' '); err != nil {
return err
}
if err := writeQuoted(w, s); err != nil {
return err
}
}
} else if s != "" {
if _, err := w.WriteString(" SYSTEM "); err != nil {
return err
}
if err := writeQuoted(w, s); err != nil {
return err
}
}
}
return w.WriteByte('>')
default:
return errors.New("html: unknown node type")
}
// Render the <xxx> opening tag.
if err := w.WriteByte('<'); err != nil {
return err
}
if _, err := w.WriteString(n.Data); err != nil {
return err
}
for _, a := range n.Attr {
if err := w.WriteByte(' '); err != nil {
return err
}
if a.Namespace != "" {
if _, err := w.WriteString(a.Namespace); err != nil {
return err
}
if err := w.WriteByte(':'); err != nil {
return err
}
}
if _, err := w.WriteString(a.Key); err != nil {
return err
}
if _, err := w.WriteString(`="`); err != nil {
return err
}
if err := escape(w, a.Val); err != nil {
return err
}
if err := w.WriteByte('"'); err != nil {
return err
}
}
if voidElements[n.Data] {
if n.FirstChild != nil {
return fmt.Errorf("html: void element <%s> has child nodes", n.Data)
}
_, err := w.WriteString("/>")
return err
}
if err := w.WriteByte('>'); err != nil {
return err
}
// Add initial newline where there is danger of a newline beging ignored.
if c := n.FirstChild; c != nil && c.Type == TextNode && strings.HasPrefix(c.Data, "\n") {
switch n.Data {
case "pre", "listing", "textarea":
if err := w.WriteByte('\n'); err != nil {
return err
}
}
}
// Render any child nodes.
switch n.Data {
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == TextNode {
if _, err := w.WriteString(c.Data); err != nil {
return err
}
} else {
if err := render1(w, c); err != nil {
return err
}
}
}
if n.Data == "plaintext" {
// Don't render anything else. <plaintext> must be the
// last element in the file, with no closing tag.
return plaintextAbort
}
default:
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err := render1(w, c); err != nil {
return err
}
}
}
// Render the </xxx> closing tag.
if _, err := w.WriteString("</"); err != nil {
return err
}
if _, err := w.WriteString(n.Data); err != nil {
return err
}
return w.WriteByte('>')
}
// writeQuoted writes s to w surrounded by quotes. Normally it will use double
// quotes, but if s contains a double quote, it will use single quotes.
// It is used for writing the identifiers in a doctype declaration.
// In valid HTML, they can't contain both types of quotes.
func writeQuoted(w writer, s string) error {
var q byte = '"'
if strings.Contains(s, `"`) {
q = '\''
}
if err := w.WriteByte(q); err != nil {
return err
}
if _, err := w.WriteString(s); err != nil {
return err
}
if err := w.WriteByte(q); err != nil {
return err
}
return nil
}
// Section 12.1.2, "Elements", gives this list of void elements. Void elements
// are those that can't have any contents.
var voidElements = map[string]bool{
"area": true,
"base": true,
"br": true,
"col": true,
"command": true,
"embed": true,
"hr": true,
"img": true,
"input": true,
"keygen": true,
"link": true,
"meta": true,
"param": true,
"source": true,
"track": true,
"wbr": true,
}

Some files were not shown because too many files have changed in this diff Show More