add DevStar模板 in /repo/create

This commit is contained in:
孟宁
2025-08-09 14:35:17 +08:00
repo.diff.parent bdefb59a5b
repo.diff.commit 13c274afd6
repo.diff.stats_desc%!(EXTRA int=9, int=667, int=234)

repo.diff.view_file

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"slices"
"strings"
@@ -90,10 +91,10 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
var orgsAvailable []*organization.Organization
for i := range orgs {
if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
orgsAvailable = append(orgsAvailable, orgs[i])
orgsAvailable = append(orgsAvailable, orgs[i])
}
}
}
ctx.Data["Orgs"] = orgsAvailable
ctx.Data["Orgs"] = orgsAvailable
// Not equal means current user is an organization.
if uid == ctx.Doer.ID || uid == 0 {
@@ -169,13 +170,14 @@ func Create(ctx *context.Context) {
ctx.Data["private"] = getRepoPrivate(ctx)
ctx.Data["default_branch"] = setting.Repository.DefaultBranch
ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
ctx.Data["devstar_template_name"] = ctx.Tr("repo.template_select")
templateID := ctx.FormInt64("template_id")
if templateID > 0 {
templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID)
if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
ctx.Data["repo_template"] = templateID
ctx.Data["repo_template_name"] = templateRepo.Name
ctx.Data["repo_template_name"] = templateRepo.FullName
ctx.Data["devstar_template_name"] = false
}
}
@@ -214,6 +216,79 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error,
}
}
// isValidCommitUrl checks whether a given URL is a valid Git URL with a commit reference.
func isValidCommitUrl(url string) bool {
// 正则表达式用于匹配包含 commit 的 Git URL
regex := `^https?:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)(\/commit\/([a-f0-9]{40}))?$`
// 编译正则表达式
pattern, err := regexp.Compile(regex)
if err != nil {
fmt.Println("Invalid regex for commit URL:", err)
return false
}
// 测试 URL 是否匹配正则表达式
return pattern.MatchString(url)
}
// isValidGitPath checks whether a given URL is a valid Git URL ending with .git.
func isValidGitPath(url string) bool {
// 正则表达式用于匹配以 .git 结尾的 Git URL
regexWithGit := `^https?:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\.git$`
// 编译正则表达式
pattern, err := regexp.Compile(regexWithGit)
if err != nil {
fmt.Println("Invalid regex for .git URL:", err)
return false
}
// 测试 URL 是否匹配正则表达式
return pattern.MatchString(url)
}
// ExtractCommitID 从给定的 URL 中提取 commitID
func ExtractCommitID(url string) (string, error) {
// 正则表达式匹配 commit URL 中的 commitID
regex := `^https?:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+\/commit\/([a-f0-9]{40})$`
// 编译正则表达式
commitPattern, err := regexp.Compile(regex)
if err != nil {
return "", err
}
// 尝试匹配 URL
if commitPattern.MatchString(url) {
// 提取 commitID
matches := commitPattern.FindStringSubmatch(url)
if len(matches) >= 2 {
return matches[1], nil // 返回 commitID
}
}
// 如果匹配失败,返回错误
return "", fmt.Errorf("URL does not match expected format: %s", url)
}
// ReplaceCommitWithGit 将 commit URL 中的 /commit/<commitID> 替换为 .git
func ReplaceCommitWithGit(url string) (string, error) {
// 定义正则表达式匹配 URL 中的 /commit/<commitID> 部分
regex := `(/commit/[a-f0-9]{40})$`
// 编译正则表达式
commitPattern, err := regexp.Compile(regex)
if err != nil {
return "", err
}
// 替换为 .git
replacedURL := commitPattern.ReplaceAllString(url, ".git")
return replacedURL, nil
}
// CreatePost response for creating repository
func CreatePost(ctx *context.Context) {
createCommon(ctx)
@@ -292,6 +367,54 @@ func CreatePost(ctx *context.Context) {
})
if err == nil {
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
if strings.TrimSpace(repo.DefaultBranch) == "" {
repo.DefaultBranch = setting.Repository.DefaultBranch
}
if isValidGitPath(form.DevstarTemplate) {
log.Info("init repo from %s", form.DevstarTemplate)
repo, err = repo_service.InitRepoFromGitURL(ctx, ctx.Doer, ctxUser, form.DevstarTemplate, "", repo)
if err != nil {
log.Error("Error initializing repo from DevstarTemplate: %v", err)
return
}
if repo == nil {
log.Error("repo is nil after initializing from DevstarTemplate")
}
}
if isValidGitPath(form.GitUrlTemplate) {
log.Info("init repo from %s", form.GitUrlTemplate)
repo, err = repo_service.InitRepoFromGitURL(ctx, ctx.Doer, ctxUser, form.GitUrlTemplate, "", repo)
if err != nil {
log.Error("Error initializing repo from GitUrlTemplate: %v", err)
return
}
if repo == nil {
log.Error("repo is nil after initializing from GitUrlTemplate")
}
}
if isValidCommitUrl(form.GitUrlTemplate) {
log.Info("init repo from %s", form.GitUrlTemplate)
commitID, err := ExtractCommitID(form.GitUrlTemplate)
if err != nil {
log.Error("Error:", err)
}
gitURL, err := ReplaceCommitWithGit(form.GitUrlTemplate)
if err != nil {
log.Error("Error:", err)
}
repo, err = repo_service.InitRepoFromGitURL(ctx, ctx.Doer, ctxUser, gitURL, commitID, repo)
if err != nil {
log.Error("Error initializing repo from GitUrlTemplate (commit): %v", err)
return
}
if repo == nil {
log.Error("repo is nil after initializing from GitUrlTemplate (commit)")
}
}
ctx.Redirect(repo.Link())
return
}
@@ -303,7 +426,7 @@ func CreatePost(ctx *context.Context) {
func handleActionError(ctx *context.Context, err error) {
switch {
case errors.Is(err, user_model.ErrBlockedUser):
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
case repo_service.IsRepositoryLimitReached(err):
limit := err.(repo_service.LimitReachedError).Limit
ctx.Flash.Error(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))

repo.diff.view_file

@@ -32,6 +32,8 @@ type CreateRepoForm struct {
Readme string
Template bool
DevstarTemplate string
GitUrlTemplate string
RepoTemplate int64
GitContent bool
Topics bool

repo.diff.view_file

@@ -160,7 +160,7 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
}
// Apply changes and commit.
if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch, "Initial commit"); err != nil {
return fmt.Errorf("initRepoCommit: %w", err)
}
}

