From 653704c102f7eb389adcbbcfd0e5a9e9aaa5b227 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 29 Jun 2021 21:42:47 +0200 Subject: [PATCH 01/19] Add Vultr and DO Marketplace links (#16297) * fix emoji img path * move cloudron * Add Vultr and DO --- custom/conf/app.example.ini | 4 +- .../doc/advanced/config-cheat-sheet.en-us.md | 4 +- .../doc/installation/from-package.en-us.md | 14 +----- .../doc/installation/on-cloud-provider.md | 44 +++++++++++++++++++ 4 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 docs/content/doc/installation/on-cloud-provider.md diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 7cae16cd79ab..900e8b25ecdc 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1031,11 +1031,11 @@ PATH = ;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. -;; For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png +;; For custom reactions, add a tightly cropped square image to public/img/emoji/reaction_name.png ;REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes ;; ;; Additional Emojis not defined in the utf8 standard -;; By default we support gitea (:gitea:), to add more copy them to public/emoji/img/emoji_name.png and add it to this config. +;; By default we support gitea (:gitea:), to add more copy them to public/img/emoji/emoji_name.png and add it to this config. ;; Dont mistake it for Reactions. ;CUSTOM_EMOJIS = gitea ;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index dc3b36cb4fc1..2b73f4365838 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -180,9 +180,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `REACTIONS`: All available reactions users can choose on issues/prs and comments Values can be emoji alias (:smile:) or a unicode emoji. - For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png + For custom reactions, add a tightly cropped square image to public/img/emoji/reaction_name.png - `CUSTOM_EMOJIS`: **gitea**: Additional Emojis not defined in the utf8 standard. - By default we support gitea (:gitea:), to add more copy them to public/emoji/img/emoji_name.png and + By default we support gitea (:gitea:), to add more copy them to public/img/emoji/emoji_name.png and add it to this config. - `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. - `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page. diff --git a/docs/content/doc/installation/from-package.en-us.md b/docs/content/doc/installation/from-package.en-us.md index cdb5833e33ca..bc349ba42ef9 100644 --- a/docs/content/doc/installation/from-package.en-us.md +++ b/docs/content/doc/installation/from-package.en-us.md @@ -2,7 +2,7 @@ date: "2016-12-01T16:00:00+02:00" title: "Installation from package" slug: "install-from-package" -weight: 10 +weight: 20 toc: false draft: false menu: @@ -92,18 +92,6 @@ is in `/usr/local/etc/rc.d/gitea`. To enable Gitea to run as a service, run `sysrc gitea_enable=YES` and start it with `service gitea start`. -## Cloudron - -Gitea is available as a 1-click install on [Cloudron](https://cloudron.io). -Cloudron makes it easy to run apps like Gitea on your server and keep them up-to-date and secure. - -[![Install](/cloudron.svg)](https://cloudron.io/button.html?app=io.gitea.cloudronapp) - -The Gitea package is maintained [here](https://git.cloudron.io/cloudron/gitea-app). - -There is a [demo instance](https://my.demo.cloudron.io) (username: cloudron password: cloudron) where -you can experiment with running Gitea. - ## Third-party Various other third-party packages of Gitea exist. diff --git a/docs/content/doc/installation/on-cloud-provider.md b/docs/content/doc/installation/on-cloud-provider.md new file mode 100644 index 000000000000..c61c042af242 --- /dev/null +++ b/docs/content/doc/installation/on-cloud-provider.md @@ -0,0 +1,44 @@ +--- +date: "2016-12-01T16:00:00+02:00" +title: "Install on Cloud Provider" +slug: "install-on-cloud-provider" +weight: 20 +toc: false +draft: false +menu: + sidebar: + parent: "installation" + name: "On cloud provider" + weight: 20 + identifier: "install-on-cloud-provider" +--- + +# Installation on Cloud Provider + +**Table of Contents** + +{{< toc >}} + +## Cloudron + +Gitea is available as a 1-click install on [Cloudron](https://cloudron.io). +Cloudron makes it easy to run apps like Gitea on your server and keep them up-to-date and secure. + +[![Install](/cloudron.svg)](https://cloudron.io/button.html?app=io.gitea.cloudronapp) + +The Gitea package is maintained [here](https://git.cloudron.io/cloudron/gitea-app). + +There is a [demo instance](https://my.demo.cloudron.io) (username: cloudron password: cloudron) where +you can experiment with running Gitea. + +## Vultr + +Gitea can found in [Vultr](https://www.vultr.com)'s marketplace. + +To deploy it have a look at https://www.vultr.com/marketplace/apps/gitea. + +## DigitalOcean + +[DigitalOcean](https://www.digitalocean.com) has gitea as droplet in his marketplace. + +Just create a new [Gitea Droplet](https://marketplace.digitalocean.com/apps/gitea). From add74fb368b4b6a5deee91e052240c0956d7dc5b Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 29 Jun 2021 21:12:43 +0100 Subject: [PATCH 02/19] Fix panic in recursive cache (#16298) There is a bug with last commit cache recursive cache where the last commit information that refers to the current tree itself will cause a panic due to its path ("") not being included in the expected tree entry paths. This PR fixes this by skipping the missing entry. Fix #16290 Signed-off-by: Andrew Thornton Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick --- modules/git/last_commit_cache_nogogit.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go index ff9f9ff1cfad..faf6e23fa816 100644 --- a/modules/git/last_commit_cache_nogogit.go +++ b/modules/git/last_commit_cache_nogogit.go @@ -94,7 +94,8 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr if err := c.Put(commit.ID.String(), path.Join(treePath, entry), entryCommit.ID.String()); err != nil { return err } - if entryMap[entry].IsDir() { + // entryMap won't contain "" therefore skip this. + if treeEntry := entryMap[entry]; treeEntry != nil && treeEntry.IsDir() { subTree, err := tree.SubTree(entry) if err != nil { return err From dea7a5c5b9fa7eea331159020268f8898a0a678d Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 29 Jun 2021 23:00:02 +0200 Subject: [PATCH 03/19] just add some unit tests (#16291) * code.gitea.io/gitea/routers/utils coverage: 100.0% * code.gitea.io/gitea/routers/install 0% -> 5.0% * ConvertUtf8ToUtf8mb4: make sure DBType is mysql --- models/convert.go | 6 ++++++ routers/install/routes_test.go | 20 ++++++++++++++++++++ routers/utils/utils_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 routers/install/routes_test.go diff --git a/models/convert.go b/models/convert.go index baa63bb38839..1deb7c66fbbd 100644 --- a/models/convert.go +++ b/models/convert.go @@ -8,10 +8,16 @@ import ( "fmt" "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm/schemas" ) // ConvertUtf8ToUtf8mb4 converts database and tables from utf8 to utf8mb4 if it's mysql and set ROW_FORMAT=dynamic func ConvertUtf8ToUtf8mb4() error { + if x.Dialect().URI().DBType != schemas.MYSQL { + return nil + } + _, err := x.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci", setting.Database.Name)) if err != nil { return err diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go new file mode 100644 index 000000000000..35a66c1c4742 --- /dev/null +++ b/routers/install/routes_test.go @@ -0,0 +1,20 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package install + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRoutes(t *testing.T) { + routes := Routes() + assert.NotNil(t, routes) + assert.Len(t, routes.R.Routes(), 1) + assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern) + assert.Nil(t, routes.R.Routes()[0].SubRoutes) + assert.Len(t, routes.R.Routes()[0].Handlers, 2) +} diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index 78ab3d20ee4f..bca526331114 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -62,7 +62,41 @@ func TestIsExternalURL(t *testing.T) { "//try.gitea.io/test?param=false"), newTest(false, "/hey/hey/hey#3244"), + newTest(true, + "://missing protocol scheme"), } { assert.Equal(t, test.Expected, IsExternalURL(test.RawURL)) } } + +func TestSanitizeFlashErrorString(t *testing.T) { + tests := []struct { + name string + arg string + want string + }{ + { + name: "no error", + arg: "", + want: "", + }, + { + name: "normal error", + arg: "can not open file: \"abc.exe\"", + want: "can not open file: "abc.exe"", + }, + { + name: "line break error", + arg: "some error:\n\nawesome!", + want: "some error:

awesome!", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SanitizeFlashErrorString(tt.arg); got != tt.want { + t.Errorf("SanitizeFlashErrorString() = '%v', want '%v'", got, tt.want) + } + }) + } +} From e8c6cead0fb926ced6595c7b22f56b1cc2540435 Mon Sep 17 00:00:00 2001 From: sebastian-sauer Date: Tue, 29 Jun 2021 23:42:23 +0200 Subject: [PATCH 04/19] Fix list_options GetStartEnd (#16303) end is start + pageSize and not start + page --- models/list_options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/list_options.go b/models/list_options.go index 9cccd05465af..ff02933f9bac 100644 --- a/models/list_options.go +++ b/models/list_options.go @@ -41,7 +41,7 @@ func (opts *ListOptions) setEnginePagination(e Engine) Engine { func (opts *ListOptions) GetStartEnd() (start, end int) { opts.setDefaultValues() start = (opts.Page - 1) * opts.PageSize - end = start + opts.Page + end = start + opts.PageSize return } From 09663493543a2ce15fc90fbb3808dda5b87335e5 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 30 Jun 2021 15:23:49 +0800 Subject: [PATCH 05/19] Make the github migration less rate limit waiting to get comment per page from repository but not per issue (#16070) * Make the github migration less rate limit waiting to get comment per page from repository but not per issue * Fix lint * adjust Downloader interface * Fix missed reviews * Fix test * Remove unused struct --- modules/migrations/base/downloader.go | 10 ++- modules/migrations/base/null_downloader.go | 9 ++- modules/migrations/base/retry_downloader.go | 7 +- modules/migrations/gitea_downloader.go | 16 ++-- modules/migrations/gitea_downloader_test.go | 4 +- modules/migrations/github.go | 87 ++++++++++++++++++++- modules/migrations/github_test.go | 4 +- modules/migrations/gitlab.go | 7 +- modules/migrations/gitlab_test.go | 4 +- modules/migrations/gogs.go | 7 +- modules/migrations/gogs_test.go | 4 +- modules/migrations/migrate.go | 71 ++++++++++++----- modules/migrations/restore.go | 14 ++-- 13 files changed, 191 insertions(+), 53 deletions(-) diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index 919f4b52a05f..2388b2dd6e77 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -11,6 +11,13 @@ import ( "code.gitea.io/gitea/modules/structs" ) +// GetCommentOptions represents an options for get comment +type GetCommentOptions struct { + IssueNumber int64 + Page int + PageSize int +} + // Downloader downloads the site repo informations type Downloader interface { SetContext(context.Context) @@ -20,7 +27,8 @@ type Downloader interface { GetReleases() ([]*Release, error) GetLabels() ([]*Label, error) GetIssues(page, perPage int) ([]*Issue, bool, error) - GetComments(issueNumber int64) ([]*Comment, error) + GetComments(opts GetCommentOptions) ([]*Comment, bool, error) + SupportGetRepoComments() bool GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) GetReviews(pullRequestNumber int64) ([]*Review, error) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) diff --git a/modules/migrations/base/null_downloader.go b/modules/migrations/base/null_downloader.go index a93c20339b60..53a536709d1f 100644 --- a/modules/migrations/base/null_downloader.go +++ b/modules/migrations/base/null_downloader.go @@ -51,8 +51,8 @@ func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { } // GetComments returns comments according issueNumber -func (n NullDownloader) GetComments(issueNumber int64) ([]*Comment, error) { - return nil, &ErrNotSupported{Entity: "Comments"} +func (n NullDownloader) GetComments(GetCommentOptions) ([]*Comment, bool, error) { + return nil, false, &ErrNotSupported{Entity: "Comments"} } // GetPullRequests returns pull requests according page and perPage @@ -80,3 +80,8 @@ func (n NullDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) ( } return remoteAddr, nil } + +// SupportGetRepoComments return true if it supports get repo comments +func (n NullDownloader) SupportGetRepoComments() bool { + return false +} diff --git a/modules/migrations/base/retry_downloader.go b/modules/migrations/base/retry_downloader.go index 82a038b98ba7..e6c80038f181 100644 --- a/modules/migrations/base/retry_downloader.go +++ b/modules/migrations/base/retry_downloader.go @@ -150,18 +150,19 @@ func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { } // GetComments returns a repository's comments with retry -func (d *RetryDownloader) GetComments(issueNumber int64) ([]*Comment, error) { +func (d *RetryDownloader) GetComments(opts GetCommentOptions) ([]*Comment, bool, error) { var ( comments []*Comment + isEnd bool err error ) err = d.retry(func() error { - comments, err = d.Downloader.GetComments(issueNumber) + comments, isEnd, err = d.Downloader.GetComments(opts) return err }) - return comments, err + return comments, isEnd, err } // GetPullRequests returns a repository's pull requests with retry diff --git a/modules/migrations/gitea_downloader.go b/modules/migrations/gitea_downloader.go index 40820ae3759c..665466ffeffd 100644 --- a/modules/migrations/gitea_downloader.go +++ b/modules/migrations/gitea_downloader.go @@ -435,37 +435,37 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err } // GetComments returns comments according issueNumber -func (g *GiteaDownloader) GetComments(index int64) ([]*base.Comment, error) { +func (g *GiteaDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { var allComments = make([]*base.Comment, 0, g.maxPerPage) // for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { case <-g.ctx.Done(): - return nil, nil + return nil, false, nil default: } - comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, index, gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{ + comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, opts.IssueNumber, gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{ // PageSize: g.maxPerPage, // Page: i, }}) if err != nil { - return nil, fmt.Errorf("error while listing comments for issue #%d. Error: %v", index, err) + return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %v", opts.IssueNumber, err) } for _, comment := range comments { reactions, err := g.getCommentReactions(comment.ID) if err != nil { - log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", index, comment.ID, g.repoOwner, g.repoName, err) + log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.IssueNumber, comment.ID, g.repoOwner, g.repoName, err) if err2 := models.CreateRepositoryNotice( - fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", index, comment.ID, g.repoOwner, g.repoName, err)); err2 != nil { + fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", opts.IssueNumber, comment.ID, g.repoOwner, g.repoName, err)); err2 != nil { log.Error("create repository notice failed: ", err2) } } allComments = append(allComments, &base.Comment{ - IssueIndex: index, + IssueIndex: opts.IssueNumber, PosterID: comment.Poster.ID, PosterName: comment.Poster.UserName, PosterEmail: comment.Poster.Email, @@ -481,7 +481,7 @@ func (g *GiteaDownloader) GetComments(index int64) ([]*base.Comment, error) { // break // } //} - return allComments, nil + return allComments, true, nil } // GetPullRequests returns pull requests according page and perPage diff --git a/modules/migrations/gitea_downloader_test.go b/modules/migrations/gitea_downloader_test.go index babf038280d7..f62b19897c63 100644 --- a/modules/migrations/gitea_downloader_test.go +++ b/modules/migrations/gitea_downloader_test.go @@ -224,7 +224,9 @@ func TestGiteaDownloadRepo(t *testing.T) { Closed: &closed2, }, issues[1]) - comments, err := downloader.GetComments(4) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: 4, + }) assert.NoError(t, err) assert.Len(t, comments, 2) assert.EqualValues(t, 1598975370, comments[0].Created.Unix()) diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 8a3f5d34c78d..9b897662d030 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "time" @@ -450,8 +451,22 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, return allIssues, len(issues) < perPage, nil } +// SupportGetRepoComments return true if it supports get repo comments +func (g *GithubDownloaderV3) SupportGetRepoComments() bool { + return true +} + // GetComments returns comments according issueNumber -func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) { +func (g *GithubDownloaderV3) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { + if opts.IssueNumber > 0 { + comments, err := g.getComments(opts.IssueNumber) + return comments, false, err + } + + return g.GetAllComments(opts.Page, opts.PageSize) +} + +func (g *GithubDownloaderV3) getComments(issueNumber int64) ([]*base.Comment, error) { var ( allComments = make([]*base.Comment, 0, g.maxPerPage) created = "created" @@ -519,6 +534,75 @@ func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, er return allComments, nil } +// GetAllComments returns repository comments according page and perPageSize +func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) { + var ( + allComments = make([]*base.Comment, 0, perPage) + created = "created" + asc = "asc" + ) + opt := &github.IssueListCommentsOptions{ + Sort: &created, + Direction: &asc, + ListOptions: github.ListOptions{ + Page: page, + PerPage: perPage, + }, + } + + g.sleep() + comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt) + if err != nil { + return nil, false, fmt.Errorf("error while listing repos: %v", err) + } + log.Trace("Request get comments %d/%d, but in fact get %d", perPage, page, len(comments)) + g.rate = &resp.Rate + for _, comment := range comments { + var email string + if comment.User.Email != nil { + email = *comment.User.Email + } + + // get reactions + var reactions []*base.Reaction + for i := 1; ; i++ { + g.sleep() + res, resp, err := g.client.Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ + Page: i, + PerPage: g.maxPerPage, + }) + if err != nil { + return nil, false, err + } + g.rate = &resp.Rate + if len(res) == 0 { + break + } + for _, reaction := range res { + reactions = append(reactions, &base.Reaction{ + UserID: reaction.User.GetID(), + UserName: reaction.User.GetLogin(), + Content: reaction.GetContent(), + }) + } + } + idx := strings.LastIndex(*comment.IssueURL, "/") + issueIndex, _ := strconv.ParseInt((*comment.IssueURL)[idx+1:], 10, 64) + allComments = append(allComments, &base.Comment{ + IssueIndex: issueIndex, + PosterID: *comment.User.ID, + PosterName: *comment.User.Login, + PosterEmail: email, + Content: *comment.Body, + Created: *comment.CreatedAt, + Updated: *comment.UpdatedAt, + Reactions: reactions, + }) + } + + return allComments, len(allComments) < perPage, nil +} + // GetPullRequests returns pull requests according page and perPage func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { if perPage > g.maxPerPage { @@ -539,6 +623,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq if err != nil { return nil, false, fmt.Errorf("error while listing repos: %v", err) } + log.Trace("Request get pull requests %d/%d, but in fact get %d", perPage, page, len(prs)) g.rate = &resp.Rate for _, pr := range prs { var body string diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 5bd980a9d8aa..e0ee2fea8447 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -240,7 +240,9 @@ func TestGitHubDownloadRepo(t *testing.T) { }, issues) // downloader.GetComments() - comments, err := downloader.GetComments(2) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: 2, + }) assert.NoError(t, err) assert.Len(t, comments, 2) assert.EqualValues(t, []*base.Comment{ diff --git a/modules/migrations/gitlab.go b/modules/migrations/gitlab.go index a697075ff892..c83989f771fb 100644 --- a/modules/migrations/gitlab.go +++ b/modules/migrations/gitlab.go @@ -430,7 +430,8 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er // GetComments returns comments according issueNumber // TODO: figure out how to transfer comment reactions -func (g *GitlabDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) { +func (g *GitlabDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { + var issueNumber = opts.IssueNumber var allComments = make([]*base.Comment, 0, g.maxPerPage) var page = 1 @@ -457,7 +458,7 @@ func (g *GitlabDownloader) GetComments(issueNumber int64) ([]*base.Comment, erro } if err != nil { - return nil, fmt.Errorf("error while listing comments: %v %v", g.repoID, err) + return nil, false, fmt.Errorf("error while listing comments: %v %v", g.repoID, err) } for _, comment := range comments { // Flatten comment threads @@ -490,7 +491,7 @@ func (g *GitlabDownloader) GetComments(issueNumber int64) ([]*base.Comment, erro } page = resp.NextPage } - return allComments, nil + return allComments, true, nil } // GetPullRequests returns pull requests according page and perPage diff --git a/modules/migrations/gitlab_test.go b/modules/migrations/gitlab_test.go index 32ed6d807a23..6a77ff3c230a 100644 --- a/modules/migrations/gitlab_test.go +++ b/modules/migrations/gitlab_test.go @@ -204,7 +204,9 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, issues) - comments, err := downloader.GetComments(2) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: 2, + }) assert.NoError(t, err) assert.Len(t, comments, 4) assert.EqualValues(t, []*base.Comment{ diff --git a/modules/migrations/gogs.go b/modules/migrations/gogs.go index b616907938ff..d689b0da1155 100644 --- a/modules/migrations/gogs.go +++ b/modules/migrations/gogs.go @@ -227,12 +227,13 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, } // GetComments returns comments according issueNumber -func (g *GogsDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) { +func (g *GogsDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { + var issueNumber = opts.IssueNumber var allComments = make([]*base.Comment, 0, 100) comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, issueNumber) if err != nil { - return nil, fmt.Errorf("error while listing repos: %v", err) + return nil, false, fmt.Errorf("error while listing repos: %v", err) } for _, comment := range comments { if len(comment.Body) == 0 || comment.Poster == nil { @@ -249,7 +250,7 @@ func (g *GogsDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) }) } - return allComments, nil + return allComments, true, nil } // GetTopics return repository topics diff --git a/modules/migrations/gogs_test.go b/modules/migrations/gogs_test.go index d173952b068e..4e384036d702 100644 --- a/modules/migrations/gogs_test.go +++ b/modules/migrations/gogs_test.go @@ -103,7 +103,9 @@ func TestGogsDownloadRepo(t *testing.T) { }, issues) // downloader.GetComments() - comments, err := downloader.GetComments(1) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: 1, + }) assert.NoError(t, err) assert.Len(t, comments, 1) assert.EqualValues(t, []*base.Comment{ diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 3cdf68ab627d..0a507d9c3341 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -292,6 +292,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts reviewBatchSize = uploader.MaxBatchInsertSize("review") ) + supportAllComments := downloader.SupportGetRepoComments() + if opts.Issues { log.Trace("migrating issues and comments") messenger("repo.migrate.migrating_issues") @@ -311,11 +313,13 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts return err } - if opts.Comments { + if opts.Comments && !supportAllComments { var allComments = make([]*base.Comment, 0, commentBatchSize) for _, issue := range issues { log.Trace("migrating issue %d's comments", issue.Number) - comments, err := downloader.GetComments(issue.Number) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: issue.Number, + }) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -366,30 +370,34 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } if opts.Comments { - // plain comments - var allComments = make([]*base.Comment, 0, commentBatchSize) - for _, pr := range prs { - log.Trace("migrating pull request %d's comments", pr.Number) - comments, err := downloader.GetComments(pr.Number) - if err != nil { - if !base.IsErrNotSupported(err) { - return err + if !supportAllComments { + // plain comments + var allComments = make([]*base.Comment, 0, commentBatchSize) + for _, pr := range prs { + log.Trace("migrating pull request %d's comments", pr.Number) + comments, _, err := downloader.GetComments(base.GetCommentOptions{ + IssueNumber: pr.Number, + }) + if err != nil { + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating comments is not supported, ignored") } - log.Warn("migrating comments is not supported, ignored") - } - allComments = append(allComments, comments...) + allComments = append(allComments, comments...) - if len(allComments) >= commentBatchSize { - if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { - return err + if len(allComments) >= commentBatchSize { + if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { + return err + } + allComments = allComments[commentBatchSize:] } - allComments = allComments[commentBatchSize:] } - } - if len(allComments) > 0 { - if err = uploader.CreateComments(allComments...); err != nil { - return err + if len(allComments) > 0 { + if err = uploader.CreateComments(allComments...); err != nil { + return err + } } } @@ -439,6 +447,27 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } } + if opts.Comments && supportAllComments { + log.Trace("migrating comments") + for i := 1; ; i++ { + comments, isEnd, err := downloader.GetComments(base.GetCommentOptions{ + Page: i, + PageSize: commentBatchSize, + }) + if err != nil { + return err + } + + if err := uploader.CreateComments(comments...); err != nil { + return err + } + + if isEnd { + break + } + } + } + return uploader.Finish() } diff --git a/modules/migrations/restore.go b/modules/migrations/restore.go index 5b44811d5292..6177f80cbbca 100644 --- a/modules/migrations/restore.go +++ b/modules/migrations/restore.go @@ -212,27 +212,27 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, } // GetComments returns comments according issueNumber -func (r *RepositoryRestorer) GetComments(issueNumber int64) ([]*base.Comment, error) { +func (r *RepositoryRestorer) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { var comments = make([]*base.Comment, 0, 10) - p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", issueNumber)) + p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", opts.IssueNumber)) _, err := os.Stat(p) if err != nil { if os.IsNotExist(err) { - return nil, nil + return nil, false, nil } - return nil, err + return nil, false, err } bs, err := ioutil.ReadFile(p) if err != nil { - return nil, err + return nil, false, err } err = yaml.Unmarshal(bs, &comments) if err != nil { - return nil, err + return nil, false, err } - return comments, nil + return comments, false, nil } // GetPullRequests returns pull requests according page and perPage From 66bf74d1b924a91c279d3c68cb01d038300b551a Mon Sep 17 00:00:00 2001 From: Adyanth H <33192449+adyanth@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:07:20 +0530 Subject: [PATCH 06/19] Escape reference to `user` table in models.SearchEmails (#16313) Fix #16312 Signed-off-by: Adyanth H --- models/user_mail.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/user_mail.go b/models/user_mail.go index 2758dfb8e84f..320f2cb05a32 100644 --- a/models/user_mail.go +++ b/models/user_mail.go @@ -316,7 +316,7 @@ type SearchEmailResult struct { // SearchEmails takes options i.e. keyword and part of email name to search, // it returns results in given range and number of total results. func SearchEmails(opts *SearchEmailOptions) ([]*SearchEmailResult, int64, error) { - var cond builder.Cond = builder.Eq{"user.`type`": UserTypeIndividual} + var cond builder.Cond = builder.Eq{"`user`.`type`": UserTypeIndividual} if len(opts.Keyword) > 0 { likeStr := "%" + strings.ToLower(opts.Keyword) + "%" cond = cond.And(builder.Or( From 7d70a6eff8a8fb03f02c606f5dcacccb9c1cab47 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 30 Jun 2021 19:49:06 +0200 Subject: [PATCH 07/19] Fix webhook commits wrong hash on HEAD reset (#16283) Use `..` instead of `...` with `rev-list`. In combination with #16282 the receiver can get the correct commit. The behaviour is now like Github. fixes #11802 --- modules/git/repo_commit.go | 11 +++++---- modules/git/repo_commit_test.go | 22 ++++++++++++++++++ .../git/tests/repos/repo4_commitsbetween/HEAD | 1 + .../tests/repos/repo4_commitsbetween/config | 7 ++++++ .../repos/repo4_commitsbetween/logs/HEAD | 4 ++++ .../repo4_commitsbetween/logs/refs/heads/main | 4 ++++ .../27/734c860ab19650d48e71f9f12d9bd194ed82ea | Bin 0 -> 53 bytes .../56/a6051ca2b02b04ef92d5150c9ef600403cb1de | Bin 0 -> 16 bytes .../78/a445db1eac62fe15e624e1137965969addf344 | 3 +++ .../a7/8e5638b66ccfe7e1b4689d3d5684e42c97d7ca | Bin 0 -> 160 bytes .../ad/74ceca1b8fde10c7d933bd2e56d347dddb4ab5 | Bin 0 -> 53 bytes .../b5/d8dd0ddd9d8d752bb47b5f781f09f478316098 | Bin 0 -> 18 bytes .../d8/263ee9860594d2806b0dfd1bfd17528b0ba2a4 | Bin 0 -> 16 bytes .../e2/3cc6a008501f1491b0480cedaef160e41cf684 | Bin 0 -> 53 bytes .../fd/c1b615bdcff0f0658b216df0c9209e5ecb7c78 | Bin 0 -> 126 bytes .../repo4_commitsbetween/refs/heads/main | 1 + 16 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 modules/git/tests/repos/repo4_commitsbetween/HEAD create mode 100644 modules/git/tests/repos/repo4_commitsbetween/config create mode 100644 modules/git/tests/repos/repo4_commitsbetween/logs/HEAD create mode 100644 modules/git/tests/repos/repo4_commitsbetween/logs/refs/heads/main create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/27/734c860ab19650d48e71f9f12d9bd194ed82ea create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/56/a6051ca2b02b04ef92d5150c9ef600403cb1de create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/78/a445db1eac62fe15e624e1137965969addf344 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/a7/8e5638b66ccfe7e1b4689d3d5684e42c97d7ca create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/ad/74ceca1b8fde10c7d933bd2e56d347dddb4ab5 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/b5/d8dd0ddd9d8d752bb47b5f781f09f478316098 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/d8/263ee9860594d2806b0dfd1bfd17528b0ba2a4 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/e2/3cc6a008501f1491b0480cedaef160e41cf684 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/objects/fd/c1b615bdcff0f0658b216df0c9209e5ecb7c78 create mode 100644 modules/git/tests/repos/repo4_commitsbetween/refs/heads/main diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 815aa141e532..5b417cd77455 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -264,14 +264,15 @@ func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (in return len(strings.Split(stdout, "\n")) - 1, nil } -// CommitsBetween returns a list that contains commits between [last, before). +// CommitsBetween returns a list that contains commits between [before, last). +// If before is detached (removed by reset + push) it is not included. func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List, error) { var stdout []byte var err error if before == nil { stdout, err = NewCommand("rev-list", last.ID.String()).RunInDirBytes(repo.Path) } else { - stdout, err = NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path) + stdout, err = NewCommand("rev-list", before.ID.String()+".."+last.ID.String()).RunInDirBytes(repo.Path) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... @@ -284,14 +285,14 @@ func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) } -// CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [last, before) +// CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [before, last) func (repo *Repository) CommitsBetweenLimit(last *Commit, before *Commit, limit, skip int) (*list.List, error) { var stdout []byte var err error if before == nil { stdout, err = NewCommand("rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), last.ID.String()).RunInDirBytes(repo.Path) } else { - stdout, err = NewCommand("rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path) + stdout, err = NewCommand("rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), before.ID.String()+".."+last.ID.String()).RunInDirBytes(repo.Path) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list --max-count n before last so let's try that... @@ -322,7 +323,7 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) (*list.List, erro // CommitsCountBetween return numbers of commits between two commits func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { - count, err := CommitsCountFiles(repo.Path, []string{start + "..." + end}, []string{}) + count, err := CommitsCountFiles(repo.Path, []string{start + ".." + end}, []string{}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 8f8acbdfed67..a6c27ea4d55b 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -78,3 +78,25 @@ func TestIsCommitInBranch(t *testing.T) { assert.NoError(t, err) assert.False(t, result) } + +func TestRepository_CommitsBetweenIDs(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo4_commitsbetween") + bareRepo1, err := OpenRepository(bareRepo1Path) + assert.NoError(t, err) + defer bareRepo1.Close() + + cases := []struct { + OldID string + NewID string + ExpectedCommits int + }{ + {"fdc1b615bdcff0f0658b216df0c9209e5ecb7c78", "78a445db1eac62fe15e624e1137965969addf344", 1}, //com1 -> com2 + {"78a445db1eac62fe15e624e1137965969addf344", "fdc1b615bdcff0f0658b216df0c9209e5ecb7c78", 0}, //reset HEAD~, com2 -> com1 + {"78a445db1eac62fe15e624e1137965969addf344", "a78e5638b66ccfe7e1b4689d3d5684e42c97d7ca", 1}, //com2 -> com2_new + } + for i, c := range cases { + commits, err := bareRepo1.CommitsBetweenIDs(c.NewID, c.OldID) + assert.NoError(t, err) + assert.Equal(t, c.ExpectedCommits, commits.Len(), "case %d", i) + } +} diff --git a/modules/git/tests/repos/repo4_commitsbetween/HEAD b/modules/git/tests/repos/repo4_commitsbetween/HEAD new file mode 100644 index 000000000000..b870d82622c1 --- /dev/null +++ b/modules/git/tests/repos/repo4_commitsbetween/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/modules/git/tests/repos/repo4_commitsbetween/config b/modules/git/tests/repos/repo4_commitsbetween/config new file mode 100644 index 000000000000..d545cdabdbdd --- /dev/null +++ b/modules/git/tests/repos/repo4_commitsbetween/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = false + logallrefupdates = true + symlinks = false + ignorecase = true diff --git a/modules/git/tests/repos/repo4_commitsbetween/logs/HEAD b/modules/git/tests/repos/repo4_commitsbetween/logs/HEAD new file mode 100644 index 000000000000..24cc684baef2 --- /dev/null +++ b/modules/git/tests/repos/repo4_commitsbetween/logs/HEAD @@ -0,0 +1,4 @@ +0000000000000000000000000000000000000000 fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 KN4CK3R 1624915979 +0200 commit (initial): com1 +fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 78a445db1eac62fe15e624e1137965969addf344 KN4CK3R 1624915993 +0200 commit: com2 +78a445db1eac62fe15e624e1137965969addf344 fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 KN4CK3R 1624916008 +0200 reset: moving to HEAD~1 +fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 a78e5638b66ccfe7e1b4689d3d5684e42c97d7ca KN4CK3R 1624916029 +0200 commit: com2_new diff --git a/modules/git/tests/repos/repo4_commitsbetween/logs/refs/heads/main b/modules/git/tests/repos/repo4_commitsbetween/logs/refs/heads/main new file mode 100644 index 000000000000..24cc684baef2 --- /dev/null +++ b/modules/git/tests/repos/repo4_commitsbetween/logs/refs/heads/main @@ -0,0 +1,4 @@ +0000000000000000000000000000000000000000 fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 KN4CK3R 1624915979 +0200 commit (initial): com1 +fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 78a445db1eac62fe15e624e1137965969addf344 KN4CK3R 1624915993 +0200 commit: com2 +78a445db1eac62fe15e624e1137965969addf344 fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 KN4CK3R 1624916008 +0200 reset: moving to HEAD~1 +fdc1b615bdcff0f0658b216df0c9209e5ecb7c78 a78e5638b66ccfe7e1b4689d3d5684e42c97d7ca KN4CK3R 1624916029 +0200 commit: com2_new diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/27/734c860ab19650d48e71f9f12d9bd194ed82ea b/modules/git/tests/repos/repo4_commitsbetween/objects/27/734c860ab19650d48e71f9f12d9bd194ed82ea new file mode 100644 index 0000000000000000000000000000000000000000..5b26f8b3a8c1020467e67093a12773150398cf54 GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9V=y!@Ff%bxC`m0Y(JQGaVc2@(F7MsBy`|b)s^crc;PUaA}_nQ@C literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/56/a6051ca2b02b04ef92d5150c9ef600403cb1de b/modules/git/tests/repos/repo4_commitsbetween/objects/56/a6051ca2b02b04ef92d5150c9ef600403cb1de new file mode 100644 index 0000000000000000000000000000000000000000..b17dfe30e64f245f6aa2284091ae1af5cba214ff GIT binary patch literal 16 Xcmb85F9rm} literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/78/a445db1eac62fe15e624e1137965969addf344 b/modules/git/tests/repos/repo4_commitsbetween/objects/78/a445db1eac62fe15e624e1137965969addf344 new file mode 100644 index 000000000000..6d23de052ee7 --- /dev/null +++ b/modules/git/tests/repos/repo4_commitsbetween/objects/78/a445db1eac62fe15e624e1137965969addf344 @@ -0,0 +1,3 @@ +xM +0@a=E̤I$Zl|G)îm̊uO"&`8GtI7#n6%09)8F(hl@MuS\1y=%?iu"O +Dmڃwź{pC_ \ No newline at end of file diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/a7/8e5638b66ccfe7e1b4689d3d5684e42c97d7ca b/modules/git/tests/repos/repo4_commitsbetween/objects/a7/8e5638b66ccfe7e1b4689d3d5684e42c97d7ca new file mode 100644 index 0000000000000000000000000000000000000000..d5c554a542dc6c9464902a890fa7b125c5e36472 GIT binary patch literal 160 zcmV;R0AK%j0iDiE3c@fD08rOGMfQTsjA=d~BDfS>cmYXfCRA)2sS&R)dIS&f;BlR% zTQfwsYKy8N@3)qNgOoA49>fOqSYknvm<6L%38bleq($duiZEt}eHJbS3b;OGLMH_{ z5=8Blvu7W=^lC$0%;{{8r|re;l1#VxP)B+4Q0q7(zHcVo8+2qNI-qFQKmZ;8hE4ym OUrg6o-`xji^F-ujTu5R7 literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/ad/74ceca1b8fde10c7d933bd2e56d347dddb4ab5 b/modules/git/tests/repos/repo4_commitsbetween/objects/ad/74ceca1b8fde10c7d933bd2e56d347dddb4ab5 new file mode 100644 index 0000000000000000000000000000000000000000..26ed785006120fefcd9aa117847b2f6f7a35d814 GIT binary patch literal 53 zcmb8vwq966pW{ literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/b5/d8dd0ddd9d8d752bb47b5f781f09f478316098 b/modules/git/tests/repos/repo4_commitsbetween/objects/b5/d8dd0ddd9d8d752bb47b5f781f09f478316098 new file mode 100644 index 0000000000000000000000000000000000000000..8060b57df0376ee37c49576083be72dcc1d66d84 GIT binary patch literal 18 Zcmbpv# literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/e2/3cc6a008501f1491b0480cedaef160e41cf684 b/modules/git/tests/repos/repo4_commitsbetween/objects/e2/3cc6a008501f1491b0480cedaef160e41cf684 new file mode 100644 index 0000000000000000000000000000000000000000..0a70530845aa7a05f3e43b22166c5f3997caa40b GIT binary patch literal 53 zcmbSw J863+5>;d&46tw^V literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo4_commitsbetween/objects/fd/c1b615bdcff0f0658b216df0c9209e5ecb7c78 b/modules/git/tests/repos/repo4_commitsbetween/objects/fd/c1b615bdcff0f0658b216df0c9209e5ecb7c78 new file mode 100644 index 0000000000000000000000000000000000000000..2e6d94584ca3ee9affbd7e4cf4899efe50d186e6 GIT binary patch literal 126 zcmV-^0D=E_0iDfD3c@fDfMM4;#q0&ivoRo2a9MES4JI=qDK-XbdVe2B@BrWcQ>%6E zV1~5os|X-RPeN$&@y=p2MNZCTwh{(*J~DImn1jNtm$t%m^_R)r;DlV~=hzm0QE6={ gNRLC6^QUZmG9kqTdu_E=^gDL>$9}O Date: Wed, 30 Jun 2021 19:40:51 +0100 Subject: [PATCH 08/19] Fix default push instructions on empty repos (#16302) * Fix default push instructions on empty repos Use script block like in `repo/clone_buttons.tmpl` to set default instructions for pushing to empty repos. Fix #16295 Signed-off-by: Andrew Thornton --- templates/repo/empty.tmpl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index 21c600545639..485a6aa4e55f 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -33,7 +33,7 @@ git init {{if ne .Repository.DefaultBranch "master"}}git checkout -b {{.Repository.DefaultBranch}}{{end}} git add README.md git commit -m "first commit" -git remote add origin {{if $.DisableSSH}}{{$.CloneLink.HTTPS}}{{else}}{{$.CloneLink.SSH}}{{end}} +git remote add origin {{$.CloneLink.HTTPS}} git push -u origin {{.Repository.DefaultBranch}} @@ -42,10 +42,19 @@ git push -u origin {{.Repository.DefaultBranch}}

{{.i18n.Tr "repo.push_exist_repo"}}

-
git remote add origin {{if $.DisableSSH}}{{$.CloneLink.HTTPS}}{{else}}{{$.CloneLink.SSH}}{{end}}
+									
git remote add origin {{$.CloneLink.HTTPS}}
 git push -u origin {{.Repository.DefaultBranch}}
+ {{end}} {{else}}
From 365c4e9316bbcc8bdf9cf68ef237bf18ae8db315 Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 30 Jun 2021 20:14:53 +0100 Subject: [PATCH 09/19] Add button to delete undeleted repositories from failed migrations (#16197) This PR adds a button to delete failed repositories if there has been a failure during migration and for whatever reason the repository doesn't get deleted automatically. Fix #16154 Signed-off-by: Andrew Thornton --- routers/web/repo/view.go | 2 ++ templates/repo/migrate/migrating.tmpl | 39 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 74e2a2959772..90d06d11c1e1 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -29,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/typesniffer" ) @@ -624,6 +625,7 @@ func Home(ctx *context.Context) { ctx.Data["Repo"] = ctx.Repo ctx.Data["MigrateTask"] = task ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr) + ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed ctx.HTML(http.StatusOK, tplMigrating) return } diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl index c1f189553f6f..cc12243205c1 100644 --- a/templates/repo/migrate/migrating.tmpl +++ b/templates/repo/migrate/migrating.tmpl @@ -28,6 +28,12 @@

{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}

+ {{if and .Failed .Permission.IsAdmin}} +
+
+ +
+ {{end}} @@ -35,4 +41,37 @@ + {{template "base/footer" .}} From 302e8b6d02dc9ccac00284b3cdf9a69c001882c0 Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 30 Jun 2021 21:07:23 +0100 Subject: [PATCH 10/19] Prevent zombie processes (#16314) Unfortunately go doesn't always ensure that execd processes are completely waited for. On linux this means that zombie processes can occur. This PR ensures that these are waited for by using signal notifier in serv and passing a context elsewhere. Signed-off-by: Andrew Thornton --- cmd/serv.go | 26 ++++++++++++++++++++++++-- modules/markup/external/external.go | 10 +++++++++- modules/ssh/ssh.go | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cmd/serv.go b/cmd/serv.go index 1c9f5dc44e9d..40f8b89c9a98 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -6,14 +6,17 @@ package cmd import ( + "context" "fmt" "net/http" "net/url" "os" "os/exec" + "os/signal" "regexp" "strconv" "strings" + "syscall" "time" "code.gitea.io/gitea/models" @@ -273,12 +276,31 @@ func runServ(c *cli.Context) error { verb = strings.Replace(verb, "-", " ", 1) } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + // install notify + signalChannel := make(chan os.Signal, 1) + + signal.Notify( + signalChannel, + syscall.SIGINT, + syscall.SIGTERM, + ) + select { + case <-signalChannel: + case <-ctx.Done(): + } + cancel() + signal.Reset() + }() + var gitcmd *exec.Cmd verbs := strings.Split(verb, " ") if len(verbs) == 2 { - gitcmd = exec.Command(verbs[0], verbs[1], repoPath) + gitcmd = exec.CommandContext(ctx, verbs[0], verbs[1], repoPath) } else { - gitcmd = exec.Command(verb, repoPath) + gitcmd = exec.CommandContext(ctx, verb, repoPath) } gitcmd.Dir = setting.RepoRootPath diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index c849f505e7ee..e35a1b99c0fd 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -5,6 +5,7 @@ package external import ( + "context" "fmt" "io" "io/ioutil" @@ -15,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -96,7 +98,13 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. args = append(args, f.Name()) } - cmd := exec.Command(commands[0], args...) + processCtx, cancel := context.WithCancel(ctx.Ctx) + defer cancel() + + pid := process.GetManager().Add(fmt.Sprintf("Render [%s] for %s", commands[0], ctx.URLPrefix), cancel) + defer process.GetManager().Remove(pid) + + cmd := exec.CommandContext(processCtx, commands[0], args...) cmd.Env = append( os.Environ(), "GITEA_PREFIX_SRC="+ctx.URLPrefix, diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index bcaae5a1806d..c0897377c56f 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -66,7 +66,7 @@ func sessionHandler(session ssh.Session) { args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf} log.Trace("SSH: Arguments: %v", args) - cmd := exec.Command(setting.AppPath, args...) + cmd := exec.CommandContext(session.Context(), setting.AppPath, args...) cmd.Env = append( os.Environ(), "SSH_ORIGINAL_COMMAND="+command, From 4f26e0ac0ea32968e94403e16cb776e7a4e5e690 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 30 Jun 2021 16:27:09 -0400 Subject: [PATCH 11/19] up current stable version in docs (#16318) --- docs/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.yaml b/docs/config.yaml index c2d9dc5e521c..9b446f35b6ab 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -18,7 +18,7 @@ params: description: Git with a cup of tea author: The Gitea Authors website: https://docs.gitea.io - version: 1.14.2 + version: 1.14.3 minGoVersion: 1.14 goVersion: 1.16 minNodeVersion: 12.17 From 99799832835aae6a8641112cb71eb87baef32afa Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 30 Jun 2021 21:58:45 +0100 Subject: [PATCH 12/19] Update Go-Git to take advantage of LargeObjectThreshold (#16316) Following the merging of https://github.com/go-git/go-git/pull/330 we can now add a setting to avoid go-git reading and caching large objects. Signed-off-by: Andrew Thornton --- custom/conf/app.example.ini | 3 + .../doc/advanced/config-cheat-sheet.en-us.md | 2 +- go.mod | 6 +- go.sum | 11 +- modules/git/repo_base_gogit.go | 4 +- modules/setting/git.go | 2 + .../v5/plumbing/format/packfile/fsobject.go | 54 +++-- .../v5/plumbing/format/packfile/packfile.go | 92 +++++++- .../plumbing/format/packfile/patch_delta.go | 210 ++++++++++++++++++ .../v5/plumbing/format/packfile/scanner.go | 15 ++ .../v5/storage/filesystem/dotgit/reader.go | 79 +++++++ .../go-git/v5/storage/filesystem/object.go | 21 +- .../go-git/v5/storage/filesystem/storage.go | 3 + .../go-git/go-git/v5/utils/ioutil/common.go | 40 ++++ vendor/golang.org/x/sys/unix/ztypes_linux.go | 2 + .../golang.org/x/sys/windows/types_windows.go | 2 +- vendor/modules.txt | 6 +- 17 files changed, 507 insertions(+), 45 deletions(-) create mode 100644 vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/reader.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 900e8b25ecdc..ba12b7ff12b8 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -573,6 +573,9 @@ PATH = ;; ;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled) ;PULL_REQUEST_PUSH_MESSAGE = true +;; +;; (Go-Git only) Don't cache objects greater than this in memory. (Set to 0 to disable.) +;LARGE_OBJECT_THRESHOLD = 1048576 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 2b73f4365838..f95a96439fa8 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -837,7 +837,7 @@ NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take ef - `PULL_REQUEST_PUSH_MESSAGE`: **true**: Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled) - `VERBOSE_PUSH`: **true**: Print status information about pushes as they are being processed. - `VERBOSE_PUSH_DELAY`: **5s**: Only print verbose information if push takes longer than this delay. - +- `LARGE_OBJECT_THRESHOLD`: **1048576**: (Go-Git only), don't cache objects greater than this in memory. (Set to 0 to disable.) ## Git - Timeout settings (`git.timeout`) - `DEFAUlT`: **360**: Git operations default timeout seconds. - `MIGRATE`: **600**: Migrate external repositories timeout seconds. diff --git a/go.mod b/go.mod index a1e013189560..c9697be431ed 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/go-chi/cors v1.2.0 github.com/go-enry/go-enry/v2 v2.7.0 github.com/go-git/go-billy/v5 v5.3.1 - github.com/go-git/go-git/v5 v5.4.2 + github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-redis/redis/v8 v8.10.0 github.com/go-sql-driver/mysql v1.6.0 @@ -123,9 +123,9 @@ require ( go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.17.0 // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/net v0.0.0-20210525063256-abc453219eb5 + golang.org/x/net v0.0.0-20210614182718-04defd469f4e golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c golang.org/x/text v0.3.6 golang.org/x/time v0.0.0-20210608053304-ed9ce3a009e4 // indirect golang.org/x/tools v0.1.0 diff --git a/go.sum b/go.sum index 72061a0c0daf..8652af18e571 100644 --- a/go.sum +++ b/go.sum @@ -313,8 +313,8 @@ github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Ai github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= +github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 h1:1RSUwVK7VjTeA82kcLIqz1EU70QRwFdZUlJW58gP4GY= +github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -1233,8 +1233,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1339,8 +1339,9 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 19a3f84571fb..6186824c0b9f 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -12,6 +12,8 @@ import ( "path/filepath" gitealog "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "github.com/go-git/go-billy/v5/osfs" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/cache" @@ -46,7 +48,7 @@ func OpenRepository(repoPath string) (*Repository, error) { return nil, err } } - storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true}) + storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold}) gogitRepo, err := gogit.Open(storage, fs) if err != nil { return nil, err diff --git a/modules/setting/git.go b/modules/setting/git.go index 7383996cb972..aa838a8d641a 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -25,6 +25,7 @@ var ( GCArgs []string `ini:"GC_ARGS" delim:" "` EnableAutoGitWireProtocol bool PullRequestPushMessage bool + LargeObjectThreshold int64 Timeout struct { Default int Migrate int @@ -45,6 +46,7 @@ var ( GCArgs: []string{}, EnableAutoGitWireProtocol: true, PullRequestPushMessage: true, + LargeObjectThreshold: 1024 * 1024, Timeout: struct { Default int Migrate int diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go index c5edaf52ee36..a395d171ce43 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go @@ -7,19 +7,21 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/cache" "github.com/go-git/go-git/v5/plumbing/format/idxfile" + "github.com/go-git/go-git/v5/utils/ioutil" ) // FSObject is an object from the packfile on the filesystem. type FSObject struct { - hash plumbing.Hash - h *ObjectHeader - offset int64 - size int64 - typ plumbing.ObjectType - index idxfile.Index - fs billy.Filesystem - path string - cache cache.Object + hash plumbing.Hash + h *ObjectHeader + offset int64 + size int64 + typ plumbing.ObjectType + index idxfile.Index + fs billy.Filesystem + path string + cache cache.Object + largeObjectThreshold int64 } // NewFSObject creates a new filesystem object. @@ -32,16 +34,18 @@ func NewFSObject( fs billy.Filesystem, path string, cache cache.Object, + largeObjectThreshold int64, ) *FSObject { return &FSObject{ - hash: hash, - offset: offset, - size: contentSize, - typ: finalType, - index: index, - fs: fs, - path: path, - cache: cache, + hash: hash, + offset: offset, + size: contentSize, + typ: finalType, + index: index, + fs: fs, + path: path, + cache: cache, + largeObjectThreshold: largeObjectThreshold, } } @@ -62,7 +66,21 @@ func (o *FSObject) Reader() (io.ReadCloser, error) { return nil, err } - p := NewPackfileWithCache(o.index, nil, f, o.cache) + p := NewPackfileWithCache(o.index, nil, f, o.cache, o.largeObjectThreshold) + if o.largeObjectThreshold > 0 && o.size > o.largeObjectThreshold { + // We have a big object + h, err := p.objectHeaderAtOffset(o.offset) + if err != nil { + return nil, err + } + + r, err := p.getReaderDirect(h) + if err != nil { + _ = f.Close() + return nil, err + } + return ioutil.NewReadCloserWithCloser(r, f.Close), nil + } r, err := p.getObjectContent(o.offset) if err != nil { _ = f.Close() diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go index ddd7f62fce49..8dd6041d5559 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go @@ -2,6 +2,8 @@ package packfile import ( "bytes" + "compress/zlib" + "fmt" "io" "os" @@ -35,11 +37,12 @@ const smallObjectThreshold = 16 * 1024 // Packfile allows retrieving information from inside a packfile. type Packfile struct { idxfile.Index - fs billy.Filesystem - file billy.File - s *Scanner - deltaBaseCache cache.Object - offsetToType map[int64]plumbing.ObjectType + fs billy.Filesystem + file billy.File + s *Scanner + deltaBaseCache cache.Object + offsetToType map[int64]plumbing.ObjectType + largeObjectThreshold int64 } // NewPackfileWithCache creates a new Packfile with the given object cache. @@ -50,6 +53,7 @@ func NewPackfileWithCache( fs billy.Filesystem, file billy.File, cache cache.Object, + largeObjectThreshold int64, ) *Packfile { s := NewScanner(file) return &Packfile{ @@ -59,6 +63,7 @@ func NewPackfileWithCache( s, cache, make(map[int64]plumbing.ObjectType), + largeObjectThreshold, } } @@ -66,8 +71,8 @@ func NewPackfileWithCache( // and packfile idx. // If the filesystem is provided, the packfile will return FSObjects, otherwise // it will return MemoryObjects. -func NewPackfile(index idxfile.Index, fs billy.Filesystem, file billy.File) *Packfile { - return NewPackfileWithCache(index, fs, file, cache.NewObjectLRUDefault()) +func NewPackfile(index idxfile.Index, fs billy.Filesystem, file billy.File, largeObjectThreshold int64) *Packfile { + return NewPackfileWithCache(index, fs, file, cache.NewObjectLRUDefault(), largeObjectThreshold) } // Get retrieves the encoded object in the packfile with the given hash. @@ -263,6 +268,7 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing. p.fs, p.file.Name(), p.deltaBaseCache, + p.largeObjectThreshold, ), nil } @@ -282,6 +288,50 @@ func (p *Packfile) getObjectContent(offset int64) (io.ReadCloser, error) { return obj.Reader() } +func asyncReader(p *Packfile) (io.ReadCloser, error) { + reader := ioutil.NewReaderUsingReaderAt(p.file, p.s.r.offset) + zr := zlibReaderPool.Get().(io.ReadCloser) + + if err := zr.(zlib.Resetter).Reset(reader, nil); err != nil { + return nil, fmt.Errorf("zlib reset error: %s", err) + } + + return ioutil.NewReadCloserWithCloser(zr, func() error { + zlibReaderPool.Put(zr) + return nil + }), nil + +} + +func (p *Packfile) getReaderDirect(h *ObjectHeader) (io.ReadCloser, error) { + switch h.Type { + case plumbing.CommitObject, plumbing.TreeObject, plumbing.BlobObject, plumbing.TagObject: + return asyncReader(p) + case plumbing.REFDeltaObject: + deltaRc, err := asyncReader(p) + if err != nil { + return nil, err + } + r, err := p.readREFDeltaObjectContent(h, deltaRc) + if err != nil { + return nil, err + } + return r, nil + case plumbing.OFSDeltaObject: + deltaRc, err := asyncReader(p) + if err != nil { + return nil, err + } + r, err := p.readOFSDeltaObjectContent(h, deltaRc) + if err != nil { + return nil, err + } + return r, nil + default: + return nil, ErrInvalidObject.AddDetails("type %q", h.Type) + } +} + func (p *Packfile) getNextMemoryObject(h *ObjectHeader) (plumbing.EncodedObject, error) { var obj = new(plumbing.MemoryObject) obj.SetSize(h.Length) @@ -334,6 +384,20 @@ func (p *Packfile) fillREFDeltaObjectContent(obj plumbing.EncodedObject, ref plu return p.fillREFDeltaObjectContentWithBuffer(obj, ref, buf) } +func (p *Packfile) readREFDeltaObjectContent(h *ObjectHeader, deltaRC io.Reader) (io.ReadCloser, error) { + var err error + + base, ok := p.cacheGet(h.Reference) + if !ok { + base, err = p.Get(h.Reference) + if err != nil { + return nil, err + } + } + + return ReaderFromDelta(base, deltaRC) +} + func (p *Packfile) fillREFDeltaObjectContentWithBuffer(obj plumbing.EncodedObject, ref plumbing.Hash, buf *bytes.Buffer) error { var err error @@ -364,6 +428,20 @@ func (p *Packfile) fillOFSDeltaObjectContent(obj plumbing.EncodedObject, offset return p.fillOFSDeltaObjectContentWithBuffer(obj, offset, buf) } +func (p *Packfile) readOFSDeltaObjectContent(h *ObjectHeader, deltaRC io.Reader) (io.ReadCloser, error) { + hash, err := p.FindHash(h.OffsetReference) + if err != nil { + return nil, err + } + + base, err := p.objectAtOffset(h.OffsetReference, hash) + if err != nil { + return nil, err + } + + return ReaderFromDelta(base, deltaRC) +} + func (p *Packfile) fillOFSDeltaObjectContentWithBuffer(obj plumbing.EncodedObject, offset int64, buf *bytes.Buffer) error { hash, err := p.FindHash(offset) if err != nil { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go index 9e90f30a72fc..17da11e03879 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go @@ -1,9 +1,11 @@ package packfile import ( + "bufio" "bytes" "errors" "io" + "math" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/utils/ioutil" @@ -73,6 +75,131 @@ func PatchDelta(src, delta []byte) ([]byte, error) { return b.Bytes(), nil } +func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadCloser, error) { + deltaBuf := bufio.NewReaderSize(deltaRC, 1024) + srcSz, err := decodeLEB128ByteReader(deltaBuf) + if err != nil { + if err == io.EOF { + return nil, ErrInvalidDelta + } + return nil, err + } + if srcSz != uint(base.Size()) { + return nil, ErrInvalidDelta + } + + targetSz, err := decodeLEB128ByteReader(deltaBuf) + if err != nil { + if err == io.EOF { + return nil, ErrInvalidDelta + } + return nil, err + } + remainingTargetSz := targetSz + + dstRd, dstWr := io.Pipe() + + go func() { + baseRd, err := base.Reader() + if err != nil { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } + defer baseRd.Close() + + baseBuf := bufio.NewReader(baseRd) + basePos := uint(0) + + for { + cmd, err := deltaBuf.ReadByte() + if err == io.EOF { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } + if err != nil { + _ = dstWr.CloseWithError(err) + return + } + + if isCopyFromSrc(cmd) { + offset, err := decodeOffsetByteReader(cmd, deltaBuf) + if err != nil { + _ = dstWr.CloseWithError(err) + return + } + sz, err := decodeSizeByteReader(cmd, deltaBuf) + if err != nil { + _ = dstWr.CloseWithError(err) + return + } + + if invalidSize(sz, targetSz) || + invalidOffsetSize(offset, sz, srcSz) { + _ = dstWr.Close() + return + } + + discard := offset - basePos + if basePos > offset { + _ = baseRd.Close() + baseRd, err = base.Reader() + if err != nil { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } + baseBuf.Reset(baseRd) + discard = offset + } + for discard > math.MaxInt32 { + n, err := baseBuf.Discard(math.MaxInt32) + if err != nil { + _ = dstWr.CloseWithError(err) + return + } + basePos += uint(n) + discard -= uint(n) + } + for discard > 0 { + n, err := baseBuf.Discard(int(discard)) + if err != nil { + _ = dstWr.CloseWithError(err) + return + } + basePos += uint(n) + discard -= uint(n) + } + if _, err := io.Copy(dstWr, io.LimitReader(baseBuf, int64(sz))); err != nil { + _ = dstWr.CloseWithError(err) + return + } + remainingTargetSz -= sz + basePos += sz + } else if isCopyFromDelta(cmd) { + sz := uint(cmd) // cmd is the size itself + if invalidSize(sz, targetSz) { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } + if _, err := io.Copy(dstWr, io.LimitReader(deltaBuf, int64(sz))); err != nil { + _ = dstWr.CloseWithError(err) + return + } + + remainingTargetSz -= sz + } else { + _ = dstWr.CloseWithError(ErrDeltaCmd) + return + } + if remainingTargetSz <= 0 { + _ = dstWr.Close() + return + } + } + }() + + return dstRd, nil +} + func patchDelta(dst *bytes.Buffer, src, delta []byte) error { if len(delta) < deltaSizeMin { return ErrInvalidDelta @@ -161,6 +288,25 @@ func decodeLEB128(input []byte) (uint, []byte) { return num, input[sz:] } +func decodeLEB128ByteReader(input io.ByteReader) (uint, error) { + var num, sz uint + for { + b, err := input.ReadByte() + if err != nil { + return 0, err + } + + num |= (uint(b) & payload) << (sz * 7) // concats 7 bits chunks + sz++ + + if uint(b)&continuation == 0 { + break + } + } + + return num, nil +} + const ( payload = 0x7f // 0111 1111 continuation = 0x80 // 1000 0000 @@ -174,6 +320,40 @@ func isCopyFromDelta(cmd byte) bool { return (cmd&0x80) == 0 && cmd != 0 } +func decodeOffsetByteReader(cmd byte, delta io.ByteReader) (uint, error) { + var offset uint + if (cmd & 0x01) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + offset = uint(next) + } + if (cmd & 0x02) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + offset |= uint(next) << 8 + } + if (cmd & 0x04) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + offset |= uint(next) << 16 + } + if (cmd & 0x08) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + offset |= uint(next) << 24 + } + + return offset, nil +} + func decodeOffset(cmd byte, delta []byte) (uint, []byte, error) { var offset uint if (cmd & 0x01) != 0 { @@ -208,6 +388,36 @@ func decodeOffset(cmd byte, delta []byte) (uint, []byte, error) { return offset, delta, nil } +func decodeSizeByteReader(cmd byte, delta io.ByteReader) (uint, error) { + var sz uint + if (cmd & 0x10) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + sz = uint(next) + } + if (cmd & 0x20) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + sz |= uint(next) << 8 + } + if (cmd & 0x40) != 0 { + next, err := delta.ReadByte() + if err != nil { + return 0, err + } + sz |= uint(next) << 16 + } + if sz == 0 { + sz = 0x10000 + } + + return sz, nil +} + func decodeSize(cmd byte, delta []byte) (uint, []byte, error) { var sz uint if (cmd & 0x10) != 0 { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go index 6e6a687886a8..5d9e8fb65aab 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go @@ -320,6 +320,21 @@ func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err erro return } +// ReadObject returns a reader for the object content and an error +func (s *Scanner) ReadObject() (io.ReadCloser, error) { + s.pendingObject = nil + zr := zlibReaderPool.Get().(io.ReadCloser) + + if err := zr.(zlib.Resetter).Reset(s.r, nil); err != nil { + return nil, fmt.Errorf("zlib reset error: %s", err) + } + + return ioutil.NewReadCloserWithCloser(zr, func() error { + zlibReaderPool.Put(zr) + return nil + }), nil +} + // ReadRegularObject reads and write a non-deltified object // from it zlib stream in an object entry in the packfile. func (s *Scanner) copyObject(w io.Writer) (n int64, err error) { diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/reader.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/reader.go new file mode 100644 index 000000000000..a82ac94eb6bd --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/reader.go @@ -0,0 +1,79 @@ +package dotgit + +import ( + "fmt" + "io" + "os" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/format/objfile" + "github.com/go-git/go-git/v5/utils/ioutil" +) + +var _ (plumbing.EncodedObject) = &EncodedObject{} + +type EncodedObject struct { + dir *DotGit + h plumbing.Hash + t plumbing.ObjectType + sz int64 +} + +func (e *EncodedObject) Hash() plumbing.Hash { + return e.h +} + +func (e *EncodedObject) Reader() (io.ReadCloser, error) { + f, err := e.dir.Object(e.h) + if err != nil { + if os.IsNotExist(err) { + return nil, plumbing.ErrObjectNotFound + } + + return nil, err + } + r, err := objfile.NewReader(f) + if err != nil { + return nil, err + } + + t, size, err := r.Header() + if err != nil { + _ = r.Close() + return nil, err + } + if t != e.t { + _ = r.Close() + return nil, objfile.ErrHeader + } + if size != e.sz { + _ = r.Close() + return nil, objfile.ErrHeader + } + return ioutil.NewReadCloserWithCloser(r, f.Close), nil +} + +func (e *EncodedObject) SetType(plumbing.ObjectType) {} + +func (e *EncodedObject) Type() plumbing.ObjectType { + return e.t +} + +func (e *EncodedObject) Size() int64 { + return e.sz +} + +func (e *EncodedObject) SetSize(int64) {} + +func (e *EncodedObject) Writer() (io.WriteCloser, error) { + return nil, fmt.Errorf("Not supported") +} + +func NewEncodedObject(dir *DotGit, h plumbing.Hash, t plumbing.ObjectType, size int64) *EncodedObject { + return &EncodedObject{ + dir: dir, + h: h, + t: t, + sz: size, + } +} diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go index 0c25dad613df..5c91bcd69940 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go @@ -204,9 +204,9 @@ func (s *ObjectStorage) packfile(idx idxfile.Index, pack plumbing.Hash) (*packfi var p *packfile.Packfile if s.objectCache != nil { - p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.objectCache) + p = packfile.NewPackfileWithCache(idx, s.dir.Fs(), f, s.objectCache, s.options.LargeObjectThreshold) } else { - p = packfile.NewPackfile(idx, s.dir.Fs(), f) + p = packfile.NewPackfile(idx, s.dir.Fs(), f, s.options.LargeObjectThreshold) } return p, s.storePackfileInCache(pack, p) @@ -389,7 +389,6 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb return cacheObj, nil } - obj = s.NewEncodedObject() r, err := objfile.NewReader(f) if err != nil { return nil, err @@ -402,6 +401,13 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb return nil, err } + if s.options.LargeObjectThreshold > 0 && size > s.options.LargeObjectThreshold { + obj = dotgit.NewEncodedObject(s.dir, h, t, size) + return obj, nil + } + + obj = s.NewEncodedObject() + obj.SetType(t) obj.SetSize(size) w, err := obj.Writer() @@ -595,6 +601,7 @@ func (s *ObjectStorage) buildPackfileIters( return newPackfileIter( s.dir.Fs(), pack, t, seen, s.index[h], s.objectCache, s.options.KeepDescriptors, + s.options.LargeObjectThreshold, ) }, }, nil @@ -684,6 +691,7 @@ func NewPackfileIter( idxFile billy.File, t plumbing.ObjectType, keepPack bool, + largeObjectThreshold int64, ) (storer.EncodedObjectIter, error) { idx := idxfile.NewMemoryIndex() if err := idxfile.NewDecoder(idxFile).Decode(idx); err != nil { @@ -695,7 +703,7 @@ func NewPackfileIter( } seen := make(map[plumbing.Hash]struct{}) - return newPackfileIter(fs, f, t, seen, idx, nil, keepPack) + return newPackfileIter(fs, f, t, seen, idx, nil, keepPack, largeObjectThreshold) } func newPackfileIter( @@ -706,12 +714,13 @@ func newPackfileIter( index idxfile.Index, cache cache.Object, keepPack bool, + largeObjectThreshold int64, ) (storer.EncodedObjectIter, error) { var p *packfile.Packfile if cache != nil { - p = packfile.NewPackfileWithCache(index, fs, f, cache) + p = packfile.NewPackfileWithCache(index, fs, f, cache, largeObjectThreshold) } else { - p = packfile.NewPackfile(index, fs, f) + p = packfile.NewPackfile(index, fs, f, largeObjectThreshold) } iter, err := p.GetByType(t) diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/storage.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/storage.go index 8b69b27b00a3..7e7a2c50f685 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/storage.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/storage.go @@ -34,6 +34,9 @@ type Options struct { // MaxOpenDescriptors is the max number of file descriptors to keep // open. If KeepDescriptors is true, all file descriptors will remain open. MaxOpenDescriptors int + // LargeObjectThreshold maximum object size (in bytes) that will be read in to memory. + // If left unset or set to 0 there is no limit + LargeObjectThreshold int64 } // NewStorage returns a new Storage backed by a given `fs.Filesystem` and cache. diff --git a/vendor/github.com/go-git/go-git/v5/utils/ioutil/common.go b/vendor/github.com/go-git/go-git/v5/utils/ioutil/common.go index b52e85a380be..b0ace4e628f7 100644 --- a/vendor/github.com/go-git/go-git/v5/utils/ioutil/common.go +++ b/vendor/github.com/go-git/go-git/v5/utils/ioutil/common.go @@ -55,6 +55,28 @@ func NewReadCloser(r io.Reader, c io.Closer) io.ReadCloser { return &readCloser{Reader: r, closer: c} } +type readCloserCloser struct { + io.ReadCloser + closer func() error +} + +func (r *readCloserCloser) Close() (err error) { + defer func() { + if err == nil { + err = r.closer() + return + } + _ = r.closer() + }() + return r.ReadCloser.Close() +} + +// NewReadCloserWithCloser creates an `io.ReadCloser` with the given `io.ReaderCloser` and +// `io.Closer` that ensures that the closer is closed on close +func NewReadCloserWithCloser(r io.ReadCloser, c func() error) io.ReadCloser { + return &readCloserCloser{ReadCloser: r, closer: c} +} + type writeCloser struct { io.Writer closer io.Closer @@ -82,6 +104,24 @@ func WriteNopCloser(w io.Writer) io.WriteCloser { return writeNopCloser{w} } +type readerAtAsReader struct { + io.ReaderAt + offset int64 +} + +func (r *readerAtAsReader) Read(bs []byte) (int, error) { + n, err := r.ReaderAt.ReadAt(bs, r.offset) + r.offset += int64(n) + return n, err +} + +func NewReaderUsingReaderAt(r io.ReaderAt, offset int64) io.Reader { + return &readerAtAsReader{ + ReaderAt: r, + offset: offset, + } +} + // CheckClose calls Close on the given io.Closer. If the given *error points to // nil, it will be assigned the error returned by Close. Otherwise, any error // returned by Close will be ignored. CheckClose is usually called with defer. diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 72887abe55b7..c9d7eb41e3d8 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -1773,6 +1773,8 @@ const ( NFPROTO_NUMPROTO = 0xd ) +const SO_ORIGINAL_DST = 0x50 + type Nfgenmsg struct { Nfgen_family uint8 Version uint8 diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 1f733398ee4c..17f03312df1c 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -680,7 +680,7 @@ const ( WTD_CHOICE_CERT = 5 WTD_STATEACTION_IGNORE = 0x00000000 - WTD_STATEACTION_VERIFY = 0x00000010 + WTD_STATEACTION_VERIFY = 0x00000001 WTD_STATEACTION_CLOSE = 0x00000002 WTD_STATEACTION_AUTO_CACHE = 0x00000003 WTD_STATEACTION_AUTO_CACHE_FLUSH = 0x00000004 diff --git a/vendor/modules.txt b/vendor/modules.txt index ac02b222a275..b26a42b5890b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -305,7 +305,7 @@ github.com/go-git/go-billy/v5/helper/polyfill github.com/go-git/go-billy/v5/memfs github.com/go-git/go-billy/v5/osfs github.com/go-git/go-billy/v5/util -# github.com/go-git/go-git/v5 v5.4.2 +# github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 ## explicit github.com/go-git/go-git/v5 github.com/go-git/go-git/v5/config @@ -887,7 +887,7 @@ golang.org/x/crypto/ssh/knownhosts # golang.org/x/mod v0.4.2 golang.org/x/mod/module golang.org/x/mod/semver -# golang.org/x/net v0.0.0-20210525063256-abc453219eb5 +# golang.org/x/net v0.0.0-20210614182718-04defd469f4e ## explicit golang.org/x/net/bpf golang.org/x/net/context @@ -913,7 +913,7 @@ golang.org/x/oauth2/google/internal/externalaccount golang.org/x/oauth2/internal golang.org/x/oauth2/jws golang.org/x/oauth2/jwt -# golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 +# golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c ## explicit golang.org/x/sys/cpu golang.org/x/sys/execabs From ce286f9d9c00ceb24fb4eab4cab56c0b0678765a Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Wed, 30 Jun 2021 23:31:54 +0200 Subject: [PATCH 13/19] Support custom mime type mapping for text files (#16304) * Support custom mime type mapping for text files * Apply suggested change to routers/common/repo.go Co-authored-by: KN4CK3R Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: KN4CK3R --- integrations/download_test.go | 28 ++++++++++++++++++ .../10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 | 2 ++ .../26/f842bcad37fa40a1bb34cbb5ee219ee35d863d | Bin 0 -> 75 bytes .../ba/1aed4e2ea2443d76cec241b96be4ec990852ec | Bin 0 -> 117 bytes .../user2/repo2.git/refs/heads/master | 2 +- routers/common/repo.go | 20 ++++++++----- 6 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 integrations/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 create mode 100644 integrations/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d create mode 100644 integrations/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec diff --git a/integrations/download_test.go b/integrations/download_test.go index 305155e9ace4..38de75f476a9 100644 --- a/integrations/download_test.go +++ b/integrations/download_test.go @@ -8,6 +8,7 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -62,3 +63,30 @@ func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) { assert.Equal(t, "image/svg+xml", resp.HeaderMap.Get("Content-Type")) assert.Equal(t, "nosniff", resp.HeaderMap.Get("X-Content-Type-Options")) } + +func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) { + defer prepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp := session.MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "text/plain; charset=utf-8", resp.HeaderMap.Get("Content-Type")) +} + +func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) { + defer prepareTestEnv(t)() + setting.MimeTypeMap.Map[".xml"] = "text/xml" + setting.MimeTypeMap.Enabled = true + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp := session.MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "text/xml; charset=utf-8", resp.HeaderMap.Get("Content-Type")) + + delete(setting.MimeTypeMap.Map, ".xml") + setting.MimeTypeMap.Enabled = false +} diff --git a/integrations/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 b/integrations/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 new file mode 100644 index 000000000000..736e40878edf --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 @@ -0,0 +1,2 @@ +xK +0Eg %":u􊕦J|p˭Q~% 9لG6G ͦw(E4}*{)`YƆleMJOܚ>%^ݿL!]N[v#E6U~/0 ZU'gpJ5 \ No newline at end of file diff --git a/integrations/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d b/integrations/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d new file mode 100644 index 0000000000000000000000000000000000000000..c3e7e778c5bc1e5a44f0ff429b4aa801c0938199 GIT binary patch literal 75 zcmV-R0JQ&j0ZYosPf{>5VX(2U$jwnGOD!tS%+Iq`GSo9rQb^59&QHnAOSe@D4RO=8 hP_nnq6l9f8-1_{7XU4N7C{`8AG`nn literal 0 HcmV?d00001 diff --git a/integrations/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec b/integrations/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec new file mode 100644 index 0000000000000000000000000000000000000000..add9a3af0d4c37c916e85c963b20a314b8fe3294 GIT binary patch literal 117 zcmV-*0E+*30V^p=O;s>7FlR6{FfcPQQSivmP1VayVR+T_r@efUH~XP%EAKClN_3cu z?N&pTJ^uzGbB&l)+hgNx13LEwg3F=Md?`@Hr!A(C8@ Date: Thu, 1 Jul 2021 12:51:24 +0200 Subject: [PATCH 14/19] Introduce NotifySubjectType (#16320) * Introduce NotifySubjectType * update swagger docs --- modules/convert/notification.go | 8 ++++---- modules/structs/notifications.go | 24 +++++++++++++++++++----- templates/swagger/v1_json.tmpl | 8 ++++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/modules/convert/notification.go b/modules/convert/notification.go index cc941678b603..b0888ee09f0c 100644 --- a/modules/convert/notification.go +++ b/modules/convert/notification.go @@ -27,7 +27,7 @@ func ToNotificationThread(n *models.Notification) *api.NotificationThread { //handle Subject switch n.Source { case models.NotificationSourceIssue: - result.Subject = &api.NotificationSubject{Type: "Issue"} + result.Subject = &api.NotificationSubject{Type: api.NotifySubjectIssue} if n.Issue != nil { result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL() @@ -38,7 +38,7 @@ func ToNotificationThread(n *models.Notification) *api.NotificationThread { } } case models.NotificationSourcePullRequest: - result.Subject = &api.NotificationSubject{Type: "Pull"} + result.Subject = &api.NotificationSubject{Type: api.NotifySubjectPull} if n.Issue != nil { result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL() @@ -55,13 +55,13 @@ func ToNotificationThread(n *models.Notification) *api.NotificationThread { } case models.NotificationSourceCommit: result.Subject = &api.NotificationSubject{ - Type: "Commit", + Type: api.NotifySubjectCommit, Title: n.CommitID, URL: n.Repository.HTMLURL() + "/commit/" + n.CommitID, } case models.NotificationSourceRepository: result.Subject = &api.NotificationSubject{ - Type: "Repository", + Type: api.NotifySubjectRepository, Title: n.Repository.FullName(), URL: n.Repository.Link(), } diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index 8daa6de1686f..675dcf76b127 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -21,14 +21,28 @@ type NotificationThread struct { // NotificationSubject contains the notification subject (Issue/Pull/Commit) type NotificationSubject struct { - Title string `json:"title"` - URL string `json:"url"` - LatestCommentURL string `json:"latest_comment_url"` - Type string `json:"type" binding:"In(Issue,Pull,Commit)"` - State StateType `json:"state"` + Title string `json:"title"` + URL string `json:"url"` + LatestCommentURL string `json:"latest_comment_url"` + Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit)"` + State StateType `json:"state"` } // NotificationCount number of unread notifications type NotificationCount struct { New int64 `json:"new"` } + +// NotifySubjectType represent type of notification subject +type NotifySubjectType string + +const ( + // NotifySubjectIssue an issue is subject of an notification + NotifySubjectIssue NotifySubjectType = "Issue" + // NotifySubjectPull an pull is subject of an notification + NotifySubjectPull NotifySubjectType = "Pull" + // NotifySubjectCommit an commit is subject of an notification + NotifySubjectCommit NotifySubjectType = "Commit" + // NotifySubjectRepository an repository is subject of an notification + NotifySubjectRepository NotifySubjectType = "Repository" +) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 40dddb06a078..dfd08bcc68f8 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15241,8 +15241,7 @@ "x-go-name": "Title" }, "type": { - "type": "string", - "x-go-name": "Type" + "$ref": "#/definitions/NotifySubjectType" }, "url": { "type": "string", @@ -15286,6 +15285,11 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "NotifySubjectType": { + "description": "NotifySubjectType represent type of notification subject", + "type": "string", + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "OAuth2Application": { "type": "object", "title": "OAuth2Application represents an OAuth2 application.", From fc1d9629c61a6beb4afc92aee2c34292a919a1c3 Mon Sep 17 00:00:00 2001 From: Norwin Date: Thu, 1 Jul 2021 14:14:09 +0000 Subject: [PATCH 15/19] Clarify GPG binary check (#14832) fixes #14817 Co-authored-by: techknowlogick --- docs/content/doc/installation/from-binary.en-us.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/content/doc/installation/from-binary.en-us.md b/docs/content/doc/installation/from-binary.en-us.md index 9d8864956b28..aa075bb239e1 100644 --- a/docs/content/doc/installation/from-binary.en-us.md +++ b/docs/content/doc/installation/from-binary.en-us.md @@ -32,13 +32,17 @@ chmod +x gitea ``` ## Verify GPG signature -Gitea signs all binaries with a [GPG key](https://keys.openpgp.org/search?q=teabot%40gitea.io) to prevent against unwanted modification of binaries. To validate the binary, download the signature file which ends in `.asc` for the binary you downloaded and use the gpg command line tool. +Gitea signs all binaries with a [GPG key](https://keys.openpgp.org/search?q=teabot%40gitea.io) to prevent against unwanted modification of binaries. +To validate the binary, download the signature file which ends in `.asc` for the binary you downloaded and use the gpg command line tool. ```sh gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 gpg --verify gitea-{{< version >}}-linux-amd64.asc gitea-{{< version >}}-linux-amd64 ``` +Look for the text `Good signature from "Teabot "` to assert a good binary, +despite warnings like `This key is not certified with a trusted signature!`. + ## Recommended server configuration **NOTE:** Many of the following directories can be configured using [Environment Variables]({{< relref "doc/advanced/environment-variables.en-us.md" >}}) as well! From 290f458d46d4774ac47dca40af4e1c8b9bea755e Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 1 Jul 2021 17:13:20 +0200 Subject: [PATCH 16/19] Reserve user/repo pattern for rss feature (#16323) --- models/repo.go | 2 +- models/user.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/repo.go b/models/repo.go index 92d8427fab52..009e7a457d07 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1035,7 +1035,7 @@ func GetRepoInitFile(tp, name string) ([]byte, error) { var ( reservedRepoNames = []string{".", ".."} - reservedRepoPatterns = []string{"*.git", "*.wiki"} + reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} ) // IsUsableRepoName returns true when repository is usable diff --git a/models/user.go b/models/user.go index 47d24aefd6aa..ce96a144af25 100644 --- a/models/user.go +++ b/models/user.go @@ -814,7 +814,7 @@ var ( "user", } - reservedUserPatterns = []string{"*.keys", "*.gpg"} + reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom"} ) // isUsableName checks if name is reserved or pattern of name is not allowed From a3476e5ad5ee87d4e985b9a3e914bf5348216745 Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Fri, 2 Jul 2021 00:02:48 +0200 Subject: [PATCH 17/19] Wrap around for previous/next buttons (#16319) Fixes #16317 Wrap around from last to first comment when clicking "Next" on last comment. Wrap around from first to last comment when clicking "Previous" on first comment. --- web_src/js/index.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/web_src/js/index.js b/web_src/js/index.js index 0693175a0098..0b5ac493ed6a 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -947,21 +947,19 @@ async function initRepository() { const $conversation = $(e.currentTarget).closest('.comment-code-cloud'); const $conversations = $('.comment-code-cloud:not(.hide)'); const index = $conversations.index($conversation); - if (index !== 0) { - const $previousConversation = $conversations.eq(index - 1); - const anchor = $previousConversation.find('.comment').first().attr('id'); - window.location.href = `#${anchor}`; - } + const previousIndex = index > 0 ? index - 1 : $conversations.length - 1; + const $previousConversation = $conversations.eq(previousIndex); + const anchor = $previousConversation.find('.comment').first().attr('id'); + window.location.href = `#${anchor}`; }); $(document).on('click', '.next-conversation', (e) => { const $conversation = $(e.currentTarget).closest('.comment-code-cloud'); const $conversations = $('.comment-code-cloud:not(.hide)'); const index = $conversations.index($conversation); - if (index !== $conversations.length - 1) { - const $nextConversation = $conversations.eq(index + 1); - const anchor = $nextConversation.find('.comment').first().attr('id'); - window.location.href = `#${anchor}`; - } + const nextIndex = index < $conversations.length - 1 ? index + 1 : 0; + const $nextConversation = $conversations.eq(nextIndex); + const anchor = $nextConversation.find('.comment').first().attr('id'); + window.location.href = `#${anchor}`; }); // Quote reply From 92328a3394be51e6200f69c91c402aa15ff6e06e Mon Sep 17 00:00:00 2001 From: sebastian-sauer Date: Fri, 2 Jul 2021 14:19:57 +0200 Subject: [PATCH 18/19] Add API to get commits of PR (#16300) * Add API to get commits of PR fixes #10918 Co-authored-by: Andrew Bezold Co-authored-by: 6543 <6543@obermui.de> --- integrations/api_pull_commits_test.go | 37 ++++++++ routers/api/v1/api.go | 1 + routers/api/v1/repo/pull.go | 121 ++++++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 56 ++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 integrations/api_pull_commits_test.go diff --git a/integrations/api_pull_commits_test.go b/integrations/api_pull_commits_test.go new file mode 100644 index 000000000000..30682d9c147e --- /dev/null +++ b/integrations/api_pull_commits_test.go @@ -0,0 +1,37 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" +) + +func TestAPIPullCommits(t *testing.T) { + defer prepareTestEnv(t)() + pullIssue := models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + assert.NoError(t, pullIssue.LoadIssue()) + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue.HeadRepoID}).(*models.Repository) + + session := loginUser(t, "user2") + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/commits", repo.OwnerName, repo.Name, pullIssue.Index) + resp := session.MakeRequest(t, req, http.StatusOK) + + var commits []*api.Commit + DecodeJSON(t, resp, &commits) + + if !assert.Len(t, commits, 2) { + return + } + + assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commits[0].SHA) + assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", commits[1].SHA) +} + +// TODO add tests for already merged PR and closed PR diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c6b4ff04dec9..b6913ea1bc78 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -905,6 +905,7 @@ func Routes() *web.Route { m.Get(".diff", repo.DownloadPullDiff) m.Get(".patch", repo.DownloadPullPatch) m.Post("/update", reqToken(), repo.UpdatePullRequest) + m.Get("/commits", repo.GetPullRequestCommits) m.Combo("/merge").Get(repo.IsPullRequestMerged). Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest) m.Group("/reviews", func() { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index eff998ee996a..0c09a9a86b0e 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -6,7 +6,9 @@ package repo import ( "fmt" + "math" "net/http" + "strconv" "strings" "time" @@ -1101,3 +1103,122 @@ func UpdatePullRequest(ctx *context.APIContext) { ctx.Status(http.StatusOK) } + +// GetPullRequestCommits gets all commits associated with a given PR +func GetPullRequestCommits(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits + // --- + // summary: Get commits for a pull request + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request to get + // type: integer + // format: int64 + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/CommitList" + // "404": + // "$ref": "#/responses/notFound" + + pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrPullRequestNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + } + return + } + + if err := pr.LoadBaseRepo(); err != nil { + ctx.InternalServerError(err) + return + } + + var prInfo *git.CompareInfo + baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) + if err != nil { + ctx.ServerError("OpenRepository", err) + return + } + defer baseGitRepo.Close() + if pr.HasMerged { + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName()) + } else { + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) + } + if err != nil { + ctx.ServerError("GetCompareInfo", err) + return + } + commits := prInfo.Commits + + listOptions := utils.GetListOptions(ctx) + + totalNumberOfCommits := commits.Len() + totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize))) + + userCache := make(map[string]*models.User) + + start, end := listOptions.GetStartEnd() + + if end > totalNumberOfCommits { + end = totalNumberOfCommits + } + + apiCommits := make([]*api.Commit, end-start) + + i := 0 + addedCommitsCount := 0 + for commitPointer := commits.Front(); commitPointer != nil; commitPointer = commitPointer.Next() { + if i < start { + i++ + continue + } + if i >= end { + break + } + + commit := commitPointer.Value.(*git.Commit) + + // Create json struct + apiCommits[addedCommitsCount], err = convert.ToCommit(ctx.Repo.Repository, commit, userCache) + addedCommitsCount++ + if err != nil { + ctx.ServerError("toCommit", err) + return + } + i++ + } + + ctx.SetLinkHeader(int(totalNumberOfCommits), listOptions.PageSize) + + ctx.Header().Set("X-Page", strconv.Itoa(listOptions.Page)) + ctx.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize)) + ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", totalNumberOfCommits)) + ctx.Header().Set("X-PageCount", strconv.Itoa(totalNumberOfPages)) + ctx.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages)) + ctx.JSON(http.StatusOK, &apiCommits) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index dfd08bcc68f8..a2e449228e1a 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7333,6 +7333,62 @@ } } }, + "/repos/{owner}/{repo}/pulls/{index}/commits": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get commits for a pull request", + "operationId": "repoGetPullRequestCommits", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the pull request to get", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/CommitList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/pulls/{index}/merge": { "get": { "produces": [ From 836884429ae6d08909f4f96fbe0f1ea288e7af12 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 2 Jul 2021 16:04:57 +0200 Subject: [PATCH 19/19] Add forge emojies (#16296) * codeberg :codeberg: * gitlab :gitlab: * git :git: * github :github: * gogs :gogs: --- custom/conf/app.example.ini | 2 +- .../doc/advanced/config-cheat-sheet.en-us.md | 4 ++-- modules/setting/setting.go | 4 ++-- public/img/emoji/codeberg.png | Bin 0 -> 8317 bytes public/img/emoji/git.png | Bin 0 -> 5007 bytes public/img/emoji/github.png | Bin 0 -> 14111 bytes public/img/emoji/gitlab.png | Bin 0 -> 6873 bytes public/img/emoji/gogs.png | Bin 0 -> 11794 bytes 8 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 public/img/emoji/codeberg.png create mode 100644 public/img/emoji/git.png create mode 100644 public/img/emoji/github.png create mode 100644 public/img/emoji/gitlab.png create mode 100644 public/img/emoji/gogs.png diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index ba12b7ff12b8..ff21613586b5 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1040,7 +1040,7 @@ PATH = ;; Additional Emojis not defined in the utf8 standard ;; By default we support gitea (:gitea:), to add more copy them to public/img/emoji/emoji_name.png and add it to this config. ;; Dont mistake it for Reactions. -;CUSTOM_EMOJIS = gitea +;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs ;; ;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. ;DEFAULT_SHOW_FULL_NAME = false diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index f95a96439fa8..67b375481608 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -181,7 +181,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `REACTIONS`: All available reactions users can choose on issues/prs and comments Values can be emoji alias (:smile:) or a unicode emoji. For custom reactions, add a tightly cropped square image to public/img/emoji/reaction_name.png -- `CUSTOM_EMOJIS`: **gitea**: Additional Emojis not defined in the utf8 standard. +- `CUSTOM_EMOJIS`: **gitea, codeberg, gitlab, git, github, gogs**: Additional Emojis not defined in the utf8 standard. By default we support gitea (:gitea:), to add more copy them to public/img/emoji/emoji_name.png and add it to this config. - `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. @@ -392,7 +392,7 @@ relation to port exhaustion. - `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue - `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. - Queues by default come with a dynamically scaling worker pool. The following settings configure this: -- `WORKERS`: **0** (v1.14 and before: **1**): Number of initial workers for the queue. +- `WORKERS`: **0** (v1.14 and before: **1**): Number of initial workers for the queue. - `MAX_WORKERS`: **10**: Maximum number of worker go-routines for the queue. - `BLOCK_TIMEOUT`: **1s**: If the queue blocks for this time, boost the number of workers - the `BLOCK_TIMEOUT` will then be doubled before boosting again whilst the boost is ongoing. - `BOOST_TIMEOUT`: **5m**: Boost workers will timeout after this long. diff --git a/modules/setting/setting.go b/modules/setting/setting.go index e37b78834202..e3da5796e426 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -258,8 +258,8 @@ var ( DefaultTheme: `gitea`, Themes: []string{`gitea`, `arc-green`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, - CustomEmojis: []string{`gitea`}, - CustomEmojisMap: map[string]string{"gitea": ":gitea:"}, + CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, + CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, Notification: struct { MinTimeout time.Duration TimeoutStep time.Duration diff --git a/public/img/emoji/codeberg.png b/public/img/emoji/codeberg.png new file mode 100644 index 0000000000000000000000000000000000000000..b91613833a2ffafc514facb6beac699ee8973b89 GIT binary patch literal 8317 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 literal 0 HcmV?d00001 diff --git a/public/img/emoji/git.png b/public/img/emoji/git.png new file mode 100644 index 0000000000000000000000000000000000000000..00a6bcfca4a2c4706628cdedf404dd7d1ebc83dd GIT binary patch literal 5007 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 literal 0 HcmV?d00001 diff --git a/public/img/emoji/gitlab.png b/public/img/emoji/gitlab.png new file mode 100644 index 0000000000000000000000000000000000000000..55a0d2b70b9e2e5b381d2eb2e36358197b98a016 GIT binary patch literal 6873 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 Lq#mMD)jslnh71Eg literal 0 HcmV?d00001 diff --git a/public/img/emoji/gogs.png b/public/img/emoji/gogs.png new file mode 100644 index 0000000000000000000000000000000000000000..6471a84dadbbe9935ac802f674a501f1c19ee84d GIT binary patch literal 11794 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