feat(tools): create_project_from_template

Generates a new repo from mathias/template-go-web via Gitea's generate
API, then substitutes __PROJECT_NAME__ and __MODULE_PATH__ placeholders
in six known files (best-effort, partial failure surfaced in result).

Validates name regex, allowlist, template flag, and destination
non-existence before generating. Adds Template field to gitea.Repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mathias Bergqvist
2026-05-04 23:02:21 +02:00
parent e95e87e8e3
commit 39dc22ec3a
6 changed files with 642 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
package gitea
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
)
// GenerateFromTemplateArgs is the request body for POST /repos/{owner}/{repo}/generate.
type GenerateFromTemplateArgs struct {
Owner string `json:"owner"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Private bool `json:"private"`
DefaultBranch string `json:"default_branch,omitempty"`
GitContent bool `json:"git_content"` // include all template files
}
// GenerateFromTemplate creates a new repository from a template via POST /repos/{tmplOwner}/{tmplName}/generate.
func (c *Client) GenerateFromTemplate(ctx context.Context, tmplOwner, tmplName string, args GenerateFromTemplateArgs) (*Repo, error) {
p := fmt.Sprintf("/api/v1/repos/%s/%s/generate", tmplOwner, tmplName)
payload, err := json.Marshal(args)
if err != nil {
return nil, err
}
body, status, err := c.PostJSON(ctx, p, payload)
if err != nil {
return nil, err
}
if err := MapStatus(status, body); err != nil {
return nil, err
}
var r Repo
if err := json.Unmarshal(body, &r); err != nil {
return nil, err
}
return &r, nil
}
// SubstituteFile reads a file from the given branch, applies string replacements,
// and writes it back if any changes were made. Best-effort — returns a wrapped error
// that includes the file path.
func (c *Client) SubstituteFile(ctx context.Context, owner, repo, branch, path string, replacements map[string]string) error {
fc, err := c.GetFileContents(ctx, owner, repo, path, branch)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
decoded, err := base64.StdEncoding.DecodeString(fc.Content)
if err != nil {
return fmt.Errorf("decode %s: %w", path, err)
}
content := string(decoded)
for k, v := range replacements {
content = strings.ReplaceAll(content, k, v)
}
if content == string(decoded) {
return nil // no changes, skip write
}
encoded := base64.StdEncoding.EncodeToString([]byte(content))
_, err = c.UpsertFile(ctx, owner, repo, path, UpsertFileArgs{
Branch: branch,
Content: encoded,
Message: "Apply template substitutions",
Sha: fc.Sha,
})
if err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}