Drop support for Bitbucket Server (#1994)

Closes #1962
This commit is contained in:
qwerty287 2023-07-13 15:47:25 +02:00 committed by GitHub
parent 874c2ea114
commit 570f5044e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 10 additions and 1546 deletions

View file

@ -421,52 +421,6 @@ var flags = []cli.Flag{
Usage: "gitlab skip ssl verification", Usage: "gitlab skip ssl verification",
}, },
// //
// Bitbucket Stash
//
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_STASH"},
Name: "stash",
Usage: "stash driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_URL"},
Name: "stash-server",
Usage: "stash server address",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_KEY"},
Name: "stash-consumer-key",
Usage: "stash oauth1 consumer key",
FilePath: os.Getenv("WOODPECKER_STASH_CONSUMER_KEY_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_RSA"},
Name: "stash-consumer-rsa",
Usage: "stash oauth1 private key file",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_RSA_STRING"},
Name: "stash-consumer-rsa-string",
Usage: "stash oauth1 private key string",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_GIT_USERNAME"},
Name: "stash-git-username",
Usage: "stash service account username",
FilePath: os.Getenv("WOODPECKER_STASH_GIT_USERNAME_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_STASH_GIT_PASSWORD"},
Name: "stash-git-password",
Usage: "stash service account password",
FilePath: os.Getenv("WOODPECKER_STASH_GIT_PASSWORD_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_STASH_SKIP_VERIFY"},
Name: "stash-skip-verify",
Usage: "stash skip ssl verification",
},
//
// development flags // development flags
// //
&cli.StringFlag{ &cli.StringFlag{

View file

@ -38,7 +38,6 @@ import (
"github.com/woodpecker-ci/woodpecker/server/cache" "github.com/woodpecker-ci/woodpecker/server/cache"
"github.com/woodpecker-ci/woodpecker/server/forge" "github.com/woodpecker-ci/woodpecker/server/forge"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucket" "github.com/woodpecker-ci/woodpecker/server/forge/bitbucket"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver"
"github.com/woodpecker-ci/woodpecker/server/forge/gitea" "github.com/woodpecker-ci/woodpecker/server/forge/gitea"
"github.com/woodpecker-ci/woodpecker/server/forge/github" "github.com/woodpecker-ci/woodpecker/server/forge/github"
"github.com/woodpecker-ci/woodpecker/server/forge/gitlab" "github.com/woodpecker-ci/woodpecker/server/forge/gitlab"
@ -193,8 +192,6 @@ func setupForge(c *cli.Context) (forge.Forge, error) {
return setupGitLab(c) return setupGitLab(c)
case c.Bool("bitbucket"): case c.Bool("bitbucket"):
return setupBitbucket(c) return setupBitbucket(c)
case c.Bool("stash"):
return setupStash(c)
case c.Bool("gitea"): case c.Bool("gitea"):
return setupGitea(c) return setupGitea(c)
default: default:
@ -231,21 +228,6 @@ func setupGitea(c *cli.Context) (forge.Forge, error) {
return gitea.New(opts) return gitea.New(opts)
} }
// setupStash helper function to setup the Stash forge from the CLI arguments.
func setupStash(c *cli.Context) (forge.Forge, error) {
opts := bitbucketserver.Opts{
URL: c.String("stash-server"),
Username: c.String("stash-git-username"),
Password: c.String("stash-git-password"),
ConsumerKey: c.String("stash-consumer-key"),
ConsumerRSA: c.String("stash-consumer-rsa"),
ConsumerRSAString: c.String("stash-consumer-rsa-string"),
SkipVerify: c.Bool("stash-skip-verify"),
}
log.Trace().Msgf("Forge (bitbucketserver) opts: %#v", opts)
return bitbucketserver.New(opts)
}
// setupGitLab helper function to setup the GitLab forge from the CLI arguments. // setupGitLab helper function to setup the GitLab forge from the CLI arguments.
func setupGitLab(c *cli.Context) (forge.Forge, error) { func setupGitLab(c *cli.Context) (forge.Forge, error) {
return gitlab.New(gitlab.Opts{ return gitlab.New(gitlab.Opts{

View file

@ -536,10 +536,6 @@ See [Gitea configuration](forges/gitea/#configuration)
See [Bitbucket configuration](forges/bitbucket/#configuration) See [Bitbucket configuration](forges/bitbucket/#configuration)
### `WOODPECKER_STASH_...`
See [Bitbucket server configuration](forges/bitbucket_server/#configuration)
### `WOODPECKER_GITLAB_...` ### `WOODPECKER_GITLAB_...`
See [Gitlab configuration](forges/gitlab/#configuration) See [Gitlab configuration](forges/gitlab/#configuration)

View file

@ -2,13 +2,13 @@
## Supported features ## Supported features
| Feature | [GitHub](github/) | [Gitea / Forgejo](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | | Feature | [GitHub](github/) | [Gitea / Forgejo](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) |
| --- | :---: | :---: | :---: | :---: | :---: | | --- | :---: | :---: | :---: | :---: |
| Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | | Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | | Event: Deploy | :white_check_mark: | :x: | :x: | :x: |
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | | [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | | [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: |
¹ for pull requests at least Gitea version 1.17 is required ¹ for pull requests at least Gitea version 1.17 is required

View file

@ -1,156 +0,0 @@
# Bitbucket Server
Woodpecker comes with experimental support for Bitbucket Server, formerly known as Atlassian Stash. To enable Bitbucket Server you should configure the Woodpecker container using the following environment variables:
```diff
# docker-compose.yml
version: '3'
services:
woodpecker-server:
[...]
environment:
- [...]
+ - WOODPECKER_STASH=true
+ - WOODPECKER_STASH_GIT_USERNAME=foo
+ - WOODPECKER_STASH_GIT_PASSWORD=bar
+ - WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
+ - WOODPECKER_STASH_CONSUMER_RSA=/etc/bitbucket/key.pem
+ - WOODPECKER_STASH_URL=http://stash.mycompany.com
volumes:
+ - /path/to/key.pem:/path/to/key.pem
woodpecker-agent:
[...]
```
## Private Key File
The OAuth process in Bitbucket server requires a private and a public RSA certificate. This is how you create the private RSA certificate.
```nohighlight
openssl genrsa -out /etc/bitbucket/key.pem 1024
```
This stores the private RSA certificate in `key.pem`. The next command generates the public RSA certificate and stores it in `key.pub`.
```nohighlight
openssl rsa -in /etc/bitbucket/key.pem -pubout >> /etc/bitbucket/key.pub
```
Please note that the private key file can be mounted into your Woodpecker container at runtime or as an environment variable
Private key file mounted into your Woodpecker container at runtime as a volume.
```diff
# docker-compose.yml
version: '3'
services:
woodpecker-server:
[...]
environment:
- [...]
- WOODPECKER_STASH=true
- WOODPECKER_STASH_GIT_USERNAME=foo
- WOODPECKER_STASH_GIT_PASSWORD=bar
- WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
+ - WOODPECKER_STASH_CONSUMER_RSA=/etc/bitbucket/key.pem
- WOODPECKER_STASH_URL=http://stash.mycompany.com
+ volumes:
+ - /etc/bitbucket/key.pem:/etc/bitbucket/key.pem
woodpecker-agent:
[...]
```
Private key as environment variable
```diff
# docker-compose.yml
version: '3'
services:
woodpecker-server:
[...]
environment:
- [...]
- WOODPECKER_STASH=true
- WOODPECKER_STASH_GIT_USERNAME=foo
- WOODPECKER_STASH_GIT_PASSWORD=bar
- WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
+ - WOODPECKER_STASH_CONSUMER_RSA_STRING=contentOfPemKeyAsString
- WOODPECKER_STASH_URL=http://stash.mycompany.com
woodpecker-agent:
[...]
```
## Service Account
Woodpecker uses `git+https` to clone repositories, however, Bitbucket Server does not currently support cloning repositories with OAuth token. To work around this limitation, you must create a service account and provide the username and password to Woodpecker. This service account will be used to authenticate and clone private repositories.
## Registration
You must register your application with Bitbucket Server in order to generate a consumer key. Navigate to your account settings and choose Applications from the menu, and click Register new application. Now copy & paste the text value from `/etc/bitbucket/key.pub` into the `Public Key` in the incoming link part of the application registration.
Please use http://woodpecker.mycompany.com/authorize as the Authorization callback URL.
## Configuration
This is a full list of configuration options. Please note that many of these options use default configuration values that should work for the majority of installations.
### `WOODPECKER_STASH`
> Default: `false`
Enables the Bitbucket Server driver.
### `WOODPECKER_STASH_URL`
> Default: empty
Configures the Bitbucket Server address.
### `WOODPECKER_STASH_CONSUMER_KEY`
> Default: empty
Configures your Bitbucket Server consumer key.
### `WOODPECKER_STASH_CONSUMER_KEY_FILE`
> Default: empty
Read the value for `WOODPECKER_STASH_CONSUMER_KEY` from the specified filepath
### `WOODPECKER_STASH_CONSUMER_RSA`
> Default: empty
Configures the path to your Bitbucket Server private key file.
### `WOODPECKER_STASH_CONSUMER_RSA_STRING`
> Default: empty
Configures your Bitbucket Server private key.
### `WOODPECKER_STASH_GIT_USERNAME`
> Default: empty
This username is used to authenticate and clone all private repositories.
### `WOODPECKER_STASH_GIT_USERNAME_FILE`
> Default: empty
Read the value for `WOODPECKER_STASH_GIT_USERNAME` from the specified filepath
### `WOODPECKER_STASH_GIT_PASSWORD`
> Default: empty
The password is used to authenticate and clone all private repositories.
### `WOODPECKER_STASH_GIT_PASSWORD_FILE`
> Default: empty
Read the value for `WOODPECKER_STASH_GIT_PASSWORD` from the specified filepath
### `WOODPECKER_STASH_SKIP_VERIFY`
> Default: `false`
Configure if SSL verification should be skipped.

View file

@ -16,7 +16,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Updated Prometheus gauge `*_job_*` to `*_step_*` - Updated Prometheus gauge `*_job_*` to `*_step_*`
- Renamed config env `WOODPECKER_MAX_PROCS` to `WOODPECKER_MAX_WORKFLOWS` (still available as fallback) - Renamed config env `WOODPECKER_MAX_PROCS` to `WOODPECKER_MAX_WORKFLOWS` (still available as fallback)
- The pipelines are now also read from `.yaml` files, the new default order is `.woodpecker/*.yml` and `.woodpecker/*.yaml` (without any prioritization) -> `.woodpecker.yml` -> `.woodpecker.yaml` - The pipelines are now also read from `.yaml` files, the new default order is `.woodpecker/*.yml` and `.woodpecker/*.yaml` (without any prioritization) -> `.woodpecker.yml` -> `.woodpecker.yaml`
- Dropped support for [Coding](https://coding.net/) and [Gogs](https://gogs.io). - Dropped support for [Coding](https://coding.net/), [Gogs](https://gogs.io) and Bitbucket Server (Stash).
- `/api/queue/resume` & `/api/queue/pause` endpoint methods were changed from `GET` to `POST` - `/api/queue/resume` & `/api/queue/pause` endpoint methods were changed from `GET` to `POST`
- rename `pipeline:` key in your workflow config to `steps:` - rename `pipeline:` key in your workflow config to `steps:`
- If you want to migrate old logs to the new format, watch the error messages on start. If there are none we are good to go, else you have to plan a migration that can take hours. Set `WOODPECKER_MIGRATIONS_ALLOW_LONG` to true and let it run. - If you want to migrate old logs to the new format, watch the error messages on start. If there are none we are good to go, else you have to plan a migration that can take hours. Set `WOODPECKER_MIGRATIONS_ALLOW_LONG` to true and let it run.

View file

@ -1,293 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitbucketserver
// WARNING! This is an work-in-progress patch and does not yet conform to the coding,
// quality or security standards expected of this project. Please use with caution.
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"os"
"github.com/mrjones/oauth"
"github.com/woodpecker-ci/woodpecker/server/forge"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/forge/common"
forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const (
requestTokenURL = "%s/plugins/servlet/oauth/request-token"
authorizeTokenURL = "%s/plugins/servlet/oauth/authorize"
accessTokenURL = "%s/plugins/servlet/oauth/access-token"
)
// Opts defines configuration options.
type Opts struct {
URL string // Stash server url.
Username string // Git machine account username.
Password string // Git machine account password.
ConsumerKey string // Oauth1 consumer key.
ConsumerRSA string // Oauth1 consumer key file.
ConsumerRSAString string
SkipVerify bool // Skip ssl verification.
}
type Config struct {
url string
Username string
Password string
SkipVerify bool
Consumer *oauth.Consumer
}
// New returns a Forge implementation that integrates with Bitbucket Server,
// the on-premise edition of Bitbucket Cloud, formerly known as Stash.
func New(opts Opts) (forge.Forge, error) {
config := &Config{
url: opts.URL,
Username: opts.Username,
Password: opts.Password,
SkipVerify: opts.SkipVerify,
}
switch {
case opts.Username == "":
return nil, fmt.Errorf("Must have a git machine account username")
case opts.Password == "":
return nil, fmt.Errorf("Must have a git machine account password")
case opts.ConsumerKey == "":
return nil, fmt.Errorf("Must have a oauth1 consumer key")
}
if opts.ConsumerRSA == "" && opts.ConsumerRSAString == "" {
return nil, fmt.Errorf("must have CONSUMER_RSA_KEY set to the path of a oauth1 consumer key file or CONSUMER_RSA_KEY_STRING set to the value of a oauth1 consumer key")
}
var keyFileBytes []byte
if opts.ConsumerRSA != "" {
var err error
keyFileBytes, err = os.ReadFile(opts.ConsumerRSA)
if err != nil {
return nil, err
}
} else {
keyFileBytes = []byte(opts.ConsumerRSAString)
}
block, _ := pem.Decode(keyFileBytes)
PrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
config.Consumer = CreateConsumer(opts.URL, opts.ConsumerKey, PrivateKey)
return config, nil
}
// Name returns the string name of this driver
func (c *Config) Name() string {
return "stash"
}
// URL returns the root url of a configured forge
func (c *Config) URL() string {
return c.url
}
func (c *Config) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
requestToken, u, err := c.Consumer.GetRequestTokenAndUrl("oob")
if err != nil {
return nil, err
}
code := req.FormValue("oauth_verifier")
if len(code) == 0 {
http.Redirect(res, req, u, http.StatusSeeOther)
return nil, nil
}
requestToken.Token = req.FormValue("oauth_token")
accessToken, err := c.Consumer.AuthorizeToken(requestToken, code)
if err != nil {
return nil, err
}
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, accessToken.Token)
user, err := client.FindCurrentUser()
if err != nil {
return nil, err
}
return convertUser(user, accessToken), nil
}
// Auth is not supported by the Stash driver.
func (*Config) Auth(_ context.Context, _, _ string) (string, error) {
return "", fmt.Errorf("Not Implemented")
}
// Teams is not supported by the Stash driver.
func (*Config) Teams(_ context.Context, _ *model.User) ([]*model.Team, error) {
var teams []*model.Team
return teams, nil
}
// TeamPerm is not supported by the Stash driver.
func (*Config) TeamPerm(_ *model.User, _ string) (*model.Perm, error) {
return nil, nil
}
func (c *Config) Repo(ctx context.Context, u *model.User, _ model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
repo, err := client.FindRepo(owner, name)
if err != nil {
return nil, err
}
perm, err := client.FindRepoPerms(repo.Project.Key, repo.Name)
if err != nil {
return nil, err
}
return convertRepo(repo, perm), nil
}
func (c *Config) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
repos, err := client.FindRepos()
if err != nil {
return nil, err
}
var all []*model.Repo
for _, repo := range repos {
perm, err := client.FindRepoPerms(repo.Project.Key, repo.Name)
if err != nil {
return nil, err
}
all = append(all, convertRepo(repo, perm))
}
return all, nil
}
func (c *Config) File(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]byte, error) {
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
return client.FindFileForRepo(r.Owner, r.Name, f, p.Ref)
}
func (c *Config) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ string) ([]*forge_types.FileMeta, error) {
return nil, forge_types.ErrNotImplemented
}
// Status is not supported by the bitbucketserver driver.
func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Workflow) error {
status := internal.PipelineStatus{
State: convertStatus(pipeline.Status),
Desc: common.GetPipelineStatusDescription(pipeline.Status),
Name: fmt.Sprintf("Woodpecker #%d - %s", pipeline.Number, pipeline.Branch),
Key: "Woodpecker",
URL: common.GetPipelineStatusLink(repo, pipeline, nil),
}
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, user.Token)
return client.CreateStatus(pipeline.Commit, &status)
}
func (c *Config) Netrc(_ *model.User, r *model.Repo) (*model.Netrc, error) {
host, err := common.ExtractHostFromCloneURL(r.Clone)
if err != nil {
return nil, err
}
return &model.Netrc{
Login: c.Username,
Password: c.Password,
Machine: host,
}, nil
}
func (c *Config) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
return client.CreateHook(r.Owner, r.Name, link)
}
// Branches returns the names of all branches for the named repository.
func (c *Config) Branches(ctx context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]string, error) {
bitbucketBranches, err := internal.NewClientWithToken(ctx, c.url, c.Consumer, common.UserToken(ctx, r, u)).ListBranches(r.Owner, r.Name, p.Page, p.PerPage)
if err != nil {
return nil, err
}
branches := make([]string, 0)
for _, branch := range bitbucketBranches {
branches = append(branches, branch.Name)
}
return branches, nil
}
// BranchHead returns the sha of the head (latest commit) of the specified branch
func (c *Config) BranchHead(_ context.Context, _ *model.User, _ *model.Repo, _ string) (string, error) {
// TODO(1138): missing implementation
return "", forge_types.ErrNotImplemented
}
func (c *Config) PullRequests(_ context.Context, _ *model.User, _ *model.Repo, _ *model.ListOptions) ([]*model.PullRequest, error) {
return nil, forge_types.ErrNotImplemented
}
func (c *Config) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
return client.DeleteHook(r.Owner, r.Name, link)
}
func (c *Config) Hook(_ context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
return parseHook(r, c.url)
}
// OrgMembership returns if user is member of organization and if user
// is admin/owner in this organization.
func (c *Config) OrgMembership(_ context.Context, _ *model.User, _ string) (*model.OrgPerm, error) {
// TODO: Not implemented currently
return nil, nil
}
func CreateConsumer(URL, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer {
consumer := oauth.NewRSAConsumer(
ConsumerKey,
PrivateKey,
oauth.ServiceProvider{
RequestTokenUrl: fmt.Sprintf(requestTokenURL, URL),
AuthorizeTokenUrl: fmt.Sprintf(authorizeTokenURL, URL),
AccessTokenUrl: fmt.Sprintf(accessTokenURL, URL),
HttpMethod: "POST",
})
consumer.HttpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
},
}
return consumer
}

View file

@ -1,138 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitbucketserver
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/url"
"strings"
"time"
"github.com/mrjones/oauth"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const (
statusPending = "INPROGRESS"
statusSuccess = "SUCCESSFUL"
statusFailure = "FAILED"
)
// convertStatus is a helper function used to convert a Woodpecker status to a
// Bitbucket commit status.
func convertStatus(status model.StatusValue) string {
switch status {
case model.StatusPending, model.StatusRunning:
return statusPending
case model.StatusSuccess:
return statusSuccess
default:
return statusFailure
}
}
// convertRepo is a helper function used to convert a Bitbucket server repository
// structure to the common Woodpecker repository structure.
func convertRepo(from *internal.Repo, perm *model.Perm) *model.Repo {
repo := model.Repo{
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)),
Name: from.Slug,
Owner: from.Project.Key,
Branch: "master",
SCMKind: model.RepoGit,
IsSCMPrivate: true, // Since we have to use Netrc it has to always be private :/
FullName: fmt.Sprintf("%s/%s", from.Project.Key, from.Slug),
Perm: perm,
}
for _, item := range from.Links.Clone {
if item.Name == "http" {
uri, err := url.Parse(item.Href)
if err != nil {
return nil
}
uri.User = nil
repo.Clone = uri.String()
}
}
for _, item := range from.Links.Self {
if item.Href != "" {
repo.Link = item.Href
}
}
return &repo
}
// convertPushHook is a helper function used to convert a Bitbucket push
// hook to the Woodpecker pipeline struct holding commit information.
func convertPushHook(hook *internal.PostHook, baseURL string) *model.Pipeline {
branch := strings.TrimPrefix(
strings.TrimPrefix(
hook.RefChanges[0].RefID,
"refs/heads/",
),
"refs/tags/",
)
// Ensuring the author label is not longer then 40 for the label of the commit author (default size in the db)
authorLabel := hook.Changesets.Values[0].ToCommit.Author.Name
if len(authorLabel) > 40 {
authorLabel = authorLabel[0:37] + "..."
}
pipeline := &model.Pipeline{
Commit: hook.RefChanges[0].ToHash, // TODO check for index value
Branch: branch,
Message: hook.Changesets.Values[0].ToCommit.Message, // TODO check for index Values
Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress),
Author: authorLabel,
Email: hook.Changesets.Values[0].ToCommit.Author.EmailAddress,
Timestamp: time.Now().UTC().Unix(),
Ref: hook.RefChanges[0].RefID, // TODO check for index Values
Link: fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", baseURL, hook.Repository.Project.Key, hook.Repository.Slug, hook.RefChanges[0].ToHash),
}
if strings.HasPrefix(hook.RefChanges[0].RefID, "refs/tags/") {
pipeline.Event = model.EventTag
} else {
pipeline.Event = model.EventPush
}
return pipeline
}
// convertUser is a helper function used to convert a Bitbucket user account
// structure to the Woodpecker User structure.
func convertUser(from *internal.User, token *oauth.AccessToken) *model.User {
return &model.User{
Login: from.Slug,
Token: token.Token,
Email: from.EmailAddress,
Avatar: avatarLink(from.EmailAddress),
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)),
}
}
func avatarLink(email string) string {
hasher := md5.New()
hasher.Write([]byte(strings.ToLower(email)))
emailHash := fmt.Sprintf("%v", hex.EncodeToString(hasher.Sum(nil)))
avatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s.jpg", emailHash)
return avatarURL
}

View file

@ -1,156 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitbucketserver
import (
"testing"
"github.com/franela/goblin"
"github.com/mrjones/oauth"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func Test_helper(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Bitbucket Server converter", func() {
g.It("should convert repository", func() {
from := &internal.Repo{
Slug: "hello-world",
}
from.Project.Key = "octocat"
// var links [1]internal.LinkType
link := internal.CloneLink{
Name: "http",
Href: "https://x7hw@server.org/foo/bar.git",
}
from.Links.Clone = append(from.Links.Clone, link)
selfRef := internal.SelfRefLink{
Href: "https://server.org/foo/bar",
}
from.Links.Self = append(from.Links.Self, selfRef)
to := convertRepo(from, &model.Perm{Pull: true})
g.Assert(to.FullName).Equal("octocat/hello-world")
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
g.Assert(to.Branch).Equal("master")
g.Assert(to.SCMKind).Equal(model.RepoGit)
g.Assert(to.IsSCMPrivate).Equal(true)
g.Assert(to.Clone).Equal("https://server.org/foo/bar.git")
g.Assert(to.Link).Equal("https://server.org/foo/bar")
g.Assert(to.Perm.Pull).IsTrue()
})
g.It("should convert user", func() {
token := &oauth.AccessToken{
Token: "foo",
}
user := &internal.User{
Slug: "x12f",
EmailAddress: "huh@huh.com",
}
result := convertUser(user, token)
g.Assert(result.Avatar).Equal(avatarLink("huh@huh.com"))
g.Assert(result.Login).Equal("x12f")
g.Assert(result.Token).Equal("foo")
})
g.It("branch should be empty", func() {
change := internal.PostHook{}
change.RefChanges = append(change.RefChanges, internal.RefChange{
RefID: "refs/heads/",
ToHash: "73f9c44d",
})
value := internal.Value{}
value.ToCommit.Author.Name = "John Doe, Appleboy, Mary, Janet E. Dawson and Ann S. Palmer"
value.ToCommit.Author.EmailAddress = "huh@huh.com"
value.ToCommit.Message = "message"
change.Changesets.Values = append(change.Changesets.Values, value)
change.Repository.Project = internal.Project{
Key: "octocat",
}
change.Repository.Slug = "hello-world"
pipeline := convertPushHook(&change, "http://base.com")
g.Assert(pipeline.Branch).Equal("")
})
g.It("should convert push hook to pipeline", func() {
change := internal.PostHook{}
change.RefChanges = append(change.RefChanges, internal.RefChange{
RefID: "refs/heads/release/some-feature",
ToHash: "73f9c44d",
})
value := internal.Value{}
value.ToCommit.Author.Name = "John Doe, Appleboy, Mary, Janet E. Dawson and Ann S. Palmer"
value.ToCommit.Author.EmailAddress = "huh@huh.com"
value.ToCommit.Message = "message"
change.Changesets.Values = append(change.Changesets.Values, value)
change.Repository.Project.Key = "octocat"
change.Repository.Slug = "hello-world"
pipeline := convertPushHook(&change, "http://base.com")
g.Assert(pipeline.Event).Equal(model.EventPush)
// Ensuring the author label is not longer then 40
g.Assert(pipeline.Author).Equal("John Doe, Appleboy, Mary, Janet E. Da...")
g.Assert(pipeline.Avatar).Equal(avatarLink("huh@huh.com"))
g.Assert(pipeline.Commit).Equal("73f9c44d")
g.Assert(pipeline.Branch).Equal("release/some-feature")
g.Assert(pipeline.Link).Equal("http://base.com/projects/octocat/repos/hello-world/commits/73f9c44d")
g.Assert(pipeline.Ref).Equal("refs/heads/release/some-feature")
g.Assert(pipeline.Message).Equal("message")
})
g.It("should convert tag hook to pipeline", func() {
change := internal.PostHook{}
change.RefChanges = append(change.RefChanges, internal.RefChange{
RefID: "refs/tags/v1",
ToHash: "73f9c44d",
})
value := internal.Value{}
value.ToCommit.Author.Name = "John Doe"
value.ToCommit.Author.EmailAddress = "huh@huh.com"
value.ToCommit.Message = "message"
change.Changesets.Values = append(change.Changesets.Values, value)
change.Repository.Project.Key = "octocat"
change.Repository.Slug = "hello-world"
pipeline := convertPushHook(&change, "http://base.com")
g.Assert(pipeline.Event).Equal(model.EventTag)
g.Assert(pipeline.Author).Equal("John Doe")
g.Assert(pipeline.Avatar).Equal(avatarLink("huh@huh.com"))
g.Assert(pipeline.Commit).Equal("73f9c44d")
g.Assert(pipeline.Branch).Equal("v1")
g.Assert(pipeline.Link).Equal("http://base.com/projects/octocat/repos/hello-world/commits/73f9c44d")
g.Assert(pipeline.Ref).Equal("refs/tags/v1")
g.Assert(pipeline.Message).Equal("message")
})
})
}

