mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
batches: implement createEmptyBatchChange mutation (#28524)
This commit is contained in:
parent
b0932887cf
commit
77fbddd631
@ -71,6 +71,11 @@ type CreateBatchSpecArgs struct {
|
||||
ChangesetSpecs []graphql.ID
|
||||
}
|
||||
|
||||
type CreateEmptyBatchChangeArgs struct {
|
||||
Namespace graphql.ID
|
||||
Name string
|
||||
}
|
||||
|
||||
type CreateBatchSpecFromRawArgs struct {
|
||||
BatchSpec string
|
||||
AllowIgnored bool
|
||||
@ -234,6 +239,7 @@ type BatchChangesResolver interface {
|
||||
//
|
||||
CreateBatchChange(ctx context.Context, args *CreateBatchChangeArgs) (BatchChangeResolver, error)
|
||||
CreateBatchSpec(ctx context.Context, args *CreateBatchSpecArgs) (BatchSpecResolver, error)
|
||||
CreateEmptyBatchChange(ctx context.Context, args *CreateEmptyBatchChangeArgs) (BatchChangeResolver, error)
|
||||
CreateBatchSpecFromRaw(ctx context.Context, args *CreateBatchSpecFromRawArgs) (BatchSpecResolver, error)
|
||||
ReplaceBatchSpecInput(ctx context.Context, args *ReplaceBatchSpecInputArgs) (BatchSpecResolver, error)
|
||||
DeleteBatchSpec(ctx context.Context, args *DeleteBatchSpecArgs) (*EmptyResponse, error)
|
||||
|
||||
@ -1324,6 +1324,24 @@ extend type Mutation {
|
||||
changesetSpecs: [ID!]!
|
||||
): BatchSpec!
|
||||
|
||||
"""
|
||||
Creates a batch change with an empty batch spec, such as for drafting a new batch
|
||||
change. The user creating the batch change must have permission to create it in the
|
||||
namespace provided. Use `replaceBatchSpecInput` to update the input batch spec after
|
||||
creating.
|
||||
"""
|
||||
createEmptyBatchChange(
|
||||
"""
|
||||
The namespace (either a user or organization) that this batch change should belong to.
|
||||
"""
|
||||
namespace: ID!
|
||||
|
||||
"""
|
||||
The (unique) name to identify the batch change by in its namespace.
|
||||
"""
|
||||
name: String!
|
||||
): BatchChange!
|
||||
|
||||
"""
|
||||
Creates a batch spec and triggers a job to evaluate the workspaces. Consumers
|
||||
need to poll the batch spec until the resolution is completed to get a full
|
||||
|
||||
@ -1445,6 +1445,32 @@ func (r *Resolver) BatchSpecs(ctx context.Context, args *graphqlbackend.ListBatc
|
||||
return &batchSpecConnectionResolver{store: r.store, opts: opts}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateEmptyBatchChange(ctx context.Context, args *graphqlbackend.CreateEmptyBatchChangeArgs) (graphqlbackend.BatchChangeResolver, error) {
|
||||
// TODO(ssbc): currently admin only.
|
||||
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, r.store.DatabaseDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := service.New(r.store)
|
||||
|
||||
var uid, oid int32
|
||||
if err := graphqlbackend.UnmarshalNamespaceID(args.Namespace, &uid, &oid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
batchChange, err := svc.CreateEmptyBatchChange(ctx, service.CreateEmptyBatchChangeOpts{
|
||||
NamespaceUserID: uid,
|
||||
NamespaceOrgID: oid,
|
||||
Name: args.Name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &batchChangeResolver{store: r.store, batchChange: batchChange}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateBatchSpecFromRaw(ctx context.Context, args *graphqlbackend.CreateBatchSpecFromRawArgs) (graphqlbackend.BatchSpecResolver, error) {
|
||||
// TODO(ssbc): currently admin only.
|
||||
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, r.store.DatabaseDB()); err != nil {
|
||||
@ -1453,6 +1479,7 @@ func (r *Resolver) CreateBatchSpecFromRaw(ctx context.Context, args *graphqlback
|
||||
|
||||
svc := service.New(r.store)
|
||||
batchSpec, err := svc.CreateBatchSpecFromRaw(ctx, service.CreateBatchSpecFromRawOpts{
|
||||
// TODO: Handle namespace like for CreateEmptyBatchChange
|
||||
NamespaceUserID: actor.FromContext(ctx).UID,
|
||||
RawSpec: args.BatchSpec,
|
||||
AllowIgnored: args.AllowIgnored,
|
||||
|
||||
@ -5,12 +5,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/batches/overridable"
|
||||
|
||||
@ -528,6 +530,91 @@ mutation($batchSpec: ID!, $ensureBatchChange: ID, $publicationStates: [Changeset
|
||||
}
|
||||
` + fragmentBatchChange
|
||||
|
||||
func TestCreateEmptyBatchChange(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
db := dbtest.NewDB(t)
|
||||
|
||||
cstore := store.New(db, &observation.TestContext, nil)
|
||||
|
||||
r := &Resolver{store: cstore}
|
||||
s, err := newSchema(database.NewDB(db), r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
userID := ct.CreateTestUser(t, db, true).ID
|
||||
namespaceID := relay.MarshalID("User", userID)
|
||||
|
||||
input := map[string]interface{}{
|
||||
"namespace": namespaceID,
|
||||
"name": "my-batch-change",
|
||||
}
|
||||
|
||||
var response struct{ CreateEmptyBatchChange apitest.BatchChange }
|
||||
actorCtx := actor.WithActor(ctx, actor.FromUser(userID))
|
||||
|
||||
// First time should work because no batch change exists
|
||||
apitest.MustExec(actorCtx, t, s, input, &response, mutationCreateEmptyBatchChange)
|
||||
|
||||
if response.CreateEmptyBatchChange.ID == "" {
|
||||
t.Fatalf("expected batch change to be created, but was not")
|
||||
}
|
||||
|
||||
// Second time should fail because namespace + name are not unique
|
||||
errors := apitest.Exec(actorCtx, t, s, input, &response, mutationCreateEmptyBatchChange)
|
||||
|
||||
if len(errors) != 1 {
|
||||
t.Fatalf("expected single errors, but got none")
|
||||
}
|
||||
if have, want := errors[0].Message, service.ErrNameNotUnique.Error(); have != want {
|
||||
t.Fatalf("wrong error. want=%q, have=%q", want, have)
|
||||
}
|
||||
|
||||
// But third time should work because a different namespace + the same name is okay
|
||||
orgID := ct.InsertTestOrg(t, db, "my-org")
|
||||
namespaceID2 := relay.MarshalID("Org", orgID)
|
||||
|
||||
input2 := map[string]interface{}{
|
||||
"namespace": namespaceID2,
|
||||
"name": "my-batch-change",
|
||||
}
|
||||
|
||||
apitest.MustExec(actorCtx, t, s, input2, &response, mutationCreateEmptyBatchChange)
|
||||
|
||||
if response.CreateEmptyBatchChange.ID == "" {
|
||||
t.Fatalf("expected batch change to be created, but was not")
|
||||
}
|
||||
|
||||
// This case should fail because the name fails validation
|
||||
input3 := map[string]interface{}{
|
||||
"namespace": namespaceID,
|
||||
"name": "not: valid:\nname",
|
||||
}
|
||||
|
||||
errors = apitest.Exec(actorCtx, t, s, input3, &response, mutationCreateEmptyBatchChange)
|
||||
|
||||
if len(errors) != 1 {
|
||||
t.Fatalf("expected single errors, but got none")
|
||||
}
|
||||
|
||||
expError := "The batch change name can only contain word characters, dots and dashes."
|
||||
if have, want := errors[0].Message, expError; !strings.Contains(have, "The batch change name can only contain word characters, dots and dashes.") {
|
||||
t.Fatalf("wrong error. want to contain=%q, have=%q", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
const mutationCreateEmptyBatchChange = `
|
||||
mutation($namespace: ID!, $name: String!){
|
||||
createEmptyBatchChange(namespace: $namespace, name: $name) {
|
||||
...batchChange
|
||||
}
|
||||
}
|
||||
` + fragmentBatchChange
|
||||
|
||||
func TestCreateBatchChange(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/opentracing/opentracing-go/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
@ -28,8 +29,13 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/repoupdater"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
batcheslib "github.com/sourcegraph/sourcegraph/lib/batches"
|
||||
)
|
||||
|
||||
// ErrNameNotUnique is returned by CreateEmptyBatchChange if the combination of name and
|
||||
// namespace provided are already used by another batch change.
|
||||
var ErrNameNotUnique = errors.New("a batch change with this name already exists in this namespace")
|
||||
|
||||
// New returns a Service.
|
||||
func New(store *store.Store) *Service {
|
||||
return NewWithClock(store, store.Clock())
|
||||
@ -139,6 +145,77 @@ func (s *Service) WithStore(store *store.Store) *Service {
|
||||
return &Service{store: store, sourcer: s.sourcer, clock: s.clock, operations: s.operations}
|
||||
}
|
||||
|
||||
type CreateEmptyBatchChangeOpts struct {
|
||||
NamespaceUserID int32
|
||||
NamespaceOrgID int32
|
||||
|
||||
Name string
|
||||
}
|
||||
|
||||
// CreateEmptyBatchChange creates a new batch change with an empty batch spec. It enforces
|
||||
// namespace permissions of the caller and validates that the combination of name +
|
||||
// namespace is unique.
|
||||
func (s *Service) CreateEmptyBatchChange(ctx context.Context, opts CreateEmptyBatchChangeOpts) (batchChange *btypes.BatchChange, err error) {
|
||||
// Check whether the current user has access to either one of the namespaces.
|
||||
err = s.CheckNamespaceAccess(ctx, opts.NamespaceUserID, opts.NamespaceOrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Construct and parse the batch spec YAML of just the provided name to validate the
|
||||
// pattern of the name is okay
|
||||
rawSpec, err := yaml.Marshal(struct {
|
||||
Name string `yaml:"name"`
|
||||
}{Name: opts.Name})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshalling name")
|
||||
}
|
||||
spec, err := batcheslib.ParseBatchSpec(rawSpec, batcheslib.ParseBatchSpecOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actor := actor.FromContext(ctx)
|
||||
batchSpec := &btypes.BatchSpec{
|
||||
RawSpec: string(rawSpec),
|
||||
Spec: spec,
|
||||
NamespaceUserID: opts.NamespaceUserID,
|
||||
NamespaceOrgID: opts.NamespaceOrgID,
|
||||
UserID: actor.UID,
|
||||
}
|
||||
|
||||
// The combination of name + namespace must be unique
|
||||
batchChange, err = s.GetBatchChangeMatchingBatchSpec(ctx, batchSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if batchChange != nil {
|
||||
return nil, ErrNameNotUnique
|
||||
}
|
||||
|
||||
tx, err := s.store.Transact(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = tx.Done(err) }()
|
||||
|
||||
if err := tx.CreateBatchSpec(ctx, batchSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
batchChange = &btypes.BatchChange{
|
||||
Name: opts.Name,
|
||||
NamespaceUserID: opts.NamespaceUserID,
|
||||
NamespaceOrgID: opts.NamespaceOrgID,
|
||||
BatchSpecID: batchSpec.ID,
|
||||
}
|
||||
if err := tx.CreateBatchChange(ctx, batchChange); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return batchChange, nil
|
||||
}
|
||||
|
||||
type CreateBatchSpecOpts struct {
|
||||
RawSpec string `json:"raw_spec"`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user