M api/graph/api/generated.go => api/graph/api/generated.go +18 -6
@@ 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")
M api/graph/resolver.go => api/graph/resolver.go +22 -0
@@ 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{}{},
+}
M api/graph/schema.graphqls => api/graph/schema.graphqls +5 -2
@@ 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)
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +57 -5
@@ 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 {