View file

@ -1,473 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/mrjones/oauth"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const (
currentUserID = "%s/plugins/servlet/applinks/whoami"
pathUser = "%s/rest/api/1.0/users/%s"
pathRepo = "%s/rest/api/1.0/projects/%s/repos/%s"
pathRepos = "%s/rest/api/1.0/repos?start=%s&limit=%s"
pathHook = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
pathSource = "%s/projects/%s/repos/%s/browse/%s?at=%s&raw"
hookName = "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook"
pathHookDetails = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
pathHookEnabled = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled"
pathHookSettings = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/settings"
pathStatus = "%s/rest/build-status/1.0/commits/%s"
pathBranches = "%s/rest/api/1.0/projects/%s/repos/%s/branches?limit=%d&start=%d"
)
type Client struct {
client *http.Client
base string
accessToken string
ctx context.Context
}
func NewClientWithToken(ctx context.Context, url string, consumer *oauth.Consumer, AccessToken string) *Client {
var token oauth.AccessToken
token.Token = AccessToken
client, err := consumer.MakeHttpClient(&token)
if err != nil {
log.Err(err).Msg("")
}
return &Client{
client: client,
base: url,
accessToken: AccessToken,
ctx: ctx,
}
}
func (c *Client) FindCurrentUser() (*User, error) {
currentUserIDResponse, err := c.doGet(fmt.Sprintf(currentUserID, c.base))
if err != nil {
return nil, err
}
defer currentUserIDResponse.Body.Close()
bits, err := io.ReadAll(currentUserIDResponse.Body)
if err != nil {
return nil, err
}
login := string(bits)
currentUserResponse, err := c.doGet(fmt.Sprintf(pathUser, c.base, login))
if currentUserResponse != nil {
defer currentUserResponse.Body.Close()
}
if err != nil {
return nil, err
}
contents, err := io.ReadAll(currentUserResponse.Body)
if err != nil {
return nil, err
}
var user User
err = json.Unmarshal(contents, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (c *Client) FindRepo(owner, name string) (*Repo, error) {
urlString := fmt.Sprintf(pathRepo, c.base, owner, name)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
log.Err(err).Msg("")
}
contents, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
repo := Repo{}
err = json.Unmarshal(contents, &repo)
if err != nil {
return nil, err
}
return &repo, nil
}
func (c *Client) FindRepos() ([]*Repo, error) {
return c.paginatedRepos(0)
}
func (c *Client) FindRepoPerms(owner, repo string) (*model.Perm, error) {
perms := new(model.Perm)
// If you don't have access return none right away
_, err := c.FindRepo(owner, repo)
if err != nil {
return perms, err
}
// Must have admin to be able to list hooks. If have access the enable perms
resp, err := c.doGet(fmt.Sprintf(pathHook, c.base, owner, repo, hookName))
if resp != nil {
defer resp.Body.Close()
}
if err == nil {
perms.Push = true
perms.Admin = true
}
perms.Pull = true
return perms, nil
}
func (c *Client) FindFileForRepo(owner, repo, fileName, ref string) ([]byte, error) {
response, err := c.doGet(fmt.Sprintf(pathSource, c.base, owner, repo, fileName, ref))
if response != nil {
defer response.Body.Close()
}
if err != nil {
log.Err(err).Msg("")
}
if response.StatusCode == 404 {
return nil, nil
}
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
log.Err(err).Msg("")
}
return responseBytes, nil
}
func (c *Client) CreateHook(owner, name, callBackLink string) error {
hookDetails, err := c.GetHookDetails(owner, name)
if err != nil {
return err
}
var hooks []string
if hookDetails.Enabled {
hookSettings, err := c.GetHooks(owner, name)
if err != nil {
return err
}
hooks = hookSettingsToArray(hookSettings)
}
if !stringInSlice(callBackLink, hooks) {
hooks = append(hooks, callBackLink)
}
putHookSettings := arrayToHookSettings(hooks)
hookBytes, err := json.Marshal(putHookSettings)
if err != nil {
return err
}
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
}
func (c *Client) CreateStatus(revision string, status *PipelineStatus) error {
uri := fmt.Sprintf(pathStatus, c.base, revision)
return c.doPost(uri, status)
}
func (c *Client) DeleteHook(owner, name, link string) error {
hookSettings, err := c.GetHooks(owner, name)
if err != nil {
return err
}
putHooks := filter(hookSettingsToArray(hookSettings), func(item string) bool {
return !strings.Contains(item, link)
})
putHookSettings := arrayToHookSettings(putHooks)
hookBytes, err := json.Marshal(putHookSettings)
if err != nil {
return err
}
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
}
func (c *Client) GetHookDetails(owner, name string) (*HookPluginDetails, error) {
urlString := fmt.Sprintf(pathHookDetails, c.base, owner, name, hookName)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
hookDetails := HookPluginDetails{}
err = json.NewDecoder(response.Body).Decode(&hookDetails)
return &hookDetails, err
}
func (c *Client) GetHooks(owner, name string) (*HookSettings, error) {
urlString := fmt.Sprintf(pathHookSettings, c.base, owner, name, hookName)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
hookSettings := HookSettings{}
err = json.NewDecoder(response.Body).Decode(&hookSettings)
return &hookSettings, err
}
// TODO: make these as as general do with the action
// Helper function to help create get
func (c *Client) doGet(url string) (*http.Response, error) {
request, err := http.NewRequestWithContext(c.ctx, "GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/json")
return c.client.Do(request)
}
// Helper function to help create the hook
func (c *Client) doPut(url string, body []byte) error {
request, err := http.NewRequestWithContext(c.ctx, "PUT", url, bytes.NewBuffer(body))
if err != nil {
return err
}
request.Header.Add("Content-Type", "application/json")
response, err := c.client.Do(request)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return err
}
return nil
}
// Helper function to help create the hook
func (c *Client) doPost(url string, status *PipelineStatus) error {
// write it to the body of the request.
var buf io.ReadWriter
if status != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(status)
if err != nil {
return err
}
}
request, err := http.NewRequestWithContext(c.ctx, "POST", url, buf)
if err != nil {
return err
}
request.Header.Add("Content-Type", "application/json")
response, err := c.client.Do(request)
if response != nil {
defer response.Body.Close()
}
return err
}
// Helper function to get repos paginated
func (c *Client) paginatedRepos(start int) ([]*Repo, error) {
limit := 1000
requestURL := fmt.Sprintf(pathRepos, c.base, strconv.Itoa(start), strconv.Itoa(limit))
response, err := c.doGet(requestURL)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
var repoResponse Repos
err = json.NewDecoder(response.Body).Decode(&repoResponse)
if err != nil {
return nil, err
}
if !repoResponse.IsLastPage {
reposList, err := c.paginatedRepos(start + limit)
if err != nil {
return nil, err
}
repoResponse.Values = append(repoResponse.Values, reposList...)
}
return repoResponse.Values, nil
}
func (c *Client) ListBranches(owner, name string, page, limit int) ([]*Branch, error) {
uri := fmt.Sprintf(pathBranches, c.base, owner, name, limit, limit*(page-1))
response, err := c.doGet(uri)
if err != nil {
return nil, err
}
defer response.Body.Close()
out := new(BranchResp)
err = json.NewDecoder(response.Body).Decode(&out)
return out.Values, err
}
func filter(vs []string, f func(string) bool) []string {
var vsf []string
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}
// TODO: find a clean way of doing these next two methods- bitbucket server hooks only support 20 cb hooks
func arrayToHookSettings(hooks []string) HookSettings {
hookSettings := HookSettings{}
for loc, value := range hooks {
switch loc {
case 0:
hookSettings.HookURL0 = value
case 1:
hookSettings.HookURL1 = value
case 2:
hookSettings.HookURL2 = value
case 3:
hookSettings.HookURL3 = value
case 4:
hookSettings.HookURL4 = value
case 5:
hookSettings.HookURL5 = value
case 6:
hookSettings.HookURL6 = value
case 7:
hookSettings.HookURL7 = value
case 8:
hookSettings.HookURL8 = value
case 9:
hookSettings.HookURL9 = value
case 10:
hookSettings.HookURL10 = value
case 11:
hookSettings.HookURL11 = value
case 12:
hookSettings.HookURL12 = value
case 13:
hookSettings.HookURL13 = value
case 14:
hookSettings.HookURL14 = value
case 15:
hookSettings.HookURL15 = value
case 16:
hookSettings.HookURL16 = value
case 17:
hookSettings.HookURL17 = value
case 18:
hookSettings.HookURL18 = value
case 19:
hookSettings.HookURL19 = value
// Since there's only 19 hooks it will add to the latest if it doesn't exist :/
default:
hookSettings.HookURL19 = value
}
}
return hookSettings
}
func hookSettingsToArray(hookSettings *HookSettings) []string {
var hooks []string
if hookSettings.HookURL0 != "" {
hooks = append(hooks, hookSettings.HookURL0)
}
if hookSettings.HookURL1 != "" {
hooks = append(hooks, hookSettings.HookURL1)
}
if hookSettings.HookURL2 != "" {
hooks = append(hooks, hookSettings.HookURL2)
}
if hookSettings.HookURL3 != "" {
hooks = append(hooks, hookSettings.HookURL3)
}
if hookSettings.HookURL4 != "" {
hooks = append(hooks, hookSettings.HookURL4)
}
if hookSettings.HookURL5 != "" {
hooks = append(hooks, hookSettings.HookURL5)
}
if hookSettings.HookURL6 != "" {
hooks = append(hooks, hookSettings.HookURL6)
}
if hookSettings.HookURL7 != "" {
hooks = append(hooks, hookSettings.HookURL7)
}
if hookSettings.HookURL8 != "" {
hooks = append(hooks, hookSettings.HookURL8)
}
if hookSettings.HookURL9 != "" {
hooks = append(hooks, hookSettings.HookURL9)
}
if hookSettings.HookURL10 != "" {
hooks = append(hooks, hookSettings.HookURL10)
}
if hookSettings.HookURL11 != "" {
hooks = append(hooks, hookSettings.HookURL11)
}
if hookSettings.HookURL12 != "" {
hooks = append(hooks, hookSettings.HookURL12)
}
if hookSettings.HookURL13 != "" {
hooks = append(hooks, hookSettings.HookURL13)
}
if hookSettings.HookURL14 != "" {
hooks = append(hooks, hookSettings.HookURL14)
}
if hookSettings.HookURL15 != "" {
hooks = append(hooks, hookSettings.HookURL15)
}
if hookSettings.HookURL16 != "" {
hooks = append(hooks, hookSettings.HookURL16)
}
if hookSettings.HookURL17 != "" {
hooks = append(hooks, hookSettings.HookURL17)
}
if hookSettings.HookURL18 != "" {
hooks = append(hooks, hookSettings.HookURL18)
}
if hookSettings.HookURL19 != "" {
hooks = append(hooks, hookSettings.HookURL19)
}
return hooks
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

View file

@ -1,215 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
type User struct {
Active bool `json:"active"`
DisplayName string `json:"displayName"`
EmailAddress string `json:"emailAddress"`
ID int `json:"id"`
Links struct {
Self []struct {
Href string `json:"href"`
} `json:"self"`
} `json:"links"`
Name string `json:"name"`
Slug string `json:"slug"`
Type string `json:"type"`
}
type CloneLink struct {
Href string `json:"href"`
Name string `json:"name"`
}
type SelfRefLink struct {
Href string `json:"href"`
}
type PipelineStatus struct {
State string `json:"state"`
Key string `json:"key"`
Name string `json:"name,omitempty"`
URL string `json:"url"`
Desc string `json:"description,omitempty"`
}
type Repo struct {
Forkable bool `json:"forkable"`
ID int `json:"id"`
Links struct {
Clone []CloneLink `json:"clone"`
Self []struct {
Href string `json:"href"`
} `json:"self"`
} `json:"links"`
Name string `json:"name"`
Project Project `json:"project"`
Public bool `json:"public"`
ScmID string `json:"scmId"`
Slug string `json:"slug"`
State string `json:"state"`
StatusMessage string `json:"statusMessage"`
}
type Project struct {
Description string `json:"description"`
ID int `json:"id"`
Key string `json:"key"`
Links struct {
Self []SelfRefLink `json:"self"`
} `json:"links"`
Name string `json:"name"`
Public bool `json:"public"`
Type string `json:"type"`
}
type Repos struct {
IsLastPage bool `json:"isLastPage"`
Limit int `json:"limit"`
Size int `json:"size"`
Start int `json:"start"`
Values []*Repo `json:"values"`
}
type Hook struct {
Enabled bool `json:"enabled"`
Details *HookDetail `json:"details"`
}
type HookDetail struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Version string `json:"version"`
ConfigFormKey string `json:"configFormKey"`
}
type Value struct {
Changes struct {
Filter interface{} `json:"filter"`
IsLastPage bool `json:"isLastPage"`
Limit int `json:"limit"`
Size int `json:"size"`
Start int `json:"start"`
Values []struct {
ContentID string `json:"contentId"`
Executable bool `json:"executable"`
Link struct {
Rel string `json:"rel"`
URL string `json:"url"`
} `json:"link"`
NodeType string `json:"nodeType"`
Path struct {
Components []string `json:"components"`
Extension string `json:"extension"`
Name string `json:"name"`
Parent string `json:"parent"`
ToString string `json:"toString"`
} `json:"path"`
PercentUnchanged int `json:"percentUnchanged"`
SrcExecutable bool `json:"srcExecutable"`
Type string `json:"type"`
} `json:"values"`
} `json:"changes"`
FromCommit struct {
DisplayID string `json:"displayId"`
ID string `json:"id"`
} `json:"fromCommit"`
Link struct {
Rel string `json:"rel"`
URL string `json:"url"`
} `json:"link"`
ToCommit struct {
Author struct {
EmailAddress string `json:"emailAddress"`
Name string `json:"name"`
} `json:"author"`
AuthorTimestamp int `json:"authorTimestamp"`
DisplayID string `json:"displayId"`
ID string `json:"id"`
Message string `json:"message"`
Parents []struct {
DisplayID string `json:"displayId"`
ID string `json:"id"`
} `json:"parents"`
} `json:"toCommit"`
}
type PostHook struct {
Changesets struct {
Filter interface{} `json:"filter"`
IsLastPage bool `json:"isLastPage"`
Limit int `json:"limit"`
Size int `json:"size"`
Start int `json:"start"`
Values []Value `json:"values"`
} `json:"changesets"`
RefChanges []RefChange `json:"refChanges"`
Repository Repo `json:"repository"`
}
type RefChange struct {
FromHash string `json:"fromHash"`
RefID string `json:"refId"`
ToHash string `json:"toHash"`
Type string `json:"type"`
}
type HookPluginDetails struct {
Details struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Version string `json:"version"`
ConfigFormKey string `json:"configFormKey"`
} `json:"details"`
Enabled bool `json:"enabled"`
Configured bool `json:"configured"`
}
type HookSettings struct {
HookURL0 string `json:"hook-url-0,omitempty"`
HookURL1 string `json:"hook-url-1,omitempty"`
HookURL2 string `json:"hook-url-2,omitempty"`
HookURL3 string `json:"hook-url-3,omitempty"`
HookURL4 string `json:"hook-url-4,omitempty"`
HookURL5 string `json:"hook-url-5,omitempty"`
HookURL6 string `json:"hook-url-6,omitempty"`
HookURL7 string `json:"hook-url-7,omitempty"`
HookURL8 string `json:"hook-url-8,omitempty"`
HookURL9 string `json:"hook-url-9,omitempty"`
HookURL10 string `json:"hook-url-10,omitempty"`
HookURL11 string `json:"hook-url-11,omitempty"`
HookURL12 string `json:"hook-url-12,omitempty"`
HookURL13 string `json:"hook-url-13,omitempty"`
HookURL14 string `json:"hook-url-14,omitempty"`
HookURL15 string `json:"hook-url-15,omitempty"`
HookURL16 string `json:"hook-url-16,omitempty"`
HookURL17 string `json:"hook-url-17,omitempty"`
HookURL18 string `json:"hook-url-18,omitempty"`
HookURL19 string `json:"hook-url-19,omitempty"`
}
type BranchResp struct {
Values []*Branch `json:"values"`
}
type Branch struct {
Name string `json:"displayId"`
}

