diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 09510ac2c5..686a3ddffa 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" + markup_service "code.gitea.io/gitea/services/markup" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -112,7 +113,7 @@ func runPR() { log.Printf("[PR] Setting up router\n") // routers.GlobalInit() external.RegisterRenderers() - markup.Init() + markup.Init(markup_service.ProcessorHelper()) c := routers.NormalRoutes(graceful.GetManager().HammerContext()) log.Printf("[PR] Ready for testing !\n") diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml new file mode 100644 index 0000000000..9326fa550b --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml @@ -0,0 +1,19 @@ +# type Milestone struct { +# ID int64 `xorm:"pk autoincr"` +# IsClosed bool +# NumIssues int +# NumClosedIssues int +# Completeness int // Percentage(1-100). +# } +- + id: 1 + is_closed: false + num_issues: 3 + num_closed_issues: 1 + completeness: 33 +- + id: 2 + is_closed: true + num_issues: 5 + num_closed_issues: 5 + completeness: 100 diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml new file mode 100644 index 0000000000..fdaacd9f68 --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml @@ -0,0 +1,25 @@ +# type Issue struct { +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` +# Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. +# MilestoneID int64 `xorm:"INDEX"` +# IsClosed bool `xorm:"INDEX"` +# } +- + id: 1 + repo_id: 1 + index: 1 + milestone_id: 1 + is_closed: false +- + id: 2 + repo_id: 1 + index: 2 + milestone_id: 1 + is_closed: true +- + id: 4 + repo_id: 1 + index: 3 + milestone_id: 1 + is_closed: false diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml new file mode 100644 index 0000000000..0bcf4cffd3 --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml @@ -0,0 +1,19 @@ +# type Milestone struct { +# ID int64 `xorm:"pk autoincr"` +# IsClosed bool +# NumIssues int +# NumClosedIssues int +# Completeness int // Percentage(1-100). +# } +- + id: 1 + is_closed: false + num_issues: 4 + num_closed_issues: 2 + completeness: 50 +- + id: 2 + is_closed: true + num_issues: 5 + num_closed_issues: 5 + completeness: 100 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 46ef052829..cca6c52d42 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -419,6 +419,8 @@ var migrations = []Migration{ NewMigration("Create key/value table for system settings", createSystemSettingsTable), // v228 -> v229 NewMigration("Add TeamInvite table", addTeamInviteTable), + // v229 -> v230 + NewMigration("Update counts of all open milestones", updateOpenMilestoneCounts), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v229.go b/models/migrations/v229.go new file mode 100644 index 0000000000..42ec2033fe --- /dev/null +++ b/models/migrations/v229.go @@ -0,0 +1,47 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "code.gitea.io/gitea/models/issues" + + "xorm.io/builder" + "xorm.io/xorm" +) + +func updateOpenMilestoneCounts(x *xorm.Engine) error { + var openMilestoneIDs []int64 + err := x.Table("milestone").Select("id").Where(builder.Neq{"is_closed": 1}).Find(&openMilestoneIDs) + if err != nil { + return fmt.Errorf("error selecting open milestone IDs: %w", err) + } + + for _, id := range openMilestoneIDs { + _, err := x.ID(id). + SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{"milestone_id": id}, + )). + SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{ + "milestone_id": id, + "is_closed": true, + }, + )). + Update(&issues.Milestone{}) + if err != nil { + return fmt.Errorf("error updating issue counts in milestone %d: %w", id, err) + } + _, err = x.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?", + id, + ) + if err != nil { + return fmt.Errorf("error setting completeness on milestone %d: %w", id, err) + } + } + + return nil +} diff --git a/models/migrations/v229_test.go b/models/migrations/v229_test.go new file mode 100644 index 0000000000..f8a147c9bd --- /dev/null +++ b/models/migrations/v229_test.go @@ -0,0 +1,46 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "testing" + + "code.gitea.io/gitea/models/issues" + + "github.com/stretchr/testify/assert" +) + +func Test_updateOpenMilestoneCounts(t *testing.T) { + type ExpectedMilestone issues.Milestone + + // Prepare and load the testing database + x, deferable := prepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) + defer deferable() + if x == nil || t.Failed() { + return + } + + if err := updateOpenMilestoneCounts(x); err != nil { + assert.NoError(t, err) + return + } + + expected := []ExpectedMilestone{} + if err := x.Table("expected_milestone").Asc("id").Find(&expected); !assert.NoError(t, err) { + return + } + + got := []issues.Milestone{} + if err := x.Table("milestone").Asc("id").Find(&got); !assert.NoError(t, err) { + return + } + + for i, e := range expected { + got := got[i] + assert.Equal(t, e.ID, got.ID) + assert.Equal(t, e.NumIssues, got.NumIssues) + assert.Equal(t, e.NumClosedIssues, got.NumClosedIssues) + } +} diff --git a/models/packages/container/search.go b/models/packages/container/search.go index a3409fe743..e4a5a53848 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -165,6 +165,7 @@ type ImageTagsSearchOptions struct { PackageID int64 Query string IsTagged bool + Sort packages.VersionSort db.Paginator } @@ -195,12 +196,26 @@ func (opts *ImageTagsSearchOptions) toConds() builder.Cond { return cond } +func (opts *ImageTagsSearchOptions) configureOrderBy(e db.Engine) { + switch opts.Sort { + case packages.SortVersionDesc: + e.Desc("package_version.version") + case packages.SortVersionAsc: + e.Asc("package_version.version") + case packages.SortCreatedAsc: + e.Asc("package_version.created_unix") + default: + e.Desc("package_version.created_unix") + } +} + // SearchImageTags gets a sorted list of the tags of an image func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*packages.PackageVersion, int64, error) { sess := db.GetEngine(ctx). Join("INNER", "package", "package.id = package_version.package_id"). - Where(opts.toConds()). - Desc("package_version.created_unix") + Where(opts.toConds()) + + opts.configureOrderBy(sess) if opts.Paginator != nil { sess = db.SetSessionPagination(sess, opts) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 5479bae1c2..f9965bcb74 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -163,6 +163,17 @@ type SearchValue struct { ExactMatch bool } +type VersionSort = string + +const ( + SortNameAsc VersionSort = "name_asc" + SortNameDesc VersionSort = "name_desc" + SortVersionAsc VersionSort = "version_asc" + SortVersionDesc VersionSort = "version_desc" + SortCreatedAsc VersionSort = "created_asc" + SortCreatedDesc VersionSort = "created_desc" +) + // PackageSearchOptions are options for SearchXXX methods // Besides IsInternal are all fields optional and are not used if they have their default value (nil, "", 0) type PackageSearchOptions struct { @@ -176,7 +187,7 @@ type PackageSearchOptions struct { IsInternal util.OptionalBool HasFileWithName string // only results are found which are associated with a file with the specific name HasFiles util.OptionalBool // only results are found which have associated files - Sort string + Sort VersionSort db.Paginator } @@ -254,15 +265,15 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { switch opts.Sort { - case "alphabetically": + case SortNameAsc: e.Asc("package.name") - case "reversealphabetically": + case SortNameDesc: e.Desc("package.name") - case "highestversion": + case SortVersionDesc: e.Desc("package_version.version") - case "lowestversion": + case SortVersionAsc: e.Asc("package_version.version") - case "oldest": + case SortCreatedAsc: e.Asc("package_version.created_unix") default: e.Desc("package_version.created_unix") diff --git a/modules/markup/html.go b/modules/markup/html.go index a5606dbb51..ae00c3905f 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -603,8 +603,14 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { start = loc.End continue } - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) - node = node.NextSibling.NextSibling + mentionedUsername := mention[1:] + + if processorHelper.IsUsernameMentionable != nil && processorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { + replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention")) + node = node.NextSibling.NextSibling + } else { + node = node.NextSibling + } start = 0 } } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 12c6288c24..fbb741d1cd 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -38,6 +38,11 @@ func TestMain(m *testing.M) { if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } + markup.Init(&markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx context.Context, username string) bool { + return username == "r-lyeh" + }, + }) os.Exit(m.Run()) } diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 5f69dc7235..b3289cb3c3 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -19,8 +19,18 @@ import ( "code.gitea.io/gitea/modules/setting" ) +type ProcessorHelper struct { + IsUsernameMentionable func(ctx context.Context, username string) bool +} + +var processorHelper ProcessorHelper + // Init initialize regexps for markdown parsing -func Init() { +func Init(ph *ProcessorHelper) { + if ph != nil { + processorHelper = *ph + } + NewSanitizer() if len(setting.Markdown.CustomURLSchemes) > 0 { CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 999ae0360a..2104b0f239 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -106,6 +106,10 @@ never = Never rss_feed = RSS Feed +[filter] +string.asc = A - Z +string.desc = Z - A + [error] occurred = An error occurred report_message = If you are sure this is a Gitea bug, please search for issues on GitHub or open a new issue if necessary. diff --git a/routers/api/v1/misc/markdown_test.go b/routers/api/v1/misc/markdown_test.go index 7809fa5cc7..65ce060278 100644 --- a/routers/api/v1/misc/markdown_test.go +++ b/routers/api/v1/misc/markdown_test.go @@ -5,6 +5,7 @@ package misc import ( + go_context "context" "io" "net/http" "net/http/httptest" @@ -13,6 +14,7 @@ import ( "testing" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" @@ -50,6 +52,11 @@ func wrap(ctx *context.Context) *context.APIContext { func TestAPI_RenderGFM(t *testing.T) { setting.AppURL = AppURL + markup.Init(&markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx go_context.Context, username string) bool { + return username == "r-lyeh" + }, + }) options := api.MarkdownOption{ Mode: "gfm", diff --git a/routers/init.go b/routers/init.go index 0f2e993413..9045437f87 100644 --- a/routers/init.go +++ b/routers/init.go @@ -41,6 +41,7 @@ import ( "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/mailer" + markup_service "code.gitea.io/gitea/services/markup" repo_migrations "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" pull_service "code.gitea.io/gitea/services/pull" @@ -123,7 +124,7 @@ func GlobalInitInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init() + markup.Init(markup_service.ProcessorHelper()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") diff --git a/routers/web/user/package.go b/routers/web/user/package.go index c72592e728..7179e2df97 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -233,6 +233,7 @@ func ListPackageVersions(ctx *context.Context) { } query := ctx.FormTrim("q") + sort := ctx.FormTrim("sort") ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true @@ -243,9 +244,11 @@ func ListPackageVersions(ctx *context.Context) { Owner: ctx.Package.Owner, } ctx.Data["Query"] = query + ctx.Data["Sort"] = sort pagerParams := map[string]string{ - "q": query, + "q": query, + "sort": sort, } var ( @@ -264,6 +267,7 @@ func ListPackageVersions(ctx *context.Context) { PackageID: p.ID, Query: query, IsTagged: tagged == "" || tagged == "tagged", + Sort: sort, }) if err != nil { ctx.ServerError("SearchImageTags", err) @@ -278,6 +282,7 @@ func ListPackageVersions(ctx *context.Context) { Value: query, }, IsInternal: util.OptionalBoolFalse, + Sort: sort, }) if err != nil { ctx.ServerError("SearchVersions", err) diff --git a/services/issue/issue.go b/services/issue/issue.go index 69b87686c1..47782e50d3 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -224,6 +224,11 @@ func deleteIssue(issue *issues_model.Issue) error { return err } + if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return fmt.Errorf("error updating counters for milestone id %d: %w", + issue.MilestoneID, err) + } + if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil { return err } diff --git a/services/lfs/server.go b/services/lfs/server.go index b868db39db..830112fac6 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -438,14 +438,21 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa } if download { - rep.Actions["download"] = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header} + var link *lfs_module.Link if setting.LFS.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid) if u != nil && err == nil { - rep.Actions["download"] = &lfs_module.Link{Href: u.String(), Header: header} + // Presigned url does not need the Authorization header + // https://github.com/go-gitea/gitea/issues/21525 + delete(header, "Authorization") + link = &lfs_module.Link{Href: u.String(), Header: header} } } + if link == nil { + link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header} + } + rep.Actions["download"] = link } if upload { rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header} diff --git a/services/markup/main_test.go b/services/markup/main_test.go new file mode 100644 index 0000000000..8efd08e69d --- /dev/null +++ b/services/markup/main_test.go @@ -0,0 +1,19 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markup + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", ".."), + FixtureFiles: []string{"user.yml"}, + }) +} diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go new file mode 100644 index 0000000000..2b1cac2a5b --- /dev/null +++ b/services/markup/processorhelper.go @@ -0,0 +1,29 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markup + +import ( + "context" + + "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" +) + +func ProcessorHelper() *markup.ProcessorHelper { + return &markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx context.Context, username string) bool { + // TODO: cast ctx to modules/context.Context and use IsUserVisibleToViewer + + // Only link if the user actually exists + userExists, err := user.IsUserExist(ctx, 0, username) + if err != nil { + log.Error("Failed to validate user in mention %q exists, assuming it does", username) + userExists = true + } + return userExists + }, + } +} diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go new file mode 100644 index 0000000000..386465bc91 --- /dev/null +++ b/services/markup/processorhelper_test.go @@ -0,0 +1,20 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markup + +import ( + "context" + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestProcessorHelper(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), "user10")) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), "no-such-user")) +} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index b62e788799..3aab2873c6 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -37,20 +37,20 @@