mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 00:38:24 +00:00
Merge pull request #719 from mcdafydd/azure-devops
Azure Devops support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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='*'`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
57
server/azuredevops_request_validator.go
Normal file
57
server/azuredevops_request_validator.go
Normal 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)
|
||||
}
|
||||
69
server/azuredevops_request_validator_test.go
Normal file
69
server/azuredevops_request_validator_test.go
Normal 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))
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v28/github"
|
||||
"github.com/mcdafydd/go-azuredevops/azuredevops"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/db"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
@@ -45,6 +46,14 @@ type GithubPullGetter interface {
|
||||
GetPullRequest(repo models.Repo, pullNum int) (*github.PullRequest, error)
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_azuredevops_pull_getter.go AzureDevopsPullGetter
|
||||
|
||||
// AzureDevopsPullGetter makes API calls to get pull requests.
|
||||
type AzureDevopsPullGetter interface {
|
||||
// GetPullRequest gets the pull request with id pullNum for the repo.
|
||||
GetPullRequest(repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error)
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_gitlab_merge_request_getter.go GitlabMergeRequestGetter
|
||||
|
||||
// GitlabMergeRequestGetter makes API calls to get merge requests.
|
||||
@@ -57,6 +66,7 @@ type GitlabMergeRequestGetter interface {
|
||||
type DefaultCommandRunner struct {
|
||||
VCSClient vcs.Client
|
||||
GithubPullGetter GithubPullGetter
|
||||
AzureDevopsPullGetter AzureDevopsPullGetter
|
||||
GitlabMergeRequestGetter GitlabMergeRequestGetter
|
||||
CommitStatusUpdater CommitStatusUpdater
|
||||
DisableApplyAll bool
|
||||
@@ -167,6 +177,8 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
|
||||
err = errors.New("pull request should not be nil–this is a bug")
|
||||
}
|
||||
pull = *maybePull
|
||||
case models.AzureDevops:
|
||||
pull, headRepo, err = c.getAzureDevopsData(baseRepo, pullNum)
|
||||
default:
|
||||
err = errors.New("Unknown VCS type–this is a bug")
|
||||
}
|
||||
@@ -353,6 +365,21 @@ func (c *DefaultCommandRunner) getGitlabData(baseRepo models.Repo, pullNum int)
|
||||
return pull, nil
|
||||
}
|
||||
|
||||
func (c *DefaultCommandRunner) getAzureDevopsData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {
|
||||
if c.AzureDevopsPullGetter == nil {
|
||||
return models.PullRequest{}, models.Repo{}, errors.New("atlantis not configured to support Azure Devops")
|
||||
}
|
||||
adPull, err := c.AzureDevopsPullGetter.GetPullRequest(baseRepo, pullNum)
|
||||
if err != nil {
|
||||
return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to Azure Devops")
|
||||
}
|
||||
pull, _, headRepo, err := c.EventParser.ParseAzureDevopsPull(adPull)
|
||||
if err != nil {
|
||||
return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data")
|
||||
}
|
||||
return pull, headRepo, nil
|
||||
}
|
||||
|
||||
func (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger {
|
||||
src := fmt.Sprintf("%s#%d", repoFullName, pullNum)
|
||||
return c.Logger.NewLogger(src, true, c.Logger.GetLevel())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
logging "github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
20
server/events/mocks/matchers/azuredevops_event.go
Normal file
20
server/events/mocks/matchers/azuredevops_event.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
114
server/events/mocks/mock_azuredevops_pull_getter.go
Normal file
114
server/events/mocks/mock_azuredevops_pull_getter.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
287
server/events/vcs/azuredevops_client.go
Normal file
287
server/events/vcs/azuredevops_client.go
Normal 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:]
|
||||
}
|
||||
473
server/events/vcs/azuredevops_client_test.go
Normal file
473
server/events/vcs/azuredevops_client_test.go
Normal 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
|
||||
}
|
||||
}`
|
||||
33
server/events/vcs/fixtures/azuredevops-pr.json
Normal file
33
server/events/vcs/fixtures/azuredevops-pr.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}`
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
117
server/mocks/mock_azuredevops_request_validator.go
Normal file
117
server/mocks/mock_azuredevops_request_validator.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
21
vendor/github.com/mcdafydd/go-azuredevops/LICENSE
generated
vendored
Normal 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.
|
||||
28
vendor/github.com/mcdafydd/go-azuredevops/LICENSE.go-github
generated
vendored
Normal file
28
vendor/github.com/mcdafydd/go-azuredevops/LICENSE.go-github
generated
vendored
Normal 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.
|
||||
|
||||
3580
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops-accessors.go
generated
vendored
Normal file
3580
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops-accessors.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
304
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops.go
generated
vendored
Normal file
304
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/azuredevops.go
generated
vendored
Normal 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 }
|
||||
124
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/boards.go
generated
vendored
Normal file
124
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/boards.go
generated
vendored
Normal 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
|
||||
}
|
||||
14
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_controller.go
generated
vendored
Normal file
14
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_controller.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
64
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_definitions.go
generated
vendored
Normal file
64
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/build_definitions.go
generated
vendored
Normal 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
|
||||
}
|
||||
203
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/builds.go
generated
vendored
Normal file
203
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/builds.go
generated
vendored
Normal 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
|
||||
}
|
||||
20
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/debug.go
generated
vendored
Normal file
20
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/debug.go
generated
vendored
Normal 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)
|
||||
}
|
||||
109
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/delivery_plans.go
generated
vendored
Normal file
109
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/delivery_plans.go
generated
vendored
Normal 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
|
||||
}
|
||||
5
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/doc.go
generated
vendored
Normal file
5
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/doc.go
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
Package azuredevops is a Go client library for accessing the Azure DevOps API.
|
||||
Installation
|
||||
*/
|
||||
package azuredevops
|
||||
49
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/event_types.go
generated
vendored
Normal file
49
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/event_types.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
106
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/events.go
generated
vendored
Normal file
106
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/events.go
generated
vendored
Normal 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
|
||||
}
|
||||
47
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/favourites.go
generated
vendored
Normal file
47
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/favourites.go
generated
vendored
Normal 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
|
||||
}
|
||||
449
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/fields.json
generated
vendored
Normal file
449
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/fields.json
generated
vendored
Normal 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
|
||||
}
|
||||
332
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/gen-accessors.go
generated
vendored
Normal file
332
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/gen-accessors.go
generated
vendored
Normal 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}}
|
||||
`
|
||||
481
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/git.go
generated
vendored
Normal file
481
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/git.go
generated
vendored
Normal 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
|
||||
}
|
||||
27
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/identity_ref.go
generated
vendored
Normal file
27
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/identity_ref.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
69
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/iterations.go
generated
vendored
Normal file
69
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/iterations.go
generated
vendored
Normal 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
|
||||
}
|
||||
162
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/messages.go
generated
vendored
Normal file
162
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/messages.go
generated
vendored
Normal 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
|
||||
}
|
||||
447
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/pull_requests.go
generated
vendored
Normal file
447
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/pull_requests.go
generated
vendored
Normal 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
|
||||
}
|
||||
11
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/release.go
generated
vendored
Normal file
11
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/release.go
generated
vendored
Normal 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) {}
|
||||
89
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/teams.go
generated
vendored
Normal file
89
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/teams.go
generated
vendored
Normal 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
|
||||
}
|
||||
160
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/tests.go
generated
vendored
Normal file
160
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/tests.go
generated
vendored
Normal 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
|
||||
}
|
||||
189
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/users.go
generated
vendored
Normal file
189
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/users.go
generated
vendored
Normal 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
|
||||
}
|
||||
260
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/work_items.go
generated
vendored
Normal file
260
vendor/github.com/mcdafydd/go-azuredevops/azuredevops/work_items.go
generated
vendored
Normal 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
|
||||
}
|
||||
1
vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml
generated
vendored
Normal file
1
vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
repo_token: x2wlA1x0X8CK45ybWpZRCVRB4g7vtkhaw
|
||||
22
vendor/github.com/microcosm-cc/bluemonday/.travis.yml
generated
vendored
Normal file
22
vendor/github.com/microcosm-cc/bluemonday/.travis.yml
generated
vendored
Normal 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 ./...
|
||||
51
vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md
generated
vendored
Normal file
51
vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md
generated
vendored
Normal 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
6
vendor/github.com/microcosm-cc/bluemonday/CREDITS.md
generated
vendored
Normal 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
28
vendor/github.com/microcosm-cc/bluemonday/LICENSE.md
generated
vendored
Normal 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
42
vendor/github.com/microcosm-cc/bluemonday/Makefile
generated
vendored
Normal 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
350
vendor/github.com/microcosm-cc/bluemonday/README.md
generated
vendored
Normal file
@@ -0,0 +1,350 @@
|
||||
# bluemonday [](https://travis-ci.org/microcosm-cc/bluemonday) [](https://godoc.org/github.com/microcosm-cc/bluemonday) [](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
104
vendor/github.com/microcosm-cc/bluemonday/doc.go
generated
vendored
Normal 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
5
vendor/github.com/microcosm-cc/bluemonday/go.mod
generated
vendored
Normal 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
2
vendor/github.com/microcosm-cc/bluemonday/go.sum
generated
vendored
Normal 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
297
vendor/github.com/microcosm-cc/bluemonday/helpers.go
generated
vendored
Normal 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
253
vendor/github.com/microcosm-cc/bluemonday/policies.go
generated
vendored
Normal 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
552
vendor/github.com/microcosm-cc/bluemonday/policy.go
generated
vendored
Normal 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
581
vendor/github.com/microcosm-cc/bluemonday/sanitize.go
generated
vendored
Normal 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
|
||||
}
|
||||
16
vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml
generated
vendored
Normal file
16
vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml
generated
vendored
Normal 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 ./...
|
||||
21
vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE
generated
vendored
Normal file
21
vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE
generated
vendored
Normal 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.
|
||||
36
vendor/github.com/shurcooL/sanitized_anchor_name/README.md
generated
vendored
Normal file
36
vendor/github.com/shurcooL/sanitized_anchor_name/README.md
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
sanitized_anchor_name
|
||||
=====================
|
||||
|
||||
[](https://travis-ci.org/shurcooL/sanitized_anchor_name) [](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)
|
||||
1
vendor/github.com/shurcooL/sanitized_anchor_name/go.mod
generated
vendored
Normal file
1
vendor/github.com/shurcooL/sanitized_anchor_name/go.mod
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module github.com/shurcooL/sanitized_anchor_name
|
||||
29
vendor/github.com/shurcooL/sanitized_anchor_name/main.go
generated
vendored
Normal file
29
vendor/github.com/shurcooL/sanitized_anchor_name/main.go
generated
vendored
Normal 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
78
vendor/golang.org/x/net/html/atom/atom.go
generated
vendored
Normal 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
709
vendor/golang.org/x/net/html/atom/gen.go
generated
vendored
Normal 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
777
vendor/golang.org/x/net/html/atom/table.go
generated
vendored
Normal 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
104
vendor/golang.org/x/net/html/const.go
generated
vendored
Normal 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
106
vendor/golang.org/x/net/html/doc.go
generated
vendored
Normal 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 "<") 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
156
vendor/golang.org/x/net/html/doctype.go
generated
vendored
Normal 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
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
258
vendor/golang.org/x/net/html/escape.go
generated
vendored
Normal 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 "<" from b[src:] and writes the
|
||||
// corresponding "<" to b[dst:], returning the incremented dst and src cursors.
|
||||
// Precondition: b[src] == '&' && dst <= src.
|
||||
// attribute should be true if parsing an attribute value.
|
||||
func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) {
|
||||
// https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference
|
||||
|
||||
// i starts at 1 because we already know that s[0] == '&'.
|
||||
i, s := 1, b[src:]
|
||||
|
||||
if len(s) <= 1 {
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
|
||||
if s[i] == '#' {
|
||||
if len(s) <= 3 { // We need to have at least "&#.".
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
i++
|
||||
c := s[i]
|
||||
hex := false
|
||||
if c == 'x' || c == 'X' {
|
||||
hex = true
|
||||
i++
|
||||
}
|
||||
|
||||
x := '\x00'
|
||||
for i < len(s) {
|
||||
c = s[i]
|
||||
i++
|
||||
if hex {
|
||||
if '0' <= c && c <= '9' {
|
||||
x = 16*x + rune(c) - '0'
|
||||
continue
|
||||
} else if 'a' <= c && c <= 'f' {
|
||||
x = 16*x + rune(c) - 'a' + 10
|
||||
continue
|
||||
} else if 'A' <= c && c <= 'F' {
|
||||
x = 16*x + rune(c) - 'A' + 10
|
||||
continue
|
||||
}
|
||||
} else if '0' <= c && c <= '9' {
|
||||
x = 10*x + rune(c) - '0'
|
||||
continue
|
||||
}
|
||||
if c != ';' {
|
||||
i--
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if i <= 3 { // No characters matched.
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
|
||||
if 0x80 <= x && x <= 0x9F {
|
||||
// Replace characters from Windows-1252 with UTF-8 equivalents.
|
||||
x = replacementTable[x-0x80]
|
||||
} else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF {
|
||||
// Replace invalid characters with the replacement character.
|
||||
x = '\uFFFD'
|
||||
}
|
||||
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + i
|
||||
}
|
||||
|
||||
// Consume the maximum number of characters possible, with the
|
||||
// consumed characters matching one of the named references.
|
||||
|
||||
for i < len(s) {
|
||||
c := s[i]
|
||||
i++
|
||||
// Lower-cased characters are more common in entities, so we check for them first.
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
||||
continue
|
||||
}
|
||||
if c != ';' {
|
||||
i--
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
entityName := string(s[1:i])
|
||||
if entityName == "" {
|
||||
// No-op.
|
||||
} else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' {
|
||||
// No-op.
|
||||
} else if x := entity[entityName]; x != 0 {
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + i
|
||||
} else if x := entity2[entityName]; x[0] != 0 {
|
||||
dst1 := dst + utf8.EncodeRune(b[dst:], x[0])
|
||||
return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i
|
||||
} else if !attribute {
|
||||
maxLen := len(entityName) - 1
|
||||
if maxLen > longestEntityWithoutSemicolon {
|
||||
maxLen = longestEntityWithoutSemicolon
|
||||
}
|
||||
for j := maxLen; j > 1; j-- {
|
||||
if x := entity[entityName[:j]]; x != 0 {
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dst1, src1 = dst+i, src+i
|
||||
copy(b[dst:dst1], b[src:src1])
|
||||
return dst1, src1
|
||||
}
|
||||
|
||||
// unescape unescapes b's entities in-place, so that "a<b" becomes "a<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 = "&"
|
||||
case '\'':
|
||||
// "'" is shorter than "'" and apos was not in HTML until HTML5.
|
||||
esc = "'"
|
||||
case '<':
|
||||
esc = "<"
|
||||
case '>':
|
||||
esc = ">"
|
||||
case '"':
|
||||
// """ is shorter than """.
|
||||
esc = """
|
||||
case '\r':
|
||||
esc = " "
|
||||
default:
|
||||
panic("unrecognized escape character")
|
||||
}
|
||||
s = s[i+1:]
|
||||
if _, err := w.WriteString(esc); err != nil {
|
||||
return err
|
||||
}
|
||||
i = strings.IndexAny(s, escapedChars)
|
||||
}
|
||||
_, err := w.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// EscapeString escapes special characters like "<" to become "<". It
|
||||
// escapes only five such characters: <, >, &, ' and ".
|
||||
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't
|
||||
// always true.
|
||||
func EscapeString(s string) string {
|
||||
if strings.IndexAny(s, escapedChars) == -1 {
|
||||
return s
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
escape(&buf, s)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// UnescapeString unescapes entities like "<" to become "<". It unescapes a
|
||||
// larger range of entities than EscapeString escapes. For example, "á"
|
||||
// unescapes to "á", as does "á" and "&xE1;".
|
||||
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't
|
||||
// always true.
|
||||
func UnescapeString(s string) string {
|
||||
for _, c := range s {
|
||||
if c == '&' {
|
||||
return string(unescape([]byte(s), false))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
226
vendor/golang.org/x/net/html/foreign.go
generated
vendored
Normal file
226
vendor/golang.org/x/net/html/foreign.go
generated
vendored
Normal 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
193
vendor/golang.org/x/net/html/node.go
generated
vendored
Normal 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<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
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
271
vendor/golang.org/x/net/html/render.go
generated
vendored
Normal 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
Reference in New Issue
Block a user