View file

@ -1,37 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitbucketserver
import (
"encoding/json"
"net/http"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/model"
)
// parseHook parses a Bitbucket hook from an http.Request request and returns
// Repo and Pipeline detail. TODO: find a way to support PR hooks
func parseHook(r *http.Request, baseURL string) (*model.Repo, *model.Pipeline, error) {
hook := new(internal.PostHook)
if err := json.NewDecoder(r.Body).Decode(hook); err != nil {
return nil, nil, err
}
pipeline := convertPushHook(hook, baseURL)
repo := convertRepo(&hook.Repository, &model.Perm{})
return repo, pipeline, nil
}

View file

@ -21,7 +21,7 @@
<Icon v-if="forge === 'github'" name="github" /> <Icon v-if="forge === 'github'" name="github" />
<Icon v-else-if="forge === 'gitea'" name="gitea" /> <Icon v-else-if="forge === 'gitea'" name="gitea" />
<Icon v-else-if="forge === 'gitlab'" name="gitlab" /> <Icon v-else-if="forge === 'gitlab'" name="gitlab" />
<Icon v-else-if="forge === 'bitbucket' || forge === 'stash'" name="bitbucket" /> <Icon v-else-if="forge === 'bitbucket'" name="bitbucket" />
<Icon v-else name="repo" /> <Icon v-else name="repo" />
</IconButton> </IconButton>
<IconButton <IconButton