added endpoints for branch summary

This commit is contained in:
Bradley Rydzewski 2020-03-13 13:27:07 -04:00
parent 6815ec61cb
commit a88f9d0743
30 changed files with 933 additions and 148 deletions

View file

@ -143,16 +143,17 @@ tasks:
test-postgres:
env:
DRONE_DATABASE_DRIVER: postgres
DRONE_DATABASE_DATASOURCE: host=localhost user=postgres dbname=postgres sslmode=disable
DRONE_DATABASE_DATASOURCE: host=localhost user=postgres password=postgres dbname=postgres sslmode=disable
GO111MODULE: 'on'
cmds:
- cmd: docker kill postgres
ignore_error: true
silent: true
- silent: true
silent: false
- silent: false
cmd: >
docker run
-p 5432:5432
--env POSTGRES_PASSWORD=postgres
--env POSTGRES_USER=postgres
--name postgres
--detach

View file

@ -71,6 +71,14 @@ type BuildStore interface {
// ListRef returns a list of builds from the datastore by ref.
ListRef(context.Context, int64, string, int, int) ([]*Build, error)
// LatestBranches returns the latest builds from the
// datastore by branch.
LatestBranches(context.Context, int64) ([]*Build, error)
// LatestPulls returns the latest builds from the
// datastore by pull requeset.
LatestPulls(context.Context, int64) ([]*Build, error)
// Pending returns a list of pending builds from the
// datastore by repository id (DEPRECATED).
Pending(context.Context) ([]*Build, error)
@ -88,6 +96,15 @@ type BuildStore interface {
// Delete deletes a build from the datastore.
Delete(context.Context, *Build) error
// DeletePull deletes a pull request index from the datastore.
DeletePull(context.Context, int64, int) error
// DeleteBranch deletes a branch index from the datastore.
DeleteBranch(context.Context, int64, string) error
// DeleteDeploy deletes a deploy index from the datastore.
DeleteDeploy(context.Context, int64, string) error
// Purge deletes builds from the database where the build number is less than n.
Purge(context.Context, int64, int64) error

View file

@ -22,6 +22,7 @@ import (
// Hook action constants.
const (
ActionOpen = "open"
ActionClose = "close"
ActionCreate = "create"
ActionDelete = "delete"
ActionSync = "sync"

2
go.mod
View file

@ -100,3 +100,5 @@ require (
)
replace github.com/h2non/gock => gopkg.in/h2non/gock.v1 v1.0.14
go 1.13

View file

@ -27,7 +27,9 @@ import (
"github.com/drone/drone/handler/api/queue"
"github.com/drone/drone/handler/api/repos"
"github.com/drone/drone/handler/api/repos/builds"
"github.com/drone/drone/handler/api/repos/builds/branches"
"github.com/drone/drone/handler/api/repos/builds/logs"
"github.com/drone/drone/handler/api/repos/builds/pulls"
"github.com/drone/drone/handler/api/repos/builds/stages"
"github.com/drone/drone/handler/api/repos/collabs"
"github.com/drone/drone/handler/api/repos/crons"
@ -184,6 +186,12 @@ func (s Server) Handler() http.Handler {
r.Get("/", builds.HandleList(s.Repos, s.Builds))
r.With(acl.CheckWriteAccess()).Post("/", builds.HandleCreate(s.Repos, s.Commits, s.Triggerer))
r.Get("/branches", branches.HandleList(s.Repos, s.Builds))
r.With(acl.CheckWriteAccess()).Delete("/branches/*", branches.HandleDelete(s.Repos, s.Builds))
r.Get("/pulls", pulls.HandleList(s.Repos, s.Builds))
r.With(acl.CheckWriteAccess()).Delete("/pulls/{pull}", pulls.HandleDelete(s.Repos, s.Builds))
r.Get("/latest", builds.HandleLast(s.Repos, s.Builds, s.Stages))
r.Get("/{number}", builds.HandleFind(s.Repos, s.Builds, s.Stages))
r.Get("/{number}/logs/{stage}/{step}", logs.HandleFind(s.Repos, s.Builds, s.Stages, s.Steps, s.Logs))

View file

@ -0,0 +1,15 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package branches

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package branches

View file

@ -0,0 +1,62 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package branches
import (
"net/http"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/drone/drone/logger"
"github.com/go-chi/chi"
)
// HandleDelete returns an http.HandlerFunc that handles an
// http.Request to delete a branch entry from the datastore.
func HandleDelete(
repos core.RepositoryStore,
builds core.BuildStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
branch = chi.URLParam(r, "*")
)
repo, err := repos.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot find repository")
return
}
err = builds.DeleteBranch(r.Context(), repo.ID, branch)
if err != nil {
render.InternalError(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot delete branch")
} else {
w.WriteHeader(http.StatusNoContent)
}
}
}

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package branches

View file

@ -0,0 +1,61 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package branches
import (
"net/http"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/drone/drone/logger"
"github.com/go-chi/chi"
)
// HandleList returns an http.HandlerFunc that writes a json-encoded
// list of build history to the response body.
func HandleList(
repos core.RepositoryStore,
builds core.BuildStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
)
repo, err := repos.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot find repository")
return
}
results, err := builds.LatestBranches(r.Context(), repo.ID)
if err != nil {
render.InternalError(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot list builds")
} else {
render.JSON(w, results, 200)
}
}
}

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package branches

View file

@ -10,8 +10,8 @@ import (
"net/http/httptest"
"testing"
"github.com/drone/drone/mock"
"github.com/drone/drone/handler/api/errors"
"github.com/drone/drone/mock"
"github.com/go-chi/chi"
"github.com/golang/mock/gomock"

View file

@ -0,0 +1,15 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pulls

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package pulls

View file

@ -0,0 +1,62 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pulls
import (
"net/http"
"strconv"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/drone/drone/logger"
"github.com/go-chi/chi"
)
// HandleDelete returns an http.HandlerFunc that handles an
// http.Request to delete a branch entry from the datastore.
func HandleDelete(
repos core.RepositoryStore,
builds core.BuildStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
number, _ = strconv.Atoi(chi.URLParam(r, "pull"))
)
repo, err := repos.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot find repository")
return
}
err = builds.DeletePull(r.Context(), repo.ID, number)
if err != nil {
render.InternalError(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot delete pr")
} else {
w.WriteHeader(http.StatusNoContent)
}
}
}

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package pulls

View file

@ -0,0 +1,61 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pulls
import (
"net/http"
"github.com/drone/drone/core"
"github.com/drone/drone/handler/api/render"
"github.com/drone/drone/logger"
"github.com/go-chi/chi"
)
// HandleList returns an http.HandlerFunc that writes a json-encoded
// list of build history to the response body.
func HandleList(
repos core.RepositoryStore,
builds core.BuildStore,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
namespace = chi.URLParam(r, "owner")
name = chi.URLParam(r, "name")
)
repo, err := repos.FindName(r.Context(), namespace, name)
if err != nil {
render.NotFound(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot find repository")
return
}
results, err := builds.LatestPulls(r.Context(), repo.ID)
if err != nil {
render.InternalError(w, err)
logger.FromRequest(r).
WithError(err).
WithField("namespace", namespace).
WithField("name", name).
Debugln("api: cannot list builds")
} else {
render.JSON(w, results, 200)
}
}
}

