add release & milestone functionality

pull/121/head
Eoin McAfee 3 years ago
parent be60c693c9
commit 8ce6b18ce6

@ -3,6 +3,8 @@ module github.com/drone/go-scm
require (
github.com/google/go-cmp v0.2.0
github.com/h2non/gock v1.0.9
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
github.com/stretchr/testify v1.7.0
)
go 1.13

@ -1,4 +1,17 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/h2non/gock v1.0.9 h1:17gCehSo8ZOgEsFKpQgqHiR7VLyjxdAG3lkhVvO9QZU=
github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -70,9 +70,11 @@ type (
// ListOptions specifies optional pagination
// parameters.
ListOptions struct {
URL string
Page int
Size int
URL string
Page int
Size int
Open bool
Closed bool
}
// Client manages communication with a version control
@ -93,8 +95,10 @@ type (
Git GitService
Organizations OrganizationService
Issues IssueService
Milestones MilestoneService
PullRequests PullRequestService
Repositories RepositoryService
Releases ReleaseService
Reviews ReviewService
Users UserService
Webhooks WebhookService

@ -240,3 +240,5 @@ func (v Visibility) String() (s string) {
return "unknown"
}
}
const SearchTimeFormat = "2006-01-02T15:04:05Z"

@ -34,9 +34,11 @@ func New(uri string) (*scm.Client, error) {
client.Contents = &contentService{client}
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Milestones = &milestoneService{client}
client.Organizations = &organizationService{client}
client.PullRequests = &pullService{&issueService{client}}
client.Repositories = &repositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -0,0 +1,31 @@
package bitbucket
import (
"context"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -0,0 +1,43 @@
package bitbucket
import (
"context"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -107,8 +107,8 @@ type (
commitInfo struct {
Sha string `json:"sha"`
Commit commit `json:"commit"`
Author user `json:"author"`
Committer user `json:"committer"`
Author User `json:"author"`
Committer User `json:"committer"`
}
// gitea signature object.
@ -176,7 +176,7 @@ func convertSignature(src signature) scm.Signature {
}
}
func convertUserSignature(src user) scm.Signature {
func convertUserSignature(src User) scm.Signature {
return scm.Signature{
Login: userLogin(&src),
Email: src.Email,

@ -35,9 +35,11 @@ func New(uri string) (*scm.Client, error) {
client.Contents = &contentService{client}
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Milestones = & milestoneService{client}
client.Organizations = &organizationService{client}
client.PullRequests = &pullService{client}
client.Repositories = &repositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -5,7 +5,10 @@
// package gitea implements a Gogs client.
package gitea
import "testing"
import (
"github.com/drone/go-scm/scm"
"testing"
)
func TestClient(t *testing.T) {
client, err := New("https://try.gitea.io")
@ -33,3 +36,20 @@ func TestClient_Error(t *testing.T) {
t.Errorf("Expect error when invalid URL")
}
}
func testPage(res *scm.Response) func(t *testing.T) {
return func(t *testing.T) {
if got, want := res.Page.Next, 2; got != want {
t.Errorf("Want next page %d, got %d", want, got)
}
if got, want := res.Page.Prev, 1; got != want {
t.Errorf("Want prev page %d, got %d", want, got)
}
if got, want := res.Page.First, 1; got != want {
t.Errorf("Want first page %d, got %d", want, got)
}
if got, want := res.Page.Last, 5; got != want {
t.Errorf("Want last page %d, got %d", want, got)
}
}
}

@ -87,9 +87,9 @@ type (
// gitea issue response object.
issue struct {
ID int `json:"id"`
Number int `json:"number"`
User user `json:"user"`
Title string `json:"title"`
Number int `json:"number"`
User User `json:"User"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
Labels []string `json:"labels"`
@ -111,9 +111,9 @@ type (
// gitea issue comment response object.
issueComment struct {
ID int `json:"id"`
HTMLURL string `json:"html_url"`
User user `json:"user"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
User User `json:"User"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

@ -0,0 +1,130 @@
package gitea
import (
"context"
"fmt"
"time"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/milestones/%d", namespace, name, id)
out := new(milestone)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.MilestoneListOptions) ([]*scm.Milestone, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/milestones%s", namespace, name, encodeMilestoneListOptions(opts))
out := []*milestone{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertMilestoneList(out), res, err
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/milestones", namespace, name)
in := &milestoneInput{
Title: input.Title,
Description: input.Description,
State: StateOpen,
Deadline: input.DueDate,
}
if input.State == "closed" {
in.State = StateClosed
}
out := new(milestone)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/milestones/%d", namespace, name, id)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/milestones/%d", namespace, name, id)
in := milestoneInput{}
if input.Title != "" {
in.Title = input.Title
}
switch input.State {
case "open":
in.State = StateOpen
case "close", "closed":
in.State = StateClosed
}
if input.Description != "" {
in.Description = input.Description
}
if input.DueDate != nil {
in.Deadline = input.DueDate
}
out := new(milestone)
res, err := s.client.do(ctx, "PATCH", path, in, out)
return convertMilestone(out), res, err
}
// StateType issue state type
type StateType string
const (
// StateOpen pr/issue is open
StateOpen StateType = "open"
// StateClosed pr/issue is closed
StateClosed StateType = "closed"
// StateAll is all
StateAll StateType = "all"
)
type milestone struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
State StateType `json:"state"`
OpenIssues int `json:"open_issues"`
ClosedIssues int `json:"closed_issues"`
Created time.Time `json:"created_at"`
Updated *time.Time `json:"updated_at"`
Closed *time.Time `json:"closed_at"`
Deadline *time.Time `json:"due_on"`
}
type milestoneInput struct {
Title string `json:"title"`
Description string `json:"description"`
State StateType `json:"state"`
Deadline *time.Time `json:"due_on"`
}
func convertMilestoneList(src []*milestone) []*scm.Milestone {
var dst []*scm.Milestone
for _, v := range src {
dst = append(dst, convertMilestone(v))
}
return dst
}
func convertMilestone(src *milestone) *scm.Milestone {
if src == nil || src.Deadline == nil {
return nil
}
return &scm.Milestone{
Number: int(src.ID),
ID: int(src.ID),
Title: src.Title,
Description: src.Description,
State: string(src.State),
DueDate: src.Deadline,
}
}

@ -0,0 +1,178 @@
package gitea
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"time"
"github.com/drone/go-scm/scm"
"github.com/google/go-cmp/cmp"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)
func TestMilestoneFind(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/api/v1/repos/jcitizen/my-repo/milestones/1").
Reply(200).
Type("application/json").
File("testdata/milestone.json")
client, _ := New("https://try.gitea.io")
got, _, err := client.Milestones.Find(context.Background(), "jcitizen/my-repo", 1)
if err != nil {
t.Error(err)
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}
func TestMilestoneList(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/api/v1/repos/jcitizen/my-repo/milestones").
Reply(200).
Type("application/json").
SetHeaders(mockPageHeaders).
File("testdata/milestones.json")
client, _ := New("https://try.gitea.io")
got, res, err := client.Milestones.List(context.Background(), "jcitizen/my-repo", scm.MilestoneListOptions{})
if err != nil {
t.Error(err)
}
want := []*scm.Milestone{}
raw, _ := ioutil.ReadFile("testdata/milestones.json.golden")
err = json.Unmarshal(raw, &want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Page", testPage(res))
}
func TestMilestoneCreate(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Post("/api/v1/repos/jcitizen/my-repo/milestones").
File("testdata/milestone_create.json").
Reply(200).
Type("application/json").
File("testdata/milestone.json")
client, _ := New("https://try.gitea.io")
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "open",
DueDate: &dueDate,
}
got, _, err := client.Milestones.Create(context.Background(), "jcitizen/my-repo", input)
if err != nil {
t.Error(err)
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}
func TestMilestoneUpdate(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Patch("/api/v1/repos/jcitizen/my-repo/milestones").
File("testdata/milestone_create.json").
Reply(200).
Type("application/json").
File("testdata/milestone.json")
client, _ := New("https://try.gitea.io")
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "open",
DueDate: &dueDate,
}
got, _, err := client.Milestones.Update(context.Background(), "jcitizen/my-repo", 1, input)
if err != nil {
t.Error(err)
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}
func TestMilestoneDelete(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Delete("/api/v1/repos/jcitizen/my-repo/milestones/1").
Reply(200).
Type("application/json")
client, _ := New("https://try.gitea.io")
_, err := client.Milestones.Delete(context.Background(), "jcitizen/my-repo", 1)
if err != nil {
t.Error(err)
}
}
var mockPageHeaders = map[string]string{
"Link": `<https://try.gitea.io/v1/resource?page=2>; rel="next",` +
`<https://try.gitea.io/v1/resource?page=1>; rel="prev",` +
`<https://try.gitea.io/v1/resource?page=1>; rel="first",` +
`<https://try.gitea.io/v1/resource?page=5>; rel="last"`,
}
func mockServerVersion() {
gock.New("https://try.gitea.io").
Get("/api/v1/version").
Reply(200).
Type("application/json").
File("testdata/version.json")
}

@ -27,7 +27,7 @@ func (s *organizationService) FindMembership(ctx context.Context, name, username
}
func (s *organizationService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Organization, *scm.Response, error) {
path := fmt.Sprintf("api/v1/user/orgs?%s", encodeListOptions(opts))
path := fmt.Sprintf("api/v1/User/orgs?%s", encodeListOptions(opts))
out := []*org{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertOrgList(out), res, err

@ -53,7 +53,7 @@ func TestOrgList(t *testing.T) {
defer gock.Off()
gock.New("https://try.gitea.io").
Get("/api/v1/user/orgs").
Get("/api/v1/User/orgs").
Reply(200).
Type("application/json").
File("testdata/organizations.json")

@ -83,9 +83,9 @@ func (s *pullService) Close(context.Context, string, int) (*scm.Response, error)
type pr struct {
ID int `json:"id"`
Number int `json:"number"`
User user `json:"user"`
Title string `json:"title"`
Number int `json:"number"`
User User `json:"User"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
HeadBranch string `json:"head_branch"`

@ -0,0 +1,150 @@
package gitea
import (
"context"
"fmt"
"time"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases/%d", namespace, name, id)
out := new(release)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertRelease(out), res, err
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases/tags/%s", namespace, name, tag)
out := new(release)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertRelease(out), res, err
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases?%s", namespace, name, encodeReleaseListOptions(opts))
out := []*release{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertReleaseList(out), res, err
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases", namespace, name)
in := &ReleaseInput{
TagName: input.Tag,
Target: input.Commitish,
Title: input.Title,
Note: input.Description,
IsDraft: input.Draft,
IsPrerelease: input.Prerelease,
}
out := new(release)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertRelease(out), res, err
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases/%d", namespace, name, id)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases/tags/%s", namespace, name, tag)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
namespace, name := scm.Split(repo)
path := fmt.Sprintf("api/v1/repos/%s/%s/releases/%d", namespace, name, id)
in := &ReleaseInput{
TagName: input.Tag,
Target: input.Commitish,
Title: input.Title,
Note: input.Description,
IsDraft: input.Draft,
IsPrerelease: input.Prerelease,
}
out := new(release)
res, err := s.client.do(ctx, "PATCH", path, in, out)
return convertRelease(out), res, err
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
rel, _, err := s.FindByTag(ctx, repo, tag)
if err != nil {
return nil, nil, err
}
return s.Update(ctx, repo, rel.ID, input)
}
type ReleaseInput struct {
TagName string `json:"tag_name"`
Target string `json:"target_commitish"`
Title string `json:"name"`
Note string `json:"body"`
IsDraft bool `json:"draft"`
IsPrerelease bool `json:"prerelease"`
}
// release represents a repository release
type release struct {
ID int64 `json:"id"`
TagName string `json:"tag_name"`
Target string `json:"target_commitish"`
Title string `json:"name"`
Note string `json:"body"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
TarURL string `json:"tarball_url"`
ZipURL string `json:"zipball_url"`
IsDraft bool `json:"draft"`
IsPrerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Publisher *User `json:"author"`
Attachments []*Attachment `json:"assets"`
}
type Attachment struct {
ID int64 `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
DownloadCount int64 `json:"download_count"`
Created time.Time `json:"created_at"`
UUID string `json:"uuid"`
DownloadURL string `json:"browser_download_url"`
}
func convertRelease(src *release) *scm.Release {
return &scm.Release{
ID: int(src.ID),
Title: src.Title,
Description: src.Note,
Link: ConvertAPIURLToHTMLURL(src.URL, src.TagName),
Tag: src.TagName,
Commitish: src.Target,
Draft: src.IsDraft,
Prerelease: src.IsPrerelease,
Created: src.CreatedAt,
Published: src.PublishedAt,
}
}
func convertReleaseList(src []*release) []*scm.Release {
var dst []*scm.Release
for _, v := range src {
dst = append(dst, convertRelease(v))
}
return dst
}

@ -0,0 +1,342 @@
package gitea
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"github.com/drone/go-scm/scm"
"github.com/google/go-cmp/cmp"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)
func TestConvertAPIURLToHTMLURL(t *testing.T) {
got := ConvertAPIURLToHTMLURL("https://try.gitea.com/api/v1/repos/octocat/Hello-World/123", "v1.0.0")
want := "https://try.gitea.com/octocat/Hello-World/releases/tag/v1.0.0"
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
t.Log("got:")
t.Log(string(got))
}
}
func TestConvertAPIURLToHTMLURLEmptyLinkWhenURLParseFails(t *testing.T) {
broken := []string{"http s://try.gitea.com/api/v1/repos/octocat/Hello-World/123", "https://try.gitea.com/api/v1/repos/octocat/Hello-World"}
for _, url := range broken {
got := ConvertAPIURLToHTMLURL(url, "v1.0.0")
want := ""
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
t.Log("got:")
t.Log(string(got))
}
}
}
func TestReleaseFind(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/repos/octocat/hello-world/releases/1").
Reply(200).
Type("application/json").
File("testdata/release.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
got, _, err := client.Releases.Find(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
}
func TestReleaseFindByTag(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/repos/octocat/hello-world/releases/tags/v1.0.0").
Reply(200).
Type("application/json").
File("testdata/release.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
got, _, err := client.Releases.FindByTag(context.Background(), "octocat/hello-world", "v1.0.0")
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
}
func TestReleaseList(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/repos/octocat/hello-world/releases").
MatchParam("page", "1").
MatchParam("limit", "30").
Reply(200).
Type("application/json").
File("testdata/releases.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
got, _, err := client.Releases.List(context.Background(), "octocat/hello-world", scm.ReleaseListOptions{Page: 1, Size: 30, Open: true, Closed: true})
if err != nil {
t.Error(err)
return
}
want := []*scm.Release{}
raw, _ := ioutil.ReadFile("testdata/releases.json.golden")
err = json.Unmarshal(raw, &want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
}
func TestReleaseCreate(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Post("/repos/octocat/hello-world/releases").
File("testdata/release_create.json").
Reply(200).
Type("application/json").
File("testdata/release.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
input := &scm.ReleaseInput{
Title: "v1.0.0",
Description: "Description of the release",
Tag: "v1.0.0",
Commitish: "master",
Draft: false,
Prerelease: false,
}
got, _, err := client.Releases.Create(context.Background(), "octocat/hello-world", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
}
func TestReleaseUpdate(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Patch("/repos/octocat/hello-world/releases/1").
File("testdata/release_update.json").
Reply(200).
Type("application/json").
File("testdata/release.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
input := &scm.ReleaseInput{
Title: "v1.0.0",
Description: "Description of the release",
Tag: "v1.0.0",
Commitish: "master",
Draft: false,
Prerelease: false,
}
got, _, err := client.Releases.Update(context.Background(), "octocat/hello-world", 1, input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}
func TestReleaseUpdateByTag(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Get("/repos/octocat/hello-world/releases/tags/v1.0.0").
Reply(200).
Type("application/json").
File("testdata/release.json")
gock.New("https://try.gitea.io").
Patch("/repos/octocat/hello-world/releases/1").
File("testdata/release_update.json").
Reply(200).
Type("application/json").
File("testdata/release.json")
client, err := New("https://try.gitea.io")
if err != nil {
t.Error(err)
return
}
input := &scm.ReleaseInput{
Title: "v1.0.0",
Description: "Description of the release",
Tag: "v1.0.0",
Commitish: "master",
Draft: false,
Prerelease: false,
}
got, _, err := client.Releases.UpdateByTag(context.Background(), "octocat/hello-world", "v1.0.0", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
}
func TestReleaseDelete(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Delete("/repos/octocat/hello-world/releases/1").
Reply(200).
Type("application/json")
client, err := New("https://try.gitea.io")
_, err = client.Releases.Delete(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
}
func TestReleaseDeleteByTag(t *testing.T) {
defer gock.Off()
mockServerVersion()
gock.New("https://try.gitea.io").
Delete("/repos/octocat/hello-world/releases/tags/v1.0.0").
Reply(200).
Type("application/json")
client, err := New("https://try.gitea.io")
_, err = client.Releases.DeleteByTag(context.Background(), "octocat/hello-world", "v1.0.0")
if err != nil {
t.Error(err)
return
}
}

@ -40,7 +40,7 @@ func (s *repositoryService) FindPerms(ctx context.Context, repo string) (*scm.Pe
}
func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Repository, *scm.Response, error) {
path := fmt.Sprintf("api/v1/user/repos?%s", encodeListOptions(opts))
path := fmt.Sprintf("api/v1/User/repos?%s", encodeListOptions(opts))
out := []*repository{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertRepositoryList(out), res, err
@ -135,9 +135,9 @@ func (s *repositoryService) DeleteHook(ctx context.Context, repo string, id stri
type (
// gitea repository resource.
repository struct {
ID int `json:"id"`
Owner user `json:"owner"`
Name string `json:"name"`
ID int `json:"id"`
Owner User `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Private bool `json:"private"`
Fork bool `json:"fork"`

@ -73,7 +73,7 @@ func TestRepoList(t *testing.T) {
defer gock.Off()
gock.New("https://try.gitea.io").
Get("/api/v1/user/repos").
Get("/api/v1/User/repos").
Reply(200).
Type("application/json").
File("testdata/repos.json")

@ -0,0 +1,12 @@
{
"closed_at": "2020-09-11T19:32:38.046Z",
"closed_issues": 0,
"created_at": "2020-09-11T19:32:38.046Z",
"description": "string",
"due_on": "2020-09-11T19:32:38.046Z",
"id": 1,
"open_issues": 0,
"state": "open",
"title": "string",
"updated_at": "2020-09-11T19:32:38.046Z"
}

@ -0,0 +1,8 @@
{
"Description": "string",
"DueDate": "2020-09-11T19:32:38.046Z",
"ID": 1,
"Number": 1,
"State": "open",
"Title": "string"
}

@ -0,0 +1,6 @@
{
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"state": "open",
"due_on": "2012-10-09T23:39:01Z"
}

@ -0,0 +1,14 @@
[
{
"closed_at": "2020-09-11T19:32:38.046Z",
"closed_issues": 0,
"created_at": "2020-09-11T19:32:38.046Z",
"description": "string",
"due_on": "2020-09-11T19:32:38.046Z",
"id": 1,
"open_issues": 0,
"state": "open",
"title": "string",
"updated_at": "2020-09-11T19:32:38.046Z"
}
]

@ -0,0 +1,10 @@
[
{
"Description": "string",
"DueDate": "2020-09-11T19:32:38.046Z",
"ID": 1,
"Number": 1,
"State": "open",
"Title": "string"
}
]

@ -0,0 +1,10 @@
{
"id": 1,
"name": "v1.0.0",
"body": "Description of the release",
"url": "https://try.gitea.com/api/v1/repos/octocat/Hello-World/123",
"tag_name": "v1.0.0",
"target_commitish": "master",
"draft": false,
"prerelease": false
}

@ -0,0 +1,10 @@
{
"ID": 1,
"Title": "v1.0.0",
"Description": "Description of the release",
"Link": "https://try.gitea.com/octocat/Hello-World/releases/tag/v1.0.0",
"Tag": "v1.0.0",
"Commitish": "master",
"Draft": false,
"Prerelease": false
}

@ -0,0 +1,8 @@
{
"tag_name": "v1.0.0",
"target_commitish": "master",
"name": "v1.0.0",
"body": "Description of the release",
"draft": false,
"prerelease": false
}

@ -0,0 +1,8 @@
{
"tag_name": "v1.0.0",
"target_commitish": "master",
"name": "v1.0.0",
"body": "Description of the release",
"draft": false,
"prerelease": false
}

@ -0,0 +1,12 @@
[
{
"id": 1,
"name": "v1.0.0",
"body": "Description of the release",
"url": "https://try.gitea.com/api/v1/repos/octocat/Hello-World/123",
"tag_name": "v1.0.0",
"target_commitish": "master",
"draft": false,
"prerelease": false
}
]

@ -0,0 +1,12 @@
[
{
"ID": 1,
"Title": "v1.0.0",
"Description": "Description of the release",
"Link": "https://try.gitea.com/octocat/Hello-World/releases/tag/v1.0.0",
"Tag": "v1.0.0",
"Commitish": "master",
"Draft": false,
"Prerelease": false
}
]

@ -16,14 +16,14 @@ type userService struct {
}
func (s *userService) Find(ctx context.Context) (*scm.User, *scm.Response, error) {
out := new(user)
res, err := s.client.do(ctx, "GET", "api/v1/user", nil, out)
out := new(User)
res, err := s.client.do(ctx, "GET", "api/v1/User", nil, out)
return convertUser(out), res, err
}
func (s *userService) FindLogin(ctx context.Context, login string) (*scm.User, *scm.Response, error) {
path := fmt.Sprintf("api/v1/users/%s", login)
out := new(user)
out := new(User)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertUser(out), res, err
}
@ -37,7 +37,7 @@ func (s *userService) FindEmail(ctx context.Context) (string, *scm.Response, err
// native data structures
//
type user struct {
type User struct {
ID int `json:"id"`
Login string `json:"login"`
Username string `json:"username"`
@ -50,7 +50,7 @@ type user struct {
// native data structure conversion
//
func convertUser(src *user) *scm.User {
func convertUser(src *User) *scm.User {
return &scm.User{
Login: userLogin(src),
Avatar: src.Avatar,
@ -59,7 +59,7 @@ func convertUser(src *user) *scm.User {
}
}
func userLogin(src *user) string {
func userLogin(src *User) string {
if src.Username != "" {
return src.Username
}

@ -20,10 +20,10 @@ func TestUserFind(t *testing.T) {
defer gock.Off()
gock.New("https://try.gitea.io").
Get("/api/v1/user").
Get("/api/v1/User").
Reply(200).
Type("application/json").
File("testdata/user.json")
File("testdata/User.json")
client, _ := New("https://try.gitea.io")
got, _, err := client.Users.Find(context.Background())
@ -32,7 +32,7 @@ func TestUserFind(t *testing.T) {
}
want := new(scm.User)
raw, _ := ioutil.ReadFile("testdata/user.json.golden")
raw, _ := ioutil.ReadFile("testdata/User.json.golden")
json.Unmarshal(raw, &want)
if diff := cmp.Diff(got, want); diff != "" {
@ -48,7 +48,7 @@ func TestUserLoginFind(t *testing.T) {
Get("/api/v1/users/jcitizen").
Reply(200).
Type("application/json").
File("testdata/user.json")
File("testdata/User.json")
client, _ := New("https://try.gitea.io")
got, _, err := client.Users.FindLogin(context.Background(), "jcitizen")
@ -57,7 +57,7 @@ func TestUserLoginFind(t *testing.T) {
}
want := new(scm.User)
raw, _ := ioutil.ReadFile("testdata/user.json.golden")
raw, _ := ioutil.ReadFile("testdata/User.json.golden")
json.Unmarshal(raw, &want)
if diff := cmp.Diff(got, want); diff != "" {
@ -70,10 +70,10 @@ func TestUserFindEmail(t *testing.T) {
defer gock.Off()
gock.New("https://try.gitea.io").
Get("/api/v1/user").
Get("/api/v1/User").
Reply(200).
Type("application/json").
File("testdata/user.json")
File("testdata/User.json")
client, _ := New("https://try.gitea.io")
email, _, err := client.Users.FindEmail(context.Background())

@ -5,8 +5,10 @@
package gitea
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/drone/go-scm/scm"
)
@ -53,3 +55,53 @@ func encodePullRequestListOptions(opts scm.PullRequestListOptions) string {
}
return params.Encode()
}
// ConvertAPIURLToHTMLURL converts an release API endpoint into a html endpoint
func ConvertAPIURLToHTMLURL(apiURL string, tagName string) string {
// "url": "https://try.gitea.com/api/v1/repos/octocat/Hello-World/123",
// "html_url": "https://try.gitea.com/octocat/Hello-World/releases/tag/v1.0.0",
// the url field is the API url, not the html url, so until go-sdk v0.13.3, build it ourselves
link, err := url.Parse(apiURL)
if err != nil {
return ""
}
pathParts := strings.Split(link.Path, "/")
if len(pathParts) != 7 {
return ""
}
link.Path = fmt.Sprintf("/%s/%s/releases/tag/%s", pathParts[4], pathParts[5], tagName)
return link.String()
}
func encodeMilestoneListOptions(opts scm.MilestoneListOptions) string {
params := url.Values{}
if opts.Page != 0 {
params.Set("page", strconv.Itoa(opts.Page))
}
if opts.Size != 0 {
params.Set("per_page", strconv.Itoa(opts.Size))
}
if opts.Open && opts.Closed {
params.Set("state", "all")
} else if opts.Closed {
params.Set("state", "closed")
}
return params.Encode()
}
func encodeReleaseListOptions(opts scm.ReleaseListOptions) string {
params := url.Values{}
if opts.Page != 0 {
params.Set("page", strconv.Itoa(opts.Page))
}
if opts.Size != 0 {
params.Set("per_page", strconv.Itoa(opts.Size))
}
if opts.Open && opts.Closed {
params.Set("state", "all")
} else if opts.Closed {
params.Set("state", "closed")
}
return params.Encode()
}

@ -146,8 +146,8 @@ type (
Compare string `json:"compare_url"`
Commits []commit `json:"commits"`
Repository repository `json:"repository"`
Pusher user `json:"pusher"`
Sender user `json:"sender"`
Pusher User `json:"pusher"`
Sender User `json:"sender"`
}
// gitea create webhook payload
@ -156,8 +156,8 @@ type (
RefType string `json:"ref_type"`
Sha string `json:"sha"`
DefaultBranch string `json:"default_branch"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
Repository repository `json:"repository"`
Sender User `json:"sender"`
}
// gitea issue webhook payload
@ -165,8 +165,8 @@ type (
Action string `json:"action"`
Issue issue `json:"issue"`
Comment issueComment `json:"comment"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
Repository repository `json:"repository"`
Sender User `json:"sender"`
}
// gitea pull request webhook payload
@ -174,8 +174,8 @@ type (
Action string `json:"action"`
Number int `json:"number"`
PullRequest pr `json:"pull_request"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
Repository repository `json:"repository"`
Sender User `json:"sender"`
}
)

@ -38,9 +38,11 @@ func New(uri string) (*scm.Client, error) {
client.Contents = &contentService{client}
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Milestones = &milestoneService{client}
client.Organizations = &organizationService{client}
client.PullRequests = &pullService{&issueService{client}}
client.Repositories = &RepositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -0,0 +1,111 @@
package github
import (
"context"
"fmt"
"time"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
type milestone struct {
ID int `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Description string `json:"description"`
State string `json:"state"`
DueOn time.Time `json:"due_on"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
LabelsURL string `json:"labels_url"`
Creator user `json:"creator"`
OpenIssues int `json:"open_issues"`
ClosedIssues int `json:"closed_issues"`
NodeID string `json:"node_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ClosedAt time.Time `json:"closed_at"`
}
type milestoneInput struct {
Title string `json:"title"`
State string `json:"state"`
Description string `json:"description"`
DueOn time.Time `json:"due_on"`
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/milestones/%d", repo, id)
out := new(milestone)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.MilestoneListOptions) ([]*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/milestones?%s", repo, encodeMilestoneListOptions(opts))
out := []*milestone{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertMilestoneList(out), res, err
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/milestones", repo)
in := &milestoneInput{
Title: input.Title,
State: input.State,
Description: input.Description,
DueOn: *input.DueDate,
}
out := new(milestone)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
path := fmt.Sprintf("repos/%s/milestones/%d", repo, id)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/milestones/%d", repo, id)
in := &milestoneInput{}
if input.Title != "" {
in.Title = input.Title
}
if input.State != "" {
in.State = input.State
}
if input.Description != "" {
in.Description = input.Description
}
if input.DueDate != nil {
in.DueOn = *input.DueDate
}
out := new(milestone)
res, err := s.client.do(ctx, "PATCH", path, in, out)
return convertMilestone(out), res, err
}
func convertMilestoneList(from []*milestone) []*scm.Milestone {
var to []*scm.Milestone
for _, m := range from {
to = append(to, convertMilestone(m))
}
return to
}
func convertMilestone(from *milestone) *scm.Milestone {
return &scm.Milestone{
Number: from.Number,
ID: from.ID,
Title: from.Title,
Description: from.Description,
Link: from.HTMLURL,
State: from.State,
DueDate: &from.DueOn,
}
}

@ -0,0 +1,177 @@
package github
import (
"context"
"encoding/json"
"github.com/drone/go-scm/scm"
"github.com/google/go-cmp/cmp"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
"io/ioutil"
"testing"
"time"
)
func TestMilestoneFind(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Get("/repos/octocat/hello-world/milestones/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
got, res, err := client.Milestones.Find(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneList(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Get("/repos/octocat/hello-world/milestones").
MatchParam("page", "1").
MatchParam("per_page", "30").
MatchParam("state", "all").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestones.json")
client := NewDefault()
got, res, err := client.Milestones.List(context.Background(), "octocat/hello-world", scm.MilestoneListOptions{Page: 1, Size: 30, Open: true, Closed: true})
if err != nil {
t.Error(err)
return
}
want := []*scm.Milestone{}
raw, _ := ioutil.ReadFile("testdata/milestones.json.golden")
err = json.Unmarshal(raw, &want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneCreate(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Post("/repos/octocat/hello-world/milestones").
File("testdata/milestone_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "open",
DueDate: &dueDate,
}
got, res, err := client.Milestones.Create(context.Background(), "octocat/hello-world", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneUpdate(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Patch("/repos/octocat/hello-world/milestones/1").
File("testdata/milestone_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "open",
DueDate: &dueDate,
}
got, res, err := client.Milestones.Update(context.Background(), "octocat/hello-world", 1, input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, _ := ioutil.ReadFile("testdata/milestone.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneDelete(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Delete("/repos/octocat/hello-world/milestones/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders)
client := NewDefault()
res, err := client.Milestones.Delete(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}

@ -0,0 +1,131 @@
package github
import (
"context"
"fmt"
"time"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
type release struct {
ID int `json:"id"`
Title string `json:"name"`
Description string `json:"body"`
Link string `json:"html_url,omitempty"`
Tag string `json:"tag_name,omitempty"`
Commitish string `json:"target_commitish,omitempty"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
Created time.Time `json:"created_at"`
Published time.Time `json:"published_at"`
}
type releaseInput struct {
Title string `json:"name"`
Description string `json:"body"`
Tag string `json:"tag_name"`
Commitish string `json:"target_commitish"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases/%d", repo, id)
out := new(release)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertRelease(out), res, err
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases/tags/%s", repo, tag)
out := new(release)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertRelease(out), res, err
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases?%s", repo, encodeReleaseListOptions(opts))
out := []*release{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertReleaseList(out), res, err
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases", repo)
in := &releaseInput{
Title: input.Title,
Commitish: input.Commitish,
Description: input.Description,
Draft: input.Draft,
Prerelease: input.Prerelease,
Tag: input.Tag,
}
out := new(release)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertRelease(out), res, err
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases/%d", repo, id)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
rel, _, _ := s.FindByTag(ctx, repo, tag)
return s.Delete(ctx, repo, rel.ID)
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("repos/%s/releases/%d", repo, id)
in := &releaseInput{}
if input.Title != "" {
in.Title = input.Title
}
if input.Description != "" {
in.Description = input.Description
}
if input.Commitish != "" {
in.Commitish = input.Commitish
}
if input.Tag != "" {
in.Tag = input.Tag
}
in.Draft = input.Draft
in.Prerelease = input.Prerelease
out := new(release)
res, err := s.client.do(ctx, "PATCH", path, in, out)
return convertRelease(out), res, err
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
rel, _, _ := s.FindByTag(ctx, repo, tag)
return s.Update(ctx, repo, rel.ID, input)
}
func convertReleaseList(from []*release) []*scm.Release {
var to []*scm.Release
for _, m := range from {
to = append(to, convertRelease(m))
}
return to
}
func convertRelease(from *release) *scm.Release {
return &scm.Release{
ID: from.ID,
Title: from.Title,
Description: from.Description,
Link: from.Link,
Tag: from.Tag,
Commitish: from.Commitish,
Draft: from.Draft,
Prerelease: from.Prerelease,
Created: from.Created,
Published: from.Published,
}
}

@ -0,0 +1,186 @@
package github
import (
"context"
"encoding/json"
"github.com/drone/go-scm/scm"
"github.com/h2non/gock"
"io/ioutil"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
func TestReleaseFind(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Get("/repos/octocat/hello-world/releases/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
got, res, err := client.Releases.Find(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseList(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Get("/repos/octocat/hello-world/releases").
MatchParam("page", "1").
MatchParam("per_page", "30").
MatchParam("state", "all").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/releases.json")
client := NewDefault()
got, res, err := client.Releases.List(context.Background(), "octocat/hello-world", scm.ReleaseListOptions{Page: 1, Size: 30, Open: true, Closed: true})
if err != nil {
t.Error(err)
return
}
want := []*scm.Release{}
raw, _ := ioutil.ReadFile("testdata/releases.json.golden")
err = json.Unmarshal(raw, &want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseCreate(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Post("/repos/octocat/hello-world/releases").
File("testdata/release_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
input := &scm.ReleaseInput{
Title: "v1.0",
Description: "Tracking release for version 1.0",
Tag: "v1.0",
}
got, res, err := client.Releases.Create(context.Background(), "octocat/hello-world", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseUpdate(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Patch("/repos/octocat/hello-world/releases/1").
File("testdata/release_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
input := &scm.ReleaseInput{
Title: "v1.0",
Description: "Tracking release for version 1.0",
Tag: "v1.0",
}
got, res, err := client.Releases.Update(context.Background(), "octocat/hello-world", 1, input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseDelete(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Delete("/repos/octocat/hello-world/releases/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders)
client := NewDefault()
res, err := client.Releases.Delete(context.Background(), "octocat/hello-world", 1)
if err != nil {
t.Error(err)
return
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}

@ -0,0 +1,37 @@
{
"url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
"html_url": "https://github.com/octocat/Hello-World/milestones/v1.0",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels",
"id": 1002604,
"node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==",
"number": 1,
"state": "open",
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"creator": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"open_issues": 4,
"closed_issues": 8,
"created_at": "2011-04-10T20:09:31Z",
"updated_at": "2014-03-03T18:58:10Z",
"closed_at": "2013-02-12T13:22:01Z",
"due_on": "2012-10-09T23:39:01Z"
}

@ -0,0 +1,9 @@
{
"Link": "https://github.com/octocat/Hello-World/milestones/v1.0",
"ID": 1002604,
"Number": 1,
"State": "open",
"Title": "v1.0",
"Description": "Tracking milestone for version 1.0",
"DueDate": "2012-10-09T23:39:01Z"
}

@ -0,0 +1,6 @@
{
"state": "open",
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"due_on": "2012-10-09T23:39:01Z"
}

@ -0,0 +1,39 @@
[
{
"url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
"html_url": "https://github.com/octocat/Hello-World/milestones/v1.0",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels",
"id": 1002604,
"node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==",
"number": 1,
"state": "open",
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"creator": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"open_issues": 4,
"closed_issues": 8,
"created_at": "2011-04-10T20:09:31Z",
"updated_at": "2014-03-03T18:58:10Z",
"closed_at": "2013-02-12T13:22:01Z",
"due_on": "2012-10-09T23:39:01Z"
}
]

@ -0,0 +1,11 @@
[
{
"Link": "https://github.com/octocat/Hello-World/milestones/v1.0",
"ID": 1002604,
"Number": 1,
"State": "open",
"Title": "v1.0",
"Description": "Tracking milestone for version 1.0",
"DueDate": "2012-10-09T23:39:01Z"
}
]

@ -0,0 +1,74 @@
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/1",
"html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0",
"assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets",
"upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
"tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0",
"zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0",
"id": 1,
"node_id": "MDc6UmVsZWFzZTE=",
"tag_name": "v1.0.0",
"target_commitish": "master",
"name": "v1.0.0",
"body": "Description of the release",
"draft": false,
"prerelease": false,
"created_at": "2013-02-27T19:35:32Z",
"published_at": "2013-02-27T19:35:32Z",
"author": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"assets": [
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/1",
"browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip",
"id": 1,
"node_id": "MDEyOlJlbGVhc2VBc3NldDE=",
"name": "example.zip",
"label": "short description",
"state": "uploaded",
"content_type": "application/zip",
"size": 1024,
"download_count": 42,
"created_at": "2013-02-27T19:35:32Z",
"updated_at": "2013-02-27T19:35:32Z",
"uploader": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
}
}
]
}

@ -0,0 +1,12 @@
{
"ID": 1,
"Title": "v1.0.0",
"Description": "Description of the release",
"Link": "https://github.com/octocat/Hello-World/releases/v1.0.0",
"Tag": "v1.0.0",
"Commitish": "master",
"Draft": false,
"Prerelease": false,
"Created": "2013-02-27T19:35:32Z",
"Published": "2013-02-27T19:35:32Z"
}

@ -0,0 +1,76 @@
[
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/1",
"html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0",
"assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets",
"upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
"tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0",
"zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0",
"id": 1,
"node_id": "MDc6UmVsZWFzZTE=",
"tag_name": "v1.0.0",
"target_commitish": "master",
"name": "v1.0.0",
"body": "Description of the release",
"draft": false,
"prerelease": false,
"created_at": "2013-02-27T19:35:32Z",
"published_at": "2013-02-27T19:35:32Z",
"author": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"assets": [
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/1",
"browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip",
"id": 1,
"node_id": "MDEyOlJlbGVhc2VBc3NldDE=",
"name": "example.zip",
"label": "short description",
"state": "uploaded",
"content_type": "application/zip",
"size": 1024,
"download_count": 42,
"created_at": "2013-02-27T19:35:32Z",
"updated_at": "2013-02-27T19:35:32Z",
"uploader": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
}
}
]
}
]

@ -0,0 +1,14 @@
[
{
"ID": 1,
"Title": "v1.0.0",
"Description": "Description of the release",
"Link": "https://github.com/octocat/Hello-World/releases/v1.0.0",
"Tag": "v1.0.0",
"Commitish": "master",
"Draft": false,
"Prerelease": false,
"Created": "2013-02-27T19:35:32Z",
"Published": "2013-02-27T19:35:32Z"
}
]

@ -67,3 +67,35 @@ func encodePullRequestListOptions(opts scm.PullRequestListOptions) string {
}
return params.Encode()
}
func encodeMilestoneListOptions(opts scm.MilestoneListOptions) string {
params := url.Values{}
if opts.Page != 0 {
params.Set("page", strconv.Itoa(opts.Page))
}
if opts.Size != 0 {
params.Set("per_page", strconv.Itoa(opts.Size))
}
if opts.Open && opts.Closed {
params.Set("state", "all")
} else if opts.Closed {
params.Set("state", "closed")
}
return params.Encode()
}
func encodeReleaseListOptions(opts scm.ReleaseListOptions) string {
params := url.Values{}
if opts.Page != 0 {
params.Set("page", strconv.Itoa(opts.Page))
}
if opts.Size != 0 {
params.Set("per_page", strconv.Itoa(opts.Size))
}
if opts.Open && opts.Closed {
params.Set("state", "all")
} else if opts.Closed {
params.Set("state", "closed")
}
return params.Encode()
}

@ -34,8 +34,10 @@ func New(uri string) (*scm.Client, error) {
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Organizations = &organizationService{client}
client.Milestones = &milestoneService{client}
client.PullRequests = &pullService{client}
client.Repositories = &repositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -0,0 +1,166 @@
package gitlab
import (
"context"
"errors"
"fmt"
"net/url"
"time"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
// isoTime represents an ISO 8601 formatted date
type isoTime time.Time
// ISO 8601 date format
const iso8601 = "2006-01-02"
// MarshalJSON implements the json.Marshaler interface
func (t isoTime) MarshalJSON() ([]byte, error) {
if y := time.Time(t).Year(); y < 0 || y >= 10000 {
// ISO 8901 uses 4 digits for the years
return nil, errors.New("json: ISOTime year outside of range [0,9999]")
}
b := make([]byte, 0, len(iso8601)+2)
b = append(b, '"')
b = time.Time(t).AppendFormat(b, iso8601)
b = append(b, '"')
return b, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (t *isoTime) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package
if string(data) == "null" {
return nil
}
isotime, err := time.Parse(`"`+iso8601+`"`, string(data))
*t = isoTime(isotime)
return err
}
// EncodeValues implements the query.Encoder interface
func (t *isoTime) EncodeValues(key string, v *url.Values) error {
if t == nil || (time.Time(*t)).IsZero() {
return nil
}
v.Add(key, t.String())
return nil
}
// String implements the Stringer interface
func (t isoTime) String() string {
return time.Time(t).Format(iso8601)
}
type milestone struct {
ID int `json:"id"`
IID int `json:"iid"`
ProjectID int `json:"project_id"`
Title string `json:"title"`
Description string `json:"description"`
State string `json:"state"`
DueDate isoTime `json:"due_date"`
StartDate isoTime `json:"start_date"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Expired bool `json:"expired"`
}
type milestoneInput struct {
Title *string `json:"title"`
StateEvent *string `json:"state_event,omitempty"`
Description *string `json:"description"`
DueDate *isoTime `json:"due_date"`
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/milestones/%d", encode(repo), id)
out := new(milestone)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.MilestoneListOptions) ([]*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/milestones?%s", encode(repo), encodeMilestoneListOptions(opts))
out := []*milestone{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertMilestoneList(out), res, err
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/milestones", encode(repo))
dueDateIso := isoTime(*input.DueDate)
in := &milestoneInput{
Title: &input.Title,
Description: &input.Description,
DueDate: &dueDateIso,
}
out := new(milestone)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertMilestone(out), res, err
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/milestones/%d", encode(repo), id)
res, err := s.client.do(ctx, "DELETE", path, nil, nil)
return res, err
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/milestones/%d", encode(repo), id)
in := &milestoneInput{}
if input.Title != "" {
in.Title = &input.Title
}
if input.State != "" {
if input.State == "open" {
activate := "activate"
in.StateEvent = &activate
} else {
in.StateEvent = &input.State
}
}
if input.Description != "" {
in.Description = &input.Description
}
if input.DueDate != nil {
dueDateIso := isoTime(*input.DueDate)
in.DueDate = &dueDateIso
}
out := new(milestone)
res, err := s.client.do(ctx, "PATCH", path, in, out)
return convertMilestone(out), res, err
}
func convertMilestoneList(from []*milestone) []*scm.Milestone {
var to []*scm.Milestone
for _, m := range from {
to = append(to, convertMilestone(m))
}
return to
}
func convertMilestone(from *milestone) *scm.Milestone {
if from == nil || from.Title == "" {
return nil
}
dueDate := time.Time(from.DueDate)
return &scm.Milestone{
Number: from.ID,
ID: from.ID,
Title: from.Title,
Description: from.Description,
State: from.State,
DueDate: &dueDate,
}
}

@ -0,0 +1,186 @@
package gitlab
import (
"context"
"encoding/json"
"github.com/drone/go-scm/scm"
"github.com/h2non/gock"
"io/ioutil"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestMilestoneFind(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Get("/api/v4/projects/diaspora/diaspora/milestones/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
got, res, err := client.Milestones.Find(context.Background(), "diaspora/diaspora", 1)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, err := ioutil.ReadFile("testdata/milestone.json.golden")
if err != nil {
t.Fatalf("ioutil.ReadFile: %v", err)
}
if err := json.Unmarshal(raw, want); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneList(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Get("/api/v4/projects/diaspora/diaspora/milestones").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestones.json")
client := NewDefault()
got, res, err := client.Milestones.List(context.Background(), "diaspora/diaspora", scm.MilestoneListOptions{})
if err != nil {
t.Error(err)
return
}
want := []*scm.Milestone{}
raw, err := ioutil.ReadFile("testdata/milestones.json.golden")
if err != nil {
t.Fatalf("ioutil.ReadFile: %v", err)
}
if err := json.Unmarshal(raw, &want); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneCreate(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Post("/api/v4/projects/diaspora/diaspora/milestones").
File("testdata/milestone_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "open",
DueDate: &dueDate,
}
got, res, err := client.Milestones.Create(context.Background(), "diaspora/diaspora", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, err := ioutil.ReadFile("testdata/milestone.json.golden")
if err != nil {
t.Fatalf("ioutil.ReadFile: %v", err)
}
if err := json.Unmarshal(raw, &want); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneUpdate(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Patch("/api/v4/projects/diaspora/diaspora/milestones/1").
File("testdata/milestone_update.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/milestone.json")
client := NewDefault()
dueDate, _ := time.Parse(scm.SearchTimeFormat, "2012-10-09T23:39:01Z")
input := &scm.MilestoneInput{
Title: "v1.0",
Description: "Tracking milestone for version 1.0",
State: "close",
DueDate: &dueDate,
}
got, res, err := client.Milestones.Update(context.Background(), "diaspora/diaspora", 1, input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Milestone)
raw, err := ioutil.ReadFile("testdata/milestone.json.golden")
if err != nil {
t.Fatalf("ioutil.ReadFile: %v", err)
}
if err := json.Unmarshal(raw, &want); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestMilestoneDelete(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Delete("/api/v4/projects/diaspora/diaspora/milestones/1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders)
client := NewDefault()
_, err := client.Milestones.Delete(context.Background(), "diaspora/diaspora", 1)
if err != nil {
t.Error(err)
return
}
}

@ -0,0 +1,111 @@
package gitlab
import (
"context"
"fmt"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
type release struct {
Title string `json:"name"`
Description string `json:"description"`
Tag string `json:"tag_name"`
Commit struct {
ID string `json:"id"`
} `json:"commit"`
}
type releaseInput struct {
Title string `json:"name"`
Description string `json:"description"`
Tag string `json:"tag_name"`
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
// this could be implemented by List and filter but would be to expensive
panic("gitlab only allows to find a release by tag")
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/releases/%s", encode(repo), tag)
out := new(release)
res, err := s.client.do(ctx, "GET", path, nil, out)
return convertRelease(out), res, err
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/releases", encode(repo))
out := []*release{}
res, err := s.client.do(ctx, "GET", path, nil, &out)
return convertReleaseList(out), res, err
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/releases", encode(repo))
in := &releaseInput{
Title: input.Title,
Description: input.Description,
Tag: input.Tag,
}
out := new(release)
res, err := s.client.do(ctx, "POST", path, in, out)
return convertRelease(out), res, err
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
// this could be implemented by List and filter but would be to expensive
panic("gitlab only allows to delete a release by tag")
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/releases/%s", encode(repo), tag)
return s.client.do(ctx, "DELETE", path, nil, nil)
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
// this could be implemented by List and filter but would be to expensive
panic("gitlab only allows to update a release by tag")
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
path := fmt.Sprintf("api/v4/projects/%s/releases/%s", encode(repo), tag)
in := &releaseInput{}
if input.Title != "" {
in.Title = input.Title
}
if input.Description != "" {
in.Description = input.Description
}
if input.Tag != "" {
in.Tag = input.Tag
}
out := new(release)
res, err := s.client.do(ctx, "PUT", path, in, out)
return convertRelease(out), res, err
}
func convertReleaseList(from []*release) []*scm.Release {
var to []*scm.Release
for _, m := range from {
to = append(to, convertRelease(m))
}
return to
}
func convertRelease(from *release) *scm.Release {
return &scm.Release{
ID: 0,
Title: from.Title,
Description: from.Description,
Link: "",
Tag: from.Tag,
Commitish: from.Commit.ID,
Draft: false, // not supported by gitlab
Prerelease: false, // not supported by gitlab
}
}

@ -0,0 +1,184 @@
package gitlab
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"github.com/drone/go-scm/scm"
"github.com/google/go-cmp/cmp"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)
func TestReleaseFindByTag(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Get("/api/v4/projects/diaspora/diaspora/releases/v1.0.1").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
got, res, err := client.Releases.FindByTag(context.Background(), "diaspora/diaspora", "v1.0.1")
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseList(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Get("/api/v4/projects/diaspora/diaspora/releases").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/releases.json")
client := NewDefault()
got, res, err := client.Releases.List(context.Background(), "diaspora/diaspora", scm.ReleaseListOptions{})
if err != nil {
t.Error(err)
return
}
want := []*scm.Release{}
raw, _ := ioutil.ReadFile("testdata/releases.json.golden")
err = json.Unmarshal(raw, &want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseCreate(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Post("/api/v4/projects/diaspora/diaspora/releases").
File("testdata/release_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
input := &scm.ReleaseInput{
Title: "v1.0",
Description: "Tracking release for version 1.0",
Tag: "v1.0",
}
got, res, err := client.Releases.Create(context.Background(), "diaspora/diaspora", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
data, _ := json.Marshal(got)
t.Log("got JSON:")
t.Log(string(data))
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseUpdateByTag(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Put("/api/v4/projects/diaspora/diaspora/releases/v1.0").
File("testdata/release_create.json").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders).
File("testdata/release.json")
client := NewDefault()
input := &scm.ReleaseInput{
Title: "v1.0",
Description: "Tracking release for version 1.0",
Tag: "v1.0",
}
got, res, err := client.Releases.UpdateByTag(context.Background(), "diaspora/diaspora", "v1.0", input)
if err != nil {
t.Error(err)
return
}
want := new(scm.Release)
raw, _ := ioutil.ReadFile("testdata/release.json.golden")
err = json.Unmarshal(raw, want)
assert.NoError(t, err)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Unexpected Results")
t.Log(diff)
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}
func TestReleaseDeleteByTag(t *testing.T) {
defer gock.Off()
gock.New("https://gitlab.com").
Delete("/api/v4/projects/diaspora/diaspora/releases/v1.0").
Reply(200).
Type("application/json").
SetHeaders(mockHeaders)
client := NewDefault()
res, err := client.Releases.DeleteByTag(context.Background(), "diaspora/diaspora", "v1.0")
if err != nil {
t.Error(err)
return
}
t.Run("Request", testRequest(res))
t.Run("Rate", testRate(res))
}

@ -0,0 +1,13 @@
{
"id": 12,
"iid": 3,
"project_id": 16,
"title": "10.0",
"description": "Version",
"due_date": "2013-11-29",
"start_date": "2013-11-10",
"state": "active",
"updated_at": "2013-10-02T09:24:18Z",
"created_at": "2013-10-02T09:24:18Z",
"expired": false
}

@ -0,0 +1,8 @@
{
"ID": 12,
"Number": 12,
"Title": "10.0",
"Description": "Version",
"DueDate": "2013-11-29T00:00:00Z",
"State": "active"
}

@ -0,0 +1,5 @@
{
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"due_date": "2012-10-09"
}

@ -0,0 +1,6 @@
{
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"state_event": "close",
"due_date": "2012-10-09"
}

@ -0,0 +1,15 @@
[
{
"id": 12,
"iid": 3,
"project_id": 16,
"title": "10.0",
"description": "Version",
"due_date": "2013-11-29",
"start_date": "2013-11-10",
"state": "active",
"updated_at": "2013-10-02T09:24:18Z",
"created_at": "2013-10-02T09:24:18Z",
"expired": false
}
]

@ -0,0 +1,10 @@
[
{
"ID": 12,
"Number": 12,
"Title": "10.0",
"Description": "Version",
"DueDate": "2013-11-29T00:00:00Z",
"State": "active"
}
]

@ -0,0 +1,100 @@
{
"tag_name":"v0.1",
"description":"## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516",
"name":"Awesome app v0.1 alpha",
"description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e",
"created_at":"2019-01-03T01:55:18.203Z",
"released_at":"2019-01-03T01:55:18.203Z",
"author":{
"id":1,
"name":"Administrator",
"username":"root",
"state":"active",
"avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/root"
},
"commit":{
"id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4",
"short_id":"f8d3d94c",
"title":"Initial commit",
"created_at":"2019-01-03T01:53:28.000Z",
"parent_ids":[
],
"message":"Initial commit",
"author_name":"Administrator",
"author_email":"admin@example.com",
"authored_date":"2019-01-03T01:53:28.000Z",
"committer_name":"Administrator",
"committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z"
},
"milestones": [
{
"id":51,
"iid":1,
"project_id":24,
"title":"v1.0-rc",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-12T19:45:44.256Z",
"updated_at":"2019-07-12T19:45:44.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/1",
"issue_stats": {
"total": 98,
"closed": 76
}
},
{
"id":52,
"iid":2,
"project_id":24,
"title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-16T14:00:12.256Z",
"updated_at":"2019-07-16T14:00:12.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2",
"issue_stats": {
"total": 24,
"closed": 21
}
}
],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{
"count":5,
"sources":[
{
"format":"zip",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip"
},
{
"format":"tar.gz",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz"
},
{
"format":"tar.bz2",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2"
},
{
"format":"tar",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar"
}
],
"links":[
{
"id":3,
"name":"hoge",
"url":"https://gitlab.example.com/root/awesome-app/-/tags/v0.11.1/binaries/linux-amd64",
"external":true,
"link_type":"other"
}
]
}
}

@ -0,0 +1,10 @@
{
"ID": 0,
"Title": "Awesome app v0.1 alpha",
"Description": "## CHANGELOG\r\n\r\n- Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516",
"Link": "",
"Tag": "v0.1",
"Commitish": "f8d3d94cbd347e924aa7b715845e439d00e80ca4",
"Draft": false,
"Prerelease": false
}

@ -0,0 +1,167 @@
[
{
"tag_name":"v0.2",
"description":"## CHANGELOG\r\n\r\n- Escape label and milestone titles to prevent XSS in GFM autocomplete. !2740\r\n- Prevent private snippets from being embeddable.\r\n- Add subresources removal to member destroy service.",
"name":"Awesome app v0.2 beta",
"description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eEscape label and milestone titles to prevent XSS in GFM autocomplete. !2740\u003c/li\u003e\n\u003cli\u003ePrevent private snippets from being embeddable.\u003c/li\u003e\n\u003cli\u003eAdd subresources removal to member destroy service.\u003c/li\u003e\n\u003c/ul\u003e",
"created_at":"2019-01-03T01:56:19.539Z",
"released_at":"2019-01-03T01:56:19.539Z",
"author":{
"id":1,
"name":"Administrator",
"username":"root",
"state":"active",
"avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/root"
},
"commit":{
"id":"079e90101242458910cccd35eab0e211dfc359c0",
"short_id":"079e9010",
"title":"Update README.md",
"created_at":"2019-01-03T01:55:38.000Z",
"parent_ids":[
"f8d3d94cbd347e924aa7b715845e439d00e80ca4"
],
"message":"Update README.md",
"author_name":"Administrator",
"author_email":"admin@example.com",
"authored_date":"2019-01-03T01:55:38.000Z",
"committer_name":"Administrator",
"committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:55:38.000Z"
},
"milestones": [
{
"id":51,
"iid":1,
"project_id":24,
"title":"v1.0-rc",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-12T19:45:44.256Z",
"updated_at":"2019-07-12T19:45:44.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/1",
"issue_stats": {
"total": 98,
"closed": 76
}
},
{
"id":52,
"iid":2,
"project_id":24,
"title":"v1.0",
"description":"Voluptate fugiat possimus quis quod aliquam expedita.",
"state":"closed",
"created_at":"2019-07-16T14:00:12.256Z",
"updated_at":"2019-07-16T14:00:12.256Z",
"due_date":"2019-08-16T11:00:00.256Z",
"start_date":"2019-07-30T12:00:00.256Z",
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2",
"issue_stats": {
"total": 24,
"closed": 21
}
}
],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{
"count":6,
"sources":[
{
"format":"zip",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.zip"
},
{
"format":"tar.gz",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.gz"
},
{
"format":"tar.bz2",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar.bz2"
},
{
"format":"tar",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.2/awesome-app-v0.2.tar"
}
],
"links":[
{
"id":2,
"name":"awesome-v0.2.msi",
"url":"http://192.168.10.15:3000/msi",
"external":true,
"link_type":"other"
},
{
"id":1,
"name":"awesome-v0.2.dmg",
"url":"http://192.168.10.15:3000",
"external":true,
"link_type":"other"
}
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json"
}
},
{
"tag_name":"v0.1",
"description":"## CHANGELOG\r\n\r\n-Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516",
"name":"Awesome app v0.1 alpha",
"description_html":"\u003ch2 dir=\"auto\"\u003e\n\u003ca id=\"user-content-changelog\" class=\"anchor\" href=\"#changelog\" aria-hidden=\"true\"\u003e\u003c/a\u003eCHANGELOG\u003c/h2\u003e\n\u003cul dir=\"auto\"\u003e\n\u003cli\u003eRemove limit of 100 when searching repository code. !8671\u003c/li\u003e\n\u003cli\u003eShow error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\u003c/li\u003e\n\u003cli\u003eFix a bug where internal email pattern wasn't respected. !22516\u003c/li\u003e\n\u003c/ul\u003e",
"created_at":"2019-01-03T01:55:18.203Z",
"released_at":"2019-01-03T01:55:18.203Z",
"author":{
"id":1,
"name":"Administrator",
"username":"root",
"state":"active",
"avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/root"
},
"commit":{
"id":"f8d3d94cbd347e924aa7b715845e439d00e80ca4",
"short_id":"f8d3d94c",
"title":"Initial commit",
"created_at":"2019-01-03T01:53:28.000Z",
"parent_ids":[
],
"message":"Initial commit",
"author_name":"Administrator",
"author_email":"admin@example.com",
"authored_date":"2019-01-03T01:53:28.000Z",
"committer_name":"Administrator",
"committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z"
},
"assets":{
"count":4,
"sources":[
{
"format":"zip",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.zip"
},
{
"format":"tar.gz",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.gz"
},
{
"format":"tar.bz2",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar.bz2"
},
{
"format":"tar",
"url":"https://gitlab.example.com/root/awesome-app/-/archive/v0.1/awesome-app-v0.1.tar"
}
],
"links":[
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json"
}
}
]

@ -0,0 +1,14 @@
[
{
"Title": "Awesome app v0.2 beta",
"Description": "## CHANGELOG\r\n\r\n- Escape label and milestone titles to prevent XSS in GFM autocomplete. !2740\r\n- Prevent private snippets from being embeddable.\r\n- Add subresources removal to member destroy service.",
"Tag": "v0.2",
"Commitish": "079e90101242458910cccd35eab0e211dfc359c0"
},
{
"Title": "Awesome app v0.1 alpha",
"Description": "## CHANGELOG\r\n\r\n-Remove limit of 100 when searching repository code. !8671\r\n- Show error message when attempting to reopen an MR and there is an open MR for the same branch. !16447 (Akos Gyimesi)\r\n- Fix a bug where internal email pattern wasn't respected. !22516",
"Tag": "v0.1",
"Commitish": "f8d3d94cbd347e924aa7b715845e439d00e80ca4"
}
]

@ -96,3 +96,19 @@ func encodePullRequestListOptions(opts scm.PullRequestListOptions) string {
}
return params.Encode()
}
func encodeMilestoneListOptions(opts scm.MilestoneListOptions) string {
params := url.Values{}
if opts.Page != 0 {
params.Set("page", strconv.Itoa(opts.Page))
}
if opts.Size != 0 {
params.Set("per_page", strconv.Itoa(opts.Size))
}
if opts.Open && opts.Closed {
params.Set("state", "all")
} else if opts.Closed {
params.Set("state", "closed")
}
return params.Encode()
}

@ -36,8 +36,10 @@ func New(uri string) (*scm.Client, error) {
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Organizations = &organizationService{client}
client.Milestones = &milestoneService{client}
client.PullRequests = &pullService{client}
client.Repositories = &repositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -0,0 +1,31 @@
package gogs
import (
"context"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.MilestoneListOptions) ([]*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -0,0 +1,42 @@
package gogs
import (
"context"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -0,0 +1,31 @@
package stash
import (
"context"
"github.com/drone/go-scm/scm"
)
type milestoneService struct {
client *wrapper
}
func (s *milestoneService) Find(ctx context.Context, repo string, id int) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) List(ctx context.Context, repo string, opts scm.MilestoneListOptions) ([]*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Create(ctx context.Context, repo string, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *milestoneService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *milestoneService) Update(ctx context.Context, repo string, id int, input *scm.MilestoneInput) (*scm.Milestone, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -0,0 +1,43 @@
package stash
import (
"context"
"github.com/drone/go-scm/scm"
)
type releaseService struct {
client *wrapper
}
func (s *releaseService) Find(ctx context.Context, repo string, id int) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) FindByTag(ctx context.Context, repo string, tag string) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) List(ctx context.Context, repo string, opts scm.ReleaseListOptions) ([]*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Create(ctx context.Context, repo string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) Delete(ctx context.Context, repo string, id int) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) DeleteByTag(ctx context.Context, repo string, tag string) (*scm.Response, error) {
return nil, scm.ErrNotSupported
}
func (s *releaseService) Update(ctx context.Context, repo string, id int, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}
func (s *releaseService) UpdateByTag(ctx context.Context, repo string, tag string, input *scm.ReleaseInput) (*scm.Release, *scm.Response, error) {
return nil, nil, scm.ErrNotSupported
}

@ -38,9 +38,11 @@ func New(uri string) (*scm.Client, error) {
client.Contents = &contentService{client}
client.Git = &gitService{client}
client.Issues = &issueService{client}
client.Milestones = &milestoneService{client}
client.Organizations = &organizationService{client}
client.PullRequests = &pullService{client}
client.Repositories = &repositoryService{client}
client.Releases = &releaseService{client}
client.Reviews = &reviewService{client}
client.Users = &userService{client}
client.Webhooks = &webhookService{client}

@ -0,0 +1,42 @@
package scm
import (
"context"
"time"
)
type (
// MilestoneInput contains the information needed to create a milestone
MilestoneInput struct {
Title string
Description string
State string
DueDate *time.Time
}
// MilestoneListOptions provides options for querying a list of repository milestones.
MilestoneListOptions struct {
Page int
Size int
Open bool
Closed bool
}
// MilestoneService provides access to creating, listing, updating, and deleting milestones
MilestoneService interface {
// Find returns the milestone for the given number in the given repository
Find(context.Context, string, int) (*Milestone, *Response, error)
// List returns a list of milestones in the given repository
List(context.Context, string, MilestoneListOptions) ([]*Milestone, *Response, error)
// Create creates a milestone in the given repository
Create(context.Context, string, *MilestoneInput) (*Milestone, *Response, error)
// Update updates a milestone in the given repository
Update(context.Context, string, int, *MilestoneInput) (*Milestone, *Response, error)
// Delete deletes a milestone in the given repository
Delete(context.Context, string, int) (*Response, error)
}
)

@ -64,6 +64,17 @@ type (
Color string
}
// Milestone the milestone
Milestone struct {
Number int
ID int
Title string
Description string
Link string
State string
DueDate *time.Time
}
// PullRequestService provides access to pull request resources.
PullRequestService interface {
// Find returns the repository pull request by number.

@ -0,0 +1,67 @@
package scm
import (
"context"
"time"
)
type (
// Release the release
Release struct {
ID int
Title string
Description string
Link string
Tag string
Commitish string
Draft bool
Prerelease bool
Created time.Time
Published time.Time
}
// ReleaseInput contains the information needed to create a release
ReleaseInput struct {
Title string
Description string
Tag string
Commitish string
Draft bool
Prerelease bool
}
// ReleaseListOptions provides options for querying a list of repository releases.
ReleaseListOptions struct {
Page int
Size int
Open bool
Closed bool
}
// ReleaseService provides access to creating, listing, updating, and deleting releases
ReleaseService interface {
// Find returns the release for the given number in the given repository
Find(context.Context, string, int) (*Release, *Response, error)
// FindByTag returns the release for the given tag in the given repository
FindByTag(context.Context, string, string) (*Release, *Response, error)
// List returns a list of releases in the given repository
List(context.Context, string, ReleaseListOptions) ([]*Release, *Response, error)
// Create creates a release in the given repository
Create(context.Context, string, *ReleaseInput) (*Release, *Response, error)
// Update updates a release in the given repository
Update(context.Context, string, int, *ReleaseInput) (*Release, *Response, error)
// UpdateByTag deletes a release in the given repository by tag
UpdateByTag(context.Context, string, string, *ReleaseInput) (*Release, *Response, error)
// Delete deletes a release in the given repository
Delete(context.Context, string, int) (*Response, error)
// DeleteByTag deletes a release in the given repository by tag
DeleteByTag(context.Context, string, string) (*Response, error)
}
)

@ -77,3 +77,4 @@ func IsPullRequest(ref string) bool {
strings.HasPrefix(ref, "refs/pull-request/") ||
strings.HasPrefix(ref, "refs/merge-requests/")
}

Loading…
Cancel
Save