From 5bb97a12d79309e799e9826badf3527f03815dd2 Mon Sep 17 00:00:00 2001
From: a1012112796 <1012112796@qq.com>
Date: Mon, 5 Jul 2021 23:29:08 +0800
Subject: [PATCH] Creating a repo from a template repo via API (#15958)

* Creating a repo from a template repo via API

fix #15934
ref:
https://docs.github.com/en/rest/reference/repos#create-a-repository-using-a-template

Signed-off-by: a1012112796 <1012112796@qq.com>
---
 integrations/api_repo_test.go     |  37 ++++++++++
 modules/structs/repo.go           |  30 ++++++++
 routers/api/v1/api.go             |   1 +
 routers/api/v1/repo/repo.go       | 109 ++++++++++++++++++++++++++++
 routers/api/v1/swagger/options.go |   2 +
 templates/swagger/v1_json.tmpl    | 117 ++++++++++++++++++++++++++++++
 6 files changed, 296 insertions(+)

diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go
index 7052e74b01..3948489f56 100644
--- a/integrations/api_repo_test.go
+++ b/integrations/api_repo_test.go
@@ -495,6 +495,43 @@ func TestAPIRepoTransfer(t *testing.T) {
 	_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
 }
 
+func TestAPIGenerateRepo(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+
+	templateRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 44}).(*models.Repository)
+
+	// user
+	repo := new(api.Repository)
+	req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
+		Owner:       user.Name,
+		Name:        "new-repo",
+		Description: "test generate repo",
+		Private:     false,
+		GitContent:  true,
+	})
+	resp := session.MakeRequest(t, req, http.StatusCreated)
+	DecodeJSON(t, resp, repo)
+
+	assert.Equal(t, "new-repo", repo.Name)
+
+	// org
+	req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
+		Owner:       "user3",
+		Name:        "new-repo",
+		Description: "test generate repo",
+		Private:     false,
+		GitContent:  true,
+	})
+	resp = session.MakeRequest(t, req, http.StatusCreated)
+	DecodeJSON(t, resp, repo)
+
+	assert.Equal(t, "new-repo", repo.Name)
+}
+
 func TestAPIRepoGetReviewers(t *testing.T) {
 	defer prepareTestEnv(t)()
 	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 4fdc1e54cb..cef864c020 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -180,6 +180,36 @@ type EditRepoOption struct {
 	MirrorInterval *string `json:"mirror_interval,omitempty"`
 }
 
+// GenerateRepoOption options when creating repository using a template
+// swagger:model
+type GenerateRepoOption struct {
+	// The organization or person who will own the new repository
+	//
+	// required: true
+	Owner string `json:"owner"`
+	// Name of the repository to create
+	//
+	// required: true
+	// unique: true
+	Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"`
+	// Description of the repository to create
+	Description string `json:"description" binding:"MaxSize(255)"`
+	// Whether the repository is private
+	Private bool `json:"private"`
+	// include git content of default branch in template repo
+	GitContent bool `json:"git_content"`
+	// include topics in template repo
+	Topics bool `json:"topics"`
+	// include git hooks in template repo
+	GitHooks bool `json:"git_hooks"`
+	// include webhooks in template repo
+	Webhooks bool `json:"webhooks"`
+	// include avatar of the template repo
+	Avatar bool `json:"avatar"`
+	// include labels in template repo
+	Labels bool `json:"labels"`
+}
+
 // CreateBranchRepoOption options when creating a branch in a repository
 // swagger:model
 type CreateBranchRepoOption struct {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b6913ea1bc..b4f14bf2d1 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -722,6 +722,7 @@ func Routes() *web.Route {
 				m.Combo("").Get(reqAnyRepoReader(), repo.Get).
 					Delete(reqToken(), reqOwner(), repo.Delete).
 					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
+				m.Post("/generate", reqToken(), reqRepoReader(models.UnitTypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
 				m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
 				m.Combo("/notifications").
 					Get(reqToken(), notify.ListRepoNotifications).
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 35d3490510..5d397191a6 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -307,6 +307,115 @@ func Create(ctx *context.APIContext) {
 	CreateUserRepo(ctx, ctx.User, *opt)
 }
 
+// Generate Create a repository using a template
+func Generate(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo
+	// ---
+	// summary: Create a repository using a template
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: template_owner
+	//   in: path
+	//   description: name of the template repository owner
+	//   type: string
+	//   required: true
+	// - name: template_repo
+	//   in: path
+	//   description: name of the template repository
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/GenerateRepoOption"
+	// responses:
+	//   "201":
+	//     "$ref": "#/responses/Repository"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "409":
+	//     description: The repository with the same name already exists.
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+	form := web.GetForm(ctx).(*api.GenerateRepoOption)
+
+	if !ctx.Repo.Repository.IsTemplate {
+		ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo")
+		return
+	}
+
+	if ctx.User.IsOrganization() {
+		ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
+		return
+	}
+
+	opts := models.GenerateRepoOptions{
+		Name:        form.Name,
+		Description: form.Description,
+		Private:     form.Private,
+		GitContent:  form.GitContent,
+		Topics:      form.Topics,
+		GitHooks:    form.GitHooks,
+		Webhooks:    form.Webhooks,
+		Avatar:      form.Avatar,
+		IssueLabels: form.Labels,
+	}
+
+	if !opts.IsValid() {
+		ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item")
+		return
+	}
+
+	ctxUser := ctx.User
+	var err error
+	if form.Owner != ctxUser.Name {
+		ctxUser, err = models.GetOrgByName(form.Owner)
+		if err != nil {
+			if models.IsErrOrgNotExist(err) {
+				ctx.JSON(http.StatusNotFound, map[string]interface{}{
+					"error": "request owner `" + form.Name + "` is not exist",
+				})
+				return
+			}
+
+			ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
+			return
+		}
+
+		if !ctx.User.IsAdmin {
+			canCreate, err := ctxUser.CanCreateOrgRepo(ctx.User.ID)
+			if err != nil {
+				ctx.ServerError("CanCreateOrgRepo", err)
+				return
+			} else if !canCreate {
+				ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.")
+				return
+			}
+		}
+	}
+
+	repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, ctx.Repo.Repository, opts)
+	if err != nil {
+		if models.IsErrRepoAlreadyExist(err) {
+			ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
+		} else if models.IsErrNameReserved(err) ||
+			models.IsErrNamePatternNotAllowed(err) {
+			ctx.Error(http.StatusUnprocessableEntity, "", err)
+		} else {
+			ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
+		}
+		return
+	}
+	log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
+
+	ctx.JSON(http.StatusCreated, convert.ToRepo(repo, models.AccessModeOwner))
+}
+
 // CreateOrgRepoDeprecated create one repository of the organization
 func CreateOrgRepoDeprecated(ctx *context.APIContext) {
 	// swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index b5f34e86a3..0ae96a9203 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -87,6 +87,8 @@ type swaggerParameterBodies struct {
 	TransferRepoOption api.TransferRepoOption
 	// in:body
 	CreateForkOption api.CreateForkOption
+	// in:body
+	GenerateRepoOption api.GenerateRepoOption
 
 	// in:body
 	CreateStatusOption api.CreateStatusOption
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index a2e449228e..669e3552cc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -9777,6 +9777,61 @@
         }
       }
     },
+    "/repos/{template_owner}/{template_repo}/generate": {
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Create a repository using a template",
+        "operationId": "generateRepo",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the template repository owner",
+            "name": "template_owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the template repository",
+            "name": "template_repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/GenerateRepoOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "$ref": "#/responses/Repository"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "409": {
+            "description": "The repository with the same name already exists."
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/repositories/{id}": {
       "get": {
         "produces": [
@@ -14551,6 +14606,68 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "GenerateRepoOption": {
+      "description": "GenerateRepoOption options when creating repository using a template",
+      "type": "object",
+      "required": [
+        "owner",
+        "name"
+      ],
+      "properties": {
+        "avatar": {
+          "description": "include avatar of the template repo",
+          "type": "boolean",
+          "x-go-name": "Avatar"
+        },
+        "description": {
+          "description": "Description of the repository to create",
+          "type": "string",
+          "x-go-name": "Description"
+        },
+        "git_content": {
+          "description": "include git content of default branch in template repo",
+          "type": "boolean",
+          "x-go-name": "GitContent"
+        },
+        "git_hooks": {
+          "description": "include git hooks in template repo",
+          "type": "boolean",
+          "x-go-name": "GitHooks"
+        },
+        "labels": {
+          "description": "include labels in template repo",
+          "type": "boolean",
+          "x-go-name": "Labels"
+        },
+        "name": {
+          "description": "Name of the repository to create",
+          "type": "string",
+          "uniqueItems": true,
+          "x-go-name": "Name"
+        },
+        "owner": {
+          "description": "The organization or person who will own the new repository",
+          "type": "string",
+          "x-go-name": "Owner"
+        },
+        "private": {
+          "description": "Whether the repository is private",
+          "type": "boolean",
+          "x-go-name": "Private"
+        },
+        "topics": {
+          "description": "include topics in template repo",
+          "type": "boolean",
+          "x-go-name": "Topics"
+        },
+        "webhooks": {
+          "description": "include webhooks in template repo",
+          "type": "boolean",
+          "x-go-name": "Webhooks"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "GitBlobResponse": {
       "description": "GitBlobResponse represents a git blob",
       "type": "object",