mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-21 16:06:29 +00:00
Drop coding support (#1644)
Coding support is likely broken and nobody will ever fix it. Also it looks like nobody wants to use it, otherwise we would have get some bug reports. --------- Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
parent
7ddc18348f
commit
37dc8a46e0
20 changed files with 11 additions and 2235 deletions
|
@ -453,65 +453,6 @@ var flags = []cli.Flag{
|
||||||
Usage: "stash skip ssl verification",
|
Usage: "stash skip ssl verification",
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
// Coding
|
|
||||||
//
|
|
||||||
&cli.BoolFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING"},
|
|
||||||
Name: "coding",
|
|
||||||
Usage: "coding driver is enabled",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_URL"},
|
|
||||||
Name: "coding-server",
|
|
||||||
Usage: "coding server address",
|
|
||||||
Value: "https://coding.net",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_CLIENT"},
|
|
||||||
Name: "coding-client",
|
|
||||||
Usage: "coding oauth2 client id",
|
|
||||||
FilePath: os.Getenv("WOODPECKER_CODING_CLIENT_FILE"),
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_SECRET"},
|
|
||||||
Name: "coding-secret",
|
|
||||||
Usage: "coding oauth2 client secret",
|
|
||||||
FilePath: os.Getenv("WOODPECKER_CODING_SECRET_FILE"),
|
|
||||||
},
|
|
||||||
&cli.StringSliceFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_SCOPE"},
|
|
||||||
Name: "coding-scope",
|
|
||||||
Usage: "coding oauth scope",
|
|
||||||
Value: cli.NewStringSlice(
|
|
||||||
"user",
|
|
||||||
"project",
|
|
||||||
"project:depot",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_GIT_MACHINE"},
|
|
||||||
Name: "coding-git-machine",
|
|
||||||
Usage: "coding machine name",
|
|
||||||
Value: "git.coding.net",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_GIT_USERNAME"},
|
|
||||||
Name: "coding-git-username",
|
|
||||||
Usage: "coding machine user username",
|
|
||||||
FilePath: os.Getenv("WOODPECKER_CODING_GIT_USERNAME_FILE"),
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_GIT_PASSWORD"},
|
|
||||||
Name: "coding-git-password",
|
|
||||||
Usage: "coding machine user password",
|
|
||||||
FilePath: os.Getenv("WOODPECKER_CODING_GIT_PASSWORD_FILE"),
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
EnvVars: []string{"WOODPECKER_CODING_SKIP_VERIFY"},
|
|
||||||
Name: "coding-skip-verify",
|
|
||||||
Usage: "coding skip ssl verification",
|
|
||||||
},
|
|
||||||
//
|
|
||||||
// development flags
|
// development flags
|
||||||
//
|
//
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
|
|
|
@ -39,7 +39,6 @@ import (
|
||||||
"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/bitbucketserver"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/forge/coding"
|
|
||||||
"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"
|
||||||
|
@ -201,8 +200,6 @@ func setupForge(c *cli.Context) (forge.Forge, error) {
|
||||||
return setupGogs(c)
|
return setupGogs(c)
|
||||||
case c.Bool("gitea"):
|
case c.Bool("gitea"):
|
||||||
return setupGitea(c)
|
return setupGitea(c)
|
||||||
case c.Bool("coding"):
|
|
||||||
return setupCoding(c)
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("version control system not configured")
|
return nil, fmt.Errorf("version control system not configured")
|
||||||
}
|
}
|
||||||
|
@ -288,21 +285,6 @@ func setupGitHub(c *cli.Context) (forge.Forge, error) {
|
||||||
return github.New(opts)
|
return github.New(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to setup the Coding forge from the CLI arguments.
|
|
||||||
func setupCoding(c *cli.Context) (forge.Forge, error) {
|
|
||||||
opts := coding.Opts{
|
|
||||||
URL: c.String("coding-server"),
|
|
||||||
Client: c.String("coding-client"),
|
|
||||||
Secret: c.String("coding-secret"),
|
|
||||||
Scopes: c.StringSlice("coding-scope"),
|
|
||||||
Username: c.String("coding-git-username"),
|
|
||||||
Password: c.String("coding-git-password"),
|
|
||||||
SkipVerify: c.Bool("coding-skip-verify"),
|
|
||||||
}
|
|
||||||
log.Trace().Msgf("Forge (coding) opts: %#v", opts)
|
|
||||||
return coding.New(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupMetrics(g *errgroup.Group, _store store.Store) {
|
func setupMetrics(g *errgroup.Group, _store store.Store) {
|
||||||
pendingSteps := promauto.NewGauge(prometheus.GaugeOpts{
|
pendingSteps := promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Namespace: "woodpecker",
|
Namespace: "woodpecker",
|
||||||
|
|
|
@ -415,7 +415,3 @@ See [Bitbucket server configuration](forges/bitbucket_server/#configuration)
|
||||||
### `WOODPECKER_GITLAB_...`
|
### `WOODPECKER_GITLAB_...`
|
||||||
|
|
||||||
See [Gitlab configuration](forges/gitlab/#configuration)
|
See [Gitlab configuration](forges/gitlab/#configuration)
|
||||||
|
|
||||||
### `WOODPECKER_CODING_...`
|
|
||||||
|
|
||||||
See [Coding configuration](forges/coding/#configuration)
|
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
## Supported features
|
## Supported features
|
||||||
|
|
||||||
| Feature | [GitHub](github/) | [Gitea](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | [Gogs](gogs/) | [Coding](coding/) |
|
| Feature | [GitHub](github/) | [Gitea](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | [Gogs](gogs/) |
|
||||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
| --- | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||||
| Event: Push | :white_check_mark: | :white_check_mark: | :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: | :white_check_mark: | :white_check_mark: |
|
||||||
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: |
|
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
|
||||||
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
|
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
|
||||||
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: |
|
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: |
|
||||||
| OAuth | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
|
| OAuth | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
|
||||||
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: |
|
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
|
||||||
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: | :x: |
|
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: |
|
||||||
|
|
||||||
¹) for Gitea versions 1.17 or lower not for pull requests
|
¹ for Gitea versions 1.17 or lower not for pull requests
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
# Coding
|
|
||||||
|
|
||||||
## 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_CODING`
|
|
||||||
> Default: `false`
|
|
||||||
|
|
||||||
Enables the Coding driver.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_URL`
|
|
||||||
> Default: `https://coding.net`
|
|
||||||
|
|
||||||
Configures the Coding server address.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_CLIENT`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Configures the Coding OAuth client id. This is used to authorize access.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_CLIENT_FILE`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Read the value for `WOODPECKER_CODING_CLIENT` from the specified filepath
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_SECRET`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Configures the Coding OAuth client secret. This is used to authorize access.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_SECRET_FILE`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Read the value for `WOODPECKER_CODING_SECRET` from the specified filepath
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_SCOPE`
|
|
||||||
> Default: `user, project, project:depot`
|
|
||||||
|
|
||||||
Comma-separated list of OAuth scopes.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_GIT_MACHINE`
|
|
||||||
> Default: `git.coding.net`
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_GIT_USERNAME`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
This username is used to authenticate and clone all private repositories.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_GIT_USERNAME_FILE`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Read the value for `WOODPECKER_CODING_GIT_USERNAME` from the specified filepath
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_GIT_PASSWORD`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
The password is used to authenticate and clone all private repositories.
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_GIT_PASSWORD_FILE`
|
|
||||||
> Default: empty
|
|
||||||
|
|
||||||
Read the value for `WOODPECKER_CODING_GIT_PASSWORD` from the specified filepath
|
|
||||||
|
|
||||||
### `WOODPECKER_CODING_SKIP_VERIFY`
|
|
||||||
> Default: `false`
|
|
||||||
|
|
||||||
Configure if SSL verification should be skipped.
|
|
|
@ -15,6 +15,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` -> `.drone.yml`
|
- 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` -> `.drone.yml`
|
||||||
|
- Dropped support for [Coding](https://coding.net/).
|
||||||
|
|
||||||
## 0.15.0
|
## 0.15.0
|
||||||
|
|
||||||
|
|
|
@ -1,374 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server"
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/forge"
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/forge/coding/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 (
|
|
||||||
defaultURL = "https://coding.net" // Default Coding URL
|
|
||||||
)
|
|
||||||
|
|
||||||
// Opts defines configuration options.
|
|
||||||
type Opts struct {
|
|
||||||
URL string // Coding server url.
|
|
||||||
Client string // Coding oauth client id.
|
|
||||||
Secret string // Coding oauth client secret.
|
|
||||||
Scopes []string // Coding oauth scopes.
|
|
||||||
Username string // Optional machine account username.
|
|
||||||
Password string // Optional machine account password.
|
|
||||||
SkipVerify bool // Skip ssl verification.
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a Forge implementation that integrates with a Coding Platform or
|
|
||||||
// Coding Enterprise version control hosting provider.
|
|
||||||
func New(opts Opts) (forge.Forge, error) {
|
|
||||||
r := &Coding{
|
|
||||||
URL: defaultURL,
|
|
||||||
Client: opts.Client,
|
|
||||||
Secret: opts.Secret,
|
|
||||||
Scopes: opts.Scopes,
|
|
||||||
Username: opts.Username,
|
|
||||||
Password: opts.Password,
|
|
||||||
SkipVerify: opts.SkipVerify,
|
|
||||||
}
|
|
||||||
if opts.URL != defaultURL {
|
|
||||||
r.URL = strings.TrimSuffix(opts.URL, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Coding struct {
|
|
||||||
URL string
|
|
||||||
Client string
|
|
||||||
Secret string
|
|
||||||
Scopes []string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
SkipVerify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the string name of this driver
|
|
||||||
func (c *Coding) Name() string {
|
|
||||||
return "coding"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login authenticates the session and returns the
|
|
||||||
// forge user details.
|
|
||||||
func (c *Coding) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
|
|
||||||
config := c.newConfig(server.Config.Server.Host)
|
|
||||||
|
|
||||||
// get the OAuth errors
|
|
||||||
if err := req.FormValue("error"); err != "" {
|
|
||||||
return nil, &forge_types.AuthError{
|
|
||||||
Err: err,
|
|
||||||
Description: req.FormValue("error_description"),
|
|
||||||
URI: req.FormValue("error_uri"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the OAuth code
|
|
||||||
code := req.FormValue("code")
|
|
||||||
if len(code) == 0 {
|
|
||||||
http.Redirect(res, req, config.AuthCodeURL("woodpecker"), http.StatusSeeOther)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := config.Exchange(c.newContext(ctx), code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := c.newClientToken(ctx, token.AccessToken).GetCurrentUser()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.User{
|
|
||||||
Login: user.GlobalKey,
|
|
||||||
Email: user.Email,
|
|
||||||
Token: token.AccessToken,
|
|
||||||
Secret: token.RefreshToken,
|
|
||||||
Expiry: token.Expiry.UTC().Unix(),
|
|
||||||
Avatar: c.resourceLink(user.Avatar),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth authenticates the session and returns the forge user
|
|
||||||
// login for the given token and secret
|
|
||||||
func (c *Coding) Auth(ctx context.Context, token, _ string) (string, error) {
|
|
||||||
user, err := c.newClientToken(ctx, token).GetCurrentUser()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return user.GlobalKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh refreshes an oauth token and expiration for the given
|
|
||||||
// user. It returns true if the token was refreshed, false if the
|
|
||||||
// token was not refreshed, and error if it failed to refresh.
|
|
||||||
func (c *Coding) Refresh(ctx context.Context, u *model.User) (bool, error) {
|
|
||||||
config := c.newConfig("")
|
|
||||||
source := config.TokenSource(c.newContext(ctx), &oauth2.Token{RefreshToken: u.Secret})
|
|
||||||
token, err := source.Token()
|
|
||||||
if err != nil || len(token.AccessToken) == 0 {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Token = token.AccessToken
|
|
||||||
u.Secret = token.RefreshToken
|
|
||||||
u.Expiry = token.Expiry.UTC().Unix()
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Teams fetches a list of team memberships from the forge.
|
|
||||||
func (c *Coding) Teams(_ context.Context, _ *model.User) ([]*model.Team, error) {
|
|
||||||
// EMPTY: not implemented in Coding OAuth API
|
|
||||||
return nil, forge_types.ErrNotImplemented
|
|
||||||
}
|
|
||||||
|
|
||||||
// TeamPerm fetches the named organization permissions from
|
|
||||||
// the forge for the specified user.
|
|
||||||
func (c *Coding) TeamPerm(_ *model.User, _ string) (*model.Perm, error) {
|
|
||||||
// EMPTY: not implemented in Coding OAuth API
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repo fetches the repository from the forge.
|
|
||||||
func (c *Coding) Repo(ctx context.Context, u *model.User, _ model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
|
|
||||||
client := c.newClient(ctx, u)
|
|
||||||
project, err := client.GetProject(owner, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
depot, err := client.GetDepot(owner, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.Repo{
|
|
||||||
// TODO(1138) ForgeID: project.ID,
|
|
||||||
Owner: project.Owner,
|
|
||||||
Name: project.Name,
|
|
||||||
FullName: projectFullName(project.Owner, project.Name),
|
|
||||||
Avatar: c.resourceLink(project.Icon),
|
|
||||||
Link: c.resourceLink(project.DepotPath),
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
Clone: project.HTTPSURL,
|
|
||||||
Branch: depot.DefaultBranch,
|
|
||||||
IsSCMPrivate: !project.IsPublic,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repos fetches a list of repos from the forge.
|
|
||||||
func (c *Coding) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
|
|
||||||
client := c.newClient(ctx, u)
|
|
||||||
projectList, err := client.GetProjectList()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
repos := make([]*model.Repo, 0)
|
|
||||||
for _, project := range projectList {
|
|
||||||
depot, err := client.GetDepot(project.Owner, project.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
repo := &model.Repo{
|
|
||||||
// TODO(1138) ForgeID: project.ID,
|
|
||||||
Owner: project.Owner,
|
|
||||||
Name: project.Name,
|
|
||||||
FullName: projectFullName(project.Owner, project.Name),
|
|
||||||
Avatar: c.resourceLink(project.Icon),
|
|
||||||
Link: c.resourceLink(project.DepotPath),
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
Clone: project.HTTPSURL,
|
|
||||||
Branch: depot.DefaultBranch,
|
|
||||||
IsSCMPrivate: !project.IsPublic,
|
|
||||||
}
|
|
||||||
repos = append(repos, repo)
|
|
||||||
}
|
|
||||||
return repos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perm fetches the named repository permissions from
|
|
||||||
// the forge for the specified user.
|
|
||||||
func (c *Coding) Perm(ctx context.Context, u *model.User, repo *model.Repo) (*model.Perm, error) {
|
|
||||||
project, err := c.newClient(ctx, u).GetProject(repo.Owner, repo.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if project.Role == "owner" || project.Role == "admin" {
|
|
||||||
return &model.Perm{Pull: true, Push: true, Admin: true}, nil
|
|
||||||
}
|
|
||||||
if project.Role == "member" {
|
|
||||||
return &model.Perm{Pull: true, Push: true, Admin: false}, nil
|
|
||||||
}
|
|
||||||
return &model.Perm{Pull: false, Push: false, Admin: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// File fetches a file from the forge repository and returns in string
|
|
||||||
// format.
|
|
||||||
func (c *Coding) File(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]byte, error) {
|
|
||||||
data, err := c.newClient(ctx, u).GetFile(r.Owner, r.Name, b.Commit, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Coding) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ string) ([]*forge_types.FileMeta, error) {
|
|
||||||
return nil, forge_types.ErrNotImplemented
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status sends the commit status to the forge.
|
|
||||||
func (c *Coding) Status(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ *model.Step) error {
|
|
||||||
// EMPTY: not implemented in Coding OAuth API
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netrc returns a .netrc file that can be used to clone
|
|
||||||
// private repositories from a forge.
|
|
||||||
func (c *Coding) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
|
||||||
host, err := common.ExtractHostFromCloneURL(r.Clone)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Password != "" {
|
|
||||||
return &model.Netrc{
|
|
||||||
Login: c.Username,
|
|
||||||
Password: c.Password,
|
|
||||||
Machine: host,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Netrc{
|
|
||||||
Login: u.Token,
|
|
||||||
Password: "x-oauth-basic",
|
|
||||||
Machine: host,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate activates a repository by creating the post-commit hook.
|
|
||||||
func (c *Coding) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
|
||||||
return c.newClient(ctx, u).AddWebhook(r.Owner, r.Name, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deactivate deactivates a repository by removing all previously created
|
|
||||||
// post-commit hooks matching the given link.
|
|
||||||
func (c *Coding) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
|
||||||
return c.newClient(ctx, u).RemoveWebhook(r.Owner, r.Name, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branches returns the names of all branches for the named repository.
|
|
||||||
func (c *Coding) Branches(_ context.Context, _ *model.User, r *model.Repo) ([]string, error) {
|
|
||||||
// TODO: fetch all branches
|
|
||||||
return []string{r.Branch}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BranchHead returns the sha of the head (latest commit) of the specified branch
|
|
||||||
func (c *Coding) BranchHead(_ context.Context, _ *model.User, _ *model.Repo, _ string) (string, error) {
|
|
||||||
// TODO(1138): missing implementation
|
|
||||||
return "", forge_types.ErrNotImplemented
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook parses the post-commit hook from the Request body and returns the
|
|
||||||
// required data in a standard format.
|
|
||||||
func (c *Coding) Hook(_ context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
|
||||||
repo, pipeline, err := parseHook(r)
|
|
||||||
if pipeline != nil {
|
|
||||||
pipeline.Avatar = c.resourceLink(pipeline.Avatar)
|
|
||||||
}
|
|
||||||
return repo, pipeline, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrgMembership returns if user is member of organization and if user
|
|
||||||
// is admin/owner in this organization.
|
|
||||||
func (c *Coding) OrgMembership(_ context.Context, _ *model.User, _ string) (*model.OrgPerm, error) {
|
|
||||||
// TODO: Not supported in Coding OAuth API
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to return the Coding oauth2 context using an HTTPClient that
|
|
||||||
// disables TLS verification if disabled in the forge settings.
|
|
||||||
func (c *Coding) newContext(ctx context.Context) context.Context {
|
|
||||||
if !c.SkipVerify {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to return the Coding oauth2 config
|
|
||||||
func (c *Coding) newConfig(redirect string) *oauth2.Config {
|
|
||||||
return &oauth2.Config{
|
|
||||||
ClientID: c.Client,
|
|
||||||
ClientSecret: c.Secret,
|
|
||||||
Scopes: []string{strings.Join(c.Scopes, ",")},
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: fmt.Sprintf("%s/oauth_authorize.html", c.URL),
|
|
||||||
TokenURL: fmt.Sprintf("%s/api/oauth/access_token_v2", c.URL),
|
|
||||||
},
|
|
||||||
RedirectURL: fmt.Sprintf("%s/authorize", redirect),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to return the Coding oauth2 client
|
|
||||||
func (c *Coding) newClient(ctx context.Context, u *model.User) *internal.Client {
|
|
||||||
return c.newClientToken(ctx, u.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to return the Coding oauth2 client
|
|
||||||
func (c *Coding) newClientToken(ctx context.Context, token string) *internal.Client {
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: c.SkipVerify,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return internal.NewClient(ctx, c.URL, "/api", token, "woodpecker", client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Coding) resourceLink(resourcePath string) string {
|
|
||||||
if strings.HasPrefix(resourcePath, "http") {
|
|
||||||
return resourcePath
|
|
||||||
}
|
|
||||||
return c.URL + resourcePath
|
|
||||||
}
|
|
|
@ -1,273 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/franela/goblin"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/forge/coding/fixtures"
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_coding(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
s := httptest.NewServer(fixtures.Handler())
|
|
||||||
c := &Coding{URL: s.URL}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
g := goblin.Goblin(t)
|
|
||||||
g.Describe("Coding", func() {
|
|
||||||
g.After(func() {
|
|
||||||
s.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Creating a forge", func() {
|
|
||||||
g.It("Should return client with specified options", func() {
|
|
||||||
forge, _ := New(Opts{
|
|
||||||
URL: "https://coding.net",
|
|
||||||
Client: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
|
||||||
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
|
||||||
Scopes: []string{"user", "project", "project:depot"},
|
|
||||||
Username: "someuser",
|
|
||||||
Password: "password",
|
|
||||||
SkipVerify: true,
|
|
||||||
})
|
|
||||||
g.Assert(forge.(*Coding).URL).Equal("https://coding.net")
|
|
||||||
g.Assert(forge.(*Coding).Client).Equal("KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP")
|
|
||||||
g.Assert(forge.(*Coding).Secret).Equal("zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp")
|
|
||||||
g.Assert(forge.(*Coding).Scopes).Equal([]string{"user", "project", "project:depot"})
|
|
||||||
g.Assert(forge.(*Coding).Username).Equal("someuser")
|
|
||||||
g.Assert(forge.(*Coding).Password).Equal("password")
|
|
||||||
g.Assert(forge.(*Coding).SkipVerify).Equal(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Given an authorization request", func() {
|
|
||||||
g.It("Should redirect to authorize", func() {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r, _ := http.NewRequest("GET", "", nil)
|
|
||||||
_, err := c.Login(ctx, w, r)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(w.Code).Equal(http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
g.It("Should return authenticated user", func() {
|
|
||||||
r, _ := http.NewRequest("GET", "?code=code", nil)
|
|
||||||
u, err := c.Login(ctx, nil, r)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(u.Login).Equal(fakeUser.Login)
|
|
||||||
g.Assert(u.Token).Equal(fakeUser.Token)
|
|
||||||
g.Assert(u.Secret).Equal(fakeUser.Secret)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Given an access token", func() {
|
|
||||||
g.It("Should return the authenticated user", func() {
|
|
||||||
login, err := c.Auth(ctx, fakeUser.Token, fakeUser.Secret)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(login).Equal(fakeUser.Login)
|
|
||||||
})
|
|
||||||
g.It("Should handle a failure to resolve user", func() {
|
|
||||||
_, err := c.Auth(ctx, fakeUserNotFound.Token, fakeUserNotFound.Secret)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Given a refresh token", func() {
|
|
||||||
g.It("Should return a refresh access token", func() {
|
|
||||||
ok, err := c.Refresh(ctx, fakeUserRefresh)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(ok).IsTrue()
|
|
||||||
g.Assert(fakeUserRefresh.Token).Equal("VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK")
|
|
||||||
g.Assert(fakeUserRefresh.Secret).Equal("BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n")
|
|
||||||
})
|
|
||||||
g.It("Should handle an invalid refresh token", func() {
|
|
||||||
ok, _ := c.Refresh(ctx, fakeUserRefreshInvalid)
|
|
||||||
g.Assert(ok).IsFalse()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When requesting a repository", func() {
|
|
||||||
g.It("Should return the details", func() {
|
|
||||||
repo, err := c.Repo(ctx, fakeUser, "", fakeRepo.Owner, fakeRepo.Name)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(repo.FullName).Equal(fakeRepo.FullName)
|
|
||||||
g.Assert(repo.Avatar).Equal(s.URL + fakeRepo.Avatar)
|
|
||||||
g.Assert(repo.Link).Equal(s.URL + fakeRepo.Link)
|
|
||||||
g.Assert(repo.SCMKind).Equal(fakeRepo.SCMKind)
|
|
||||||
g.Assert(repo.Clone).Equal(fakeRepo.Clone)
|
|
||||||
g.Assert(repo.Branch).Equal(fakeRepo.Branch)
|
|
||||||
g.Assert(repo.IsSCMPrivate).Equal(fakeRepo.IsSCMPrivate)
|
|
||||||
})
|
|
||||||
g.It("Should handle not found errors", func() {
|
|
||||||
_, err := c.Repo(ctx, fakeUser, "", fakeRepoNotFound.Owner, fakeRepoNotFound.Name)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When requesting repository permissions", func() {
|
|
||||||
g.It("Should authorize admin access for project owner", func() {
|
|
||||||
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_owner"})
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(perm.Pull).IsTrue()
|
|
||||||
g.Assert(perm.Push).IsTrue()
|
|
||||||
g.Assert(perm.Admin).IsTrue()
|
|
||||||
})
|
|
||||||
g.It("Should authorize admin access for project admin", func() {
|
|
||||||
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_admin"})
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(perm.Pull).IsTrue()
|
|
||||||
g.Assert(perm.Push).IsTrue()
|
|
||||||
g.Assert(perm.Admin).IsTrue()
|
|
||||||
})
|
|
||||||
g.It("Should authorize read access for project member", func() {
|
|
||||||
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_member"})
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(perm.Pull).IsTrue()
|
|
||||||
g.Assert(perm.Push).IsTrue()
|
|
||||||
g.Assert(perm.Admin).IsFalse()
|
|
||||||
})
|
|
||||||
g.It("Should authorize no access for project guest", func() {
|
|
||||||
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_guest"})
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(perm.Pull).IsFalse()
|
|
||||||
g.Assert(perm.Push).IsFalse()
|
|
||||||
g.Assert(perm.Admin).IsFalse()
|
|
||||||
})
|
|
||||||
g.It("Should handle not found errors", func() {
|
|
||||||
_, err := c.Perm(ctx, fakeUser, fakeRepoNotFound)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When downloading a file", func() {
|
|
||||||
g.It("Should return file for specified pipeline", func() {
|
|
||||||
data, err := c.File(ctx, fakeUser, fakeRepo, fakePipeline, ".woodpecker.yml")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(string(data)).Equal("pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When requesting a netrc config", func() {
|
|
||||||
g.It("Should return the netrc file for global credential", func() {
|
|
||||||
forge, _ := New(Opts{
|
|
||||||
Username: "someuser",
|
|
||||||
Password: "password",
|
|
||||||
})
|
|
||||||
netrc, err := forge.Netrc(fakeUser, fakeRepo)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(netrc.Login).Equal("someuser")
|
|
||||||
g.Assert(netrc.Password).Equal("password")
|
|
||||||
g.Assert(netrc.Machine).Equal("git.coding.net")
|
|
||||||
})
|
|
||||||
g.It("Should return the netrc file for specified user", func() {
|
|
||||||
forge, _ := New(Opts{})
|
|
||||||
netrc, err := forge.Netrc(fakeUser, fakeRepo)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(netrc.Login).Equal(fakeUser.Token)
|
|
||||||
g.Assert(netrc.Password).Equal("x-oauth-basic")
|
|
||||||
g.Assert(netrc.Machine).Equal("git.coding.net")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When activating a repository", func() {
|
|
||||||
g.It("Should create the hook", func() {
|
|
||||||
err := c.Activate(ctx, fakeUser, fakeRepo, "http://127.0.0.1")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
})
|
|
||||||
g.It("Should update the hook when exists", func() {
|
|
||||||
err := c.Activate(ctx, fakeUser, fakeRepo, "http://127.0.0.2")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When deactivating a repository", func() {
|
|
||||||
g.It("Should successfully remove hook", func() {
|
|
||||||
err := c.Deactivate(ctx, fakeUser, fakeRepo, "http://127.0.0.3")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
})
|
|
||||||
g.It("Should successfully deactivate when hook already removed", func() {
|
|
||||||
err := c.Deactivate(ctx, fakeUser, fakeRepo, "http://127.0.0.4")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("When parsing post-commit hook body", func() {
|
|
||||||
g.It("Should parse the hook", func() {
|
|
||||||
buf := bytes.NewBufferString(fixtures.PushHook)
|
|
||||||
req, _ := http.NewRequest("POST", "/hook", buf)
|
|
||||||
req.Header = http.Header{}
|
|
||||||
req.Header.Set(hookEvent, hookPush)
|
|
||||||
|
|
||||||
r, _, err := c.Hook(ctx, req)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(r.FullName).Equal("demo1/test1")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
fakeUser = &model.User{
|
|
||||||
Login: "demo1",
|
|
||||||
Token: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
|
||||||
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeUserNotFound = &model.User{
|
|
||||||
Login: "demo1",
|
|
||||||
Token: "8DpqlE0hI6yr5MLlq8ysAL4p72cKGwT0",
|
|
||||||
Secret: "8Em2dkFE8Xsze88Ar8LMG7TF4CO3VCQMgpKa0VCm",
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeUserRefresh = &model.User{
|
|
||||||
Login: "demo1",
|
|
||||||
Secret: "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT",
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeUserRefreshInvalid = &model.User{
|
|
||||||
Login: "demo1",
|
|
||||||
Secret: "invalid_refresh_token",
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeRepo = &model.Repo{
|
|
||||||
Owner: "demo1",
|
|
||||||
Name: "test1",
|
|
||||||
FullName: "demo1/test1",
|
|
||||||
Avatar: "/static/project_icon/scenery-5.png",
|
|
||||||
Link: "/u/gilala/p/abp/git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
Clone: "https://git.coding.net/demo1/test1.git",
|
|
||||||
Branch: "master",
|
|
||||||
IsSCMPrivate: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeRepoNotFound = &model.Repo{
|
|
||||||
Owner: "not_found_owner",
|
|
||||||
Name: "not_found_project",
|
|
||||||
}
|
|
||||||
|
|
||||||
fakePipeline = &model.Pipeline{
|
|
||||||
Commit: "4504a072cc",
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,305 +0,0 @@
|
||||||
// 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 fixtures
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler returns an http.Handler that is capable of handing a variety of mock
|
|
||||||
// Coding requests and returns mock responses.
|
|
||||||
func Handler() http.Handler {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
e := gin.New()
|
|
||||||
e.POST("/api/oauth/access_token_v2", getToken)
|
|
||||||
e.GET("/api/account/current_user", getUser)
|
|
||||||
e.GET("/api/user/:gk/project/:prj", getProject)
|
|
||||||
e.GET("/api/user/:gk/project/:prj/git", getDepot)
|
|
||||||
e.GET("/api/user/:gk/project/:prj/git/blob/:ref/:path", getFile)
|
|
||||||
e.GET("/api/user/:gk/project/:prj/git/hooks", getHooks)
|
|
||||||
e.POST("/api/user/:gk/project/:prj/git/hook", postHook)
|
|
||||||
e.PUT("/api/user/:gk/project/:prj/git/hook/:id", putHook)
|
|
||||||
e.DELETE("/api/user/:gk/project/:prj/git/hook/:id", deleteHook)
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func getToken(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch c.PostForm("grant_type") {
|
|
||||||
case "refresh_token":
|
|
||||||
switch c.PostForm("refresh_token") {
|
|
||||||
case "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT":
|
|
||||||
c.String(200, refreshedTokenPayload)
|
|
||||||
default:
|
|
||||||
c.String(200, invalidRefreshTokenPayload)
|
|
||||||
}
|
|
||||||
case "authorization_code":
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
switch c.PostForm("code") {
|
|
||||||
case "code":
|
|
||||||
c.String(200, tokenPayload)
|
|
||||||
default:
|
|
||||||
c.String(200, invalidCodePayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUser(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch c.Query("access_token") {
|
|
||||||
case "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP":
|
|
||||||
c.String(200, userPayload)
|
|
||||||
default:
|
|
||||||
c.String(200, userNotFoundPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProject(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
|
|
||||||
case "demo1/test1":
|
|
||||||
c.String(200, fakeProjectPayload)
|
|
||||||
case "demo1/perm_owner":
|
|
||||||
c.String(200, fakePermOwnerPayload)
|
|
||||||
case "demo1/perm_admin":
|
|
||||||
c.String(200, fakePermAdminPayload)
|
|
||||||
case "demo1/perm_member":
|
|
||||||
c.String(200, fakePermMemberPayload)
|
|
||||||
case "demo1/perm_guest":
|
|
||||||
c.String(200, fakePermGuestPayload)
|
|
||||||
default:
|
|
||||||
c.String(200, projectNotFoundPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDepot(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
|
|
||||||
case "demo1/test1":
|
|
||||||
c.String(200, fakeDepotPayload)
|
|
||||||
default:
|
|
||||||
c.String(200, projectNotFoundPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFile(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch fmt.Sprintf("%s/%s/%s/%s", c.Param("gk"), c.Param("prj"), c.Param("ref"), c.Param("path")) {
|
|
||||||
case "demo1/test1/master/.woodpecker.yml", "demo1/test1/4504a072cc/.woodpecker.yml":
|
|
||||||
c.String(200, fakeFilePayload)
|
|
||||||
default:
|
|
||||||
c.String(200, fileNotFoundPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHooks(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
c.String(200, fakeHooksPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func postHook(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch c.PostForm("hook_url") {
|
|
||||||
case "http://127.0.0.1":
|
|
||||||
c.String(200, `{"code":0}`)
|
|
||||||
default:
|
|
||||||
c.String(200, `{"code":1}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func putHook(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch c.Param("id") {
|
|
||||||
case "2":
|
|
||||||
c.String(200, `{"code":0}`)
|
|
||||||
default:
|
|
||||||
c.String(200, `{"code":1}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteHook(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
|
||||||
switch c.Param("id") {
|
|
||||||
case "3":
|
|
||||||
c.String(200, `{"code":0}`)
|
|
||||||
default:
|
|
||||||
c.String(200, `{"code":1}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenPayload = `
|
|
||||||
{
|
|
||||||
"access_token":"KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
|
||||||
"refresh_token":"zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
|
||||||
"expires_in":36000
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const refreshedTokenPayload = `
|
|
||||||
{
|
|
||||||
"access_token":"VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK",
|
|
||||||
"refresh_token":"BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n",
|
|
||||||
"expires_in":36000
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const invalidRefreshTokenPayload = `
|
|
||||||
{
|
|
||||||
"code":3006,
|
|
||||||
"msg":{
|
|
||||||
"oauth_refresh_token_error":"Token校验失败"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const invalidCodePayload = `
|
|
||||||
{
|
|
||||||
"code":3003,
|
|
||||||
"msg":{
|
|
||||||
"oauth_validate_code_error":"code校验失败"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const userPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"global_key":"demo1",
|
|
||||||
"email":"demo1@gmail.com",
|
|
||||||
"avatar":"/static/fruit_avatar/Fruit-20.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const userNotFoundPayload = `
|
|
||||||
{
|
|
||||||
"code":1,
|
|
||||||
"msg":{
|
|
||||||
"user_not_login":"用户未登录"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakeProjectPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"owner_user_name":"demo1",
|
|
||||||
"name":"test1",
|
|
||||||
"depot_path":"/u/gilala/p/abp/git",
|
|
||||||
"https_url":"https://git.coding.net/demo1/test1.git",
|
|
||||||
"is_public": false,
|
|
||||||
"icon":"/static/project_icon/scenery-5.png",
|
|
||||||
"current_user_role":"owner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakePermOwnerPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"current_user_role":"owner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakePermAdminPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"current_user_role":"admin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakePermMemberPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"current_user_role":"member"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakePermGuestPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"current_user_role":"guest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakeDepotPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"default_branch":"master"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const projectNotFoundPayload = `
|
|
||||||
{
|
|
||||||
"code":1100,
|
|
||||||
"msg":{
|
|
||||||
"project_not_exists":"项目不存在"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakeFilePayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"file":{
|
|
||||||
"data":"pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fileNotFoundPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":{
|
|
||||||
"ref":"master"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fakeHooksPayload = `
|
|
||||||
{
|
|
||||||
"code":0,
|
|
||||||
"data":[
|
|
||||||
{
|
|
||||||
"id":2,
|
|
||||||
"hook_url":"http://127.0.0.2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":3,
|
|
||||||
"hook_url":"http://127.0.0.3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -1,189 +0,0 @@
|
||||||
// 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 fixtures
|
|
||||||
|
|
||||||
const PushHook = `
|
|
||||||
{
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"before": "861f2315056e8925e627a6f46518b9df05896e24",
|
|
||||||
"commits": [
|
|
||||||
{
|
|
||||||
"committer": {
|
|
||||||
"name": "demo1",
|
|
||||||
"email": "demo1@gmail.com"
|
|
||||||
},
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
"short_message": "new file .woodpecker.yml\n",
|
|
||||||
"sha": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"after": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
"event": "push",
|
|
||||||
"repository": {
|
|
||||||
"owner": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
},
|
|
||||||
"https_url": "https://git.coding.net/demo1/test1.git",
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test1",
|
|
||||||
"project_id": "99999999",
|
|
||||||
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
|
||||||
"name": "test1",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const DeleteBranchPushHook = `
|
|
||||||
{
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"before": "861f2315056e8925e627a6f46518b9df05896e24",
|
|
||||||
"after": "0000000000000000000000000000000000000000",
|
|
||||||
"event": "push",
|
|
||||||
"repository": {
|
|
||||||
"owner": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
},
|
|
||||||
"https_url": "https://git.coding.net/demo1/test1.git",
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test1",
|
|
||||||
"project_id": "99999999",
|
|
||||||
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
|
||||||
"name": "test1",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PullRequestHook = `
|
|
||||||
{
|
|
||||||
"pull_request": {
|
|
||||||
"target_branch": "master",
|
|
||||||
"title": "pr1",
|
|
||||||
"body": "pr message",
|
|
||||||
"source_sha": "",
|
|
||||||
"source_repository": {
|
|
||||||
"owner": {
|
|
||||||
"path": "/u/demo2",
|
|
||||||
"web_url": "https://coding.net/u/demo2",
|
|
||||||
"global_key": "demo2",
|
|
||||||
"name": "demo2",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-2.png"
|
|
||||||
},
|
|
||||||
"https_url": "https://git.coding.net/demo2/test2.git",
|
|
||||||
"web_url": "https://coding.net/u/demo2/p/test2",
|
|
||||||
"project_id": "7777777",
|
|
||||||
"ssh_url": "git@git.coding.net:demo2/test2.git",
|
|
||||||
"name": "test2",
|
|
||||||
"description": "",
|
|
||||||
"git_url": "git://git.coding.net/demo2/test2.git"
|
|
||||||
},
|
|
||||||
"source_branch": "master",
|
|
||||||
"number": 1,
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test2/git/pull/1",
|
|
||||||
"merge_commit_sha": "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
|
|
||||||
"target_sha": "",
|
|
||||||
"action": "create",
|
|
||||||
"id": 7586,
|
|
||||||
"user": {
|
|
||||||
"path": "/u/demo2",
|
|
||||||
"web_url": "https://coding.net/u/demo2",
|
|
||||||
"global_key": "demo2",
|
|
||||||
"name": "demo2",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-2.png"
|
|
||||||
},
|
|
||||||
"status": "CANMERGE"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"owner": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
},
|
|
||||||
"https_url": "https://git.coding.net/demo1/test2.git",
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test2",
|
|
||||||
"project_id": "6666666",
|
|
||||||
"ssh_url": "git@git.coding.net:demo1/test2.git",
|
|
||||||
"name": "test2",
|
|
||||||
"description": "",
|
|
||||||
"git_url": "git://git.coding.net/demo1/test2.git"
|
|
||||||
},
|
|
||||||
"event": "pull_request"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const MergeRequestHook = `
|
|
||||||
{
|
|
||||||
"merge_request": {
|
|
||||||
"target_branch": "master",
|
|
||||||
"title": "mr1",
|
|
||||||
"body": "<p>mr message</p>",
|
|
||||||
"source_sha": "",
|
|
||||||
"source_branch": "branch1",
|
|
||||||
"number": 1,
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test1/git/merge/1",
|
|
||||||
"merge_commit_sha": "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
|
|
||||||
"target_sha": "",
|
|
||||||
"action": "create",
|
|
||||||
"id": 533428,
|
|
||||||
"user": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
},
|
|
||||||
"status": "CANMERGE"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"owner": {
|
|
||||||
"path": "/u/demo1",
|
|
||||||
"web_url": "https://coding.net/u/demo1",
|
|
||||||
"global_key": "demo1",
|
|
||||||
"name": "demo1",
|
|
||||||
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
|
||||||
},
|
|
||||||
"https_url": "https://git.coding.net/demo1/test1.git",
|
|
||||||
"web_url": "https://coding.net/u/demo1/p/test1",
|
|
||||||
"project_id": "99999999",
|
|
||||||
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
|
||||||
"name": "test1",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"event": "merge_request"
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -1,246 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hookEvent = "X-Coding-Event"
|
|
||||||
hookPush = "push"
|
|
||||||
hookPR = "pull_request"
|
|
||||||
hookMR = "merge_request"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
GlobalKey string `json:"global_key"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Repository struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
HTTPSURL string `json:"https_url"`
|
|
||||||
SSHURL string `json:"ssh_url"`
|
|
||||||
WebURL string `json:"web_url"`
|
|
||||||
Owner *User `json:"owner"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Committer struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Commit struct {
|
|
||||||
SHA string `json:"sha"`
|
|
||||||
ShortMessage string `json:"short_message"`
|
|
||||||
Committer *Committer `json:"committer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PullRequest MergeRequest
|
|
||||||
|
|
||||||
type MergeRequest struct {
|
|
||||||
SourceBranch string `json:"source_branch"`
|
|
||||||
TargetBranch string `json:"target_branch"`
|
|
||||||
CommitSHA string `json:"merge_commit_sha"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Number float64 `json:"number"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Body string `json:"body"`
|
|
||||||
WebURL string `json:"web_url"`
|
|
||||||
User *User `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PushHook struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Repository *Repository `json:"repository"`
|
|
||||||
Ref string `json:"ref"`
|
|
||||||
Before string `json:"before"`
|
|
||||||
After string `json:"after"`
|
|
||||||
Commits []*Commit `json:"commits"`
|
|
||||||
User *User `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PullRequestHook struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Repository *Repository `json:"repository"`
|
|
||||||
PullRequest *PullRequest `json:"pull_request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MergeRequestHook struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Repository *Repository `json:"repository"`
|
|
||||||
MergeRequest *MergeRequest `json:"merge_request"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
|
||||||
raw, err := io.ReadAll(r.Body)
|
|
||||||
defer r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Header.Get(hookEvent) {
|
|
||||||
case hookPush:
|
|
||||||
return parsePushHook(raw)
|
|
||||||
case hookPR:
|
|
||||||
return parsePullRequestHook(raw)
|
|
||||||
case hookMR:
|
|
||||||
return parseMergeRequestHook(raw)
|
|
||||||
}
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLastCommit(commits []*Commit, sha string) *Commit {
|
|
||||||
var lastCommit *Commit
|
|
||||||
for _, commit := range commits {
|
|
||||||
if commit.SHA == sha {
|
|
||||||
lastCommit = commit
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lastCommit == nil {
|
|
||||||
lastCommit = &Commit{}
|
|
||||||
}
|
|
||||||
if lastCommit.Committer == nil {
|
|
||||||
lastCommit.Committer = &Committer{}
|
|
||||||
}
|
|
||||||
return lastCommit
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertRepository(repo *Repository) (*model.Repo, error) {
|
|
||||||
// tricky stuff for a team project without a team owner instead of a user owner
|
|
||||||
re := regexp.MustCompile(`git@.+:([^/]+)/.+\.git`)
|
|
||||||
matches := re.FindStringSubmatch(repo.SSHURL)
|
|
||||||
if len(matches) != 2 {
|
|
||||||
return nil, fmt.Errorf("Unable to resolve owner from ssh url %q", repo.SSHURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Repo{
|
|
||||||
// TODO ForgeID: repo.ID,
|
|
||||||
Owner: matches[1],
|
|
||||||
Name: repo.Name,
|
|
||||||
FullName: projectFullName(repo.Owner.GlobalKey, repo.Name),
|
|
||||||
Link: repo.WebURL,
|
|
||||||
Clone: repo.HTTPSURL,
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePushHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
|
|
||||||
hook := &PushHook{}
|
|
||||||
err := json.Unmarshal(raw, hook)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// no pipeline triggered when removing ref
|
|
||||||
if hook.After == "0000000000000000000000000000000000000000" {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := convertRepository(hook.Repository)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommit := findLastCommit(hook.Commits, hook.After)
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPush,
|
|
||||||
Commit: hook.After,
|
|
||||||
Ref: hook.Ref,
|
|
||||||
Link: fmt.Sprintf("%s/git/commit/%s", hook.Repository.WebURL, hook.After),
|
|
||||||
Branch: strings.Replace(hook.Ref, "refs/heads/", "", -1),
|
|
||||||
Message: lastCommit.ShortMessage,
|
|
||||||
Email: lastCommit.Committer.Email,
|
|
||||||
Avatar: hook.User.Avatar,
|
|
||||||
Author: hook.User.GlobalKey,
|
|
||||||
CloneURL: hook.Repository.HTTPSURL,
|
|
||||||
}
|
|
||||||
return repo, pipeline, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePullRequestHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
|
|
||||||
hook := &PullRequestHook{}
|
|
||||||
err := json.Unmarshal(raw, hook)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if hook.PullRequest.Status != "CANMERGE" ||
|
|
||||||
(hook.PullRequest.Action != "create" && hook.PullRequest.Action != "synchronize") {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := convertRepository(hook.Repository)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPull,
|
|
||||||
Commit: hook.PullRequest.CommitSHA,
|
|
||||||
Link: hook.PullRequest.WebURL,
|
|
||||||
Ref: fmt.Sprintf("refs/pull/%d/MERGE", int(hook.PullRequest.Number)),
|
|
||||||
Branch: hook.PullRequest.TargetBranch,
|
|
||||||
Message: hook.PullRequest.Body,
|
|
||||||
Author: hook.PullRequest.User.GlobalKey,
|
|
||||||
Avatar: hook.PullRequest.User.Avatar,
|
|
||||||
Title: hook.PullRequest.Title,
|
|
||||||
CloneURL: hook.Repository.HTTPSURL,
|
|
||||||
Refspec: fmt.Sprintf("%s:%s", hook.PullRequest.SourceBranch, hook.PullRequest.TargetBranch),
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo, pipeline, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMergeRequestHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
|
|
||||||
hook := &MergeRequestHook{}
|
|
||||||
err := json.Unmarshal(raw, hook)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if hook.MergeRequest.Status != "CANMERGE" ||
|
|
||||||
(hook.MergeRequest.Action != "create" && hook.MergeRequest.Action != "synchronize") {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := convertRepository(hook.Repository)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPull,
|
|
||||||
Commit: hook.MergeRequest.CommitSHA,
|
|
||||||
Link: hook.MergeRequest.WebURL,
|
|
||||||
Ref: fmt.Sprintf("refs/merge/%d/MERGE", int(hook.MergeRequest.Number)),
|
|
||||||
Branch: hook.MergeRequest.TargetBranch,
|
|
||||||
Message: hook.MergeRequest.Body,
|
|
||||||
Author: hook.MergeRequest.User.GlobalKey,
|
|
||||||
Avatar: hook.MergeRequest.User.Avatar,
|
|
||||||
Title: hook.MergeRequest.Title,
|
|
||||||
CloneURL: hook.Repository.HTTPSURL,
|
|
||||||
Refspec: fmt.Sprintf("%s:%s", hook.MergeRequest.SourceBranch, hook.MergeRequest.TargetBranch),
|
|
||||||
}
|
|
||||||
return repo, pipeline, nil
|
|
||||||
}
|
|
|
@ -1,205 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/franela/goblin"
|
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/forge/coding/fixtures"
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_hook(t *testing.T) {
|
|
||||||
g := goblin.Goblin(t)
|
|
||||||
g.Describe("Coding hook", func() {
|
|
||||||
g.It("Should parse hook", func() {
|
|
||||||
reader := io.NopCloser(strings.NewReader(fixtures.PushHook))
|
|
||||||
r := &http.Request{
|
|
||||||
Header: map[string][]string{
|
|
||||||
hookEvent: {hookPush},
|
|
||||||
},
|
|
||||||
Body: reader,
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := &model.Repo{
|
|
||||||
Owner: "demo1",
|
|
||||||
Name: "test1",
|
|
||||||
FullName: "demo1/test1",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1",
|
|
||||||
Clone: "https://git.coding.net/demo1/test1.git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPush,
|
|
||||||
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
Ref: "refs/heads/master",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
Branch: "master",
|
|
||||||
Message: "new file .woodpecker.yml\n",
|
|
||||||
Email: "demo1@gmail.com",
|
|
||||||
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
|
||||||
Author: "demo1",
|
|
||||||
CloneURL: "https://git.coding.net/demo1/test1.git",
|
|
||||||
}
|
|
||||||
|
|
||||||
actualRepo, actualPipeline, err := parseHook(r)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actualRepo).Equal(repo)
|
|
||||||
g.Assert(actualPipeline).Equal(pipeline)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should find last commit", func() {
|
|
||||||
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
|
|
||||||
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
|
|
||||||
commits := []*Commit{commit1, commit2}
|
|
||||||
g.Assert(findLastCommit(commits, "abcdef1234")).Equal(commit2)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should find last commit", func() {
|
|
||||||
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
|
|
||||||
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
|
|
||||||
commits := []*Commit{commit1, commit2}
|
|
||||||
emptyCommit := &Commit{Committer: &Committer{}}
|
|
||||||
g.Assert(findLastCommit(commits, "00000000000")).Equal(emptyCommit)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should convert repository", func() {
|
|
||||||
repository := &Repository{
|
|
||||||
Name: "test_project",
|
|
||||||
HTTPSURL: "https://git.coding.net/kelvin/test_project.git",
|
|
||||||
SSHURL: "git@git.coding.net:kelvin/test_project.git",
|
|
||||||
WebURL: "https://coding.net/u/kelvin/p/test_project",
|
|
||||||
Owner: &User{
|
|
||||||
GlobalKey: "kelvin",
|
|
||||||
Avatar: "https://dn-coding-net-production-static.qbox.me/9ed11de3-65e3-4cd8-b6aa-5abe7285ab43.jpeg?imageMogr2/auto-orient/format/jpeg/crop/!209x209a0a0",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
repo := &model.Repo{
|
|
||||||
Owner: "kelvin",
|
|
||||||
Name: "test_project",
|
|
||||||
FullName: "kelvin/test_project",
|
|
||||||
Link: "https://coding.net/u/kelvin/p/test_project",
|
|
||||||
Clone: "https://git.coding.net/kelvin/test_project.git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}
|
|
||||||
actual, err := convertRepository(repository)
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actual).Equal(repo)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should parse push hook", func() {
|
|
||||||
repo := &model.Repo{
|
|
||||||
Owner: "demo1",
|
|
||||||
Name: "test1",
|
|
||||||
FullName: "demo1/test1",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1",
|
|
||||||
Clone: "https://git.coding.net/demo1/test1.git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPush,
|
|
||||||
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
Ref: "refs/heads/master",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
|
||||||
Branch: "master",
|
|
||||||
Message: "new file .woodpecker.yml\n",
|
|
||||||
Email: "demo1@gmail.com",
|
|
||||||
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
|
||||||
Author: "demo1",
|
|
||||||
CloneURL: "https://git.coding.net/demo1/test1.git",
|
|
||||||
}
|
|
||||||
|
|
||||||
actualRepo, actualPipeline, err := parsePushHook([]byte(fixtures.PushHook))
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actualRepo).Equal(repo)
|
|
||||||
g.Assert(actualPipeline).Equal(pipeline)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should parse delete branch push hook", func() {
|
|
||||||
actualRepo, actualPipeline, err := parsePushHook([]byte(fixtures.DeleteBranchPushHook))
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actualRepo).IsNil()
|
|
||||||
g.Assert(actualPipeline).IsNil()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should parse pull request hook", func() {
|
|
||||||
repo := &model.Repo{
|
|
||||||
Owner: "demo1",
|
|
||||||
Name: "test2",
|
|
||||||
FullName: "demo1/test2",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test2",
|
|
||||||
Clone: "https://git.coding.net/demo1/test2.git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPull,
|
|
||||||
Commit: "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test2/git/pull/1",
|
|
||||||
Ref: "refs/pull/1/MERGE",
|
|
||||||
Branch: "master",
|
|
||||||
Message: "pr message",
|
|
||||||
Author: "demo2",
|
|
||||||
Avatar: "/static/fruit_avatar/Fruit-2.png",
|
|
||||||
Title: "pr1",
|
|
||||||
CloneURL: "https://git.coding.net/demo1/test2.git",
|
|
||||||
Refspec: "master:master",
|
|
||||||
}
|
|
||||||
|
|
||||||
actualRepo, actualPipeline, err := parsePullRequestHook([]byte(fixtures.PullRequestHook))
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actualRepo).Equal(repo)
|
|
||||||
g.Assert(actualPipeline).Equal(pipeline)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("Should parse merge request hook", func() {
|
|
||||||
repo := &model.Repo{
|
|
||||||
Owner: "demo1",
|
|
||||||
Name: "test1",
|
|
||||||
FullName: "demo1/test1",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1",
|
|
||||||
Clone: "https://git.coding.net/demo1/test1.git",
|
|
||||||
SCMKind: model.RepoGit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline := &model.Pipeline{
|
|
||||||
Event: model.EventPull,
|
|
||||||
Commit: "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
|
|
||||||
Link: "https://coding.net/u/demo1/p/test1/git/merge/1",
|
|
||||||
Ref: "refs/merge/1/MERGE",
|
|
||||||
Branch: "master",
|
|
||||||
Message: "<p>mr message</p>",
|
|
||||||
Author: "demo1",
|
|
||||||
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
|
||||||
Title: "mr1",
|
|
||||||
CloneURL: "https://git.coding.net/demo1/test1.git",
|
|
||||||
Refspec: "branch1:master",
|
|
||||||
}
|
|
||||||
|
|
||||||
actualRepo, actualPipeline, err := parseMergeRequestHook([]byte(fixtures.MergeRequestHook))
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(actualRepo).Equal(repo)
|
|
||||||
g.Assert(actualPipeline).Equal(pipeline)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
baseURL string
|
|
||||||
apiPath string
|
|
||||||
token string
|
|
||||||
agent string
|
|
||||||
client *http.Client
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenericAPIResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(ctx context.Context, baseURL, apiPath, token, agent string, client *http.Client) *Client {
|
|
||||||
return &Client{
|
|
||||||
baseURL: baseURL,
|
|
||||||
apiPath: apiPath,
|
|
||||||
token: token,
|
|
||||||
agent: agent,
|
|
||||||
client: client,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Generic GET for requesting Coding OAuth API
|
|
||||||
func (c *Client) Get(u string, params url.Values) ([]byte, error) {
|
|
||||||
return c.Do(http.MethodGet, u, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do Generic method for requesting Coding OAuth API
|
|
||||||
func (c *Client) Do(method, u string, params url.Values) ([]byte, error) {
|
|
||||||
if params == nil {
|
|
||||||
params = url.Values{}
|
|
||||||
}
|
|
||||||
params.Set("access_token", c.token)
|
|
||||||
|
|
||||||
rawURL := c.baseURL + c.apiPath + u
|
|
||||||
|
|
||||||
var req *http.Request
|
|
||||||
var err error
|
|
||||||
if method != "GET" {
|
|
||||||
req, err = http.NewRequestWithContext(c.ctx, method, rawURL+"?access_token="+c.token, strings.NewReader(params.Encode()))
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
|
|
||||||
} else {
|
|
||||||
req, err = http.NewRequestWithContext(c.ctx, "GET", rawURL+"?"+params.Encode(), nil)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fail to create request for url %q: %w", rawURL, err)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", c.agent)
|
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fail to request %s %s: %w", req.Method, req.URL, err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("%s %s respond %d", req.Method, req.URL, resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fail to read response from %s %s: %w", req.Method, req.URL.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiResp := &GenericAPIResponse{}
|
|
||||||
err = json.Unmarshal(body, apiResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fail to parse response from %s %s: %w", req.Method, req.URL.String(), err)
|
|
||||||
}
|
|
||||||
if apiResp.Code != 0 {
|
|
||||||
return nil, fmt.Errorf("Coding OAuth API respond error: %s", string(body))
|
|
||||||
}
|
|
||||||
return apiResp.Data, nil
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type APIClientErr struct {
|
|
||||||
Message string
|
|
||||||
URL string
|
|
||||||
Cause error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e APIClientErr) Error() string {
|
|
||||||
return fmt.Sprintf("%s (Requested %s): %v", e.Message, e.URL, e.Cause)
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Commit struct {
|
|
||||||
File *File `json:"file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetFile(globalKey, projectName, ref, path string) ([]byte, error) {
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git/blob/%s/%s", globalKey, projectName, ref, path)
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commit := &Commit{}
|
|
||||||
err = json.Unmarshal(resp, commit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse file data", u, err}
|
|
||||||
}
|
|
||||||
if commit == nil || commit.File == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return []byte(commit.File.Data), nil
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Project struct {
|
|
||||||
Owner string `json:"owner_user_name"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
DepotPath string `json:"depot_path"`
|
|
||||||
HTTPSURL string `json:"https_url"`
|
|
||||||
IsPublic bool `json:"is_public"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Role string `json:"current_user_role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Depot struct {
|
|
||||||
DefaultBranch string `json:"default_branch"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectListData struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"pageSize"`
|
|
||||||
TotalPage int `json:"totalPage"`
|
|
||||||
TotalRow int `json:"totalRow"`
|
|
||||||
List []*Project `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetProject(globalKey, projectName string) (*Project, error) {
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s", globalKey, projectName)
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
project := &Project{}
|
|
||||||
err = json.Unmarshal(resp, project)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse project data", u, err}
|
|
||||||
}
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetDepot(globalKey, projectName string) (*Depot, error) {
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git", globalKey, projectName)
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
depot := &Depot{}
|
|
||||||
err = json.Unmarshal(resp, depot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse depot data", u, err}
|
|
||||||
}
|
|
||||||
return depot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetProjectList() ([]*Project, error) {
|
|
||||||
u := "/user/projects"
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data := &ProjectListData{}
|
|
||||||
err = json.Unmarshal(resp, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse project list data", u, err}
|
|
||||||
}
|
|
||||||
if data.TotalPage == 1 {
|
|
||||||
return data.List, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
projectList := make([]*Project, 0)
|
|
||||||
projectList = append(projectList, data.List...)
|
|
||||||
for i := 2; i <= data.TotalPage; i++ {
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("page", fmt.Sprintf("%d", i))
|
|
||||||
resp, err := c.Get(u, params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data := &ProjectListData{}
|
|
||||||
err = json.Unmarshal(resp, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse project list data", u, err}
|
|
||||||
}
|
|
||||||
projectList = append(projectList, data.List...)
|
|
||||||
}
|
|
||||||
return projectList, nil
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
GlobalKey string `json:"global_key"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetCurrentUser() (*User, error) {
|
|
||||||
u := "/account/current_user"
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user := &User{}
|
|
||||||
err = json.Unmarshal(resp, user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse current user data", u, err}
|
|
||||||
}
|
|
||||||
return user, nil
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Webhook struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
HookURL string `json:"hook_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetWebhooks(globalKey, projectName string) ([]*Webhook, error) {
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git/hooks", globalKey, projectName)
|
|
||||||
resp, err := c.Get(u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
webhooks := make([]*Webhook, 0)
|
|
||||||
err = json.Unmarshal(resp, &webhooks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, APIClientErr{"fail to parse webhooks data", u, err}
|
|
||||||
}
|
|
||||||
return webhooks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) AddWebhook(globalKey, projectName, link string) error {
|
|
||||||
webhooks, err := c.GetWebhooks(globalKey, projectName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
webhook := matchingHooks(webhooks, link)
|
|
||||||
if webhook != nil {
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.ID)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("hook_url", link)
|
|
||||||
params.Set("type_push", "true")
|
|
||||||
params.Set("type_mr_pr", "true")
|
|
||||||
|
|
||||||
_, err := c.Do("PUT", u, params)
|
|
||||||
if err != nil {
|
|
||||||
return APIClientErr{"fail to edit webhook", u, err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git/hook", globalKey, projectName)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("hook_url", link)
|
|
||||||
params.Set("type_push", "true")
|
|
||||||
params.Set("type_mr_pr", "true")
|
|
||||||
|
|
||||||
_, err = c.Do("POST", u, params)
|
|
||||||
if err != nil {
|
|
||||||
return APIClientErr{"fail to add webhook", u, err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RemoveWebhook(globalKey, projectName, link string) error {
|
|
||||||
webhooks, err := c.GetWebhooks(globalKey, projectName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
webhook := matchingHooks(webhooks, link)
|
|
||||||
if webhook == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.ID)
|
|
||||||
_, err = c.Do("DELETE", u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return APIClientErr{"fail to remove webhook", u, err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to return matching hook.
|
|
||||||
func matchingHooks(hooks []*Webhook, rawurl string) *Webhook {
|
|
||||||
link, err := url.Parse(rawurl)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, hook := range hooks {
|
|
||||||
hookurl, err := url.Parse(hook.HookURL)
|
|
||||||
if err == nil && hookurl.Host == link.Host {
|
|
||||||
return hook
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,24 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func projectFullName(owner, name string) string {
|
|
||||||
return fmt.Sprintf("%s/%s", owner, name)
|
|
||||||
}
|
|
|
@ -1,31 +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 coding
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/franela/goblin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_util(t *testing.T) {
|
|
||||||
g := goblin.Goblin(t)
|
|
||||||
g.Describe("Coding util", func() {
|
|
||||||
g.It("Should form project full name", func() {
|
|
||||||
g.Assert(projectFullName("gk", "prj")).Equal("gk/prj")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Reference in a new issue