diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index dfc484aeb2..781148e734 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{ } func runCreateUser(c *cli.Context) error { + // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first + // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. + setting.LoadSettings() + if err := argsSet(c, "email"); err != nil { return err } diff --git a/cmd/main.go b/cmd/main.go index b48a6143d7..9a28722b4b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -206,6 +206,7 @@ func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmd app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index 432f2b993c..8a9ec14b2e 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -114,37 +113,17 @@ func TestCliCmd(t *testing.T) { _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) return nil }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - require.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + require.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } diff --git a/cmd/web.go b/cmd/web.go index 44babd51c5..3fc64f7748 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strconv" "strings" + "time" _ "net/http/pprof" // Used for debugging if enabled and a web server is running @@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) { log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* ConfigFile: %s", setting.CustomConf) log.Info("%s", msg) // show startup message + + if setting.CORSConfig.Enabled { + log.Info("CORS Service Enabled") + } + if setting.DefaultUILocation != time.Local { + log.Info("Default UI Location is %v", setting.DefaultUILocation.String()) + } + if setting.MailService != nil { + log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail) + } } func serveInstall(ctx *cli.Context) error { diff --git a/models/issues/comment.go b/models/issues/comment.go index c955f02e98..0bf53bb4dd 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -194,6 +194,20 @@ func (t CommentType) HasMailReplySupport() bool { return false } +func (t CommentType) CountedAsConversation() bool { + for _, ct := range ConversationCountedCommentType() { + if t == ct { + return true + } + } + return false +} + +// ConversationCountedCommentType returns the comment types that are counted as a conversation +func ConversationCountedCommentType() []CommentType { + return []CommentType{CommentTypeComment, CommentTypeReview} +} + // RoleInRepo presents the user's participation in the repo type RoleInRepo string @@ -887,7 +901,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } fallthrough case CommentTypeComment: - if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil { return err } fallthrough @@ -1182,8 +1196,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error { return err } - if comment.Type == CommentTypeComment { - if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { + if comment.Type.CountedAsConversation() { + if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil { return err } } @@ -1300,6 +1314,21 @@ func (c *Comment) HasOriginalAuthor() bool { return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 } +func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder { + subQuery := builder.Select("COUNT(*)").From("`comment`").Where( + builder.Eq{"issue_id": issueID}.And( + builder.In("`type`", ConversationCountedCommentType()), + )) + + return builder.Update(builder.Eq{"num_comments": subQuery}). + From("`issue`").Where(builder.Eq{"id": issueID}) +} + +func UpdateIssueNumComments(ctx context.Context, issueID int64) error { + _, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID)) + return err +} + // InsertIssueComments inserts many comments of issues. func InsertIssueComments(ctx context.Context, comments []*Comment) error { if len(comments) == 0 { @@ -1332,8 +1361,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { } for _, issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", - issueID, CommentTypeComment, issueID); err != nil { + if err := UpdateIssueNumComments(ctx, issueID); err != nil { return err } } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index f7088cc96c..88d2200bd1 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -125,3 +125,12 @@ func TestUpdateCommentsMigrationsByType(t *testing.T) { assert.Empty(t, comment.OriginalAuthorID) assert.EqualValues(t, 513, comment.PosterID) } + +func Test_UpdateIssueNumComments(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + + require.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID)) + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/issues/review.go b/models/issues/review.go index a39c12069b..cd3190f119 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -614,6 +614,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error { return err } } + + if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil { + return err + } } return committer.Commit() diff --git a/models/project/project.go b/models/project/project.go index 245838abb5..beffffcdfc 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint + return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) +} + // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { if p.OwnerID > 0 { @@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadOwner: %v", err) return "" } - return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID) + return ProjectLinkForOrg(p.Owner, p.ID) } if p.RepoID > 0 { err := p.LoadRepo(ctx) @@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadRepo: %v", err) return "" } - return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID) + return ProjectLinkForRepo(p.Repo, p.ID) } return "" } diff --git a/models/repo.go b/models/repo.go index 0dc8ee5df3..598f8df6a4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -19,6 +19,8 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + + "xorm.io/builder" ) // Init initialize model @@ -27,7 +29,7 @@ func Init(ctx context.Context) error { } type repoChecker struct { - querySQL func(ctx context.Context) ([]map[string][]byte, error) + querySQL func(ctx context.Context) ([]int64, error) correctSQL func(ctx context.Context, id int64) error desc string } @@ -38,8 +40,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { log.Error("Select %s: %v", checker.desc, err) return } - for _, result := range results { - id, _ := strconv.ParseInt(string(result["id"]), 10, 64) + for _, id := range results { select { case <-ctx.Done(): log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id) @@ -54,21 +55,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { } } -func StatsCorrectSQL(ctx context.Context, sql string, id int64) error { - _, err := db.GetEngine(ctx).Exec(sql, id, id) +func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error { + args := []any{sql} + args = append(args, ids...) + _, err := db.GetEngine(ctx).Exec(args...) return err } func repoStatsCorrectNumWatches(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id) } func repoStatsCorrectNumStars(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssues(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { @@ -105,11 +108,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { } func userStatsCorrectNumRepos(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id) } func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id) + return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id)) } func repoStatsCorrectNumIssues(ctx context.Context, id int64) error { @@ -128,9 +131,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true) } -func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) { - return func(ctx context.Context) ([]map[string][]byte, error) { - return db.GetEngine(ctx).Query(args...) +// statsQuery returns a function that queries the database for a list of IDs +// sql could be a string or a *builder.Builder +func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) { + return func(ctx context.Context) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids) } } @@ -201,7 +207,16 @@ func CheckRepoStats(ctx context.Context) error { }, // Issue.NumComments { - statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"), + statsQuery(builder.Select("`issue`.id").From("`issue`").Where( + builder.Neq{ + "`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where( + builder.Expr("issue_id = `issue`.id").And( + builder.In("type", issues_model.ConversationCountedCommentType()), + ), + ), + }, + ), + ), repoStatsCorrectIssueNumComments, "issue count 'num_comments'", }, diff --git a/models/repo_test.go b/models/repo_test.go index 958725fe53..52f028bbb2 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -7,8 +7,10 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,3 +24,16 @@ func TestDoctorUserStarNum(t *testing.T) { require.NoError(t, DoctorUserStarNum(db.DefaultContext)) } + +func Test_repoStatsCorrectIssueNumComments(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NotNil(t, issue2) + assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here + + require.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2)) + // reload the issue + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 94a3253644..70110c4962 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -59,6 +59,13 @@ func InitSettings() { _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + setting.InitGiteaEnvVars() + + // Avoid loading the git's system config. + // On macOS, system config sets the osxkeychain credential helper, which will cause tests to freeze with a dialog. + // But we do not set it in production at the moment, because it might be a "breaking" change, + // more details are in "modules/git.commonBaseEnvs". + _ = os.Setenv("GIT_CONFIG_NOSYSTEM", "true") } // TestOptions represents test options diff --git a/modules/git/tree.go b/modules/git/tree.go index 5b06cbf359..f6201f6cc9 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -176,3 +176,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return filelist, err } + +// GetTreePathLatestCommitID returns the latest commit of a tree path +func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { + stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). + AddDynamicArguments(refName).AddDashesAndList(treePath). + RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + return repo.GetCommit(strings.TrimSpace(stdout)) +} diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 6e5d7f4415..7e439628f2 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -26,3 +26,18 @@ func TestSubTree_Issue29101(t *testing.T) { assert.True(t, IsErrNotExist(err)) } } + +func Test_GetTreePathLatestCommit(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame")) + require.NoError(t, err) + defer repo.Close() + + commitID, err := repo.GetBranchCommitID("master") + require.NoError(t, err) + assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) + + commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") + require.NoError(t, err) + assert.NotNil(t, commit) + assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) +} diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index fa0100dba2..2bc1a5c341 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -168,3 +168,22 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { } return changed } + +// InitGiteaEnvVars initializes the environment variables for gitea +func InitGiteaEnvVars() { + // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, + // so at the moment we could still keep "unsetting the unnecessary environments" + + // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". + // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, + // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. + _ = os.Unsetenv("XDG_CONFIG_HOME") + + _ = os.Unsetenv("GIT_AUTHOR_NAME") + _ = os.Unsetenv("GIT_AUTHOR_EMAIL") + _ = os.Unsetenv("GIT_AUTHOR_DATE") + _ = os.Unsetenv("GIT_COMMITTER_NAME") + _ = os.Unsetenv("GIT_COMMITTER_EMAIL") + _ = os.Unsetenv("GIT_COMMITTER_DATE") +} diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 63daaad60b..5260887d9d 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -5,8 +5,6 @@ package setting import ( "time" - - "code.gitea.io/gitea/modules/log" ) // CORSConfig defines CORS settings @@ -28,7 +26,4 @@ var CORSConfig = struct { func loadCorsFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "cors", &CORSConfig) - if CORSConfig.Enabled { - log.Info("CORS Service Enabled") - } } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 3c96b58740..4c4f63bc61 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -109,7 +109,7 @@ func IndexerGlobFromString(globstr string) []Glob { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { - log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) + log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err) } else { extarr = append(extarr, Glob{glob: g, pattern: expr}) } diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index 136d932b8d..0804fbd717 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -263,8 +263,6 @@ func loadMailerFrom(rootCfg ConfigProvider) { MailService.OverrideEnvelopeFrom = true MailService.EnvelopeFrom = parsed.Address } - - log.Info("Mail Service Enabled") } func loadRegisterMailFrom(rootCfg ConfigProvider) { @@ -275,7 +273,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) { return } Service.RegisterEmailConfirm = true - log.Info("Register Mail Service Enabled") } func loadNotifyMailFrom(rootCfg ConfigProvider) { @@ -286,7 +283,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) { return } Service.EnableNotifyMail = true - log.Info("Notify Mail Service Enabled") } func tryResolveAddr(addr string) []net.IPAddr { diff --git a/modules/setting/session.go b/modules/setting/session.go index e9637fdfc5..29ee67914d 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) { SessionConfig.ProviderConfig = string(shadowConfig) SessionConfig.OriginalProvider = SessionConfig.Provider SessionConfig.Provider = "VirtualSession" - - log.Info("Session Service Enabled") } diff --git a/modules/setting/time.go b/modules/setting/time.go index 39acba12ef..97988211a9 100644 --- a/modules/setting/time.go +++ b/modules/setting/time.go @@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) { if err != nil { log.Fatal("Load time zone failed: %v", err) } - log.Info("Default UI Location is %v", zone) } if DefaultUILocation == nil { DefaultUILocation = time.Local diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6280a35c13..eaeab11a9d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3643,6 +3643,7 @@ versions = Versions versions.view_all = View all dependency.id = ID dependency.version = Version +search_in_external_registry = Search in %s alpine.registry = Setup this registry by adding the url in your /etc/apk/repositories file: alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: alpine.registry.info = Choose $branch and $repository from the list below. diff --git a/release-notes/6471.md b/release-notes/6471.md new file mode 100644 index 0000000000..4e70286eee --- /dev/null +++ b/release-notes/6471.md @@ -0,0 +1 @@ +feat: [commit](https://codeberg.org/forgejo/forgejo/commit/76a85d26c8576fc410dc6494f2907ffc2b353c39) Use `Project-URL` metadata field to get a PyPI package's homepage URL diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 7824db1823..9d8b60b4b4 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -10,6 +10,7 @@ import ( "regexp" "sort" "strings" + "unicode" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" @@ -139,9 +140,30 @@ func UploadPackageFile(ctx *context.Context) { return } - projectURL := ctx.Req.FormValue("home_page") - if !validation.IsValidURL(projectURL) { - projectURL = "" + // Ensure ctx.Req.Form exists. + _ = ctx.Req.ParseForm() + + var homepageURL string + projectURLs := ctx.Req.Form["project_urls"] + for _, purl := range projectURLs { + label, url, found := strings.Cut(purl, ",") + if !found { + continue + } + if normalizeLabel(label) != "homepage" { + continue + } + homepageURL = strings.TrimSpace(url) + break + } + + if len(homepageURL) == 0 { + // TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec. + homepageURL = ctx.Req.FormValue("home_page") + } + + if !validation.IsValidURL(homepageURL) { + homepageURL = "" } _, _, err = packages_service.CreatePackageOrAddFileToExisting( @@ -160,7 +182,7 @@ func UploadPackageFile(ctx *context.Context) { Description: ctx.Req.FormValue("description"), LongDescription: ctx.Req.FormValue("long_description"), Summary: ctx.Req.FormValue("summary"), - ProjectURL: projectURL, + ProjectURL: homepageURL, License: ctx.Req.FormValue("license"), RequiresPython: ctx.Req.FormValue("requires_python"), }, @@ -189,6 +211,23 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } +// Normalizes a Project-URL label. +// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. +func normalizeLabel(label string) string { + var builder strings.Builder + + // "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result + // to lowercase." + for _, r := range label { + if unicode.IsPunct(r) || unicode.IsSpace(r) { + continue + } + builder.WriteRune(unicode.ToLower(r)) + } + + return builder.String() +} + func isValidNameAndVersion(packageName, packageVersion string) bool { return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) } diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go index 3023692177..786105693f 100644 --- a/routers/api/packages/pypi/pypi_test.go +++ b/routers/api/packages/pypi/pypi_test.go @@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) { assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) } + +func TestNormalizeLabel(t *testing.T) { + // Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. + assert.Equal(t, "homepage", normalizeLabel("Homepage")) + assert.Equal(t, "homepage", normalizeLabel("Home-page")) + assert.Equal(t, "homepage", normalizeLabel("Home page")) + assert.Equal(t, "changelog", normalizeLabel("Change_Log")) + assert.Equal(t, "whatsnew", normalizeLabel("What's New?")) + assert.Equal(t, "github", normalizeLabel("github")) +} diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2cea538b72..55b245676d 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "net/http" - "path" "strings" "time" @@ -247,19 +246,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn return nil, nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err) + ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err) return nil, nil, nil } + when := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, entry, lastModified + return entry.Blob(), entry, when } // GetArchive get archive of a repository diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 64d233fc45..32eb6eeef1 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -209,7 +209,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id)) + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id)) } // DeleteProject delete a project @@ -259,7 +259,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["redirect"] = ctx.FormString("redirect") ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["card_type"] = p.CardType - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -273,7 +273,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) shared_user.RenderUserHeader(ctx) diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 1e87bbf015..d7fe368474 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -5,7 +5,6 @@ package repo import ( - "path" "time" git_model "code.gitea.io/gitea/models/git" @@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified) } -func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) { +func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { @@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T return nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.ServerError("GetCommitsInfo", err) + ctx.ServerError("GetTreePathLatestCommit", err) return nil, nil } + lastModified := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, lastModified + return entry.Blob(), lastModified } // SingleDownload download a file by repos path diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index b537afdab5..61711095b9 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1291,10 +1291,17 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index f4b027dae1..55a422453f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -183,7 +183,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id)) + ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id)) } // DeleteProject delete a project @@ -237,7 +237,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["content"] = p.Description ctx.Data["card_type"] = p.CardType ctx.Data["redirect"] = ctx.FormString("redirect") - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -251,7 +251,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID) if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 197d8102dd..6dda46eaec 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,6 +5,7 @@ package pull import ( "fmt" + "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -66,7 +67,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { // add trailer - message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) + if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) { + message += fmt.Sprintf("\nCo-authored-by: %s", sig.String()) + } + message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String()) } cmdCommit := git.NewCommand(ctx, "commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 816940a2b5..2ebbbe4a51 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -10,9 +10,12 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -98,3 +101,11 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { }) } } + +func TestWebhookUserMail(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + setting.Service.NoReplyAddress = "no-reply.com" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) + assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) +} diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index ea665c7bbc..c8568845f1 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -35,11 +35,12 @@ + {{$tooltipSearchInNuget := ctx.Locale.Tr "packages.search_in_external_registry" "nuget.org"}} {{range $framework, $dependencies := .PackageDescriptor.Metadata.Dependencies}} {{range $dependencies}} - {{.ID}} - {{.Version}} + {{.ID}} {{svg "octicon-link-external"}} + {{.Version}} {{svg "octicon-link-external"}} {{$framework}} {{end}} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index b8c89625c0..88b254b6f9 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -40,13 +40,6 @@ func TestMain(m *testing.M) { initChangedFiles() testE2eWebRoutes = routers.NormalRoutes() - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"), diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go index ef03dbe509..a75bae85ac 100644 --- a/tests/integration/api_packages_pypi_test.go +++ b/tests/integration/api_packages_pypi_test.go @@ -33,15 +33,16 @@ func TestPackagePyPI(t *testing.T) { packageVersion := "1!1.0.1+r1234" packageAuthor := "KN4CK3R" packageDescription := "Test Description" + projectURL := "https://example.com" content := "test" hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" root := fmt.Sprintf("/api/packages/%s/pypi", user.Name) - uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) + createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) { + body = &bytes.Buffer{} + writer = multipart.NewWriter(body) part, _ := writer.CreateFormFile("content", filename) _, _ = io.Copy(part, strings.NewReader(content)) @@ -53,14 +54,27 @@ func TestPackagePyPI(t *testing.T) { writer.WriteField("sha256_digest", hashSHA256) writer.WriteField("requires_python", "3.6") - _ = writer.Close() + return body, writer, writer.Close + } + uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) { req := NewRequestWithBody(t, "POST", root, body). - SetHeader("Content-Type", writer.FormDataContentType()). + SetHeader("Content-Type", contentType). AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } + uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { + body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content) + + writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org") + writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL)) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), expectedStatus) + } + t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -75,6 +89,7 @@ func TestPackagePyPI(t *testing.T) { require.NoError(t, err) assert.Nil(t, pd.SemVer) assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) assert.Equal(t, packageName, pd.Package.Name) assert.Equal(t, packageVersion, pd.Version.Version) @@ -134,6 +149,48 @@ func TestPackagePyPI(t *testing.T) { uploadFile(t, "test.tar.gz", content, http.StatusConflict) }) + t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + writer.WriteField("home_page", projectURL) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + require.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + require.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + + t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "no-project-url-or-homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + require.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + require.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -148,7 +205,7 @@ func TestPackagePyPI(t *testing.T) { downloadFile("test.whl") downloadFile("test.tar.gz") - pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI) + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName) require.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(2), pvs[0].DownloadCount) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 65b715a688..06d2586d1c 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -159,18 +159,6 @@ func TestMain(m *testing.M) { } } - os.Unsetenv("GIT_AUTHOR_NAME") - os.Unsetenv("GIT_AUTHOR_EMAIL") - os.Unsetenv("GIT_AUTHOR_DATE") - os.Unsetenv("GIT_COMMITTER_NAME") - os.Unsetenv("GIT_COMMITTER_EMAIL") - os.Unsetenv("GIT_COMMITTER_DATE") - - // Avoid loading the default system config. On MacOS, this config - // sets the osxkeychain credential helper, which will cause tests - // to freeze with a dialog. - os.Setenv("GIT_CONFIG_NOSYSTEM", "true") - err := unittest.InitFixtures( unittest.FixturesOptions{ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),