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 @@