diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e080b0be72..f201ff1d19 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2650,7 +2650,7 @@ LEVEL = Info ;; Limit the number of pointers in each batch request to this number ;BATCH_SIZE = 20 ;; Limit the number of concurrent upload/download operations within a batch -;BATCH_OPERATION_CONCURRENCY = 3 +;BATCH_OPERATION_CONCURRENCY = 8 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/actions/schedule_spec_test.go b/models/actions/schedule_spec_test.go index 0c26fce4b2..57221461df 100644 --- a/models/actions/schedule_spec_test.go +++ b/models/actions/schedule_spec_test.go @@ -7,19 +7,17 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestActionScheduleSpec_Parse(t *testing.T) { // Mock the local timezone is not UTC - local := time.Local tz, err := time.LoadLocation("Asia/Shanghai") require.NoError(t, err) - defer func() { - time.Local = local - }() - time.Local = tz + defer test.MockVariableValue(&time.Local, tz)() now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") require.NoError(t, err) diff --git a/models/activities/action.go b/models/activities/action.go index 9b4ffd7725..c83dba9d46 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -171,7 +171,10 @@ func (a *Action) TableIndices() []*schemas.Index { cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) cudIndex.AddColumn("created_unix", "user_id", "is_deleted") - indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex} + cuIndex := schemas.NewIndex("c_u", schemas.IndexType) + cuIndex.AddColumn("user_id", "is_deleted") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} return indices } diff --git a/models/issues/milestone.go b/models/issues/milestone.go index db0312adf0..4c9bae58f7 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -84,10 +84,9 @@ func (m *Milestone) BeforeUpdate() { // this object. func (m *Milestone) AfterLoad() { m.NumOpenIssues = m.NumIssues - m.NumClosedIssues - if m.DeadlineUnix.Year() == 9999 { + if m.DeadlineUnix == 0 { return } - m.DeadlineString = m.DeadlineUnix.FormatDate() if m.IsClosed { m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f6e4b57aa4..5969d21960 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -364,7 +364,9 @@ func prepareMigrationTasks() []*migration { newMigration(304, "Add index for release sha1", v1_23.AddIndexForReleaseSha1), newMigration(305, "Add Repository Licenses", v1_23.AddRepositoryLicenses), newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection), - newMigration(307, "Add DeleteBranchAfterMerge to AutoMerge", v1_23.AddDeleteBranchAfterMergeForAutoMerge), + newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), + newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), + newMigration(309, "Add DeleteBranchAfterMerge to AutoMerge", v1_23.AddDeleteBranchAfterMergeForAutoMerge), } return preparedMigrations } diff --git a/models/migrations/v1_23/v307.go b/models/migrations/v1_23/v307.go index 6a2868ea4c..ef7f5f2c3f 100644 --- a/models/migrations/v1_23/v307.go +++ b/models/migrations/v1_23/v307.go @@ -9,21 +9,13 @@ import ( "xorm.io/xorm" ) -type pullAutoMerge struct { - ID int64 `xorm:"pk autoincr"` - PullID int64 `xorm:"UNIQUE"` - DoerID int64 `xorm:"INDEX NOT NULL"` - MergeStyle string `xorm:"varchar(30)"` - Message string `xorm:"LONGTEXT"` - DeleteBranchAfterMerge bool - CreatedUnix timeutil.TimeStamp `xorm:"created"` -} - -// TableName return database table name for xorm -func (pullAutoMerge) TableName() string { - return "pull_auto_merge" -} - -func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error { - return x.Sync(new(pullAutoMerge)) +func FixMilestoneNoDueDate(x *xorm.Engine) error { + type Milestone struct { + DeadlineUnix timeutil.TimeStamp + } + // Wednesday, December 1, 9999 12:00:00 AM GMT+00:00 + _, err := x.Table("milestone").Where("deadline_unix > 253399622400"). + Cols("deadline_unix"). + Update(&Milestone{DeadlineUnix: 0}) + return err } diff --git a/models/migrations/v1_23/v308.go b/models/migrations/v1_23/v308.go new file mode 100644 index 0000000000..1e8a9b0af2 --- /dev/null +++ b/models/migrations/v1_23/v308.go @@ -0,0 +1,52 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type improveActionTableIndicesAction struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX"` // Receiver user id. + OpType int + ActUserID int64 // Action user id. + RepoID int64 + CommentID int64 `xorm:"INDEX"` + IsDeleted bool `xorm:"NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// TableName sets the name of this table +func (*improveActionTableIndicesAction) TableName() string { + return "action" +} + +func (a *improveActionTableIndicesAction) TableIndices() []*schemas.Index { + repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) + repoIndex.AddColumn("repo_id", "user_id", "is_deleted") + + actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) + actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") + + cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) + cudIndex.AddColumn("created_unix", "user_id", "is_deleted") + + cuIndex := schemas.NewIndex("c_u", schemas.IndexType) + cuIndex.AddColumn("user_id", "is_deleted") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} + + return indices +} + +func AddNewIndexForUserDashboard(x *xorm.Engine) error { + return x.Sync(new(improveActionTableIndicesAction)) +} diff --git a/models/migrations/v1_23/v309.go b/models/migrations/v1_23/v309.go new file mode 100644 index 0000000000..6a2868ea4c --- /dev/null +++ b/models/migrations/v1_23/v309.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type pullAutoMerge struct { + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"UNIQUE"` + DoerID int64 `xorm:"INDEX NOT NULL"` + MergeStyle string `xorm:"varchar(30)"` + Message string `xorm:"LONGTEXT"` + DeleteBranchAfterMerge bool + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// TableName return database table name for xorm +func (pullAutoMerge) TableName() string { + return "pull_auto_merge" +} + +func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error { + return x.Sync(new(pullAutoMerge)) +} diff --git a/models/perm/access_mode.go b/models/perm/access_mode.go index 0364191e2e..6baeb5531a 100644 --- a/models/perm/access_mode.go +++ b/models/perm/access_mode.go @@ -60,3 +60,6 @@ func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode { } return util.Iif(slices.Contains(allowed, m), m, AccessModeNone) } + +// ErrInvalidAccessMode is returned when an invalid access mode is used +var ErrInvalidAccessMode = util.NewInvalidArgumentErrorf("Invalid access mode") diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index c305603e02..ecc9216950 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -110,26 +110,28 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us return nil, err } - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { - return nil, err - } - uniqueUserIDs := make(container.Set[int64]) uniqueUserIDs.AddMultiple(userIDs...) - uniqueUserIDs.AddMultiple(additionalUserIDs...) + + if repo.Owner.IsOrganization() { + additionalUserIDs := make([]int64, 0, 10) + if err = e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", + repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). + Distinct("`team_user`.uid"). + Select("`team_user`.uid"). + Find(&additionalUserIDs); err != nil { + return nil, err + } + uniqueUserIDs.AddMultiple(additionalUserIDs...) + } // Leave a seat for owner itself to append later, but if owner is an organization // and just waste 1 unit is cheaper than re-allocate memory once. users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) - if len(userIDs) > 0 { + if len(uniqueUserIDs) > 0 { if err = e.In("id", uniqueUserIDs.Values()). Where(builder.Eq{"`user`.is_active": true}). OrderBy(user_model.GetOrderByName()). diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 331ad6b218..7e12be030f 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -32,7 +32,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo graphCmd.AddArguments("--all") } - graphCmd.AddArguments("-C", "-M", "--date=iso"). + graphCmd.AddArguments("-C", "-M", "--date=iso-strict"). AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page). AddOptionFormat("--pretty=format:%s", format) diff --git a/modules/gitgraph/graph_models.go b/modules/gitgraph/graph_models.go index e48fef8b9d..191b0b3afc 100644 --- a/modules/gitgraph/graph_models.go +++ b/modules/gitgraph/graph_models.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "strings" + "time" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" @@ -192,6 +193,14 @@ var RelationCommit = &Commit{ Row: -1, } +func parseGitTime(timeStr string) time.Time { + t, err := time.Parse(time.RFC3339, timeStr) + if err != nil { + return time.Unix(0, 0) + } + return t +} + // NewCommit creates a new commit from a provided line func NewCommit(row, column int, line []byte) (*Commit, error) { data := bytes.SplitN(line, []byte("|"), 5) @@ -206,7 +215,7 @@ func NewCommit(row, column int, line []byte) (*Commit, error) { // 1 matches git log --pretty=format:%H => commit hash Rev: string(data[1]), // 2 matches git log --pretty=format:%ad => author date (format respects --date= option) - Date: string(data[2]), + Date: parseGitTime(string(data[2])), // 3 matches git log --pretty=format:%h => abbreviated commit hash ShortRev: string(data[3]), // 4 matches git log --pretty=format:%s => subject @@ -245,7 +254,7 @@ type Commit struct { Column int Refs []git.Reference Rev string - Date string + Date time.Time ShortRev string Subject string } diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index 90e5e62bcb..772317fa59 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -31,6 +31,7 @@ import ( "github.com/blevesearch/bleve/v2/analysis/token/camelcase" "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/letter" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" @@ -69,7 +70,7 @@ const ( filenameIndexerAnalyzer = "filenameIndexerAnalyzer" filenameIndexerTokenizer = "filenameIndexerTokenizer" repoIndexerDocType = "repoIndexerDocType" - repoIndexerLatestVersion = 7 + repoIndexerLatestVersion = 8 ) // generateBleveIndexMapping generates a bleve index mapping for the repo indexer @@ -105,7 +106,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) { } else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]any{ "type": analyzer_custom.Name, "char_filters": []string{}, - "tokenizer": unicode.Name, + "tokenizer": letter.Name, "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, }); err != nil { return nil, err diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 669a1bafcc..1c4dd39eff 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -30,7 +30,7 @@ import ( ) const ( - esRepoIndexerLatestVersion = 2 + esRepoIndexerLatestVersion = 3 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -60,6 +60,10 @@ const ( "settings": { "analysis": { "analyzer": { + "content_analyzer": { + "tokenizer": "content_tokenizer", + "filter" : ["lowercase"] + }, "filename_path_analyzer": { "tokenizer": "path_tokenizer" }, @@ -68,6 +72,10 @@ const ( } }, "tokenizer": { + "content_tokenizer": { + "type": "simple_pattern_split", + "pattern": "[^a-zA-Z0-9]" + }, "path_tokenizer": { "type": "path_hierarchy", "delimiter": "/" @@ -104,7 +112,8 @@ const ( "content": { "type": "text", "term_vector": "with_positions_offsets", - "index": true + "index": true, + "analyzer": "content_analyzer" }, "commit_id": { "type": "keyword", diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 5b33528dcd..020ccc72f8 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -181,6 +181,55 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { }, }, }, + // Search for matches on the contents of files regardless of case. + { + RepoIDs: nil, + Keyword: "dESCRIPTION", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "README.md", + Content: "# repo1\n\nDescription for repo1", + }, + }, + }, + // Search for an exact match on the filename within the repo '62' (case insenstive). + // This scenario yields a single result (the file avocado.md on the repo '62') + { + RepoIDs: []int64{62}, + Keyword: "AVOCADO.MD", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "avocado.md", + Content: "# repo1\n\npineaple pie of cucumber juice", + }, + }, + }, + // Search for matches on the contents of files when the criteria is a expression. + { + RepoIDs: []int64{62}, + Keyword: "console.log", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "example-file.js", + Content: "console.log(\"Hello, World!\")", + }, + }, + }, + // Search for matches on the contents of files when the criteria is part of a expression. + { + RepoIDs: []int64{62}, + Keyword: "log", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "example-file.js", + Content: "console.log(\"Hello, World!\")", + }, + }, + }, } for _, kw := range keywords { diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index b426b39bc2..a0c3dc4ad4 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -6,12 +6,13 @@ package bleve import ( "errors" "os" + "unicode" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" - "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + unicode_tokenizer "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" "github.com/blevesearch/bleve/v2/index/upsidedown" "github.com/ethantkoenig/rupture" ) @@ -57,7 +58,7 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { // may be different on two string and they still be considered equivalent. // Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { - tokenizer := unicode.NewUnicodeTokenizer() + tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) if len(tokens) > 0 { @@ -77,8 +78,10 @@ func guessFuzzinessByKeyword(s string) int { // according to https://github.com/blevesearch/bleve/issues/1563, the supported max fuzziness is 2 // magic number 4 was chosen to determine the levenshtein distance per each character of a keyword // BUT, when using CJK (eg: `갃갃갃` `啊啊啊`), it mismatches a lot. + // Likewise, queries whose terms contains characters that are *not* letters should not use fuzziness + for _, r := range s { - if r >= 128 { + if r >= 128 || !unicode.IsLetter(r) { return 0 } } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index ae0b12c08d..8f7844464e 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -35,6 +35,14 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { Input: "갃갃갃", Fuzziness: 0, }, + { + Input: "repo1", + Fuzziness: 0, + }, + { + Input: "avocado.md", + Fuzziness: 0, + }, } for _, scenario := range scenarios { diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 411c4248c4..3060e25754 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -136,6 +136,9 @@ func (c *HTTPClient) performOperation(ctx context.Context, objects []Pointer, dc return fmt.Errorf("TransferAdapter not found: %s", result.Transfer) } + if setting.LFSClient.BatchOperationConcurrency <= 0 { + panic("BatchOperationConcurrency must be greater than 0, forgot to init?") + } errGroup, groupCtx := errgroup.WithContext(ctx) errGroup.SetLimit(setting.LFSClient.BatchOperationConcurrency) for _, object := range result.Objects { diff --git a/modules/lfs/http_client_test.go b/modules/lfs/http_client_test.go index d22735147a..aa7e3c45c4 100644 --- a/modules/lfs/http_client_test.go +++ b/modules/lfs/http_client_test.go @@ -237,7 +237,7 @@ func TestHTTPClientDownload(t *testing.T) { }, } - defer test.MockVariableValue(&setting.LFSClient.BatchOperationConcurrency, 3)() + defer test.MockVariableValue(&setting.LFSClient.BatchOperationConcurrency, 8)() for _, c := range cases { t.Run(c.endpoint, func(t *testing.T) { client := &HTTPClient{ @@ -337,7 +337,7 @@ func TestHTTPClientUpload(t *testing.T) { }, } - defer test.MockVariableValue(&setting.LFSClient.BatchOperationConcurrency, 3)() + defer test.MockVariableValue(&setting.LFSClient.BatchOperationConcurrency, 8)() for _, c := range cases { t.Run(c.endpoint, func(t *testing.T) { client := &HTTPClient{ diff --git a/modules/markup/html.go b/modules/markup/html.go index a9c3dc9ba2..e2eefefc4b 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -442,7 +442,10 @@ func createLink(href, content, class string) *html.Node { a := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), - Attr: []html.Attribute{{Key: "href", Val: href}}, + Attr: []html.Attribute{ + {Key: "href", Val: href}, + {Key: "data-markdown-generated-content"}, + }, } if class != "" { diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go index d33630d040..a90de278f5 100644 --- a/modules/markup/html_codepreview_test.go +++ b/modules/markup/html_codepreview_test.go @@ -30,5 +30,5 @@ func TestRenderCodePreview(t *testing.T) { assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "
http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`) + test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`) } diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 74089cffdd..8f516751b0 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -33,11 +33,9 @@ func numericIssueLink(baseURL, class string, index int, marker string) string { // link an HTML link func link(href, class, contents string) string { - if class != "" { - class = " class=\"" + class + "\"" - } - - return fmt.Sprintf("%s", href, class, contents) + extra := ` data-markdown-generated-content=""` + extra += util.Iif(class != "", ` class="`+class+`"`, "") + return fmt.Sprintf(`%s`, href, extra, contents) } var numericMetas = map[string]string{ @@ -353,7 +351,9 @@ func TestRender_FullIssueURLs(t *testing.T) { Metas: localMetas, }, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) assert.NoError(t, err) - assert.Equal(t, expected, result.String()) + actual := result.String() + actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") + assert.Equal(t, expected, actual) } test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", "Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 32858dbd6b..82aded4407 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -116,7 +116,9 @@ func TestRender_CrossReferences(t *testing.T) { Metas: localMetas, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + actual := strings.TrimSpace(buffer) + actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") + assert.Equal(t, strings.TrimSpace(expected), actual) } test( @@ -156,7 +158,9 @@ func TestRender_links(t *testing.T) { }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + actual := strings.TrimSpace(buffer) + actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") + assert.Equal(t, strings.TrimSpace(expected), actual) } oldCustomURLSchemes := setting.Markdown.CustomURLSchemes @@ -267,7 +271,9 @@ func TestRender_email(t *testing.T) { }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) + actual := strings.TrimSpace(res) + actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") + assert.Equal(t, strings.TrimSpace(expected), actual) } // Text that should be turned into email link @@ -616,7 +622,9 @@ func TestPostProcess_RenderDocument(t *testing.T) { Metas: localMetas, }, strings.NewReader(input), &res) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) + actual := strings.TrimSpace(res.String()) + actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") + assert.Equal(t, strings.TrimSpace(expected), actual) } // Issue index shouldn't be post processing in a document. diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index cfb821ab19..ad38e7a088 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -311,7 +311,8 @@ func TestTotal_RenderWiki(t *testing.T) { IsWiki: true, }, sameCases[i]) assert.NoError(t, err) - assert.Equal(t, template.HTML(answers[i]), line) + actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") + assert.Equal(t, answers[i], actual) } testCases := []string{ @@ -336,7 +337,8 @@ func TestTotal_RenderWiki(t *testing.T) { IsWiki: true, }, testCases[i]) assert.NoError(t, err) - assert.Equal(t, template.HTML(testCases[i+1]), line) + actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") + assert.EqualValues(t, testCases[i+1], actual) } } @@ -356,7 +358,8 @@ func TestTotal_RenderString(t *testing.T) { Metas: localMetas, }, sameCases[i]) assert.NoError(t, err) - assert.Equal(t, template.HTML(answers[i]), line) + actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") + assert.Equal(t, answers[i], actual) } testCases := []string{} @@ -996,7 +999,8 @@ space for i, c := range cases { result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input) assert.NoError(t, err, "Unexpected error in testcase: %v", i) - assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i) + actual := strings.ReplaceAll(string(result), ` data-markdown-generated-content=""`, "") + assert.Equal(t, c.Expected, actual, "Unexpected result in testcase %v", i) } } diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 669dc24eae..476ae5e26f 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -107,6 +107,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { "start", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "vspace", "width", "itemprop", + "data-markdown-generated-content", } generalSafeElements := []string{ diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go deleted file mode 100644 index f71c58fbdf..0000000000 --- a/modules/repository/collaborator.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "context" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - - "xorm.io/builder" -) - -func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error { - if err := repo.LoadOwner(ctx); err != nil { - return err - } - - if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { - return user_model.ErrBlockedUser - } - - return db.WithTx(ctx, func(ctx context.Context) error { - has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{ - "repo_id": repo.ID, - "user_id": u.ID, - }) - if err != nil { - return err - } else if has { - return nil - } - - if err = db.Insert(ctx, &repo_model.Collaboration{ - RepoID: repo.ID, - UserID: u.ID, - Mode: perm.AccessModeWrite, - }); err != nil { - return err - } - - return access_model.RecalculateUserAccess(ctx, repo, u.ID) - }) -} diff --git a/modules/repository/collaborator_test.go b/modules/repository/collaborator_test.go deleted file mode 100644 index 622f6abce4..0000000000 --- a/modules/repository/collaborator_test.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - perm_model "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - "github.com/stretchr/testify/assert" -) - -func TestRepository_AddCollaborator(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testSuccess := func(repoID, userID int64) { - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - assert.NoError(t, repo.LoadOwner(db.DefaultContext)) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID}) - } - testSuccess(1, 4) - testSuccess(1, 4) - testSuccess(3, 4) -} - -func TestRepoPermissionPublicNonOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // public non-organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // collaborator - collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, collaborator) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPrivateNonOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // private non-organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.False(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPublicOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // public organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // org member team owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // org member team tester - member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, member) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - } - assert.True(t, perm.CanWrite(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} - -func TestRepoPermissionPrivateOrgRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // private organization repo - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24}) - assert.NoError(t, repo.LoadUnits(db.DefaultContext)) - - // plain user - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - perm, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.False(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // change to collaborator to default write access - assert.NoError(t, AddCollaborator(db.DefaultContext, repo, user)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - assert.NoError(t, repo_model.ChangeCollaborationAccessMode(db.DefaultContext, repo, user.ID, perm_model.AccessModeRead)) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, user) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.False(t, perm.CanWrite(unit.Type)) - } - - // org member team owner - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // update team information and then check permission - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5}) - err = organization.UpdateTeamUnits(db.DefaultContext, team, nil) - assert.NoError(t, err) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, owner) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } - - // org member team tester - tester := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, tester) - assert.NoError(t, err) - assert.True(t, perm.CanWrite(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - assert.False(t, perm.CanRead(unit.TypeCode)) - - // org member team reviewer - reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, reviewer) - assert.NoError(t, err) - assert.False(t, perm.CanRead(unit.TypeIssues)) - assert.False(t, perm.CanWrite(unit.TypeCode)) - assert.True(t, perm.CanRead(unit.TypeCode)) - - // admin - admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - perm, err = access_model.GetUserRepoPermission(db.DefaultContext, repo, admin) - assert.NoError(t, err) - for _, unit := range repo.Units { - assert.True(t, perm.CanRead(unit.Type)) - assert.True(t, perm.CanWrite(unit.Type)) - } -} diff --git a/modules/repository/commits.go b/modules/repository/commits.go index ede60429a1..6e4b75d5ca 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -42,8 +42,8 @@ func NewPushCommits() *PushCommits { return &PushCommits{} } -// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object. -func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { +// ToAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object. +func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { var err error authorUsername := "" author, ok := emailUsers[commit.AuthorEmail] @@ -105,7 +105,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi emailUsers := make(map[string]*user_model.User) for i, commit := range pc.Commits { - apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit) + apiCommit, err := ToAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit) if err != nil { return nil, nil, err } @@ -117,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi } if pc.HeadCommit != nil && headCommit == nil { var err error - headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit) + headCommit, err = ToAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit) if err != nil { return nil, nil, err } diff --git a/modules/repository/create.go b/modules/repository/create.go index 4f18b9b3fa..b4f7033bd7 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -11,160 +11,17 @@ import ( "path/filepath" "strings" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/models/webhook" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" ) -// CreateRepositoryByExample creates a repository for the user/organization. -func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) { - if err = repo_model.IsUsableRepoName(repo.Name); err != nil { - return err - } - - has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) - if err != nil { - return fmt.Errorf("IsRepositoryExist: %w", err) - } else if has { - return repo_model.ErrRepoAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - repoPath := repo_model.RepoPath(u.Name, repo.Name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if !overwriteOrAdopt && isExist { - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return repo_model.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - if err = db.Insert(ctx, repo); err != nil { - return err - } - if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil { - return err - } - - // insert units for repo - defaultUnits := unit.DefaultRepoUnits - if isFork { - defaultUnits = unit.DefaultForkRepoUnits - } - units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) - for _, tp := range defaultUnits { - if tp == unit.TypeIssues { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.IssuesConfig{ - EnableTimetracker: setting.Service.DefaultEnableTimetracking, - AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, - EnableDependencies: setting.Service.DefaultEnableDependencies, - }, - }) - } else if tp == unit.TypePullRequests { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.PullRequestsConfig{ - AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, - DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), - AllowRebaseUpdate: true, - }, - }) - } else if tp == unit.TypeProjects { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, - }) - } else { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: tp, - }) - } - } - - if err = db.Insert(ctx, units); err != nil { - return err - } - - // Remember visibility preference. - u.LastRepoVisibility = repo.IsPrivate - if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil { - return fmt.Errorf("UpdateUserCols: %w", err) - } - - if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil { - return fmt.Errorf("IncrUserRepoNum: %w", err) - } - u.NumRepos++ - - // Give access to all members in teams with access to all repositories. - if u.IsOrganization() { - teams, err := organization.FindOrgTeams(ctx, u.ID) - if err != nil { - return fmt.Errorf("FindOrgTeams: %w", err) - } - for _, t := range teams { - if t.IncludesAllRepositories { - if err := models.AddRepository(ctx, t, repo); err != nil { - return fmt.Errorf("AddRepository: %w", err) - } - } - } - - if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil { - return fmt.Errorf("IsUserRepoAdmin: %w", err) - } else if !isAdmin { - // Make creator repo admin if it wasn't assigned automatically - if err = AddCollaborator(ctx, repo, doer); err != nil { - return fmt.Errorf("AddCollaborator: %w", err) - } - if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil { - return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err) - } - } - } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { - // Organization automatically called this in AddRepository method. - return fmt.Errorf("RecalculateAccesses: %w", err) - } - - if setting.Service.AutoWatchNewRepos { - if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { - return fmt.Errorf("WatchRepo: %w", err) - } - } - - if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil { - return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err) - } - - return nil -} - const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular // getDirectorySize returns the disk consumption for a given path diff --git a/modules/repository/main_test.go b/modules/repository/main_test.go index f81dfcdafb..799e8c17c3 100644 --- a/modules/repository/main_test.go +++ b/modules/repository/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 6b54ac0a60..7f2d0ae159 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -68,8 +68,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error { } if LFSClient.BatchOperationConcurrency < 1 { - // match the default git-lfs's `lfs.concurrenttransfers` - LFSClient.BatchOperationConcurrency = 3 + // match the default git-lfs's `lfs.concurrenttransfers` https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.adoc#upload-and-download-transfer-settings + LFSClient.BatchOperationConcurrency = 8 } LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(24 * time.Hour) diff --git a/modules/setting/lfs_test.go b/modules/setting/lfs_test.go index 471fa8bff3..d27dd7c5bf 100644 --- a/modules/setting/lfs_test.go +++ b/modules/setting/lfs_test.go @@ -114,7 +114,7 @@ BATCH_SIZE = 0 assert.NoError(t, loadLFSFrom(cfg)) assert.EqualValues(t, 100, LFS.MaxBatchSize) assert.EqualValues(t, 20, LFSClient.BatchSize) - assert.EqualValues(t, 3, LFSClient.BatchOperationConcurrency) + assert.EqualValues(t, 8, LFSClient.BatchOperationConcurrency) iniStr = ` [lfs_client] diff --git a/modules/structs/hook.go b/modules/structs/hook.go index db8b20e7e5..ce5742e5c7 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -262,13 +262,6 @@ func (p *ReleasePayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -// __________ .__ -// \______ \__ __ _____| |__ -// | ___/ | \/ ___/ | \ -// | | | | /\___ \| Y \ -// |____| |____//____ >___| / -// \/ \/ - // PushPayload represents a payload information of push event. type PushPayload struct { Ref string `json:"ref"` @@ -509,3 +502,26 @@ type WorkflowDispatchPayload struct { func (p *WorkflowDispatchPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } + +// CommitStatusPayload represents a payload information of commit status event. +type CommitStatusPayload struct { + // TODO: add Branches per https://docs.github.com/en/webhooks/webhook-events-and-payloads#status + Commit *PayloadCommit `json:"commit"` + Context string `json:"context"` + // swagger:strfmt date-time + CreatedAt time.Time `json:"created_at"` + Description string `json:"description"` + ID int64 `json:"id"` + Repo *Repository `json:"repository"` + Sender *User `json:"sender"` + SHA string `json:"sha"` + State string `json:"state"` + TargetURL string `json:"target_url"` + // swagger:strfmt date-time + UpdatedAt *time.Time `json:"updated_at"` +} + +// JSONPayload implements Payload +func (p *CommitStatusPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a01aad06a1..efaa10624b 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -73,11 +73,6 @@ func NewFuncMap() template.FuncMap { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" }, - // for backward compatibility only, do not use them anymore - "TimeSince": timeSinceLegacy, - "TimeSinceUnix": timeSinceLegacy, - "DateTime": dateTimeLegacy, - // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -156,18 +151,8 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // render - "RenderCommitMessage": RenderCommitMessage, - "RenderCommitMessageLinkSubject": renderCommitMessageLinkSubject, - - "RenderCommitBody": renderCommitBody, - "RenderCodeBlock": renderCodeBlock, - "RenderIssueTitle": renderIssueTitle, - "RenderEmoji": renderEmoji, - "ReactionToEmoji": reactionToEmoji, - - "RenderMarkdownToHtml": RenderMarkdownToHtml, - "RenderLabel": renderLabel, - "RenderLabels": RenderLabels, + "RenderCodeBlock": renderCodeBlock, + "ReactionToEmoji": reactionToEmoji, // ----------------------------------------------------------------- // misc @@ -179,6 +164,22 @@ func NewFuncMap() template.FuncMap { "FilenameIsImage": filenameIsImage, "TabSizeClass": tabSizeClass, + + // for backward compatibility only, do not use them anymore + "TimeSince": timeSinceLegacy, + "TimeSinceUnix": timeSinceLegacy, + "DateTime": dateTimeLegacy, + + "RenderEmoji": renderEmojiLegacy, + "RenderLabel": renderLabelLegacy, + "RenderLabels": renderLabelsLegacy, + "RenderIssueTitle": renderIssueTitleLegacy, + + "RenderMarkdownToHtml": renderMarkdownToHtmlLegacy, + + "RenderCommitMessage": renderCommitMessageLegacy, + "RenderCommitMessageLinkSubject": renderCommitMessageLinkSubjectLegacy, + "RenderCommitBody": renderCommitBodyLegacy, } } @@ -296,3 +297,9 @@ func userThemeName(user *user_model.User) string { } return setting.UI.DefaultTheme } + +func panicIfDevOrTesting() { + if !setting.IsProd || setting.IsInTesting { + panic("legacy template functions are for backward compatibility only, do not use them in new code") + } +} diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index b9e04401f1..66f83d23fe 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/translation" ) type DateUtils struct{} @@ -28,7 +27,7 @@ func (du *DateUtils) AbsoluteShort(time any) template.HTML { // AbsoluteLong renders in "January 01, 2006" format func (du *DateUtils) AbsoluteLong(time any) template.HTML { - return dateTimeFormat("short", time) + return dateTimeFormat("long", time) } // FullTime renders in "Jan 01, 2006 20:33:44" format @@ -54,23 +53,6 @@ func parseLegacy(datetime string) time.Time { return t } -func dateTimeLegacy(format string, datetime any, _ ...string) template.HTML { - if !setting.IsProd || setting.IsInTesting { - panic("dateTimeLegacy is for backward compatibility only, do not use it in new code") - } - if s, ok := datetime.(string); ok { - datetime = parseLegacy(s) - } - return dateTimeFormat(format, datetime) -} - -func timeSinceLegacy(time any, _ translation.Locale) template.HTML { - if !setting.IsProd || setting.IsInTesting { - panic("timeSinceLegacy is for backward compatibility only, do not use it in new code") - } - return TimeSince(time) -} - func anyToTime(any any) (t time.Time, isZero bool) { switch v := any.(type) { case nil: diff --git a/modules/templates/util_date_legacy.go b/modules/templates/util_date_legacy.go new file mode 100644 index 0000000000..ceefb00447 --- /dev/null +++ b/modules/templates/util_date_legacy.go @@ -0,0 +1,23 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "html/template" + + "code.gitea.io/gitea/modules/translation" +) + +func dateTimeLegacy(format string, datetime any, _ ...string) template.HTML { + panicIfDevOrTesting() + if s, ok := datetime.(string); ok { + datetime = parseLegacy(s) + } + return dateTimeFormat(format, datetime) +} + +func timeSinceLegacy(time any, _ translation.Locale) template.HTML { + panicIfDevOrTesting() + return TimeSince(time) +} diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 6eee007f34..1201828345 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -24,13 +24,21 @@ import ( "code.gitea.io/gitea/modules/util" ) +type RenderUtils struct { + ctx context.Context +} + +func NewRenderUtils(ctx context.Context) *RenderUtils { + return &RenderUtils{ctx: ctx} +} + // RenderCommitMessage renders commit message with XSS-safe and special links. -func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string) template.HTML { cleanMsg := template.HTMLEscapeString(msg) // we can safely assume that it will not return any error, since there // shouldn't be any special HTML. fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ - Ctx: ctx, + Ctx: ut.ctx, Metas: metas, }, cleanMsg) if err != nil { @@ -44,9 +52,9 @@ func RenderCommitMessage(ctx context.Context, msg string, metas map[string]strin return renderCodeBlock(template.HTML(msgLines[0])) } -// renderCommitMessageLinkSubject renders commit message as a XSS-safe link to +// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. -func renderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, metas map[string]string) template.HTML { msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -60,7 +68,7 @@ func renderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, // we can safely assume that it will not return any error, since there // shouldn't be any special HTML. renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ - Ctx: ctx, + Ctx: ut.ctx, DefaultLink: urlDefault, Metas: metas, }, template.HTMLEscapeString(msgLine)) @@ -71,8 +79,8 @@ func renderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, return renderCodeBlock(template.HTML(renderedMessage)) } -// renderCommitBody extracts the body of a commit message without its title. -func renderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML { +// RenderCommitBody extracts the body of a commit message without its title. +func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) template.HTML { msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -86,7 +94,7 @@ func renderCommitBody(ctx context.Context, msg string, metas map[string]string) } renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ - Ctx: ctx, + Ctx: ut.ctx, Metas: metas, }, template.HTMLEscapeString(msgLine)) if err != nil { @@ -105,22 +113,22 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { return template.HTML(htmlWithCodeTags) } -// renderIssueTitle renders issue/pull title with defined post processors -func renderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML { +// RenderIssueTitle renders issue/pull title with defined post processors +func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ - Ctx: ctx, + Ctx: ut.ctx, Metas: metas, }, template.HTMLEscapeString(text)) if err != nil { log.Error("RenderIssueTitle: %v", err) - return template.HTML("") + return "" } return template.HTML(renderedText) } -// renderLabel renders a label -// locale is needed due to an import cycle with our context providing the `Tr` function -func renderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { +// RenderLabel renders a label +func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { + locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) var extraCSSClasses string textColor := util.ContrastColor(label.Color) labelScope := label.ExclusiveScope() @@ -134,12 +142,12 @@ func renderLabel(ctx context.Context, locale translation.Locale, label *issues_m if labelScope == "" { // Regular label return HTMLFormat(`@no-such-user @mention-user @mention-user
`, strings.TrimSpace(string(rendered))) + rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user") + assert.EqualValues(t, `@no-such-user @mention-user @mention-user
`, strings.TrimSpace(string(rendered))) } diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 0013691c02..fbec889272 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -32,6 +32,7 @@ const ( HookEventRelease HookEventType = "release" HookEventPackage HookEventType = "package" HookEventSchedule HookEventType = "schedule" + HookEventStatus HookEventType = "status" ) // Event returns the HookEventType as an event string diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bfc601c835..23f466873b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1172,7 +1172,7 @@ func Routes() *web.Router { m.Get("", reqAnyRepoReader(), repo.ListCollaborators) m.Group("/{collaborator}", func() { m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator). - Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator). + Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddOrUpdateCollaborator). Delete(reqAdmin(), repo.DeleteCollaborator) m.Get("/permission", repo.GetRepoPermissions) }) diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index e2ab7141b7..abffdf3516 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -38,7 +38,8 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") web.SetForm(ctx, &options) Markup(ctx) - assert.Equal(t, expectedBody, resp.Body.String()) + actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "") + assert.Equal(t, expectedBody, actual) assert.Equal(t, expectedCode, resp.Code) resp.Body.Reset() } @@ -58,7 +59,8 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") web.SetForm(ctx, &options) Markdown(ctx) - assert.Equal(t, responseBody, resp.Body.String()) + actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "") + assert.Equal(t, responseBody, actual) assert.Equal(t, responseCode, resp.Code) resp.Body.Reset() } diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 39c9ba527d..ea9d8b0f37 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -12,7 +12,6 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -123,11 +122,11 @@ func IsCollaborator(ctx *context.APIContext) { } } -// AddCollaborator add a collaborator to a repository -func AddCollaborator(ctx *context.APIContext) { +// AddOrUpdateCollaborator add or update a collaborator to a repository +func AddOrUpdateCollaborator(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/collaborators/{collaborator} repository repoAddCollaborator // --- - // summary: Add a collaborator to a repository + // summary: Add or Update a collaborator to a repository // produces: // - application/json // parameters: @@ -177,20 +176,18 @@ func AddCollaborator(ctx *context.APIContext) { return } - if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil { - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "AddCollaborator", err) - } else { - ctx.Error(http.StatusInternalServerError, "AddCollaborator", err) - } - return + p := perm.AccessModeWrite + if form.Permission != nil { + p = perm.ParseAccessMode(*form.Permission) } - if form.Permission != nil { - if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeCollaborationAccessMode", err) - return + if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Error(http.StatusForbidden, "AddOrUpdateCollaborator", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddOrUpdateCollaborator", err) } + return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 97f7a49390..05650cc9be 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -56,12 +56,12 @@ func GetRawFile(ctx *context.APIContext) { // required: true // - name: filepath // in: path - // description: filepath of the file to get + // description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch // type: string // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)" + // description: "The name of the commit/branch/tag. Default the repository’s default branch" // type: string // required: false // responses: @@ -109,12 +109,12 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // required: true // - name: filepath // in: path - // description: filepath of the file to get + // description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch // type: string // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)" + // description: "The name of the commit/branch/tag. Default the repository’s default branch" // type: string // required: false // responses: diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index e86fb3ccb1..cbe709c030 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" issue_service "code.gitea.io/gitea/services/issue" @@ -1046,18 +1047,11 @@ func UpdateIssueDeadline(ctx *context.APIContext) { return } - var deadlineUnix timeutil.TimeStamp - var deadline time.Time - if form.Deadline != nil && !form.Deadline.IsZero() { - deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), - 23, 59, 59, 0, time.Local) - deadlineUnix = timeutil.TimeStamp(deadline.Unix()) - } - + deadlineUnix, _ := common.ParseAPIDeadlineToEndOfDay(form.Deadline) if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) return } - ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) + ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) } diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index 27c7af2282..d0bcadde37 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/attachment" + attachment_service "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/convert" @@ -181,7 +181,7 @@ func CreateIssueAttachment(ctx *context.APIContext) { filename = query } - attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ + attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, @@ -247,6 +247,8 @@ func EditIssueAttachment(ctx *context.APIContext) { // "$ref": "#/responses/Attachment" // "404": // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" // "423": // "$ref": "#/responses/repoArchivedError" @@ -261,8 +263,13 @@ func EditIssueAttachment(ctx *context.APIContext) { attachment.Name = form.Name } - if err := repo_model.UpdateAttachment(ctx, attachment); err != nil { + if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil { + if upload.IsErrFileTypeForbidden(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + return + } ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err) + return } ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment)) diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index 0863ebd182..a556a803e5 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/attachment" + attachment_service "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/convert" @@ -189,7 +189,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { filename = query } - attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ + attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, @@ -263,6 +263,8 @@ func EditIssueCommentAttachment(ctx *context.APIContext) { // "$ref": "#/responses/Attachment" // "404": // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" // "423": // "$ref": "#/responses/repoArchivedError" attach := getIssueCommentAttachmentSafeWrite(ctx) @@ -275,8 +277,13 @@ func EditIssueCommentAttachment(ctx *context.APIContext) { attach.Name = form.Name } - if err := repo_model.UpdateAttachment(ctx, attach); err != nil { + if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil { + if upload.IsErrFileTypeForbidden(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + return + } ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) + return } ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach)) } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index abe9e4006a..78907c85a5 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -7,7 +7,6 @@ package repo import ( "net/http" "strconv" - "time" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" @@ -16,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" ) @@ -155,16 +155,16 @@ func CreateMilestone(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.CreateMilestoneOption) - if form.Deadline == nil { - defaultDeadline, _ := time.ParseInLocation("2006-01-02", "9999-12-31", time.Local) - form.Deadline = &defaultDeadline + var deadlineUnix int64 + if form.Deadline != nil { + deadlineUnix = form.Deadline.Unix() } milestone := &issues_model.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Description, - DeadlineUnix: timeutil.TimeStamp(form.Deadline.Unix()), + DeadlineUnix: timeutil.TimeStamp(deadlineUnix), } if form.State == "closed" { @@ -225,9 +225,7 @@ func EditMilestone(ctx *context.APIContext) { if form.Description != nil { milestone.Content = *form.Description } - if form.Deadline != nil && !form.Deadline.IsZero() { - milestone.DeadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) - } + milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline) oldIsClosed := milestone.IsClosed if form.State != nil { diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 4a2371e012..ed6cc8e1ea 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/attachment" + attachment_service "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/convert" @@ -234,7 +234,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Create a new attachment and save the file - attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{ + attach, err := attachment_service.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, @@ -291,6 +291,8 @@ func EditReleaseAttachment(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Attachment" + // "422": + // "$ref": "#/responses/validationError" // "404": // "$ref": "#/responses/notFound" @@ -322,8 +324,13 @@ func EditReleaseAttachment(ctx *context.APIContext) { attach.Name = form.Name } - if err := repo_model.UpdateAttachment(ctx, attach); err != nil { + if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil { + if upload.IsErrFileTypeForbidden(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + return + } ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) + return } ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach)) } diff --git a/routers/common/deadline.go b/routers/common/deadline.go new file mode 100644 index 0000000000..152e94597b --- /dev/null +++ b/routers/common/deadline.go @@ -0,0 +1,31 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" +) + +func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) { + if date == "" { + return 0, nil + } + deadline, err := time.ParseInLocation("2006-01-02", date, setting.DefaultUILocation) + if err != nil { + return 0, err + } + deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) + return timeutil.TimeStamp(deadline.Unix()), nil +} + +func ParseAPIDeadlineToEndOfDay(t *time.Time) (timeutil.TimeStamp, error) { + if t == nil || t.IsZero() || t.Unix() == 0 { + return 0, nil + } + deadline := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation) + return timeutil.TimeStamp(deadline.Unix()), nil +} diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 774644e29a..3f33e9933a 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -71,6 +71,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content // feedActionsToFeedItems convert gitea's Action feed to feeds Item func feedActionsToFeedItems(ctx *context.Context, actions activities_model.ActionList) (items []*feeds.Item, err error) { + renderUtils := templates.NewRenderUtils(ctx) for _, act := range actions { act.LoadActUser(ctx) @@ -215,7 +216,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio desc += fmt.Sprintf("%s\n%s", html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)), commit.Sha1, - templates.RenderCommitMessage(ctx, commit.Message, nil), + renderUtils.RenderCommitMessage(commit.Message, nil), ) } diff --git a/routers/web/goget.go b/routers/web/goget.go index 8d5612ebfe..3714dd8eb0 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -65,7 +65,7 @@ func goGet(ctx *context.Context) { insecure = "--insecure " } - goGetImport := context.ComposeGoGetImport(ownerName, trimmedRepoName) + goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 2a5434b414..6dfefbf68d 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -103,9 +103,9 @@ func Projects(ctx *context.Context) { } else { ctx.Data["State"] = "open" } - + renderUtils := templates.NewRenderUtils(ctx) for _, project := range projects { - project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description) + project.RenderedContent = renderUtils.MarkdownToHtml(project.Description) } err = shared_user.LoadHeaderCount(ctx) @@ -435,7 +435,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["SelectLabels"] = selectLabels ctx.Data["AssigneeID"] = assigneeID - project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description) + project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description) ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 507b5af9d9..1ee6e98afb 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -17,7 +17,6 @@ import ( "sort" "strconv" "strings" - "time" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" @@ -45,9 +44,9 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/utils" shared_user "code.gitea.io/gitea/routers/web/shared/user" asymkey_service "code.gitea.io/gitea/services/asymkey" @@ -2215,7 +2214,7 @@ func GetIssueInfo(ctx *context.Context) { ctx.JSON(http.StatusOK, map[string]any{ "convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue), - "renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue), + "renderedLabels": templates.NewRenderUtils(ctx).RenderLabels(issue.Labels, ctx.Repo.RepoLink, issue), }) } @@ -2329,7 +2328,6 @@ func UpdateIssueContent(ctx *context.Context) { // UpdateIssueDeadline updates an issue deadline func UpdateIssueDeadline(ctx *context.Context) { - form := web.GetForm(ctx).(*api.EditDeadlineOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { @@ -2345,20 +2343,13 @@ func UpdateIssueDeadline(ctx *context.Context) { return } - var deadlineUnix timeutil.TimeStamp - var deadline time.Time - if form.Deadline != nil && !form.Deadline.IsZero() { - deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), - 23, 59, 59, 0, time.Local) - deadlineUnix = timeutil.TimeStamp(deadline.Unix()) - } - + deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline")) if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) return } - ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) + ctx.JSONRedirect("") } // UpdateIssueMilestone change issue's milestone diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index e4ee025875..5c0972188c 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/url" - "time" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" @@ -16,8 +15,8 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/issue" @@ -134,22 +133,18 @@ func NewMilestonePost(ctx *context.Context) { return } - if len(form.Deadline) == 0 { - form.Deadline = "9999-12-31" - } - deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) + deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } - deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) - if err = issues_model.NewMilestone(ctx, &issues_model.Milestone{ + if err := issues_model.NewMilestone(ctx, &issues_model.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Content, - DeadlineUnix: timeutil.TimeStamp(deadline.Unix()), + DeadlineUnix: deadlineUnix, }); err != nil { ctx.ServerError("NewMilestone", err) return @@ -194,17 +189,13 @@ func EditMilestonePost(ctx *context.Context) { return } - if len(form.Deadline) == 0 { - form.Deadline = "9999-12-31" - } - deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) + deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } - deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { @@ -216,7 +207,7 @@ func EditMilestonePost(ctx *context.Context) { } m.Name = form.Title m.Content = form.Content - m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix()) + m.DeadlineUnix = deadlineUnix if err = issues_model.UpdateMilestone(ctx, m, m.IsClosed); err != nil { ctx.ServerError("UpdateMilestone", err) return diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index 31f9f76d0f..18ecff8250 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -14,7 +14,6 @@ import ( unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/mailer" @@ -100,12 +99,12 @@ func CollaborationPost(ctx *context.Context) { } } - if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { + if err = repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, u, perm.AccessModeWrite); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") } else { - ctx.ServerError("AddCollaborator", err) + ctx.ServerError("AddOrUpdateCollaborator", err) } return } diff --git a/routers/web/web.go b/routers/web/web.go index 83d116babd..29dd8a8edc 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) { m.Group("/{index}", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) - m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) + m.Post("/deadline", repo.UpdateIssueDeadline) m.Post("/watch", repo.IssueWatch) m.Post("/ref", repo.UpdateIssueRef) m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin) @@ -1323,7 +1323,7 @@ func registerRoutes(m *web.Router) { m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), - repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) + repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) m.Post("/tags/delete", repo.DeleteTag, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef()) }, ignSignIn, context.RepoAssignment, reqRepoCodeReader) @@ -1337,7 +1337,7 @@ func registerRoutes(m *web.Router) { m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), - repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) + repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload) m.Group("/releases", func() { @@ -1535,7 +1535,7 @@ func registerRoutes(m *web.Router) { m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS) m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS) // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownloadOrLFS) + m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownloadOrLFS) }, repo.MustBeNotEmpty) m.Group("/raw", func() { @@ -1544,7 +1544,7 @@ func registerRoutes(m *web.Router) { m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload) m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID) // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload) + m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownload) }, repo.MustBeNotEmpty) m.Group("/render", func() { @@ -1559,7 +1559,7 @@ func registerRoutes(m *web.Router) { m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits) // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits) + m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.RefCommits) }, repo.MustBeNotEmpty) m.Group("/blame", func() { @@ -1582,7 +1582,7 @@ func registerRoutes(m *web.Router) { m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home) m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home) m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home) - m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home) // "/*" route is deprecated, and kept for backward compatibility + m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.Home) // "/*" route is deprecated, and kept for backward compatibility }, repo.SetEditorconfigIfExists) m.Get("/forks", context.RepoRef(), repo.Forks) diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 8d86ec4dfa..7f52c9d31b 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -128,18 +128,16 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er if err != nil { return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err) } - if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{ + status := git_model.CommitStatus{ SHA: sha, TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index), Description: description, Context: ctxname, CreatorID: creator.ID, State: state, - }); err != nil { - return fmt.Errorf("NewCommitStatus: %w", err) } - return nil + return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status) } func toCommitStatus(status actions_model.Status) api.CommitStatusState { diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go index 0fd51e4fa5..ccb97c66c8 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -50,3 +50,12 @@ func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize) } + +// UpdateAttachment updates an attachment, verifying that its name is among the allowed types. +func UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error { + if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil { + return err + } + + return repo_model.UpdateAttachment(ctx, attach) +} diff --git a/services/automerge/notify.go b/services/automerge/notify.go index cb078214f6..b6bbca333b 100644 --- a/services/automerge/notify.go +++ b/services/automerge/notify.go @@ -6,9 +6,12 @@ package automerge import ( "context" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/repository" notify_service "code.gitea.io/gitea/services/notify" ) @@ -44,3 +47,11 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo // as reviews could have blocked a pending automerge let's recheck StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest) } + +func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { + if status.State.IsSuccess() { + if err := StartPRCheckAndAutoMergeBySHA(ctx, commit.Sha1, repo); err != nil { + log.Error("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, sender.ID, commit.Sha1, err) + } + } +} diff --git a/services/context/api.go b/services/context/api.go index 00cfd6afd9..b45e80a329 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" @@ -306,24 +305,8 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } - if ref := ctx.FormTrim("ref"); len(ref) > 0 { - commit, err := ctx.Repo.GitRepo.GetCommit(ref) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound() - } else { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) - } - return - } - ctx.Repo.Commit = commit - ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - ctx.Repo.TreePath = ctx.PathParam("*") - next.ServeHTTP(w, req) - return - } - - refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny) + // NOTICE: the "ref" here for internal usage only (e.g. woodpecker) + refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.FormTrim("ref")) var err error if ctx.Repo.GitRepo.IsBranchExist(refName) { diff --git a/services/context/context.go b/services/context/context.go index 42f7c3d9d1..6c7128ef68 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -100,6 +100,7 @@ func NewTemplateContextForWeb(ctx *Context) TemplateContext { tmplCtx := NewTemplateContext(ctx) tmplCtx["Locale"] = ctx.Base.Locale tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx) + tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx) tmplCtx["RootData"] = ctx.Data tmplCtx["Consts"] = map[string]any{ "RepoUnitTypeCode": unit.TypeCode, @@ -154,7 +155,9 @@ func Contexter() func(next http.Handler) http.Handler { ctx := NewWebContext(base, rnd, session.GetContextSession(req)) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this + if setting.IsProd && !setting.IsInTesting { + ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this + } ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["Link"] = ctx.Link diff --git a/services/context/repo.go b/services/context/repo.go index 2df2b7ea40..e7b32d6283 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/httplib" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -306,11 +307,9 @@ func RetrieveTemplateRepo(ctx *Context, repo *repo_model.Repository) { } // ComposeGoGetImport returns go-get-import meta content. -func ComposeGoGetImport(owner, repo string) string { - /// setting.AppUrl is guaranteed to be parse as url - appURL, _ := url.Parse(setting.AppURL) - - return path.Join(appURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) +func ComposeGoGetImport(ctx context.Context, owner, repo string) string { + curAppURL, _ := url.Parse(httplib.GuessCurrentAppURL(ctx)) + return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) } // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 @@ -332,7 +331,7 @@ func EarlyResponseForGoGetMeta(ctx *Context) { } else { cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame) } - goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), cloneURL) + goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) ctx.PlainText(http.StatusOK, htmlMeta) } @@ -744,7 +743,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } if ctx.FormString("go-get") == "1" { - ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name) + ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, owner.Name, repo.Name) fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" @@ -756,19 +755,11 @@ func RepoAssignment(ctx *Context) context.CancelFunc { type RepoRefType int const ( - // RepoRefLegacy unknown type, make educated guess and redirect. - // for backward compatibility with previous URL scheme - RepoRefLegacy RepoRefType = iota - // RepoRefAny is for usage where educated guess is needed - // but redirect can not be made - RepoRefAny - // RepoRefBranch branch + // RepoRefUnknown is for legacy support, makes the code to "guess" the ref type + RepoRefUnknown RepoRefType = iota RepoRefBranch - // RepoRefTag tag RepoRefTag - // RepoRefCommit commit RepoRefCommit - // RepoRefBlob blob RepoRefBlob ) @@ -781,22 +772,6 @@ func RepoRef() func(*Context) context.CancelFunc { return RepoRefByType(RepoRefBranch) } -// RefTypeIncludesBranches returns true if ref type can be a branch -func (rt RepoRefType) RefTypeIncludesBranches() bool { - if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefBranch { - return true - } - return false -} - -// RefTypeIncludesTags returns true if ref type can be a tag -func (rt RepoRefType) RefTypeIncludesTags() bool { - if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefTag { - return true - } - return false -} - func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string { refName := "" parts := strings.Split(path, "/") @@ -810,28 +785,50 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } +func isStringLikelyCommitID(objFmt git.ObjectFormat, s string, minLength ...int) bool { + minLen := util.OptionalArg(minLength, objFmt.FullLength()) + if len(s) < minLen || len(s) > objFmt.FullLength() { + return false + } + for _, c := range s { + isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + if !isHex { + return false + } + } + return true +} + +func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { + extraRef := util.OptionalArg(optionalExtraRef) + reqPath := ctx.PathParam("*") + reqPath = path.Join(extraRef, reqPath) + + if refName := getRefName(ctx, repo, RepoRefBranch); refName != "" { + return refName, RepoRefBranch + } + if refName := getRefName(ctx, repo, RepoRefTag); refName != "" { + return refName, RepoRefTag + } + + // For legacy support only full commit sha + parts := strings.Split(reqPath, "/") + if isStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { + // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists + repo.TreePath = strings.Join(parts[1:], "/") + return parts[0], RepoRefCommit + } + + if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 { + return refName, RepoRefBlob + } + repo.TreePath = reqPath + return repo.Repository.DefaultBranch, RepoRefBranch +} + func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { path := ctx.PathParam("*") switch pathType { - case RepoRefLegacy, RepoRefAny: - if refName := getRefName(ctx, repo, RepoRefBranch); len(refName) > 0 { - return refName - } - if refName := getRefName(ctx, repo, RepoRefTag); len(refName) > 0 { - return refName - } - // For legacy and API support only full commit sha - parts := strings.Split(path, "/") - - if len(parts) > 0 && len(parts[0]) == git.ObjectFormatFromName(repo.Repository.ObjectFormatName).FullLength() { - repo.TreePath = strings.Join(parts[1:], "/") - return parts[0] - } - if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 { - return refName - } - repo.TreePath = path - return repo.Repository.DefaultBranch case RepoRefBranch: ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist) if len(ref) == 0 { @@ -866,13 +863,13 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") - - if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= repo.GetObjectFormat().FullLength() { + if isStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { + // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } - if len(parts) > 0 && parts[0] == headRefName { + if parts[0] == headRefName { // HEAD ref points to last default branch commit commit, err := repo.GitRepo.GetBranchCommit(repo.Repository.DefaultBranch) if err != nil { @@ -888,15 +885,21 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { } return path default: - log.Error("Unrecognized path type: %v", path) + panic(fmt.Sprintf("Unrecognized path type: %v", pathType)) } return "" } +type RepoRefByTypeOptions struct { + IgnoreNotExistErr bool +} + // RepoRefByType handles repository reference name for a specific type // of repository reference -func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context) context.CancelFunc { +func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) context.CancelFunc { + opt := util.OptionalArg(opts) return func(ctx *Context) (cancel context.CancelFunc) { + refType := detectRefType // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { // assume the user is viewing the (non-existent) default branch @@ -956,7 +959,12 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context } ctx.Repo.IsViewBranch = true } else { - refName = getRefName(ctx.Base, ctx.Repo, refType) + guessLegacyPath := refType == RepoRefUnknown + if guessLegacyPath { + refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo) + } else { + refName = getRefName(ctx.Base, ctx.Repo, refType) + } ctx.Repo.RefName = refName isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) if isRenamedBranch && has { @@ -967,7 +975,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } - if refType.RefTypeIncludesBranches() && ctx.Repo.GitRepo.IsBranchExist(refName) { + if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { ctx.Repo.IsViewBranch = true ctx.Repo.BranchName = refName @@ -977,7 +985,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if refType.RefTypeIncludesTags() && ctx.Repo.GitRepo.IsTagExist(refName) { + } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { ctx.Repo.IsViewTag = true ctx.Repo.TagName = refName @@ -991,7 +999,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) >= 7 && len(refName) <= ctx.Repo.GetObjectFormat().FullLength() { + } else if isStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { ctx.Repo.IsViewCommit = true ctx.Repo.CommitID = refName @@ -1002,18 +1010,18 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context } // If short commit ID add canonical link header if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { - ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"", - util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)))) + canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)) + ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL)) } } else { - if len(ignoreNotExistErr) > 0 && ignoreNotExistErr[0] { + if opt.IgnoreNotExistErr { return cancel } ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) return cancel } - if refType == RepoRefLegacy { + if guessLegacyPath { // redirect from old URL scheme to new URL scheme prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.PathParam("*"))), strings.ToLower(ctx.Repo.RepoLink)) redirect := path.Join( diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 7123420e99..cefd13ebb6 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -28,12 +28,13 @@ func IsErrFileTypeForbidden(err error) bool { } func (err ErrFileTypeForbidden) Error() string { - return "This file extension or type is not allowed to be uploaded." + return "This file cannot be uploaded or modified due to a forbidden file extension or type." } var wildcardTypeRe = regexp.MustCompile(`^[a-z]+/\*$`) -// Verify validates whether a file is allowed to be uploaded. +// Verify validates whether a file is allowed to be uploaded. If buf is empty, it will just check if the file +// has an allowed file extension. func Verify(buf []byte, fileName, allowedTypesStr string) error { allowedTypesStr = strings.ReplaceAll(allowedTypesStr, "|", ",") // compat for old config format @@ -56,21 +57,31 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error { return ErrFileTypeForbidden{Type: fullMimeType} } extension := strings.ToLower(path.Ext(fileName)) + isBufEmpty := len(buf) <= 1 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers for _, allowEntry := range allowedTypes { if allowEntry == "*/*" { return nil // everything allowed - } else if strings.HasPrefix(allowEntry, ".") && allowEntry == extension { + } + if strings.HasPrefix(allowEntry, ".") && allowEntry == extension { return nil // extension is allowed - } else if mimeType == allowEntry { + } + if isBufEmpty { + continue // skip mime type checks if buffer is empty + } + if mimeType == allowEntry { return nil // mime type is allowed - } else if wildcardTypeRe.MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) { + } + if wildcardTypeRe.MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) { return nil // wildcard match, e.g. image/* } } - log.Info("Attachment with type %s blocked from upload", fullMimeType) + if !isBufEmpty { + log.Info("Attachment with type %s blocked from upload", fullMimeType) + } + return ErrFileTypeForbidden{Type: fullMimeType} } diff --git a/services/convert/issue.go b/services/convert/issue.go index f514dc4313..e3124efd64 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -260,7 +260,7 @@ func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone { if m.IsClosed { apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() } - if m.DeadlineUnix.Year() < 9999 { + if m.DeadlineUnix > 0 { apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() } return apiMilestone diff --git a/services/feed/action_test.go b/services/feed/action_test.go index e1b071d8f6..60cf7fbb49 100644 --- a/services/feed/action_test.go +++ b/services/feed/action_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" "github.com/stretchr/testify/assert" diff --git a/services/issue/main_test.go b/services/issue/main_test.go index 5dac54183b..819c5d98c3 100644 --- a/services/issue/main_test.go +++ b/services/issue/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index f803c736ca..5591bea02b 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" ) diff --git a/services/notify/notifier.go b/services/notify/notifier.go index ed053a812a..29bbb5702b 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -6,6 +6,7 @@ package notify import ( "context" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -74,4 +75,6 @@ type Notifier interface { PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) + + CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) } diff --git a/services/notify/notify.go b/services/notify/notify.go index 0c8262ef7a..3b5f24340b 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -6,6 +6,7 @@ package notify import ( "context" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -367,3 +368,9 @@ func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) { notifier.ChangeDefaultBranch(ctx, repo) } } + +func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { + for _, notifier := range notifiers { + notifier.CreateCommitStatus(ctx, repo, commit, sender, status) + } +} diff --git a/services/notify/null.go b/services/notify/null.go index dddd421bef..7354efd701 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -6,6 +6,7 @@ package notify import ( "context" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -208,3 +209,6 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p // ChangeDefaultBranch places a place holder function func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) { } + +func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { +} diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 3d6fe71a09..615f4d482c 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -66,7 +66,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR } } - if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { + if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { return err } diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go index 4a43ae2a28..abe0489fc5 100644 --- a/services/repository/collaboration.go +++ b/services/repository/collaboration.go @@ -9,11 +9,60 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + + "xorm.io/builder" ) +func AddOrUpdateCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User, mode perm.AccessMode) error { + // only allow valid access modes, read, write and admin + if mode < perm.AccessModeRead || mode > perm.AccessModeAdmin { + return perm.ErrInvalidAccessMode + } + + if err := repo.LoadOwner(ctx); err != nil { + return err + } + + if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) { + return user_model.ErrBlockedUser + } + + return db.WithTx(ctx, func(ctx context.Context) error { + collaboration, has, err := db.Get[repo_model.Collaboration](ctx, builder.Eq{ + "repo_id": repo.ID, + "user_id": u.ID, + }) + if err != nil { + return err + } else if has { + if collaboration.Mode == mode { + return nil + } + if _, err = db.GetEngine(ctx). + Where("repo_id=?", repo.ID). + And("user_id=?", u.ID). + Cols("mode"). + Update(&repo_model.Collaboration{ + Mode: mode, + }); err != nil { + return err + } + } else if err = db.Insert(ctx, &repo_model.Collaboration{ + RepoID: repo.ID, + UserID: u.ID, + Mode: mode, + }); err != nil { + return err + } + + return access_model.RecalculateUserAccess(ctx, repo, u.ID) + }) +} + // DeleteCollaboration removes collaboration relation between the user and repository. func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) { collaboration := &repo_model.Collaboration{ diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go index a2eb06b81a..2b9a5d0b8b 100644 --- a/services/repository/collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -14,6 +15,21 @@ import ( "github.com/stretchr/testify/assert" ) +func TestRepository_AddCollaborator(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testSuccess := func(repoID, userID int64) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + assert.NoError(t, AddOrUpdateCollaborator(db.DefaultContext, repo, user, perm.AccessModeWrite)) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}, &user_model.User{ID: userID}) + } + testSuccess(1, 4) + testSuccess(1, 4) + testSuccess(3, 4) +} + func TestRepository_DeleteCollaboration(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go index adc59abed8..f369a303e6 100644 --- a/services/repository/commitstatus/commitstatus.go +++ b/services/repository/commitstatus/commitstatus.go @@ -18,8 +18,9 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/services/automerge" + "code.gitea.io/gitea/services/notify" ) func getCacheKey(repoID int64, brancheName string) string { @@ -103,6 +104,8 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato return err } + notify.CreateCommitStatus(ctx, repo, repo_module.CommitToPushCommit(commit), creator, status) + defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) if err != nil { return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err) @@ -114,12 +117,6 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato } } - if status.State.IsSuccess() { - if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil { - return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) - } - } - return nil } diff --git a/services/repository/create.go b/services/repository/create.go index 282b2d3e58..261ac7fccc 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -12,9 +12,15 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" @@ -243,7 +249,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt var rollbackRepo *repo_model.Repository if err := db.WithTx(ctx, func(ctx context.Context) error { - if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { + if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { return err } @@ -335,3 +341,136 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt return repo, nil } + +// CreateRepositoryByExample creates a repository for the user/organization. +func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) { + if err = repo_model.IsUsableRepoName(repo.Name); err != nil { + return err + } + + has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %w", err) + } else if has { + return repo_model.ErrRepoAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + repoPath := repo_model.RepoPath(u.Name, repo.Name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if !overwriteOrAdopt && isExist { + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return repo_model.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + if err = db.Insert(ctx, repo); err != nil { + return err + } + if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil { + return err + } + + // insert units for repo + defaultUnits := unit.DefaultRepoUnits + if isFork { + defaultUnits = unit.DefaultForkRepoUnits + } + units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) + for _, tp := range defaultUnits { + if tp == unit.TypeIssues { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.IssuesConfig{ + EnableTimetracker: setting.Service.DefaultEnableTimetracking, + AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, + EnableDependencies: setting.Service.DefaultEnableDependencies, + }, + }) + } else if tp == unit.TypePullRequests { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.PullRequestsConfig{ + AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, + DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), + AllowRebaseUpdate: true, + }, + }) + } else if tp == unit.TypeProjects { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, + }) + } else { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: tp, + }) + } + } + + if err = db.Insert(ctx, units); err != nil { + return err + } + + // Remember visibility preference. + u.LastRepoVisibility = repo.IsPrivate + if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil { + return fmt.Errorf("UpdateUserCols: %w", err) + } + + if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil { + return fmt.Errorf("IncrUserRepoNum: %w", err) + } + u.NumRepos++ + + // Give access to all members in teams with access to all repositories. + if u.IsOrganization() { + teams, err := organization.FindOrgTeams(ctx, u.ID) + if err != nil { + return fmt.Errorf("FindOrgTeams: %w", err) + } + for _, t := range teams { + if t.IncludesAllRepositories { + if err := models.AddRepository(ctx, t, repo); err != nil { + return fmt.Errorf("AddRepository: %w", err) + } + } + } + + if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil { + return fmt.Errorf("IsUserRepoAdmin: %w", err) + } else if !isAdmin { + // Make creator repo admin if it wasn't assigned automatically + if err = AddOrUpdateCollaborator(ctx, repo, doer, perm.AccessModeAdmin); err != nil { + return fmt.Errorf("AddCollaborator: %w", err) + } + } + } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { + // Organization automatically called this in AddRepository method. + return fmt.Errorf("RecalculateAccesses: %w", err) + } + + if setting.Service.AutoWatchNewRepos { + if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { + return fmt.Errorf("WatchRepo: %w", err) + } + } + + if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil { + return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err) + } + + return nil +} diff --git a/services/repository/fork.go b/services/repository/fork.go index e114555679..5b24015a03 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -134,7 +134,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork }() err = db.WithTx(ctx, func(txCtx context.Context) error { - if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil { + if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil { return err } diff --git a/services/repository/generate.go b/services/repository/generate.go index 2b95bbcd4d..f2280de8b2 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -343,7 +343,7 @@ func generateRepository(ctx context.Context, doer, owner *user_model.User, templ ObjectFormatName: templateRepo.ObjectFormatName, } - if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil { + if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil { return nil, err } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 7ad6b46fa4..301d895337 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -20,7 +20,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -419,10 +418,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use return err } if !hasAccess { - if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil { - return err - } - if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil { + if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { return err } } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 38fad7f5e8..cc263947e9 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -6,6 +6,7 @@ package webhook import ( "context" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" @@ -861,6 +862,36 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode } } +func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { + apiSender := convert.ToUser(ctx, sender, nil) + apiCommit, err := repository.ToAPIPayloadCommit(ctx, map[string]*user_model.User{}, repo.RepoPath(), repo.HTMLURL(), commit) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + + payload := api.CommitStatusPayload{ + Context: status.Context, + CreatedAt: status.CreatedUnix.AsTime().UTC(), + Description: status.Description, + ID: status.ID, + SHA: commit.Sha1, + State: status.State.String(), + TargetURL: status.TargetURL, + + Commit: apiCommit, + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), + Sender: apiSender, + } + if !status.UpdatedUnix.IsZero() { + t := status.UpdatedUnix.AsTime().UTC() + payload.UpdatedAt = &t + } + if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventStatus, &payload); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +} + func (m *webhookNotifier) SyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { m.CreateRef(ctx, pusher, repo, refFullName, refID) } diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 22e08e9c8f..c0c7235e3b 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -21,10 +21,10 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{if or .Participants .Assignees .MentionableTeams}} mentionValues: Array.from(new Map([ {{- range .Participants -}} - ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}], + ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink ctx}}'}], {{- end -}} {{- range .Assignees -}} - ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}], + ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink ctx}}'}], {{- end -}} {{- range .MentionableTeams -}} ['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}', name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}], diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 742e83834d..219b1255c0 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -49,7 +49,7 @@ - {{$description := .DescriptionHTML $.Context}} + {{$description := .DescriptionHTML ctx}} {{if $description}}{{.PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.LongDescription}}{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.LongDescription}}{{end}} + {{if .PackageDescriptor.Metadata.LongDescription}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}}{{end}}{{if .PackageDescriptor.Metadata.Summary}}{{.PackageDescriptor.Metadata.Summary}}{{end}}
{{if .PackageDescriptor.Metadata.LongDescription}} - {{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.LongDescription}} + {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}} {{else if .PackageDescriptor.Metadata.Description}} - {{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}} + {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Description}} {{end}}{{template "repo/issue/labels/label_archived" .}}
{{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 6e2a5570c7..5484024ff8 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -27,7 +27,7 @@ {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}
+{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}
{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}
+{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}
{{end}}{{RenderCommitBody $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}+
{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeMetas ctx)}}{{end}} {{template "repo/commit_load_branches_and_tags" .}}
{{.item.Attributes.label}}{{if .item.Validations.required}}{{end}}
{{end}} {{if .item.Attributes.description}} - {{RenderMarkdownToHtml .Context .item.Attributes.description}} + {{ctx.RenderUtils.MarkdownToHtml .item.Attributes.description}} {{end}} diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl index 934699ed05..f6995328dd 100644 --- a/templates/repo/issue/fields/markdown.tmpl +++ b/templates/repo/issue/fields/markdown.tmpl @@ -1,3 +1,3 @@{{template "repo/issue/labels/label_archived" .}}
{{end}} diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl index 3651ba118f..f40c792da7 100644 --- a/templates/repo/issue/labels/label.tmpl +++ b/templates/repo/issue/labels/label.tmpl @@ -3,5 +3,5 @@ id="label_{{.label.ID}}" href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}} > - {{- RenderLabel $.Context ctx.Locale .label -}} + {{- ctx.RenderUtils.RenderLabel .label -}} diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 413d6405b2..07a548a8af 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -30,8 +30,8 @@ {{range .Labels}}{{.Description | RenderEmoji $.Context}}{{end}} + {{ctx.RenderUtils.RenderLabel .}} + {{if .Description}}
{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}}
{{.Description | RenderEmoji $.Context}}{{end}} + {{ctx.RenderUtils.RenderLabel .}} + {{if .Description}}
{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}}
{{.Description | RenderEmoji $.Context}}{{end}} + {{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}} {{ctx.RenderUtils.RenderLabel .}} + {{if .Description}}
{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}}
{{template "repo/issue/labels/label_archived" .}}
{{end}} @@ -34,8 +34,8 @@ {{end}} {{$previousExclusiveScope = $exclusiveScope}} - {{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}} {{RenderLabel $.Context ctx.Locale .}} - {{if .Description}}{{.Description | RenderEmoji $.Context}}{{end}} + {{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}} {{ctx.RenderUtils.RenderLabel .}} + {{if .Description}}
{{.Description | ctx.RenderUtils.RenderEmoji}}{{end}}
{{template "repo/issue/labels/label_archived" .}}
{{end}} diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl index 9f32df00e3..736a75d73a 100644 --- a/templates/repo/issue/milestone_new.tmpl +++ b/templates/repo/issue/milestone_new.tmpl @@ -30,9 +30,9 @@-
{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}
+ {{ctx.Locale.Tr "repo.issues.due_date_not_set"}} {{end}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} -- {{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} + {{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} #{{.Issue.Index}}
{{ctx.Locale.Tr "repo.activity.merged_prs_label"}} - #{{.Index}} {{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}} + #{{.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{DateUtils.TimeSince .MergedUnix}}
{{end}} @@ -164,7 +164,7 @@ {{range .Activity.OpenedPRs}}{{ctx.Locale.Tr "repo.activity.opened_prs_label"}} - #{{.Index}} {{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}} + #{{.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{DateUtils.TimeSince .Issue.CreatedUnix}}
{{end}} @@ -183,7 +183,7 @@ {{range .Activity.ClosedIssues}}{{ctx.Locale.Tr "repo.activity.closed_issue_label"}} - #{{.Index}} {{.Title | RenderEmoji $.Context | RenderCodeBlock}} + #{{.Index}} {{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{DateUtils.TimeSince .ClosedUnix}}
{{end}} @@ -202,7 +202,7 @@ {{range .Activity.OpenedIssues}}{{ctx.Locale.Tr "repo.activity.new_issue_label"}} - #{{.Index}} {{.Title | RenderEmoji $.Context | RenderCodeBlock}} + #{{.Index}} {{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{DateUtils.TimeSince .CreatedUnix}}
{{end}} @@ -220,9 +220,9 @@ {{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}} #{{.Index}} {{if .IsPull}} - {{.Title | RenderEmoji $.Context | RenderCodeBlock}} + {{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{else}} - {{.Title | RenderEmoji $.Context | RenderCodeBlock}} + {{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{end}} {{DateUtils.TimeSince .UpdatedUnix}} diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index dcf070f6ac..9f90f0a2b9 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -69,7 +69,7 @@ {{if or (eq .AccessMode 1) (eq .AccessMode 2)}} {{$first := true}}{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}
{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}
{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}
{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}
h
`))).toBe('# h'); + expect(convertHtmlToMarkdown(h(`txt`))).toBe('**txt**'); + expect(convertHtmlToMarkdown(h(`txt`))).toBe('_txt_'); + expect(convertHtmlToMarkdown(h(`txt`))).toBe('~~txt~~'); + + expect(convertHtmlToMarkdown(h(`txt`))).toBe('[txt](link)'); + expect(convertHtmlToMarkdown(h(`https://link`))).toBe('https://link'); + + expect(convertHtmlToMarkdown(h(`txt
`))).toBe('txt\n'); + expect(convertHtmlToMarkdown(h(` `))).toBe('> a\n> b\n'); + + expect(convertHtmlToMarkdown(h(`- a
- b
`))).toBe('1. a\n * b\n\n'); + expect(convertHtmlToMarkdown(h(`- a
`))).toBe('1. [x] a\n'); +}); diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts new file mode 100644 index 0000000000..c690e0c8b1 --- /dev/null +++ b/web_src/js/markup/html2markdown.ts @@ -0,0 +1,119 @@ +import {htmlEscape} from 'escape-goat'; + +type Processors = { + [tagName: string]: (el: HTMLElement) => string | HTMLElement | void; +} + +type ProcessorContext = { + elementIsFirst: boolean; + elementIsLast: boolean; + listNestingLevel: number; +} + +function prepareProcessors(ctx:ProcessorContext): Processors { + const processors = { + H1(el) { + const level = parseInt(el.tagName.slice(1)); + el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; + }, + STRONG(el) { + return `**${el.textContent}**`; + }, + EM(el) { + return `_${el.textContent}_`; + }, + DEL(el) { + return `~~${el.textContent}~~`; + }, + + A(el) { + const text = el.textContent || 'link'; + const href = el.getAttribute('href'); + if (/^https?:/.test(text) && text === href) { + return text; + } + return href ? `[${text}](${href})` : text; + }, + IMG(el) { + const alt = el.getAttribute('alt') || 'image'; + const src = el.getAttribute('src'); + const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : ''; + const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : ''; + if (widthAttr || heightAttr) { + return `