View file

@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
package pulls

View file

@ -84,146 +84,6 @@ func HandleHook(
// TODO handle ping requests
// TODO consider using scm.Repository in the function callback.
// // TODO break this to a separate function that
// // we can unit test.
// fn := func(webhook interface{}) (string, error) {
// var remote scm.Repository
// switch v := core.(type) {
// case *scm.PushHook:
// remote = v.Repo
// case *scm.BranchHook:
// remote = v.Repo
// case *scm.TagHook:
// remote = v.Repo
// case *scm.PullRequestHook:
// remote = v.Repo
// case *scm.IssueHook:
// remote = v.Repo
// case *scm.IssueCommentHook:
// remote = v.Repo
// case *scm.PullRequestCommentHook:
// remote = v.Repo
// case *scm.ReviewCommentHook:
// remote = v.Repo
// }
// repo, err := repos.FindName(r.Context(), remote.Namespace, remote.Name)
// if err != nil {
// hlog.FromRequest(r).Error().
// Err(err).
// Str("namespace", remote.Namespace).
// Str("name", remote.Name).
// Msg("cannot find repository")
// return "", err
// }
// return repo.Token, nil
// }
// hook, err := client.Webhooks.Parse(r, fn)
// if err != nil {
// hlog.FromRequest(r).Error().
// Err(err).
// Msg("cannot parse webhook")
// writeError(w, err)
// return
// }
// var name, namespace string
// var base = new(core.Build)
// switch v := hook.(type) {
// case *scm.PushHook:
// namespace, name = v.Repo.Namespace, v.Repo.Name
// base.Event = core.EventPush
// base.Link = v.Commit.Link
// base.Timestamp = v.Commit.Author.Date.Unix()
// base.Title = ""
// base.Message = v.Commit.Message
// base.Before = ""
// base.After = v.Commit.Sha
// base.Ref = v.Ref
// base.Source = strings.TrimPrefix(v.Ref, "refs/heads/")
// base.Target = strings.TrimPrefix(v.Ref, "refs/heads/")
// base.Author = v.Commit.Author.Login
// base.AuthorName = v.Commit.Author.Name
// base.AuthorEmail = v.Commit.Author.Email
// base.AuthorAvatar = v.Commit.Author.Avatar
// base.Sender = v.Sender.Login
// // TODO: this is a deficiency with gogs
// if base.AuthorAvatar == "" {
// base.AuthorAvatar = v.Sender.Avatar
// }
// case *scm.TagHook:
// namespace, name = v.Repo.Namespace, v.Repo.Name
// base.Event = core.EventTag
// base.Action = v.Action.String()
// base.Link = "" // TODO
// base.Timestamp = 0 // TODO
// base.Title = ""
// base.Message = "" // TODO
// base.Before = "" // TODO
// base.After = v.Ref.Sha
// base.Ref = v.Ref.Name // TODO prepend refs/tags?
// base.Source = ""
// base.Target = ""
// base.Author = v.Sender.Login
// base.AuthorName = v.Sender.Name
// base.AuthorEmail = v.Sender.Email
// base.AuthorAvatar = v.Sender.Avatar
// base.Sender = v.Sender.Login
// switch v.Action {
// case scm.ActionCreate:
// default:
// hlog.FromRequest(r).Debug().
// Str("namespace", namespace).
// Str("name", name).
// Str("event", base.Event).
// Str("action", base.Action).
// Msgf("ignore webhook with action %s", base.Action)
// w.WriteHeader(200)
// return
// }
// case *scm.PullRequestHook:
// namespace, name = v.Repo.Namespace, v.Repo.Name
// base.Event = core.EventPullRequest
// base.Action = v.Action.String()
// base.Link = v.PullRequest.Link
// base.Timestamp = v.PullRequest.Created.Unix()
// base.Title = v.PullRequest.Title
// base.Message = "" // TODO
// base.Before = "" // TODO
// base.After = v.PullRequest.Sha
// base.Ref = v.PullRequest.Ref
// base.Source = v.PullRequest.Source
// base.Target = v.PullRequest.Target
// base.Author = v.PullRequest.Author.Login
// base.AuthorName = v.PullRequest.Author.Name
// base.AuthorEmail = v.PullRequest.Author.Email
// base.AuthorAvatar = v.PullRequest.Author.Avatar
// base.Sender = v.Sender.Login
// switch v.Action {
// case scm.ActionCreate, scm.ActionOpen, scm.ActionSync:
// default:
// hlog.FromRequest(r).Debug().
// Str("namespace", namespace).
// Str("name", name).
// Str("event", base.Event).
// Str("action", base.Action).
// Msgf("ignore pull request hook with action %s", base.Action)
// w.WriteHeader(200)
// return
// }
// default:
// w.WriteHeader(200)
// return
// }
log := logrus.WithFields(logrus.Fields{
"namespace": remote.Namespace,
"name": remote.Name,
@ -251,6 +111,19 @@ func HandleHook(
ctx = logger.WithContext(ctx, log)
defer cancel()
if hook.Event == core.EventPush && hook.Action == core.ActionDelete {
log.WithField("branch", hook.Target).Debugln("branch deleted")
builds.DeleteBranch(ctx, repo.ID, hook.Target)
w.WriteHeader(http.StatusNoContent)
return
}
if hook.Event == core.EventPullRequest && hook.Action == core.ActionClose {
log.WithField("ref", hook.Ref).Debugln("pull request closed")
builds.DeletePull(ctx, repo.ID, scm.ExtractPullRequest(hook.Ref))
w.WriteHeader(http.StatusNoContent)
return
}
builds, err := triggerer.Trigger(ctx, repo, hook)
if err != nil {
writeError(w, err)

View file

@ -737,6 +737,48 @@ func (mr *MockBuildStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBuildStore)(nil).Delete), arg0, arg1)
}
// DeleteBranch mocks base method
func (m *MockBuildStore) DeleteBranch(arg0 context.Context, arg1 int64, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBranch", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBranch indicates an expected call of DeleteBranch
func (mr *MockBuildStoreMockRecorder) DeleteBranch(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBranch", reflect.TypeOf((*MockBuildStore)(nil).DeleteBranch), arg0, arg1, arg2)
}
// DeleteDeploy mocks base method
func (m *MockBuildStore) DeleteDeploy(arg0 context.Context, arg1 int64, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDeploy", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDeploy indicates an expected call of DeleteDeploy
func (mr *MockBuildStoreMockRecorder) DeleteDeploy(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDeploy", reflect.TypeOf((*MockBuildStore)(nil).DeleteDeploy), arg0, arg1, arg2)
}
// DeletePull mocks base method
func (m *MockBuildStore) DeletePull(arg0 context.Context, arg1 int64, arg2 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePull", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeletePull indicates an expected call of DeletePull
func (mr *MockBuildStoreMockRecorder) DeletePull(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePull", reflect.TypeOf((*MockBuildStore)(nil).DeletePull), arg0, arg1, arg2)
}
// Find mocks base method
func (m *MockBuildStore) Find(arg0 context.Context, arg1 int64) (*core.Build, error) {
m.ctrl.T.Helper()
@ -782,6 +824,36 @@ func (mr *MockBuildStoreMockRecorder) FindRef(arg0, arg1, arg2 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRef", reflect.TypeOf((*MockBuildStore)(nil).FindRef), arg0, arg1, arg2)
}
// LatestBranches mocks base method
func (m *MockBuildStore) LatestBranches(arg0 context.Context, arg1 int64) ([]*core.Build, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LatestBranches", arg0, arg1)
ret0, _ := ret[0].([]*core.Build)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LatestBranches indicates an expected call of LatestBranches
func (mr *MockBuildStoreMockRecorder) LatestBranches(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestBranches", reflect.TypeOf((*MockBuildStore)(nil).LatestBranches), arg0, arg1)
}
// LatestPulls mocks base method
func (m *MockBuildStore) LatestPulls(arg0 context.Context, arg1 int64) ([]*core.Build, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LatestPulls", arg0, arg1)
ret0, _ := ret[0].([]*core.Build)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LatestPulls indicates an expected call of LatestPulls
func (mr *MockBuildStoreMockRecorder) LatestPulls(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestPulls", reflect.TypeOf((*MockBuildStore)(nil).LatestPulls), arg0, arg1)
}
// List mocks base method
func (m *MockBuildStore) List(arg0 context.Context, arg1 int64, arg2, arg3 int) ([]*core.Build, error) {
m.ctrl.T.Helper()

View file

@ -232,6 +232,23 @@ func (p *parser) Parse(req *http.Request, secretFunc func(string) string) (*core
}
return hook, repo, nil
case *scm.PullRequestHook:
// TODO(bradrydzewski) cleanup the pr close hook code.
if v.Action == scm.ActionClose {
return &core.Hook{
Trigger: core.TriggerHook,
Event: core.EventPullRequest,
Action: core.ActionClose,
After: v.PullRequest.Sha,
Ref: v.PullRequest.Ref,
}, &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
}, nil
}
if v.Action != scm.ActionOpen && v.Action != scm.ActionSync {
return nil, nil, nil
}
@ -284,6 +301,23 @@ func (p *parser) Parse(req *http.Request, secretFunc func(string) string) (*core
}
return hook, repo, nil
case *scm.BranchHook:
// TODO(bradrydzewski) cleanup the branch hook code.
if v.Action == scm.ActionDelete {
return &core.Hook{
Trigger: core.TriggerHook,
Event: core.EventPush,
After: v.Ref.Sha,
Action: core.ActionDelete,
Target: scm.TrimRef(v.Ref.Name),
}, &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
}, nil
}
if v.Action != scm.ActionCreate {
return nil, nil, nil
}

View file

@ -16,11 +16,18 @@ package build
import (
"context"
"fmt"
"regexp"
"time"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
)
// regular expression to extract the pull request number
// from the git ref (e.g. refs/pulls/{d}/head)
var pr = regexp.MustCompile("\\d+")
// New returns a new Buildcore.
func New(db *db.DB) core.BuildStore {
return &buildStore{db}
@ -122,6 +129,42 @@ func (s *buildStore) ListRef(ctx context.Context, repo int64, ref string, limit,
return out, err
}
// LatestBranches returns a list of the latest build by branch.
func (s *buildStore) LatestBranches(ctx context.Context, repo int64) ([]*core.Build, error) {
return s.latest(ctx, repo, "branch")
}
// LatestPulls returns a list of the latest builds by pull requests.
func (s *buildStore) LatestPulls(ctx context.Context, repo int64) ([]*core.Build, error) {
return s.latest(ctx, repo, "pull_request")
}
// LatestDeploys returns a list of the latest builds by target deploy.
func (s *buildStore) LatestDeploys(ctx context.Context, repo int64) ([]*core.Build, error) {
return s.latest(ctx, repo, "deployment")
}
func (s *buildStore) latest(ctx context.Context, repo int64, event string) ([]*core.Build, error) {
var out []*core.Build
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
params := map[string]interface{}{
"latest_repo_id": repo,
"latest_type": event,
}
stmt, args, err := binder.BindNamed(queryLatestList, params)
if err != nil {
return err
}
rows, err := queryer.Query(stmt, args...)
if err != nil {
return err
}
out, err = scanRows(rows)
return err
})
return out, err
}
// Pending returns a list of pending builds from the datastore by repository id.
func (s *buildStore) Pending(ctx context.Context) ([]*core.Build, error) {
var out []*core.Build
@ -152,10 +195,31 @@ func (s *buildStore) Running(ctx context.Context) ([]*core.Build, error) {
// Create persists a build to the datacore.
func (s *buildStore) Create(ctx context.Context, build *core.Build, stages []*core.Stage) error {
if s.db.Driver() == db.Postgres {
return s.createPostgres(ctx, build, stages)
var err error
switch s.db.Driver() {
case db.Postgres:
err = s.createPostgres(ctx, build, stages)
default:
err = s.create(ctx, build, stages)
}
return s.create(ctx, build, stages)
if err != nil {
return err
}
var event, name string
switch build.Event {
case core.EventPullRequest:
event = "pull_request"
name = pr.FindString(build.Ref)
case core.EventPush:
event = "branch"
name = build.Target
case core.EventPromote, core.EventRollback:
event = "deployment"
name = build.Deploy
default:
return nil
}
return s.index(ctx, build.ID, build.RepoID, event, name)
}
func (s *buildStore) create(ctx context.Context, build *core.Build, stages []*core.Stage) error {
@ -255,6 +319,33 @@ func (s *buildStore) Update(ctx context.Context, build *core.Build) error {
return err
}
func (s *buildStore) index(ctx context.Context, build, repo int64, event, name string) error {
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"latest_repo_id": repo,
"latest_build_id": build,
"latest_type": event,
"latest_name": name,
"latest_created": time.Now().Unix(),
"latest_updated": time.Now().Unix(),
"latest_deleted": time.Now().Unix(),
}
stmtInsert := stmtInsertLatest
switch s.db.Driver() {
case db.Postgres:
stmtInsert = stmtInsertLatestPg
case db.Mysql:
stmtInsert = stmtInsertLatestMysql
}
stmt, args, err := binder.BindNamed(stmtInsert, params)
if err != nil {
return err
}
_, err = execer.Exec(stmt, args...)
return err
})
}
// Delete deletes a build from the datacore.
func (s *buildStore) Delete(ctx context.Context, build *core.Build) error {
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
@ -268,6 +359,57 @@ func (s *buildStore) Delete(ctx context.Context, build *core.Build) error {
})
}
// DeletePull deletes a pull request index from the datastore.
func (s *buildStore) DeletePull(ctx context.Context, repo int64, number int) error {
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"latest_repo_id": repo,
"latest_name": fmt.Sprint(number),
"latest_type": "pull_request",
}
stmt, args, err := binder.BindNamed(stmtDeleteLatest, params)
if err != nil {
return err
}
_, err = execer.Exec(stmt, args...)
return err
})
}
// DeleteBranch deletes a branch index from the datastore.
func (s *buildStore) DeleteBranch(ctx context.Context, repo int64, branch string) error {
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"latest_repo_id": repo,
"latest_name": branch,
"latest_type": "branch",
}
stmt, args, err := binder.BindNamed(stmtDeleteLatest, params)
if err != nil {
return err
}
_, err = execer.Exec(stmt, args...)
return err
})
}
// DeleteDeploy deletes a deploy index from the datastore.
func (s *buildStore) DeleteDeploy(ctx context.Context, repo int64, environment string) error {
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
params := map[string]interface{}{
"latest_repo_id": repo,
"latest_name": environment,
"latest_type": "deployment",
}
stmt, args, err := binder.BindNamed(stmtDeleteLatest, params)
if err != nil {
return err
}
_, err = execer.Exec(stmt, args...)
return err
})
}
// Purge deletes builds from the database where the build number is less than n.
func (s *buildStore) Purge(ctx context.Context, repo, number int64) error {
build := &core.Build{
@ -580,3 +722,86 @@ DELETE FROM builds
WHERE build_repo_id = :build_repo_id
AND build_number < :build_number
`
//
// latest builds index
//
const stmtInsertLatest = `
INSERT INTO latest (
latest_repo_id
,latest_build_id
,latest_type
,latest_name
,latest_created
,latest_updated
,latest_deleted
) VALUES (
:latest_repo_id
,:latest_build_id
,:latest_type
,:latest_name
,:latest_created
,:latest_updated
,:latest_deleted
) ON CONFLICT (latest_repo_id, latest_type, latest_name)
DO UPDATE SET latest_build_id = EXCLUDED.latest_build_id
`
const stmtInsertLatestPg = `
INSERT INTO latest (
latest_repo_id
,latest_build_id
,latest_type
,latest_name
,latest_created
,latest_updated
,latest_deleted
) VALUES (
:latest_repo_id
,:latest_build_id
,:latest_type
,:latest_name
,:latest_created
,:latest_updated
,:latest_deleted
) ON CONFLICT (latest_repo_id, latest_type, latest_name)
DO UPDATE SET latest_build_id = EXCLUDED.latest_build_id
`
const stmtInsertLatestMysql = `
INSERT INTO latest (
latest_repo_id
,latest_build_id
,latest_type
,latest_name
,latest_created
,latest_updated
,latest_deleted
) VALUES (
:latest_repo_id
,:latest_build_id
,:latest_type
,:latest_name
,:latest_created
,:latest_updated
,:latest_deleted
) ON DUPLICATE KEY UPDATE latest_build_id = :latest_build_id
`
const stmtDeleteLatest = `
DELETE FROM latest
WHERE latest_repo_id = :latest_repo_id
AND latest_type = :latest_type
AND latest_name = :latest_name
`
const queryLatestList = queryBase + `
FROM builds
WHERE build_id IN (
SELECT latest_build_id
FROM latest
WHERE latest_repo_id = :latest_repo_id
AND latest_type = :latest_type
)
`

View file

@ -9,8 +9,8 @@ import (
"database/sql"
"testing"
"github.com/drone/drone/store/shared/db"
"github.com/drone/drone/core"
"github.com/drone/drone/store/shared/db"
"github.com/drone/drone/store/shared/db/dbtest"
)
@ -34,6 +34,7 @@ func TestBuild(t *testing.T) {
t.Run("Count", testBuildCount(store))
t.Run("Pending", testBuildPending(store))
t.Run("Running", testBuildRunning(store))
t.Run("Latest", testBuildLatest(store))
}
func testBuildCreate(store *buildStore) func(t *testing.T) {
@ -41,7 +42,9 @@ func testBuildCreate(store *buildStore) func(t *testing.T) {
build := &core.Build{
RepoID: 1,
Number: 99,
Event: core.EventPush,
Ref: "refs/heads/master",
Target: "master",
}
stage := &core.Stage{
RepoID: 42,
@ -307,6 +310,113 @@ func testBuildRunning(store *buildStore) func(t *testing.T) {
}
}
func testBuildLatest(store *buildStore) func(t *testing.T) {
return func(t *testing.T) {
store.db.Update(func(execer db.Execer, binder db.Binder) error {
execer.Exec("DELETE FROM stages")
execer.Exec("DELETE FROM latest")
execer.Exec("DELETE FROM builds")
return nil
})
//
// step 1: insert the initial builds
//
build := &core.Build{
RepoID: 1,
Number: 99,
Event: core.EventPush,
Ref: "refs/heads/master",
Target: "master",
}
err := store.Create(noContext, build, []*core.Stage{})
if err != nil {
t.Error(err)
return
}
develop := &core.Build{
RepoID: 1,
Number: 100,
Event: core.EventPush,
Ref: "refs/heads/develop",
Target: "develop",
}
err = store.Create(noContext, develop, []*core.Stage{})
if err != nil {
t.Error(err)
return
}
err = store.Create(noContext, &core.Build{
RepoID: 1,
Number: 999,
Event: core.EventPullRequest,
Ref: "refs/pulls/10/head",
Source: "develop",
Target: "master",
}, []*core.Stage{})
if err != nil {
t.Error(err)
return
}
//
// step 2: verify the latest build number was captured
//
latest, _ := store.LatestBranches(noContext, build.RepoID)
if len(latest) != 2 {
t.Errorf("Expect latest branch list == 1, got %d", len(latest))
return
}
if got, want := latest[0].Number, build.Number; got != want {
t.Errorf("Expected latest master build number %d, got %d", want, got)
}
if got, want := latest[1].Number, develop.Number; got != want {
t.Errorf("Expected latest develop build number %d, got %d", want, got)
return
}
build = &core.Build{
RepoID: 1,
Number: 101,
Event: core.EventPush,
Ref: "refs/heads/master",
Target: "master",
}
err = store.Create(noContext, build, []*core.Stage{})
if err != nil {
t.Error(err)
return
}
latest, _ = store.LatestBranches(noContext, build.RepoID)
if len(latest) != 2 {
t.Errorf("Expect latest branch list == 1")
return
}
if got, want := latest[1].Number, build.Number; got != want {
t.Errorf("Expected latest build number %d, got %d", want, got)
return
}
err = store.DeleteBranch(noContext, build.RepoID, build.Target)
if err != nil {
t.Error(err)
return
}
latest, _ = store.LatestBranches(noContext, build.RepoID)
if len(latest) != 1 {
t.Errorf("Expect latest branch list == 1 after delete")
return
}
}
}
func testBuild(item *core.Build) func(t *testing.T) {
return func(t *testing.T) {
if got, want := item.RepoID, int64(1); got != want {

View file

@ -47,6 +47,7 @@ func Reset(d *db.DB) {
tx.Exec("DELETE FROM logs")
tx.Exec("DELETE FROM steps")
tx.Exec("DELETE FROM stages")
tx.Exec("DELETE FROM latest")
tx.Exec("DELETE FROM builds")
tx.Exec("DELETE FROM perms")
tx.Exec("DELETE FROM repos")

View file

@ -136,6 +136,14 @@ var migrations = []struct {
name: "alter-table-builds-add-column-deploy-id",
stmt: alterTableBuildsAddColumnDeployId,
},
{
name: "create-table-latest",
stmt: createTableLatest,
},
{
name: "create-index-latest-repo",
stmt: createIndexLatestRepo,
},
}
// Migrate performs the database migration. If the migration fails
@ -605,3 +613,24 @@ CREATE TABLE IF NOT EXISTS orgsecrets (
var alterTableBuildsAddColumnDeployId = `
ALTER TABLE builds ADD COLUMN build_deploy_id INTEGER NOT NULL DEFAULT 0;
`
//
// 014_create_table_refs.sql
//
var createTableLatest = `
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type VARCHAR(50)
,latest_name VARCHAR(500)
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
`
var createIndexLatestRepo = `
CREATE INDEX ix_latest_repo ON latest (latest_repo_id);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-latest
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type VARCHAR(50)
,latest_name VARCHAR(500)
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
-- name: create-index-latest-repo
CREATE INDEX ix_latest_repo ON latest (latest_repo_id);

View file

@ -132,6 +132,14 @@ var migrations = []struct {
name: "alter-table-builds-add-column-deploy-id",
stmt: alterTableBuildsAddColumnDeployId,
},
{
name: "create-table-latest",
stmt: createTableLatest,
},
{
name: "create-index-latest-repo",
stmt: createIndexLatestRepo,
},
}
// Migrate performs the database migration. If the migration fails
@ -583,3 +591,24 @@ CREATE TABLE IF NOT EXISTS orgsecrets (
var alterTableBuildsAddColumnDeployId = `
ALTER TABLE builds ADD COLUMN build_deploy_id INTEGER NOT NULL DEFAULT 0;
`
//
// 015_create_table_refs.sql
//
var createTableLatest = `
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type VARCHAR(50)
,latest_name VARCHAR(500)
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
`
var createIndexLatestRepo = `
CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-latest
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type VARCHAR(50)
,latest_name VARCHAR(500)
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
-- name: create-index-latest-repo
CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);

View file

@ -132,6 +132,14 @@ var migrations = []struct {
name: "alter-table-builds-add-column-deploy-id",
stmt: alterTableBuildsAddColumnDeployId,
},
{
name: "create-table-latest",
stmt: createTableLatest,
},
{
name: "create-index-latest-repo",
stmt: createIndexLatestRepo,
},
}
// Migrate performs the database migration. If the migration fails
@ -585,3 +593,24 @@ CREATE TABLE IF NOT EXISTS orgsecrets (
var alterTableBuildsAddColumnDeployId = `
ALTER TABLE builds ADD COLUMN build_deploy_id NUMBER NOT NULL DEFAULT 0;
`
//
// 014_create_table_refs.sql
//
var createTableLatest = `
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type TEXT -- branch | tag | pull_request | promote
,latest_name TEXT -- master | v1.0.0, | 42 | production
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
`
var createIndexLatestRepo = `
CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);
`

View file

@ -0,0 +1,16 @@
-- name: create-table-latest
CREATE TABLE IF NOT EXISTS latest (
latest_repo_id INTEGER
,latest_build_id INTEGER
,latest_type TEXT -- branch | tag | pull_request | promote
,latest_name TEXT -- master | v1.0.0, | 42 | production
,latest_created INTEGER
,latest_updated INTEGER
,latest_deleted INTEGER
,PRIMARY KEY(latest_repo_id, latest_type, latest_name)
);
-- name: create-index-latest-repo
CREATE INDEX IF NOT EXISTS ix_latest_repo ON latest (latest_repo_id);