Added integration for coding.net

This commit is contained in:
mingshun 2017-07-22 17:12:09 +08:00
parent 44450bce8d
commit eb94dc0419
16 changed files with 1974 additions and 0 deletions

View file

@ -380,6 +380,58 @@ var flags = []cli.Flag{
Name: "stash-skip-verify", Name: "stash-skip-verify",
Usage: "stash skip ssl verification", 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 { func server(c *cli.Context) error {

View file

@ -10,6 +10,7 @@ import (
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/remote/bitbucket" "github.com/drone/drone/remote/bitbucket"
"github.com/drone/drone/remote/bitbucketserver" "github.com/drone/drone/remote/bitbucketserver"
"github.com/drone/drone/remote/coding"
"github.com/drone/drone/remote/gitea" "github.com/drone/drone/remote/gitea"
"github.com/drone/drone/remote/github" "github.com/drone/drone/remote/github"
"github.com/drone/drone/remote/gitlab" "github.com/drone/drone/remote/gitlab"
@ -62,6 +63,8 @@ func SetupRemote(c *cli.Context) (remote.Remote, 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")
} }
@ -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 } func before(c *cli.Context) error { return nil }

334
remote/coding/coding.go Normal file
View 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
}

View 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",
}
)

View 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"
}
]
}
`

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

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

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

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

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

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

View 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
View file

@ -0,0 +1,9 @@
package coding
import (
"fmt"
)
func projectFullName(owner, name string) string {
return fmt.Sprintf("%s/%s", owner, name)
}

View 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")
})
})
}