From c06bc64d778555ef1bdbfeaefa0a17a53ae01c7b Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Thu, 20 Jan 2022 07:11:51 -0500 Subject: [PATCH] api/graph: Add cloneUrl to createRepository Add support for cloning repositories by URL using the optional cloneUrl parameter. --- api/graph/api/generated.go | 24 ++++++++++---- api/graph/resolver.go | 22 +++++++++++++ api/graph/schema.graphqls | 7 ++-- api/graph/schema.resolvers.go | 62 ++++++++++++++++++++++++++++++++--- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/api/graph/api/generated.go b/api/graph/api/generated.go index b1f302d..3f8a02c 100644 --- a/api/graph/api/generated.go +++ b/api/graph/api/generated.go @@ -116,7 +116,7 @@ type ComplexityRoot struct { } Mutation struct { - CreateRepository func(childComplexity int, name string, visibility model.Visibility, description *string) int + CreateRepository func(childComplexity int, name string, visibility model.Visibility, description *string, cloneURL *string) int CreateWebhook func(childComplexity int, config model.UserWebhookInput) int DeleteACL func(childComplexity int, id int) int DeleteArtifact func(childComplexity int, id int) int @@ -301,7 +301,7 @@ type CommitResolver interface { Diff(ctx context.Context, obj *model.Commit) (string, error) } type MutationResolver interface { - CreateRepository(ctx context.Context, name string, visibility model.Visibility, description *string) (*model.Repository, error) + CreateRepository(ctx context.Context, name string, visibility model.Visibility, description *string, cloneURL *string) (*model.Repository, error) UpdateRepository(ctx context.Context, id int, input map[string]interface{}) (*model.Repository, error) DeleteRepository(ctx context.Context, id int) (*model.Repository, error) UpdateACL(ctx context.Context, repoID int, mode model.AccessMode, entity string) (*model.ACL, error) @@ -607,7 +607,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.CreateRepository(childComplexity, args["name"].(string), args["visibility"].(model.Visibility), args["description"].(*string)), true + return e.complexity.Mutation.CreateRepository(childComplexity, args["name"].(string), args["visibility"].(model.Visibility), args["description"].(*string), args["cloneUrl"].(*string)), true case "Mutation.createWebhook": if e.complexity.Mutation.CreateWebhook == nil { @@ -2097,8 +2097,11 @@ input UserWebhookInput { } type Mutation { - "Creates a new git repository" - createRepository(name: String!, visibility: Visibility!, description: String): Repository @access(scope: REPOSITORIES, kind: RW) + """ + Creates a new git repository. If the cloneUrl parameter is specified, the + repository will be cloned from the given URL. + """ + createRepository(name: String!, visibility: Visibility!, description: String, cloneUrl: String): Repository @access(scope: REPOSITORIES, kind: RW) "Updates the metadata for a git repository" updateRepository(id: Int!, input: RepoInput!): Repository @access(scope: REPOSITORIES, kind: RW) @@ -2221,6 +2224,15 @@ func (ec *executionContext) field_Mutation_createRepository_args(ctx context.Con } } args["description"] = arg2 + var arg3 *string + if tmp, ok := rawArgs["cloneUrl"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("cloneUrl")) + arg3, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["cloneUrl"] = arg3 return args, nil } @@ -3954,7 +3966,7 @@ func (ec *executionContext) _Mutation_createRepository(ctx context.Context, fiel resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateRepository(rctx, args["name"].(string), args["visibility"].(model.Visibility), args["description"].(*string)) + return ec.resolvers.Mutation().CreateRepository(rctx, args["name"].(string), args["visibility"].(model.Visibility), args["description"].(*string), args["cloneUrl"].(*string)) } directive1 := func(ctx context.Context) (interface{}, error) { scope, err := ec.unmarshalNAccessScope2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "REPOSITORIES") diff --git a/api/graph/resolver.go b/api/graph/resolver.go index c8a657f..8e63e6f 100644 --- a/api/graph/resolver.go +++ b/api/graph/resolver.go @@ -1,7 +1,12 @@ package graph import ( + "context" + "fmt" "regexp" + + "github.com/99designs/gqlgen/graphql" + "github.com/vektah/gqlparser/v2/gqlerror" ) type Resolver struct{} @@ -9,3 +14,20 @@ type Resolver struct{} var ( repoNameRE = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) ) + +func gqlErrorf(ctx context.Context, field string, message string, items ...interface{}) *gqlerror.Error { + err := &gqlerror.Error{ + Message: fmt.Sprintf(message, items...), + Path: graphql.GetPath(ctx), + Extensions: map[string]interface{}{ + "field": field, + }, + } + return err +} + +var allowedCloneSchemes = map[string]struct{}{ + "https": struct{}{}, + "http": struct{}{}, + "git": struct{}{}, +} diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 8b08523..f4bdbf7 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -522,8 +522,11 @@ input UserWebhookInput { } type Mutation { - "Creates a new git repository" - createRepository(name: String!, visibility: Visibility!, description: String): Repository @access(scope: REPOSITORIES, kind: RW) + """ + Creates a new git repository. If the cloneUrl parameter is specified, the + repository will be cloned from the given URL. + """ + createRepository(name: String!, visibility: Visibility!, description: String, cloneUrl: String): Repository @access(scope: REPOSITORIES, kind: RW) "Updates the metadata for a git repository" updateRepository(id: Int!, input: RepoInput!): Repository @access(scope: REPOSITORIES, kind: RW) diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 5868b5c..1b43fc8 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -71,7 +71,7 @@ func (r *commitResolver) Diff(ctx context.Context, obj *model.Commit) (string, e return obj.DiffContext(ctx), nil } -func (r *mutationResolver) CreateRepository(ctx context.Context, name string, visibility model.Visibility, description *string) (*model.Repository, error) { +func (r *mutationResolver) CreateRepository(ctx context.Context, name string, visibility model.Visibility, description *string, cloneURL *string) (*model.Repository, error) { if !repoNameRE.MatchString(name) { return nil, fmt.Errorf("Invalid repository name '%s' (must match %s)", name, repoNameRE.String()) @@ -141,15 +141,67 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi &repo.Description, &repo.RawVisibility, &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 gqlErrorf(ctx, "name", "A repository with this name already exists.") } return err } - gitrepo, err := git.PlainInit(repoPath, true) - if err != nil { - return err + var gitrepo *git.Repository + if cloneURL != nil { + u, err := url.Parse(*cloneURL) + if err != nil { + return err + } else if u.Host == "" { + return gqlErrorf(ctx, "cloneUrl", "Cannot use URL without host") + } else if _, ok := allowedCloneSchemes[u.Scheme]; !ok { + return gqlErrorf(ctx, "cloneUrl", "Unsupported protocol %q", u.Scheme) + } + + origin := config.GetOrigin(conf, "git.sr.ht", true) + o, err := url.Parse(origin) + if err != nil { + panic(err) + } + + // Check if this is a local repository + if u.Scheme == o.Scheme && u.Host == o.Host { + u.Path = strings.TrimPrefix(u.Path, "/") + split := strings.SplitN(u.Path, "/", 2) + if len(split) != 2 { + return gqlErrorf(ctx, "cloneUrl", "Invalid clone URL") + } + canonicalName, repoName := split[0], split[1] + entity := canonicalName + if strings.HasPrefix(entity, "~") { + entity = entity[1:] + } else { + return gqlErrorf(ctx, "cloneUrl", "Invalid username") + } + repo, err := loaders.ForContext(ctx). + RepositoriesByOwnerRepoName.Load([2]string{entity, repoName}) + if err != nil { + return err + } else if repo == nil { + return gqlErrorf(ctx, "cloneUrl", "Repository not found") + } + cloneURL = &repo.Path + } + + gitrepo, err = git.PlainClone(repoPath, true, &git.CloneOptions{ + URL: *cloneURL, + RecurseSubmodules: git.NoRecurseSubmodules, + }) + if err != nil { + return gqlErrorf(ctx, "cloneUrl", "%s", err) + } + } else { + var err error + gitrepo, err = git.PlainInit(repoPath, true) + if err != nil { + return err + } } + repoCreated = true config, err := gitrepo.Config() if err != nil { -- 2.38.4