repo.diff.view_file

@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
@@ -21,7 +22,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
@@ -225,9 +225,9 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
if giteaTemplateFile != nil {
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile)
if err != nil {
return err
}
if err != nil {
return err
}
}
if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
@@ -250,38 +250,51 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
defaultBranch = templateRepo.DefaultBranch
}
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch, "Initial commit")
}
// GenerateGitContent generates git content from a template repository
func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) (err error) {
tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + generateRepo.Name)
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
if err != nil {
return fmt.Errorf("failed to create temp dir for repository %s: %w", generateRepo.FullName(), err)
return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
}
defer cleanup()
if err = generateRepoCommit(ctx, generateRepo, templateRepo, generateRepo, tmpDir); err != nil {
defer func() {
if err := util.RemoveAll(tmpDir); err != nil {
log.Error("RemoveAll: %v", err)
}
}()
if err = generateRepoCommit(ctx, repo, templateRepo, generateRepo, tmpDir); err != nil {
return fmt.Errorf("generateRepoCommit: %w", err)
}
// re-fetch repo
if generateRepo, err = repo_model.GetRepositoryByID(ctx, generateRepo.ID); err != nil {
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
return fmt.Errorf("getRepositoryByID: %w", err)
}
// if there was no default branch supplied when generating the repo, use the default one from the template
if strings.TrimSpace(generateRepo.DefaultBranch) == "" {
generateRepo.DefaultBranch = templateRepo.DefaultBranch
if strings.TrimSpace(repo.DefaultBranch) == "" {
repo.DefaultBranch = templateRepo.DefaultBranch
}
if err = gitrepo.SetDefaultBranch(ctx, generateRepo, generateRepo.DefaultBranch); err != nil {
if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "default_branch"); err != nil {
if err = UpdateRepository(ctx, repo, false); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return nil
}
// GenerateGitContent generates git content from a template repository
func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error {
if err := generateGitContent(ctx, generateRepo, templateRepo, generateRepo); err != nil {
return err
}
if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil {
return fmt.Errorf("failed to update size for repository: %w", err)
}
@@ -292,6 +305,81 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
return nil
}
// GenerateGitContent generates git content from a Git URL
func GenerateGitContentFromGitURL(ctx context.Context, gitURL, commitID string, repo *repo_model.Repository) error {
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
if err != nil {
return fmt.Errorf("Failed to create temp dir for repository %s: %w", gitURL, err)
}
defer func() {
if err := util.RemoveAll(tmpDir); err != nil {
log.Error("RemoveAll: %v", err)
}
}()
// generateRepoCommit
commitTimeStr := time.Now().Format(time.RFC3339)
authorSig := repo.Owner.NewGitSig()
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
"GIT_AUTHOR_EMAIL="+authorSig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+authorSig.Name,
"GIT_COMMITTER_EMAIL="+authorSig.Email,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
// Clone to temporary path with a specified depth.
if err := git.Clone(ctx, gitURL, tmpDir, git.CloneRepoOptions{
Depth: 1, // 仍然可以保持深度克隆以优化性能
}); err != nil {
return fmt.Errorf("git clone: %w", err)
}
// Change to the specified commit ID after cloning
if commitID != "" {
if err := git.NewCommand("checkout").AddDynamicArguments(commitID).Run(ctx, &git.RunOpts{Dir: tmpDir}); err != nil {
return fmt.Errorf("git checkout %s: %w", commitID, err)
}
}
// Get the current SHA1 version of the working directory
var sha1Buffer bytes.Buffer
if err := git.NewCommand("rev-parse", "HEAD").Run(ctx, &git.RunOpts{
Dir: tmpDir,
Stdout: &sha1Buffer, // 使用 bytes.Buffer
}); err != nil {
return fmt.Errorf("git rev-parse HEAD: %w", err)
}
// 获取SHA1字符串并修整空白
sha1 := strings.TrimSpace(sha1Buffer.String())
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
return fmt.Errorf("remove git dir: %w", err)
}
if err := git.InitRepository(ctx, tmpDir, false, repo.ObjectFormatName); err != nil {
return err
}
repoPath := repo.RepoPath()
if stdout, _, err := git.NewCommand("remote", "add", "origin").AddDynamicArguments(repoPath).
//SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", gitURL, tmpDir)).
RunStdString(ctx, &git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
return fmt.Errorf("git remote add: %w", err)
}
commitMessage := "Initial commit from " + gitURL + " ( " + sha1 + " ) "
initRepoCommit(ctx, tmpDir, repo, repo.Owner, repo.DefaultBranch, commitMessage)
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
return fmt.Errorf("failed to update size for repository: %w", err)
}
log.Info("GenerateGitContentFromGitURL init repo from %s : %s", gitURL, commitID)
return nil
}
// GenerateRepoOptions contains the template units to generate
type GenerateRepoOptions struct {
Name string

repo.diff.view_file

@@ -19,9 +19,9 @@ import (
)
// initRepoCommit temporarily changes with work directory.
func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string, commitMessage string) (err error) {
commitTimeStr := time.Now().Format(time.RFC3339)
commitMsg := "--message=" + commitMessage
sig := u.NewGitSig()
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
@@ -39,7 +39,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
return fmt.Errorf("git add --all: %w", err)
}
cmd := git.NewCommand("commit", "--message=Initial commit").
cmd := git.NewCommand("commit").AddOptionFormat(commitMsg).
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)

