mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-29 05:11:04 +00:00
Added integration for coding.net
This commit is contained in:
parent
44450bce8d
commit
eb94dc0419
16 changed files with 1974 additions and 0 deletions
|
@ -380,6 +380,58 @@ var flags = []cli.Flag{
|
|||
Name: "stash-skip-verify",
|
||||
Usage: "stash skip ssl verification",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
EnvVar: "DRONE_CODING",
|
||||
Name: "coding",
|
||||
Usage: "coding driver is enabled",
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_URL",
|
||||
Name: "coding-server",
|
||||
Usage: "coding server address",
|
||||
Value: "https://coding.net",
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_CLIENT",
|
||||
Name: "coding-client",
|
||||
Usage: "coding oauth2 client id",
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_SECRET",
|
||||
Name: "coding-secret",
|
||||
Usage: "coding oauth2 client secret",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
EnvVar: "DRONE_CODING_SCOPE",
|
||||
Name: "coding-scope",
|
||||
Usage: "coding oauth scope",
|
||||
Value: &cli.StringSlice{
|
||||
"user",
|
||||
"project",
|
||||
"project:depot",
|
||||
},
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_GIT_MACHINE",
|
||||
Name: "coding-git-machine",
|
||||
Usage: "coding machine name",
|
||||
Value: "git.coding.net",
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_GIT_USERNAME",
|
||||
Name: "coding-git-username",
|
||||
Usage: "coding machine user username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
EnvVar: "DRONE_CODING_GIT_PASSWORD",
|
||||
Name: "coding-git-password",
|
||||
Usage: "coding machine user password",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
EnvVar: "DRONE_CODING_SKIP_VERIFY",
|
||||
Name: "coding-skip-verify",
|
||||
Usage: "coding skip ssl verification",
|
||||
},
|
||||
}
|
||||
|
||||
func server(c *cli.Context) error {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/drone/drone/remote"
|
||||
"github.com/drone/drone/remote/bitbucket"
|
||||
"github.com/drone/drone/remote/bitbucketserver"
|
||||
"github.com/drone/drone/remote/coding"
|
||||
"github.com/drone/drone/remote/gitea"
|
||||
"github.com/drone/drone/remote/github"
|
||||
"github.com/drone/drone/remote/gitlab"
|
||||
|
@ -62,6 +63,8 @@ func SetupRemote(c *cli.Context) (remote.Remote, error) {
|
|||
return setupGogs(c)
|
||||
case c.Bool("gitea"):
|
||||
return setupGitea(c)
|
||||
case c.Bool("coding"):
|
||||
return setupCoding(c)
|
||||
default:
|
||||
return nil, fmt.Errorf("version control system not configured")
|
||||
}
|
||||
|
@ -139,4 +142,18 @@ func setupGithub(c *cli.Context) (remote.Remote, error) {
|
|||
})
|
||||
}
|
||||
|
||||
// helper function to setup the Coding remote from the CLI arguments.
|
||||
func setupCoding(c *cli.Context) (remote.Remote, error) {
|
||||
return coding.New(coding.Opts{
|
||||
URL: c.String("coding-server"),
|
||||
Client: c.String("coding-client"),
|
||||
Secret: c.String("coding-secret"),
|
||||
Scopes: c.StringSlice("coding-scope"),
|
||||
Machine: c.String("coding-git-machine"),
|
||||
Username: c.String("coding-git-username"),
|
||||
Password: c.String("coding-git-password"),
|
||||
SkipVerify: c.Bool("coding-skip-verify"),
|
||||
})
|
||||
}
|
||||
|
||||
func before(c *cli.Context) error { return nil }
|
||||
|
|
334
remote/coding/coding.go
Normal file
334
remote/coding/coding.go
Normal file
|
@ -0,0 +1,334 @@
|
|||
package coding
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone/model"
|
||||
"github.com/drone/drone/remote"
|
||||
"github.com/drone/drone/remote/coding/internal"
|
||||
"github.com/drone/drone/shared/httputil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
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.
|
||||
Machine string // Optional machine name.
|
||||
Username string // Optional machine account username.
|
||||
Password string // Optional machine account password.
|
||||
SkipVerify bool // Skip ssl verification.
|
||||
}
|
||||
|
||||
// New returns a Remote implementation that integrates with a Coding Platform or
|
||||
// Coding Enterprise version control hosting provider.
|
||||
func New(opts Opts) (remote.Remote, error) {
|
||||
remote := &Coding{
|
||||
URL: defaultURL,
|
||||
Client: opts.Client,
|
||||
Secret: opts.Secret,
|
||||
Scopes: opts.Scopes,
|
||||
Machine: opts.Machine,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
SkipVerify: opts.SkipVerify,
|
||||
}
|
||||
if opts.URL != defaultURL {
|
||||
remote.URL = strings.TrimSuffix(opts.URL, "/")
|
||||
}
|
||||
|
||||
// Hack to enable oauth2 access in coding's implementation
|
||||
oauth2.RegisterBrokenAuthHeaderProvider(remote.URL)
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
type Coding struct {
|
||||
URL string
|
||||
Client string
|
||||
Secret string
|
||||
Scopes []string
|
||||
Machine string
|
||||
Username string
|
||||
Password string
|
||||
SkipVerify bool
|
||||
}
|
||||
|
||||
// Login authenticates the session and returns the
|
||||
// remote user details.
|
||||
func (c *Coding) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
|
||||
config := c.newConfig(httputil.GetURL(req))
|
||||
|
||||
// get the OAuth errors
|
||||
if err := req.FormValue("error"); err != "" {
|
||||
return nil, &remote.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("drone"), http.StatusSeeOther)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
token, err := config.Exchange(c.newContext(), code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := c.newClientToken(token.AccessToken, token.RefreshToken).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 remote user
|
||||
// login for the given token and secret
|
||||
func (c *Coding) Auth(token, secret string) (string, error) {
|
||||
user, err := c.newClientToken(token, secret).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 refersh.
|
||||
func (c *Coding) Refresh(u *model.User) (bool, error) {
|
||||
config := c.newConfig("")
|
||||
source := config.TokenSource(c.newContext(), &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 remote system.
|
||||
func (c *Coding) Teams(u *model.User) ([]*model.Team, error) {
|
||||
// EMPTY: not implemented in Coding OAuth API
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TeamPerm fetches the named organization permissions from
|
||||
// the remote system for the specified user.
|
||||
func (c *Coding) TeamPerm(u *model.User, org string) (*model.Perm, error) {
|
||||
// EMPTY: not implemented in Coding OAuth API
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Repo fetches the named repository from the remote system.
|
||||
func (c *Coding) Repo(u *model.User, owner, repo string) (*model.Repo, error) {
|
||||
project, err := c.newClient(u).GetProject(owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
depot, err := c.newClient(u).GetDepot(owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Repo{
|
||||
Owner: project.Owner,
|
||||
Name: project.Name,
|
||||
FullName: projectFullName(project.Owner, project.Name),
|
||||
Avatar: c.resourceLink(project.Icon),
|
||||
Link: c.resourceLink(project.DepotPath),
|
||||
Kind: model.RepoGit,
|
||||
Clone: project.HttpsURL,
|
||||
Branch: depot.DefaultBranch,
|
||||
IsPrivate: !project.IsPublic,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Repos fetches a list of repos from the remote system.
|
||||
func (c *Coding) Repos(u *model.User) ([]*model.Repo, error) {
|
||||
projectList, err := c.newClient(u).GetProjectList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos := make([]*model.Repo, 0)
|
||||
for _, project := range projectList {
|
||||
depot, err := c.newClient(u).GetDepot(project.Owner, project.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := &model.Repo{
|
||||
Owner: project.Owner,
|
||||
Name: project.Name,
|
||||
FullName: projectFullName(project.Owner, project.Name),
|
||||
Avatar: c.resourceLink(project.Icon),
|
||||
Link: c.resourceLink(project.DepotPath),
|
||||
Kind: model.RepoGit,
|
||||
Clone: project.HttpsURL,
|
||||
Branch: depot.DefaultBranch,
|
||||
IsPrivate: !project.IsPublic,
|
||||
}
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// Perm fetches the named repository permissions from
|
||||
// the remote system for the specified user.
|
||||
func (c *Coding) Perm(u *model.User, owner, repo string) (*model.Perm, error) {
|
||||
project, err := c.newClient(u).GetProject(owner, repo)
|
||||
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 remote repository and returns in string
|
||||
// format.
|
||||
func (c *Coding) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
|
||||
data, err := c.newClient(u).GetFile(r.Owner, r.Name, b.Commit, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// FileRef fetches a file from the remote repository for the given ref
|
||||
// and returns in string format.
|
||||
func (c *Coding) FileRef(u *model.User, r *model.Repo, ref, f string) ([]byte, error) {
|
||||
data, err := c.newClient(u).GetFile(r.Owner, r.Name, ref, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Status sends the commit status to the remote system.
|
||||
func (c *Coding) Status(u *model.User, r *model.Repo, b *model.Build, link string) 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 remote system.
|
||||
func (c *Coding) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
||||
if c.Password != "" {
|
||||
return &model.Netrc{
|
||||
Login: c.Username,
|
||||
Password: c.Password,
|
||||
Machine: c.Machine,
|
||||
}, nil
|
||||
}
|
||||
return &model.Netrc{
|
||||
Login: u.Token,
|
||||
Password: "x-oauth-basic",
|
||||
Machine: c.Machine,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Activate activates a repository by creating the post-commit hook.
|
||||
func (c *Coding) Activate(u *model.User, r *model.Repo, link string) error {
|
||||
return c.newClient(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(u *model.User, r *model.Repo, link string) error {
|
||||
return c.newClient(u).RemoveWebhook(r.Owner, r.Name, link)
|
||||
}
|
||||
|
||||
// Hook parses the post-commit hook from the Request body and returns the
|
||||
// required data in a standard format.
|
||||
func (c *Coding) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
|
||||
repo, build, err := parseHook(r)
|
||||
if build != nil {
|
||||
build.Avatar = c.resourceLink(build.Avatar)
|
||||
}
|
||||
return repo, build, err
|
||||
}
|
||||
|
||||
// helper function to return the Coding oauth2 context using an HTTPClient that
|
||||
// disables TLS verification if disabled in the remote settings.
|
||||
func (c *Coding) newContext() context.Context {
|
||||
if !c.SkipVerify {
|
||||
return oauth2.NoContext
|
||||
}
|
||||
return context.WithValue(nil, 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(u *model.User) *internal.Client {
|
||||
return c.newClientToken(u.Token, u.Secret)
|
||||
}
|
||||
|
||||
// helper function to return the Coding oauth2 client
|
||||
func (c *Coding) newClientToken(token, secret string) *internal.Client {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: c.SkipVerify,
|
||||
},
|
||||
},
|
||||
}
|
||||
return internal.NewClient(c.URL, "/api", token, "drone", client)
|
||||
}
|
||||
|
||||
func (c *Coding) resourceLink(resourcePath string) string {
|
||||
if strings.HasPrefix(resourcePath, "http") {
|
||||
return resourcePath
|
||||
}
|
||||
return c.URL + resourcePath
|
||||
}
|
295
remote/coding/coding_test.go
Normal file
295
remote/coding/coding_test.go
Normal file
|
@ -0,0 +1,295 @@
|
|||
package coding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/model"
|
||||
"github.com/drone/drone/remote/coding/fixtures"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Test_coding(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
s := httptest.NewServer(fixtures.Handler())
|
||||
c := &Coding{URL: s.URL}
|
||||
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Coding", func() {
|
||||
|
||||
g.After(func() {
|
||||
s.Close()
|
||||
})
|
||||
|
||||
g.Describe("Creating a remote", func() {
|
||||
g.It("Should return client with specified options", func() {
|
||||
remote, _ := New(Opts{
|
||||
URL: "https://coding.net",
|
||||
Client: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
||||
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
||||
Scopes: []string{"user", "project", "project:depot"},
|
||||
Machine: "git.coding.net",
|
||||
Username: "someuser",
|
||||
Password: "password",
|
||||
SkipVerify: true,
|
||||
})
|
||||
g.Assert(remote.(*Coding).URL).Equal("https://coding.net")
|
||||
g.Assert(remote.(*Coding).Client).Equal("KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP")
|
||||
g.Assert(remote.(*Coding).Secret).Equal("zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp")
|
||||
g.Assert(remote.(*Coding).Scopes).Equal([]string{"user", "project", "project:depot"})
|
||||
g.Assert(remote.(*Coding).Machine).Equal("git.coding.net")
|
||||
g.Assert(remote.(*Coding).Username).Equal("someuser")
|
||||
g.Assert(remote.(*Coding).Password).Equal("password")
|
||||
g.Assert(remote.(*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(w, r)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(nil, r)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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 anthenticated user", func() {
|
||||
login, err := c.Auth(
|
||||
fakeUser.Token,
|
||||
fakeUser.Secret,
|
||||
)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(login).Equal(fakeUser.Login)
|
||||
})
|
||||
g.It("Should handle a failure to resolve user", func() {
|
||||
_, err := c.Auth(
|
||||
fakeUserNotFound.Token,
|
||||
fakeUserNotFound.Secret,
|
||||
)
|
||||
g.Assert(err != nil).IsTrue()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("Given a refresh token", func() {
|
||||
g.It("Should return a refresh access token", func() {
|
||||
ok, err := c.Refresh(fakeUserRefresh)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(fakeUserRefreshInvalid)
|
||||
g.Assert(ok).IsFalse()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("When requesting a repository", func() {
|
||||
g.It("Should return the details", func() {
|
||||
repo, err := c.Repo(
|
||||
fakeUser,
|
||||
fakeRepo.Owner,
|
||||
fakeRepo.Name,
|
||||
)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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.Kind).Equal(fakeRepo.Kind)
|
||||
g.Assert(repo.Clone).Equal(fakeRepo.Clone)
|
||||
g.Assert(repo.Branch).Equal(fakeRepo.Branch)
|
||||
g.Assert(repo.IsPrivate).Equal(fakeRepo.IsPrivate)
|
||||
})
|
||||
g.It("Should handle not found errors", func() {
|
||||
_, err := c.Repo(
|
||||
fakeUser,
|
||||
fakeRepoNotFound.Owner,
|
||||
fakeRepoNotFound.Name,
|
||||
)
|
||||
g.Assert(err != nil).IsTrue()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("When requesting repository permissions", func() {
|
||||
g.It("Should authorize admin access for project owner", func() {
|
||||
perm, err := c.Perm(fakeUser, "demo1", "perm_owner")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(fakeUser, "demo1", "perm_admin")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(fakeUser, "demo1", "perm_member")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(fakeUser, "demo1", "perm_guest")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(
|
||||
fakeUser,
|
||||
fakeRepoNotFound.Owner,
|
||||
fakeRepoNotFound.Name,
|
||||
)
|
||||
g.Assert(err != nil).IsTrue()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("When downloading a file", func() {
|
||||
g.It("Should return file for specified build", func() {
|
||||
data, err := c.File(fakeUser, fakeRepo, fakeBuild, ".drone.yml")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(string(data)).Equal("pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n")
|
||||
})
|
||||
g.It("Should return file for specified ref", func() {
|
||||
data, err := c.FileRef(fakeUser, fakeRepo, "master", ".drone.yml")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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() {
|
||||
remote, _ := New(Opts{
|
||||
Machine: "git.coding.net",
|
||||
Username: "someuser",
|
||||
Password: "password",
|
||||
})
|
||||
netrc, err := remote.Netrc(fakeUser, nil)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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() {
|
||||
remote, _ := New(Opts{
|
||||
Machine: "git.coding.net",
|
||||
})
|
||||
netrc, err := remote.Netrc(fakeUser, nil)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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(fakeUser, fakeRepo, "http://127.0.0.1")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
})
|
||||
g.It("Should update the hook when exists", func() {
|
||||
err := c.Activate(fakeUser, fakeRepo, "http://127.0.0.2")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("When deactivating a repository", func() {
|
||||
g.It("Should successfully remove hook", func() {
|
||||
err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.3")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
})
|
||||
g.It("Should successfully deactivate when hook already removed", func() {
|
||||
err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.4")
|
||||
g.Assert(err == nil).IsTrue()
|
||||
})
|
||||
})
|
||||
|
||||
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(req)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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",
|
||||
Kind: model.RepoGit,
|
||||
Clone: "https://git.coding.net/demo1/test1.git",
|
||||
Branch: "master",
|
||||
IsPrivate: true,
|
||||
}
|
||||
|
||||
fakeRepoNotFound = &model.Repo{
|
||||
Owner: "not_found_owner",
|
||||
Name: "not_found_project",
|
||||
}
|
||||
|
||||
fakeRepos = []*model.RepoLite{
|
||||
&model.RepoLite{
|
||||
Owner: "demo1",
|
||||
Name: "test1",
|
||||
FullName: "demo1/test1",
|
||||
Avatar: "/static/project_icon/scenery-5.png",
|
||||
},
|
||||
}
|
||||
|
||||
fakeBuild = &model.Build{
|
||||
Commit: "4504a072cc",
|
||||
}
|
||||
)
|
313
remote/coding/fixtures/handler.go
Normal file
313
remote/coding/fixtures/handler.go
Normal file
|
@ -0,0 +1,313 @@
|
|||
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 getProjects(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||
c.String(200, fakeProjectsPayload)
|
||||
}
|
||||
|
||||
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/.drone.yml", "demo1/test1/4504a072cc/.drone.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 fakeProjectsPayload = `
|
||||
{
|
||||
"code":0,
|
||||
"data":{
|
||||
"list":{
|
||||
"owner_user_name":"demo1",
|
||||
"name":"test1",
|
||||
"icon":"/static/project_icon/scenery-5.png",
|
||||
},
|
||||
"page":1,
|
||||
"pageSize":1,
|
||||
"totalPage":1,
|
||||
"totalRow":1
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
175
remote/coding/fixtures/hooks.go
Normal file
175
remote/coding/fixtures/hooks.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
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 .drone.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"
|
||||
}
|
||||
`
|
229
remote/coding/hook.go
Normal file
229
remote/coding/hook.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
package coding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone/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.Build, error) {
|
||||
raw, err := ioutil.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 parseMergeReuqestHook(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{
|
||||
Owner: matches[1],
|
||||
Name: repo.Name,
|
||||
FullName: projectFullName(repo.Owner.GlobalKey, repo.Name),
|
||||
Link: repo.WebURL,
|
||||
Kind: model.RepoGit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parsePushHook(raw []byte) (*model.Repo, *model.Build, error) {
|
||||
hook := &PushHook{}
|
||||
err := json.Unmarshal(raw, hook)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// no build 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)
|
||||
build := &model.Build{
|
||||
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,
|
||||
Remote: hook.Repository.HttpsURL,
|
||||
}
|
||||
return repo, build, nil
|
||||
}
|
||||
|
||||
func parsePullRequestHook(raw []byte) (*model.Repo, *model.Build, 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
|
||||
}
|
||||
build := &model.Build{
|
||||
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,
|
||||
Remote: hook.Repository.HttpsURL,
|
||||
Refspec: fmt.Sprintf("%s:%s", hook.PullRequest.SourceBranch, hook.PullRequest.TargetBranch),
|
||||
}
|
||||
|
||||
return repo, build, nil
|
||||
}
|
||||
|
||||
func parseMergeReuqestHook(raw []byte) (*model.Repo, *model.Build, 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
|
||||
}
|
||||
|
||||
build := &model.Build{
|
||||
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,
|
||||
Remote: hook.Repository.HttpsURL,
|
||||
Refspec: fmt.Sprintf("%s:%s", hook.MergeRequest.SourceBranch, hook.MergeRequest.TargetBranch),
|
||||
}
|
||||
return repo, build, nil
|
||||
}
|
192
remote/coding/hook_test.go
Normal file
192
remote/coding/hook_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package coding
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/model"
|
||||
"github.com/drone/drone/remote/coding/fixtures"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
)
|
||||
|
||||
func Test_hook(t *testing.T) {
|
||||
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Coding hook", func() {
|
||||
|
||||
g.It("Should parse hook", func() {
|
||||
|
||||
reader := ioutil.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",
|
||||
Kind: model.RepoGit,
|
||||
}
|
||||
|
||||
build := &model.Build{
|
||||
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 .drone.yml\n",
|
||||
Email: "demo1@gmail.com",
|
||||
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
||||
Author: "demo1",
|
||||
Remote: "https://git.coding.net/demo1/test1.git",
|
||||
}
|
||||
|
||||
actualRepo, actualBuild, err := parseHook(r)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(actualRepo).Equal(repo)
|
||||
g.Assert(actualBuild).Equal(build)
|
||||
})
|
||||
|
||||
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",
|
||||
Kind: model.RepoGit,
|
||||
}
|
||||
actual, err := convertRepository(repository)
|
||||
g.Assert(err == nil).IsTrue()
|
||||
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",
|
||||
Kind: model.RepoGit,
|
||||
}
|
||||
|
||||
build := &model.Build{
|
||||
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 .drone.yml\n",
|
||||
Email: "demo1@gmail.com",
|
||||
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
||||
Author: "demo1",
|
||||
Remote: "https://git.coding.net/demo1/test1.git",
|
||||
}
|
||||
|
||||
actualRepo, actualBuild, err := parsePushHook([]byte(fixtures.PushHook))
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(actualRepo).Equal(repo)
|
||||
g.Assert(actualBuild).Equal(build)
|
||||
})
|
||||
|
||||
g.It("Should parse delete branch push hook", func() {
|
||||
actualRepo, actualBuild, err := parsePushHook([]byte(fixtures.DeleteBranchPushHook))
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(actualRepo == nil).IsTrue()
|
||||
g.Assert(actualBuild == nil).IsTrue()
|
||||
})
|
||||
|
||||
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",
|
||||
Kind: model.RepoGit,
|
||||
}
|
||||
|
||||
build := &model.Build{
|
||||
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",
|
||||
Remote: "https://git.coding.net/demo1/test2.git",
|
||||
Refspec: "master:master",
|
||||
}
|
||||
|
||||
actualRepo, actualBuild, err := parsePullRequestHook([]byte(fixtures.PullRequestHook))
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(actualRepo).Equal(repo)
|
||||
g.Assert(actualBuild).Equal(build)
|
||||
})
|
||||
|
||||
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",
|
||||
Kind: model.RepoGit,
|
||||
}
|
||||
|
||||
build := &model.Build{
|
||||
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",
|
||||
Remote: "https://git.coding.net/demo1/test1.git",
|
||||
Refspec: "branch1:master",
|
||||
}
|
||||
|
||||
actualRepo, actualBuild, err := parseMergeReuqestHook([]byte(fixtures.MergeRequestHook))
|
||||
g.Assert(err == nil).IsTrue()
|
||||
g.Assert(actualRepo).Equal(repo)
|
||||
g.Assert(actualBuild).Equal(build)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
84
remote/coding/internal/coding.go
Normal file
84
remote/coding/internal/coding.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiPath string
|
||||
token string
|
||||
agent string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type GenericAPIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiPath, token, agent string, client *http.Client) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
apiPath: apiPath,
|
||||
token: token,
|
||||
agent: agent,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.NewRequest(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.NewRequest("GET", rawURL+"?"+params.Encode(), nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to create request for url %q: %v", 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: %v", 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 := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read response from %s %s: %v", 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: %v", 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
|
||||
}
|
15
remote/coding/internal/error.go
Normal file
15
remote/coding/internal/error.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
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)
|
||||
}
|
31
remote/coding/internal/file.go
Normal file
31
remote/coding/internal/file.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
}
|
93
remote/coding/internal/project.go
Normal file
93
remote/coding/internal/project.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
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
|
||||
}
|
25
remote/coding/internal/user.go
Normal file
25
remote/coding/internal/user.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
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
|
||||
}
|
92
remote/coding/internal/webhook.go
Normal file
92
remote/coding/internal/webhook.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
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_pust", "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
|
||||
}
|
9
remote/coding/util.go
Normal file
9
remote/coding/util.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package coding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func projectFullName(owner, name string) string {
|
||||
return fmt.Sprintf("%s/%s", owner, name)
|
||||
}
|
18
remote/coding/util_test.go
Normal file
18
remote/coding/util_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
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