batches: implement createEmptyBatchChange mutation (#28524)

This commit is contained in:
Kelli Rockwell 2021-12-07 09:44:05 -08:00 committed by GitHub
parent b0932887cf
commit 77fbddd631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 215 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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"`