repo.diff.view_file

@@ -134,53 +134,53 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
}
// 5 - generate the repository contents according to the template
// Git Content
if opts.GitContent && !templateRepo.IsEmpty {
if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
// Git Content
if opts.GitContent && !templateRepo.IsEmpty {
if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// Topics
if opts.Topics {
if err = repo_model.GenerateTopics(ctx, templateRepo, generateRepo); err != nil {
// Topics
if opts.Topics {
if err = repo_model.GenerateTopics(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// Git Hooks
if opts.GitHooks {
if err = GenerateGitHooks(ctx, templateRepo, generateRepo); err != nil {
// Git Hooks
if opts.GitHooks {
if err = GenerateGitHooks(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// Webhooks
if opts.Webhooks {
if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
// Webhooks
if opts.Webhooks {
if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// Avatar
if opts.Avatar && len(templateRepo.Avatar) > 0 {
if err = generateAvatar(ctx, templateRepo, generateRepo); err != nil {
// Avatar
if opts.Avatar && len(templateRepo.Avatar) > 0 {
if err = generateAvatar(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// Issue Labels
if opts.IssueLabels {
if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
// Issue Labels
if opts.IssueLabels {
if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
if opts.ProtectedBranch {
if err = GenerateProtectedBranch(ctx, templateRepo, generateRepo); err != nil {
if opts.ProtectedBranch {
if err = GenerateProtectedBranch(ctx, templateRepo, generateRepo); err != nil {
return nil, err
}
}
}
// 6 - update repository status to be ready
generateRepo.Status = repo_model.RepositoryReady
@@ -192,3 +192,17 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
return generateRepo, nil
}
// InitRepoFromGitURL for a repository from a Git URL
func InitRepoFromGitURL(ctx context.Context, doer, owner *user_model.User, gitURL, commitID string, repo *repo_model.Repository) (_ *repo_model.Repository, err error) {
if !doer.IsAdmin {
return nil, nil
}
err = GenerateGitContentFromGitURL(ctx, gitURL, commitID, repo)
if err != nil {
return nil, err
}
return repo, nil
}

repo.diff.view_file

@@ -1,216 +1,233 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository new-repo">
<div class="ui container medium-width">
<h3 class="ui top attached header">
{{ctx.Locale.Tr "new_repo"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
{{template "repo/create_helper" .}}
<h3 class="ui top attached header">
{{ctx.Locale.Tr "new_repo"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
{{template "repo/create_helper" .}}
<form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection dropdown ellipsis-text-items" id="repo_owner_dropdown">
<input type="hidden" name="uid" value="{{.ContextUser.ID}}">
<span class="text"></span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
{{if not .CanCreateRepoInDoer}}
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimitOfDoer}}"
{{end}}
>
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
{{.SignedUser.ShortName 40}}
</div>
{{range .Orgs}}
<div class="item" data-value="{{.ID}}" title="{{.Name}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}}
{{.ShortName 40}}
</div>
{{end}}
{{range .Orgs}}
<div class="item" data-value="{{.ID}}" title="{{.Name}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}}
{{.ShortName 40}}
</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span>
</div>
<span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span>
</div>
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
<span class="help" data-help-for-repo-name>{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
<span class="help tw-hidden" data-help-for-repo-name=".profile">{{ctx.Locale.Tr "repo.repo_name_profile_public_hint"}}</span>
<span class="help tw-hidden" data-help-for-repo-name=".profile-private">{{ctx.Locale.Tr "repo.repo_name_profile_private_hint"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
<div class="ui checkbox">
{{if .IsForcedPrivate}}
<input name="private" type="checkbox" checked disabled>
<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
{{else}}
<input name="private" type="checkbox" {{if .private}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
{{end}}
</div>
<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
</div>
<div class="inline field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
<textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div id="repo_template_search" class="ui search selection dropdown">
<input type="hidden" id="repo_template" name="repo_template" value="{{or .repo_template ""}}">
<div class="default text">{{.repo_template_name}}</div>
<div class="menu">
</div>
</div>
</div>
<div id="template_units" class="tw-hidden">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
<div class="ui checkbox">
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_content"}}</label>
{{if .IsForcedPrivate}}
<input name="private" type="checkbox" checked disabled>
<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
{{else}}
<input name="private" type="checkbox" {{if .private}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
{{end}}
</div>
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
</div>
<div class="inline field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
<textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea>
</div>
{{if .devstar_template_name}}
<div id="devstar_template_area" class="inline field">
<label>{{ctx.Locale.Tr "repo.devstar_template"}}</label>
<div id="devstar_template_search" class="ui search selection dropdown">
<input type="hidden" id="devstar_template" name="devstar_template" value="">
<div id="devstar_template_name" class="default text">{{.devstar_template_name}}</div>
<div class="menu">
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.devstar_template_desc" "https://DevStar.cn/"}}</span>
</div>
{{end}}
<div id="repo_template_area">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div id="repo_template_search" class="ui search selection dropdown">
<input type="hidden" id="repo_template" name="repo_template" value="{{.repo_template}}">
<div id="repo_template_name" class="default text">{{.repo_template_name}}</div>
<div class="menu">
</div>
</div>
</div>
<div id="template_units" {{if not .repo_template}}class="tw-hidden"{{end}}>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
<div class="ui checkbox">
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_content"}}</label>
</div>
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label>
</div>
<div class="ui checkbox">
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.topics"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.avatar"}}</label>
</div>
<div class="ui checkbox">
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label>
</div>
</div>
</div>
</div>
<div id="non_template" {{if .repo_template}}class="tw-hidden"{{end}}>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.issue_labels"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="issue_labels" value="{{.issueLabels}}">
<div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
{{range .LabelTemplateFiles}}
<div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><i>({{.Description}})</i></div>
{{end}}
</div>
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<label>.gitignore</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="gitignores" value="{{.gitignores}}">
<div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div>
<div class="menu">
{{range .Gitignores}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.license"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="license" value="{{.license}}">
<div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div>
{{range .Licenses}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.readme"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="readme" value="{{.readme}}">
<div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div>
<div class="menu">
{{range .Readmes}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox" id="auto-init">
<input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.auto_init"}}</label>
</div>
</div>
<div class="inline field">
<label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label>
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.object_format"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required>
<div class="default text">{{.DefaultObjectFormat.Name}}</div>
<div class="menu">
{{range .SupportedObjectFormats}}
<div class="item" data-value="{{.Name}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div class="ui checkbox">
<input name="template" type="checkbox">
<label>{{ctx.Locale.Tr "repo.template_helper"}}</label>
</div>
</div>
</div>
<br>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label>
</div>
<div class="ui checkbox">
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.topics"}}</label>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.avatar"}}</label>
</div>
<div class="ui checkbox">
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label>
</div>
</div>
</div>
<div id="non_template">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.issue_labels"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="issue_labels" value="{{.issueLabels}}">
<div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
{{range .LabelTemplateFiles}}
<div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><i>({{.Description}})</i></div>
{{end}}
</div>
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<label>.gitignore</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="gitignores" value="{{.gitignores}}">
<div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div>
<div class="menu">
{{range .Gitignores}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.license"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="license" value="{{.license}}">
<div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div>
{{range .Licenses}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.readme"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="readme" value="{{.readme}}">
<div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div>
<div class="menu">
{{range .Readmes}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox" id="auto-init">
<input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.auto_init"}}</label>
</div>
</div>
<div class="inline field">
<label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label>
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.object_format"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="object_format_name" name="object_format_name" value="{{or .object_format_name .DefaultObjectFormat.Name}}" required>
<div class="default text">{{.DefaultObjectFormat.Name}}</div>
<div class="menu">
{{range .SupportedObjectFormats}}
<div class="item" data-value="{{.Name}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div class="ui checkbox">
<input name="template" type="checkbox">
<label>{{ctx.Locale.Tr "repo.template_helper"}}</label>
</div>
</div>
</div>
<br>
<div class="inline field">
<label></label>
<button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
</div>
</form>
</div>

repo.diff.view_file

@@ -0,0 +1,187 @@
import $ from 'jquery';
import { htmlEscape } from '../utils/html.ts';
import { hideElem, showElem } from '../utils/dom.ts';
const {appSubUrl} = window.config;
export function initRepoTemplateSearch() {
function isValidGitUrl(url) {
// 正则表达式支持两种格式的 Git URL
const regex = /^https:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)(\/commit\/([a-f0-9]{40}))?$/;
// 或者,以 .git 结尾
const regexWithGit = /^https:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\.git$/;
return regex.test(url) || regexWithGit.test(url);
}
function getRepoNameFromGitUrl(url) {
// 正则表达式支持两种格式的 Git URL
const regexWithCommit = /^https:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\/commit\/([a-f0-9]{40})$/; // 针对 commit ID 的 URL
const regexWithoutCommit = /^https:\/\/([a-zA-Z0-9.-]+)\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\.git$/; // 针对 .git 结尾的 URL
// 尝试匹配 commit URL
let match = url.match(regexWithCommit);
if (match) {
return match[3]; // 返回 repo-name
}
// 尝试匹配 .git URL
match = url.match(regexWithoutCommit);
if (match) {
return match[3]; // 返回 repo-name
}
return null; // 如果没有匹配,返回 null
}
const $repoTemplate = $('#repo_template');
const $devstarTemplate = $('#devstar_template');
const $gitURLTemplate = $('#git_url_template');
const $templateUnits = $('#template_units');
const $nonTemplate = $('#non_template');
const $repoTemplateArea = $('#repo_template_area');
const $gitURLTemplateArea = $('#git_url_template_area');
const $devstarTemplateArea = $('#devstar_template_area');
const isDevstar = /^(https?:\/\/)?(www\.)?(devstar\.cn)(:\d+)?(\/.*)?$/i.test(document.URL);
if (isDevstar) {
hideElem($devstarTemplateArea);
$devstarTemplateArea.remove();
}
const GitURL = document.URL.match(/[?&]template_url=([^&]*)/);
if (GitURL) {
if (isValidGitUrl(GitURL[1])) {
$gitURLTemplate.val(GitURL[1]);
}
$devstarTemplateArea.val('');
hideElem($devstarTemplateArea);
hideElem($repoTemplateArea);
}
const template_id = document.URL.match(/[?&]template_id=([^&]*)/);
if (template_id) {
$devstarTemplateArea.val('');
hideElem($devstarTemplateArea);
$gitURLTemplate.val("");
hideElem($gitURLTemplateArea);
}
const checkTemplate = function () {
if ($repoTemplate.val() !== '' && $repoTemplate.val() !== '0') {
showElem($templateUnits);
hideElem($nonTemplate);
hideElem($gitURLTemplateArea);
$devstarTemplateArea.val('');
hideElem($devstarTemplateArea);
} else {
showElem($devstarTemplateArea);
hideElem($templateUnits);
showElem($nonTemplate);
showElem($gitURLTemplateArea);
}
};
$repoTemplate.on('change', checkTemplate);
checkTemplate();
const checkGitURLTemplate = function () {
if ($gitURLTemplate.val() !== '' && $gitURLTemplate.val() !== '0') {
if ($('#repo_name').val() == '') {
$('#repo_name').val(getRepoNameFromGitUrl($gitURLTemplate.val()));
}
if ($('#description').val() == '') {
$('#description').val("init repo from " + $gitURLTemplate.val());
}
hideElem($repoTemplateArea);
hideElem($nonTemplate);
$devstarTemplateArea.val('');
hideElem($devstarTemplateArea);
} else {
showElem($devstarTemplateArea);
showElem($repoTemplateArea);
}
};
if($gitURLTemplate.length){
$gitURLTemplate.on('change', checkGitURLTemplate);
checkGitURLTemplate();
}
const checkDevStarTemplate = function () {
if ($devstarTemplate.val() !== '' && $devstarTemplate.val() !== '0') {
if ($('#repo_name').val() == '') {
$('#repo_name').val(getRepoNameFromGitUrl($devstarTemplate.val()));
}
if ($('#description').val() == '') {
$('#description').val("init repo from " + $devstarTemplate.val());
}
$repoTemplate.val('');
hideElem($gitURLTemplateArea);
hideElem($repoTemplateArea);
hideElem($nonTemplate);
} else {
showElem($repoTemplateArea);
showElem($gitURLTemplateArea);
hideElem($templateUnits);
showElem($nonTemplate);
}
};
if($devstarTemplate.length){
$devstarTemplate.on('change', checkDevStarTemplate);
}
const initDevStarTemplateSearch = function () {
$('#devstar_template_search')
.dropdown({
apiSettings: {
url: `https://devstar.cn/api/v1/repos/search?q={query}&template=true`,
onResponse(response) {
console.log(response.data);
const filteredResponse = { success: true, results: [] };
filteredResponse.results.push({
name: '-----',
value: '',
});
// Parse the response from the api to work with our dropdown
$.each(response.data, (_r, repo) => {
filteredResponse.results.push({
name: htmlEscape("DevStar.cn/" + repo.full_name),
value: repo.clone_url,
});
});
return filteredResponse;
},
cache: true,
},
fullTextSearch: true,
});
};
initDevStarTemplateSearch();
const changeOwner = function () {
$('#repo_template_search')
.dropdown({
apiSettings: {
url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`,
onResponse(response) {
const filteredResponse = { success: true, results: [] };
filteredResponse.results.push({
name: '-----',
value: '',
});
// Parse the response from the api to work with our dropdown
$.each(response.data, (_r, repo) => {
filteredResponse.results.push({
name: htmlEscape(repo.repository.full_name),
value: repo.repository.id,
});
});
return filteredResponse;
},
cache: false,
},
fullTextSearch: true,
});
};
$('#uid').on('change', changeOwner);
changeOwner();
}

repo.diff.view_file

@@ -66,6 +66,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
import {initRepoTemplateSearch} from './features/repo-template.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@@ -164,6 +165,7 @@ const initPerformanceTracer = callInitFunctions([
initOAuth2SettingsDisableCheckbox,
initRepoFileView,
initRepoTemplateSearch,
]);
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.