diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 828ec08a8a..11aa820e67 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "slices" "strings" @@ -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/ 替换为 .git +func ReplaceCommitWithGit(url string) (string, error) { + // 定义正则表达式匹配 URL 中的 /commit/ 部分 + 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 } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index cb267f891c..dd89d6618b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -32,6 +32,8 @@ type CreateRepoForm struct { Readme string Template bool + DevstarTemplate string + GitUrlTemplate string RepoTemplate int64 GitContent bool Topics bool diff --git a/services/repository/create.go b/services/repository/create.go index bed02e5d7e..75fec0a453 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -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) } } diff --git a/services/repository/generate.go b/services/repository/generate.go index 867b5d7855..ece4b6d17e 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "os" + "path" "path/filepath" "regexp" "strconv" @@ -250,7 +251,7 @@ 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 @@ -292,6 +293,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 diff --git a/services/repository/init.go b/services/repository/init.go index 1eeeb4aa4f..72f7c6c1b1 100644 --- a/services/repository/init.go +++ b/services/repository/init.go @@ -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) diff --git a/services/repository/template.go b/services/repository/template.go index 6906a60083..9c462bc528 100644 --- a/services/repository/template.go +++ b/services/repository/template.go @@ -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 +} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index ada7e0c092..55f7ca2f32 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -61,17 +61,31 @@ + + {{if .devstar_template_name}} +
+ + + {{ctx.Locale.Tr "repo.devstar_template_desc" "https://DevStar.cn/"}} +
+ {{end}} + +
-
+
@@ -113,8 +127,9 @@
+
-
+