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:
72
internal/gitea/templates.go
Normal file
72
internal/gitea/templates.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user