From 3f954b33bd69f6dfec350066e6a09c000377393c Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 26 Nov 2020 11:01:21 -0500 Subject: [PATCH] API: implement mutation { createRepository } --- api/go.mod | 2 +- api/go.sum | 3 + api/graph/api/generated.go | 32 +++++------ api/graph/model/models_gen.go | 6 +- api/graph/resolver.go | 8 +++ api/graph/schema.graphqls | 14 ++++- api/graph/schema.resolvers.go | 101 +++++++++++++++++++++++++++++++++- gitsrht-shell/main.go | 1 + 8 files changed, 143 insertions(+), 24 deletions(-) diff --git a/api/go.mod b/api/go.mod index ba3d548..ac83629 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,7 +3,7 @@ module git.sr.ht/~sircmpwn/git.sr.ht/api go 1.14 require ( - git.sr.ht/~sircmpwn/core-go v0.0.0-20201121171719-31fc9fce43e9 + git.sr.ht/~sircmpwn/core-go v0.0.0-20201126154911-33562018fec2 git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 github.com/99designs/gqlgen v0.13.0 github.com/Masterminds/squirrel v1.4.0 diff --git a/api/go.sum b/api/go.sum index 42ad465..c38cd0d 100644 --- a/api/go.sum +++ b/api/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= git.sr.ht/~sircmpwn/core-go v0.0.0-20201121171719-31fc9fce43e9 h1:w0toCjfdNh2JE4tes7on3pr41reASuYewcH5wGwUz+U= git.sr.ht/~sircmpwn/core-go v0.0.0-20201121171719-31fc9fce43e9/go.mod h1:LLLvDJIgVgmA/sHl0fzj9UvpFiLi0v8a/aiBc3g22ik= +git.sr.ht/~sircmpwn/core-go v0.0.0-20201126154911-33562018fec2 h1:qtKGxaNnO86GZtT4IYGHzQx3fN7P6NgxjhZaLeTdR84= +git.sr.ht/~sircmpwn/core-go v0.0.0-20201126154911-33562018fec2/go.mod h1:LLLvDJIgVgmA/sHl0fzj9UvpFiLi0v8a/aiBc3g22ik= git.sr.ht/~sircmpwn/dowork v0.0.0-20201013160733-35ca012e4dc8 h1:ltrdYYclC4wQEg3QdcG2hgYAFCk+6/l2vU1OXygKXVA= git.sr.ht/~sircmpwn/dowork v0.0.0-20201013160733-35ca012e4dc8/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw= git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 h1:4wDp4BKF7NQqoh73VXpZsB/t1OEhDpz/zEpmdQfbjDk= @@ -561,6 +563,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a h1:gILuVKC+ZPD6g/tj6zBOdnOH1ZHI0zZ86+KLMogc6/s= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/api/graph/api/generated.go b/api/graph/api/generated.go index 5d30b96..e9fc925 100644 --- a/api/graph/api/generated.go +++ b/api/graph/api/generated.go @@ -1580,18 +1580,30 @@ type Query { input RepoInput { name: String! description: String - visibility: Visibility + visibility: Visibility! } type Mutation { + # Creates a new git repository createRepository(params: RepoInput): Repository! @access(scope: REPOSITORIES, kind: RW) + + # Updates the metadata for a git repository updateRepository(id: ID!, params: RepoInput): Repository! @access(scope: REPOSITORIES, kind: RW) + + # Deletes a git repository deleteRepository(id: ID!): Repository! @access(scope: REPOSITORIES, kind: RW) + # Adds or updates a user in the access control list updateACL(repoId: ID!, mode: AccessMode!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) + + # Deletes an entry from the access control list deleteACL(repoId: Int!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) + # Uploads an artifact. revspec must match a specific git tag, and the + # filename must be unique among artifacts for this repository. uploadArtifact(repoId: Int!, revspec: String!, file: Upload!): Artifact! @access(scope: OBJECTS, kind: RW) + + # Deletes an artifact. deleteArtifact(id: Int!): Artifact! @access(scope: OBJECTS, kind: RW) } `, BuiltIn: false}, @@ -7950,7 +7962,7 @@ func (ec *executionContext) unmarshalInputRepoInput(ctx context.Context, obj int var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility")) - it.Visibility, err = ec.unmarshalOVisibility2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx, v) + it.Visibility, err = ec.unmarshalNVisibility2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx, v) if err != nil { return it, err } @@ -10536,22 +10548,6 @@ func (ec *executionContext) marshalOUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗs return ec._User(ctx, sel, v) } -func (ec *executionContext) unmarshalOVisibility2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx context.Context, v interface{}) (*model.Visibility, error) { - if v == nil { - return nil, nil - } - var res = new(model.Visibility) - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalOVisibility2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx context.Context, sel ast.SelectionSet, v *model.Visibility) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return v -} - func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index 53d4a3c..9c2adf3 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -40,9 +40,9 @@ type ReferenceCursor struct { } type RepoInput struct { - Name string `json:"name"` - Description *string `json:"description"` - Visibility *Visibility `json:"visibility"` + Name string `json:"name"` + Description *string `json:"description"` + Visibility Visibility `json:"visibility"` } type RepositoryCursor struct { diff --git a/api/graph/resolver.go b/api/graph/resolver.go index 9b48c95..cd01693 100644 --- a/api/graph/resolver.go +++ b/api/graph/resolver.go @@ -1,5 +1,13 @@ package graph +import ( + "regexp" +) + //go:generate go run github.com/99designs/gqlgen type Resolver struct {} + +var ( + repoNameRE = regexp.MustCompile(`^[A-Za-z._-][A-Za-z0-9._-]*$`) +) diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 22c0825..2aeaa59 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -333,17 +333,29 @@ type Query { input RepoInput { name: String! description: String - visibility: Visibility + visibility: Visibility! } type Mutation { + # Creates a new git repository createRepository(params: RepoInput): Repository! @access(scope: REPOSITORIES, kind: RW) + + # Updates the metadata for a git repository updateRepository(id: ID!, params: RepoInput): Repository! @access(scope: REPOSITORIES, kind: RW) + + # Deletes a git repository deleteRepository(id: ID!): Repository! @access(scope: REPOSITORIES, kind: RW) + # Adds or updates a user in the access control list updateACL(repoId: ID!, mode: AccessMode!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) + + # Deletes an entry from the access control list deleteACL(repoId: Int!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) + # Uploads an artifact. revspec must match a specific git tag, and the + # filename must be unique among artifacts for this repository. uploadArtifact(repoId: Int!, revspec: String!, file: Upload!): Artifact! @access(scope: OBJECTS, kind: RW) + + # Deletes an artifact. deleteArtifact(id: Int!): Artifact! @access(scope: OBJECTS, kind: RW) } diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 40a3174..fdcbe2c 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -7,7 +7,10 @@ import ( "context" "database/sql" "fmt" + "os" + "path" "sort" + "strconv" "strings" "git.sr.ht/~sircmpwn/core-go/auth" @@ -54,7 +57,103 @@ func (r *commitResolver) Diff(ctx context.Context, obj *model.Commit) (string, e } func (r *mutationResolver) CreateRepository(ctx context.Context, params *model.RepoInput) (*model.Repository, error) { - panic(fmt.Errorf("createRepository: not implemented")) + if !repoNameRE.MatchString(params.Name) { + return nil, fmt.Errorf("Invalid repository name '%s' (must match %s)", + params.Name, repoNameRE.String()) + } + + conf := config.ForContext(ctx) + repoStore, ok := conf.Get("git.sr.ht", "repos") + if !ok || repoStore == "" { + panic(fmt.Errorf("Configuration error: [git.sr.ht]repos is unset")) + } + postUpdate, ok := conf.Get("git.sr.ht", "post-update-script") + if !ok { + panic(fmt.Errorf("Configuration error: [git.sr.ht]post-update is unset")) + } + + user := auth.ForContext(ctx) + repoPath := path.Join(repoStore, "~" + user.Username, params.Name) + + var ( + repoCreated bool + repo model.Repository + ) + if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { + vismap := map[model.Visibility]string{ + model.VisibilityPublic: "public", + model.VisibilityUnlisted: "unlisted", + model.VisibilityPrivate: "private", + } + var ( + dvis string + ok bool + ) + if dvis, ok = vismap[params.Visibility]; !ok { + panic(fmt.Errorf("Unknown visibility %s", params.Visibility)) // Invariant + } + + row := tx.QueryRowContext(ctx, ` + INSERT INTO repository ( + created, updated, name, description, path, visibility, owner_id + ) VALUES ( + NOW() at time zone 'utc', + NOW() at time zone 'utc', + $1, $2, $3, $4, $5 + ) RETURNING + id, created, updated, name, description, visibility, + upstream_uri, path, owner_id; + `, params.Name, params.Description, repoPath, dvis, user.UserID) + if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, &repo.Name, + &repo.Description, &repo.Visibility, &repo.UpstreamURL, &repo.Path, + &repo.OwnerID); err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return fmt.Errorf("A repository with this name already exists.") + } + + return err + } + + gitrepo, err := git.PlainInit(repoPath, true) + if err != nil { + return err + } + repoCreated = true + config, err := gitrepo.Config() + if err != nil { + return err + } + config.Raw.SetOption("core", "", "repositoryformatversion", "0") + config.Raw.SetOption("core", "", "filemode", "true") + config.Raw.SetOption("srht", "", "repo-id", strconv.Itoa(repo.ID)) + config.Raw.SetOption("receive", "", "denyDeleteCurrent", "ignore") + config.Raw.SetOption("receive", "", "advertisePushOptions", "true") + if err = gitrepo.Storer.SetConfig(config); err != nil { + return err + } + + hookdir := path.Join(repoPath, "hooks") + if err = os.Mkdir(hookdir, os.ModePerm); err != nil { + return err + } + for _, hook := range []string{"pre-receive", "update", "post-update"} { + if err = os.Symlink(postUpdate, path.Join(hookdir, hook)); err != nil { + return err + } + } + + return nil + }); err != nil { + if repoCreated { + err := os.RemoveAll(repoPath) + if err != nil { + panic(err) + } + } + return nil, err + } + + return &repo, nil } func (r *mutationResolver) UpdateRepository(ctx context.Context, id string, params *model.RepoInput) (*model.Repository, error) { diff --git a/gitsrht-shell/main.go b/gitsrht-shell/main.go index a3fcec0..308de4e 100644 --- a/gitsrht-shell/main.go +++ b/gitsrht-shell/main.go @@ -300,6 +300,7 @@ func main() { } // Note: update gitsrht/repos.py when changing this + // Also update api/graph/schema.resolvers.go:CreateRepository repo, err := git.PlainInit(path, true) if err != nil { notFound("git init", err) -- 2.38.4