diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000000..55a12cc9e479 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,39 @@ +tasks: + - name: Setup + init: | + make deps + make build + command: | + gp sync-done setup + exit 0 + - name: Run frontend + command: | + gp sync-await setup + make watch-frontend + - name: Run backend + command: | + gp sync-await setup + mkdir -p custom/conf/ + echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini + echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini + export TAGS="sqlite sqlite_unlock_notify" + make watch-backend + - name: Run docs + before: sudo bash -c "$(grep 'https://github.com/gohugoio/hugo/releases/download' Makefile | tr -d '\')" # install hugo + command: cd docs && make clean update && hugo server -D -F --baseUrl $(gp url 1313) --liveReloadPort=443 --appendPort=false --bind=0.0.0.0 + +vscode: + extensions: + - editorconfig.editorconfig + - dbaeumer.vscode-eslint + - golang.go + - stylelint.vscode-stylelint + - DavidAnson.vscode-markdownlint + - johnsoncodehk.volar + - ms-azuretools.vscode-docker + +ports: + - name: Gitea + port: 3000 + - name: Docs + port: 1313 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b13af805bcf5..4aff27fc65c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -170,17 +170,22 @@ import ( To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The Gitea code is divided into the following parts: -- **integration:** Integration tests - **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependencies to other code in Gitea should be avoided although some modules might be needed (for example for logging). - **models/fixtures:** Sample model data used in integration tests. - **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step. -- **modules:** Different modules to handle specific functionality in Gitea. +- **modules:** Different modules to handle specific functionality in Gitea. Shall only depend on other modules but not other packages (models, services). - **public:** Frontend files (javascript, images, css, etc.) -- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers +- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers. - **services:** Support functions for common routing operations. Uses models and modules to handle the request. - **templates:** Golang templates for generating the html output. +- **tests/e2e:** End to end tests +- **tests/integration:** Integration tests - **vendor:** External code that Gitea depends on. +## Documentation + +If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated. + ## API v1 The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [GitHub API v3](https://developer.github.com/v3/). @@ -229,27 +234,6 @@ An endpoint which changes/edits an object expects all fields to be optional (exc - support pagination (`page` & `limit` options in query) - set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) -## Large Character Comments - -Throughout the codebase there are large-text comments for sections of code, e.g.: - -```go -// __________ .__ -// \______ \ _______ _|__| ______ _ __ -// | _// __ \ \/ / |/ __ \ \/ \/ / -// | | \ ___/\ /| \ ___/\ / -// |____|_ /\___ >\_/ |__|\___ >\/\_/ -// \/ \/ \/ -``` - -These were created using the `figlet` tool with the `graffiti` font. - -A simple way of creating these is to use the following: - -```bash -figlet -f graffiti Review | sed -e's+^+// +' - | xclip -sel clip -in -``` - ## Backports and Frontports Occasionally backports of PRs are required. diff --git a/README.md b/README.md index 80f1159aea1f..115cafb58d4a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ + + Contribute with Gitpod + diff --git a/README_ZH.md b/README_ZH.md index 2be35742672f..0e58ad6d4a46 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -33,6 +33,12 @@ + + Contribute with Gitpod + diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index 79c34564bc97..d205c6ee8ba6 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -49,12 +49,8 @@ After=network.target ### [Service] -# Modify these two values and uncomment them if you have -# repos with lots of files and get an HTTP error 500 because -# of that -### -#LimitMEMLOCK=infinity -#LimitNOFILE=65535 +# Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that +# LimitNOFILE=524288:524288 RestartSec=2s Type=simple User=git 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 50571bd4433f..3fd853fb148f 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -537,9 +537,9 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o ## Camo (`camo`) - `ENABLED`: **false**: Enable media proxy, we support images only at the moment. -- `SERVER_URL`: ****: url of camo server, it **is required** if camo is enabled. -- `HMAC_KEY`: ****: Provide the HMAC key for encoding urls, it **is required** if camo is enabled. -- `ALLWAYS`: **false**: Set to true to use camo for https too lese only non https urls are proxyed +- `SERVER_URL`: ****: URL of camo server, it **is required** if camo is enabled. +- `HMAC_KEY`: ****: Provide the HMAC key for encoding URLs, it **is required** if camo is enabled. +- `ALLWAYS`: **false**: Set to true to use camo for both HTTP and HTTPS content, otherwise only non-HTTPS URLs are proxied ## OpenID (`openid`) diff --git a/docs/content/doc/developers/guidelines-frontend.md b/docs/content/doc/developers/guidelines-frontend.en-us.md similarity index 100% rename from docs/content/doc/developers/guidelines-frontend.md rename to docs/content/doc/developers/guidelines-frontend.en-us.md diff --git a/docs/content/doc/developers/hacking-on-gitea.en-us.md b/docs/content/doc/developers/hacking-on-gitea.en-us.md index 361ab530fa47..d8427b58f32b 100644 --- a/docs/content/doc/developers/hacking-on-gitea.en-us.md +++ b/docs/content/doc/developers/hacking-on-gitea.en-us.md @@ -19,6 +19,12 @@ menu: {{< toc >}} +## Quickstart + +To get a quick working development environment you could use Gitpod. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/go-gitea/gitea) + ## Installing go You should [install go](https://golang.org/doc/install) and set up your go @@ -171,7 +177,7 @@ server as mentioned above. ### Working on JS and CSS -Frontend development should follow [Guidelines for Frontend Development](./guidelines-frontend.md) +Frontend development should follow [Guidelines for Frontend Development]({{< relref "doc/developers/guidelines-frontend.en-us.md" >}}) To build with frontend resources, either use the `watch-frontend` target mentioned above or just build once: diff --git a/go.mod b/go.mod index 2ddf6f2ff3b7..5bafaf414257 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( golang.org/x/net v0.0.0-20220927171203-f486391704dc golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec - golang.org/x/text v0.3.7 + golang.org/x/text v0.3.8 golang.org/x/tools v0.1.12 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 diff --git a/go.sum b/go.sum index 7378dfb9320a..b53732f6a5da 100644 --- a/go.sum +++ b/go.sum @@ -1891,8 +1891,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 16fb4bac8c86..f349b94ac8bf 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -18,13 +18,11 @@ import ( type ActionList []*Action func (actions ActionList) getUserIDs() []int64 { - userIDs := make(map[int64]struct{}, len(actions)) + userIDs := make(container.Set[int64], len(actions)) for _, action := range actions { - if _, ok := userIDs[action.ActUserID]; !ok { - userIDs[action.ActUserID] = struct{}{} - } + userIDs.Add(action.ActUserID) } - return container.KeysInt64(userIDs) + return userIDs.Values() } func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) { @@ -48,13 +46,11 @@ func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model. } func (actions ActionList) getRepoIDs() []int64 { - repoIDs := make(map[int64]struct{}, len(actions)) + repoIDs := make(container.Set[int64], len(actions)) for _, action := range actions { - if _, ok := repoIDs[action.RepoID]; !ok { - repoIDs[action.RepoID] = struct{}{} - } + repoIDs.Add(action.RepoID) } - return container.KeysInt64(repoIDs) + return repoIDs.Values() } func (actions ActionList) loadRepositories(ctx context.Context) error { diff --git a/models/activities/notification.go b/models/activities/notification.go index 88776db42bdc..2f21dc74d1f7 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -200,7 +200,7 @@ func CreateOrUpdateIssueNotifications(issueID, commentID, notificationAuthorID, func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { // init - var toNotify map[int64]struct{} + var toNotify container.Set[int64] notifications, err := getNotificationsByIssueID(ctx, issueID) if err != nil { return err @@ -212,33 +212,27 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n } if receiverID > 0 { - toNotify = make(map[int64]struct{}, 1) - toNotify[receiverID] = struct{}{} + toNotify = make(container.Set[int64], 1) + toNotify.Add(receiverID) } else { - toNotify = make(map[int64]struct{}, 32) + toNotify = make(container.Set[int64], 32) issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) if err != nil { return err } - for _, id := range issueWatches { - toNotify[id] = struct{}{} - } + toNotify.AddMultiple(issueWatches...) if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) if err != nil { return err } - for _, id := range repoWatches { - toNotify[id] = struct{}{} - } + toNotify.AddMultiple(repoWatches...) } issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) if err != nil { return err } - for _, id := range issueParticipants { - toNotify[id] = struct{}{} - } + toNotify.AddMultiple(issueParticipants...) // dont notify user who cause notification delete(toNotify, notificationAuthorID) @@ -248,7 +242,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n return err } for _, id := range issueUnWatches { - delete(toNotify, id) + toNotify.Remove(id) } } @@ -499,16 +493,14 @@ func (nl NotificationList) LoadAttributes() error { } func (nl NotificationList) getPendingRepoIDs() []int64 { - ids := make(map[int64]struct{}, len(nl)) + ids := make(container.Set[int64], len(nl)) for _, notification := range nl { if notification.Repository != nil { continue } - if _, ok := ids[notification.RepoID]; !ok { - ids[notification.RepoID] = struct{}{} - } + ids.Add(notification.RepoID) } - return container.KeysInt64(ids) + return ids.Values() } // LoadRepos loads repositories from database @@ -575,16 +567,14 @@ func (nl NotificationList) LoadRepos() (repo_model.RepositoryList, []int, error) } func (nl NotificationList) getPendingIssueIDs() []int64 { - ids := make(map[int64]struct{}, len(nl)) + ids := make(container.Set[int64], len(nl)) for _, notification := range nl { if notification.Issue != nil { continue } - if _, ok := ids[notification.IssueID]; !ok { - ids[notification.IssueID] = struct{}{} - } + ids.Add(notification.IssueID) } - return container.KeysInt64(ids) + return ids.Values() } // LoadIssues loads issues from database @@ -661,16 +651,14 @@ func (nl NotificationList) Without(failures []int) NotificationList { } func (nl NotificationList) getPendingCommentIDs() []int64 { - ids := make(map[int64]struct{}, len(nl)) + ids := make(container.Set[int64], len(nl)) for _, notification := range nl { if notification.CommentID == 0 || notification.Comment != nil { continue } - if _, ok := ids[notification.CommentID]; !ok { - ids[notification.CommentID] = struct{}{} - } + ids.Add(notification.CommentID) } - return container.KeysInt64(ids) + return ids.Values() } // LoadComments loads comments from database diff --git a/models/fixtures/oauth2_application.yml b/models/fixtures/oauth2_application.yml index a13e20b10e2a..34d5a887776a 100644 --- a/models/fixtures/oauth2_application.yml +++ b/models/fixtures/oauth2_application.yml @@ -4,6 +4,6 @@ name: "Test" client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA= - redirect_uris: '["a"]' + redirect_uris: '["a", "https://example.com/xyzzy"]' created_unix: 1546869730 updated_unix: 1546869730 diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index e3406a5cbe08..70105d7ff056 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -17,13 +17,11 @@ import ( type CommentList []*Comment func (comments CommentList) getPosterIDs() []int64 { - posterIDs := make(map[int64]struct{}, len(comments)) + posterIDs := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := posterIDs[comment.PosterID]; !ok { - posterIDs[comment.PosterID] = struct{}{} - } + posterIDs.Add(comment.PosterID) } - return container.KeysInt64(posterIDs) + return posterIDs.Values() } func (comments CommentList) loadPosters(ctx context.Context) error { @@ -70,13 +68,11 @@ func (comments CommentList) getCommentIDs() []int64 { } func (comments CommentList) getLabelIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := ids[comment.LabelID]; !ok { - ids[comment.LabelID] = struct{}{} - } + ids.Add(comment.LabelID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadLabels(ctx context.Context) error { //nolint @@ -120,13 +116,11 @@ func (comments CommentList) loadLabels(ctx context.Context) error { //nolint } func (comments CommentList) getMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := ids[comment.MilestoneID]; !ok { - ids[comment.MilestoneID] = struct{}{} - } + ids.Add(comment.MilestoneID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadMilestones(ctx context.Context) error { @@ -163,13 +157,11 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { } func (comments CommentList) getOldMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := ids[comment.OldMilestoneID]; !ok { - ids[comment.OldMilestoneID] = struct{}{} - } + ids.Add(comment.OldMilestoneID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadOldMilestones(ctx context.Context) error { @@ -206,13 +198,11 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { } func (comments CommentList) getAssigneeIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := ids[comment.AssigneeID]; !ok { - ids[comment.AssigneeID] = struct{}{} - } + ids.Add(comment.AssigneeID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadAssignees(ctx context.Context) error { @@ -259,16 +249,14 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { // getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded func (comments CommentList) getIssueIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { if comment.Issue != nil { continue } - if _, ok := ids[comment.IssueID]; !ok { - ids[comment.IssueID] = struct{}{} - } + ids.Add(comment.IssueID) } - return container.KeysInt64(ids) + return ids.Values() } // Issues returns all the issues of comments @@ -334,16 +322,14 @@ func (comments CommentList) loadIssues(ctx context.Context) error { } func (comments CommentList) getDependentIssueIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { if comment.DependentIssue != nil { continue } - if _, ok := ids[comment.DependentIssueID]; !ok { - ids[comment.DependentIssueID] = struct{}{} - } + ids.Add(comment.DependentIssueID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadDependentIssues(ctx context.Context) error { @@ -439,13 +425,11 @@ func (comments CommentList) loadAttachments(ctx context.Context) (err error) { } func (comments CommentList) getReviewIDs() []int64 { - ids := make(map[int64]struct{}, len(comments)) + ids := make(container.Set[int64], len(comments)) for _, comment := range comments { - if _, ok := ids[comment.ReviewID]; !ok { - ids[comment.ReviewID] = struct{}{} - } + ids.Add(comment.ReviewID) } - return container.KeysInt64(ids) + return ids.Values() } func (comments CommentList) loadReviews(ctx context.Context) error { //nolint diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 874f2a636815..deadb6a5649f 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -22,16 +22,16 @@ type IssueList []*Issue // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo func (issues IssueList) getRepoIDs() []int64 { - repoIDs := make(map[int64]struct{}, len(issues)) + repoIDs := make(container.Set[int64], len(issues)) for _, issue := range issues { if issue.Repo == nil { - repoIDs[issue.RepoID] = struct{}{} + repoIDs.Add(issue.RepoID) } if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil { - repoIDs[issue.PullRequest.HeadRepoID] = struct{}{} + repoIDs.Add(issue.PullRequest.HeadRepoID) } } - return container.KeysInt64(repoIDs) + return repoIDs.Values() } func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Repository, error) { @@ -79,13 +79,11 @@ func (issues IssueList) LoadRepositories() ([]*repo_model.Repository, error) { } func (issues IssueList) getPosterIDs() []int64 { - posterIDs := make(map[int64]struct{}, len(issues)) + posterIDs := make(container.Set[int64], len(issues)) for _, issue := range issues { - if _, ok := posterIDs[issue.PosterID]; !ok { - posterIDs[issue.PosterID] = struct{}{} - } + posterIDs.Add(issue.PosterID) } - return container.KeysInt64(posterIDs) + return posterIDs.Values() } func (issues IssueList) loadPosters(ctx context.Context) error { @@ -185,13 +183,11 @@ func (issues IssueList) loadLabels(ctx context.Context) error { } func (issues IssueList) getMilestoneIDs() []int64 { - ids := make(map[int64]struct{}, len(issues)) + ids := make(container.Set[int64], len(issues)) for _, issue := range issues { - if _, ok := ids[issue.MilestoneID]; !ok { - ids[issue.MilestoneID] = struct{}{} - } + ids.Add(issue.MilestoneID) } - return container.KeysInt64(ids) + return ids.Values() } func (issues IssueList) loadMilestones(ctx context.Context) error { @@ -224,14 +220,11 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { } func (issues IssueList) getProjectIDs() []int64 { - ids := make(map[int64]struct{}, len(issues)) + ids := make(container.Set[int64], len(issues)) for _, issue := range issues { - projectID := issue.ProjectID() - if _, ok := ids[projectID]; !ok { - ids[projectID] = struct{}{} - } + ids.Add(issue.ProjectID()) } - return container.KeysInt64(ids) + return ids.Values() } func (issues IssueList) loadProjects(ctx context.Context) error { diff --git a/models/issues/reaction.go b/models/issues/reaction.go index e7295c8af819..ccda10be2ce9 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -211,7 +211,7 @@ type ReactionOptions struct { // CreateReaction creates reaction for issue or comment. func CreateReaction(opts *ReactionOptions) (*Reaction, error) { - if !setting.UI.ReactionsMap[opts.Type] { + if !setting.UI.ReactionsLookup.Contains(opts.Type) { return nil, ErrForbiddenIssueReaction{opts.Type} } @@ -316,16 +316,14 @@ func (list ReactionList) GroupByType() map[string]ReactionList { } func (list ReactionList) getUserIDs() []int64 { - userIDs := make(map[int64]struct{}, len(list)) + userIDs := make(container.Set[int64], len(list)) for _, reaction := range list { if reaction.OriginalAuthor != "" { continue } - if _, ok := userIDs[reaction.UserID]; !ok { - userIDs[reaction.UserID] = struct{}{} - } + userIDs.Add(reaction.UserID) } - return container.KeysInt64(userIDs) + return userIDs.Values() } func valuesUser(m map[int64]*user_model.User) []*user_model.User { diff --git a/models/migrate.go b/models/migrate.go index f6bceaa019f3..d842fb967bfb 100644 --- a/models/migrate.go +++ b/models/migrate.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/structs" ) @@ -99,9 +100,9 @@ func InsertIssueComments(comments []*issues_model.Comment) error { return nil } - issueIDs := make(map[int64]bool) + issueIDs := make(container.Set[int64]) for _, comment := range comments { - issueIDs[comment.IssueID] = true + issueIDs.Add(comment.IssueID) } ctx, committer, err := db.TxContext() diff --git a/models/migrations/v115.go b/models/migrations/v115.go index 7708ed5e28af..b242ccf23891 100644 --- a/models/migrations/v115.go +++ b/models/migrations/v115.go @@ -13,6 +13,7 @@ import ( "path/filepath" "time" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -39,7 +40,7 @@ func renameExistingUserAvatarName(x *xorm.Engine) error { } log.Info("%d User Avatar(s) to migrate ...", count) - deleteList := make(map[string]struct{}) + deleteList := make(container.Set[string]) start := 0 migrated := 0 for { @@ -86,7 +87,7 @@ func renameExistingUserAvatarName(x *xorm.Engine) error { return fmt.Errorf("[user: %s] user table update: %v", user.LowerName, err) } - deleteList[filepath.Join(setting.Avatar.Path, oldAvatar)] = struct{}{} + deleteList.Add(filepath.Join(setting.Avatar.Path, oldAvatar)) migrated++ select { case <-ticker.C: diff --git a/models/packages/conan/search.go b/models/packages/conan/search.go index 6a2cfa38f595..39a90004597d 100644 --- a/models/packages/conan/search.go +++ b/models/packages/conan/search.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/container" conan_module "code.gitea.io/gitea/modules/packages/conan" "xorm.io/builder" @@ -88,7 +89,7 @@ func SearchRecipes(ctx context.Context, opts *RecipeSearchOptions) ([]string, er return nil, err } - unique := make(map[string]bool) + unique := make(container.Set[string]) for _, info := range results { recipe := fmt.Sprintf("%s/%s", info.Name, info.Version) @@ -111,7 +112,7 @@ func SearchRecipes(ctx context.Context, opts *RecipeSearchOptions) ([]string, er } } - unique[recipe] = true + unique.Add(recipe) } recipes := make([]string, 0, len(unique)) diff --git a/models/repo.go b/models/repo.go index f2676a569614..558678bfe54a 100644 --- a/models/repo.go +++ b/models/repo.go @@ -438,15 +438,27 @@ func CheckRepoStats(ctx context.Context) error { repoStatsCorrectNumStars, "repository count 'num_stars'", }, + // Repository.NumIssues + { + statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, false), + repoStatsCorrectNumIssues, + "repository count 'num_issues'", + }, // Repository.NumClosedIssues { statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, false), repoStatsCorrectNumClosedIssues, "repository count 'num_closed_issues'", }, + // Repository.NumPulls + { + statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, true), + repoStatsCorrectNumPulls, + "repository count 'num_pulls'", + }, // Repository.NumClosedPulls { - statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, true), + statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, true), repoStatsCorrectNumClosedPulls, "repository count 'num_closed_pulls'", }, diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index cc524a417e6f..0cd0a3c8e35e 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -68,10 +68,10 @@ func (repos RepositoryList) loadAttributes(ctx context.Context) error { return nil } - set := make(map[int64]struct{}) + set := make(container.Set[int64]) repoIDs := make([]int64, len(repos)) for i := range repos { - set[repos[i].OwnerID] = struct{}{} + set.Add(repos[i].OwnerID) repoIDs[i] = repos[i].ID } @@ -79,7 +79,7 @@ func (repos RepositoryList) loadAttributes(ctx context.Context) error { users := make(map[int64]*user_model.User, len(set)) if err := db.GetEngine(ctx). Where("id > 0"). - In("id", container.KeysInt64(set)). + In("id", set.Values()). Find(&users); err != nil { return fmt.Errorf("find users: %v", err) } diff --git a/models/repo/topic.go b/models/repo/topic.go index 2a16467215d3..7ba9a49e894e 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -11,6 +11,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -62,7 +63,7 @@ func ValidateTopic(topic string) bool { // SanitizeAndValidateTopics sanitizes and checks an array or topics func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []string) { validTopics = make([]string, 0) - mValidTopics := make(map[string]struct{}) + mValidTopics := make(container.Set[string]) invalidTopics = make([]string, 0) for _, topic := range topics { @@ -72,12 +73,12 @@ func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []st continue } // ignore same topic twice - if _, ok := mValidTopics[topic]; ok { + if mValidTopics.Contains(topic) { continue } if ValidateTopic(topic) { validTopics = append(validTopics, topic) - mValidTopics[topic] = struct{}{} + mValidTopics.Add(topic) } else { invalidTopics = append(invalidTopics, topic) } diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 6c0a241dc562..e7125f70f8f9 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" api "code.gitea.io/gitea/modules/structs" "xorm.io/builder" @@ -83,37 +84,19 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us return nil, err } - uidMap := map[int64]bool{} - i := 0 - for _, uid := range userIDs { - if uidMap[uid] { - continue - } - uidMap[uid] = true - userIDs[i] = uid - i++ - } - userIDs = userIDs[:i] - userIDs = append(userIDs, additionalUserIDs...) - - for _, uid := range additionalUserIDs { - if uidMap[uid] { - continue - } - userIDs[i] = uid - i++ - } - userIDs = userIDs[:i] + uniqueUserIDs := make(container.Set[int64]) + uniqueUserIDs.AddMultiple(userIDs...) + uniqueUserIDs.AddMultiple(additionalUserIDs...) // Leave a seat for owner itself to append later, but if owner is an organization // and just waste 1 unit is cheaper than re-allocate memory once. - users := make([]*user_model.User, 0, len(userIDs)+1) + users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) if len(userIDs) > 0 { - if err = e.In("id", userIDs).OrderBy(user_model.GetOrderByName()).Find(&users); err != nil { + if err = e.In("id", uniqueUserIDs.Values()).OrderBy(user_model.GetOrderByName()).Find(&users); err != nil { return nil, err } } - if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] { + if !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(repo.OwnerID) { users = append(users, repo.Owner) } diff --git a/models/user/email_address.go b/models/user/email_address.go index c931db9c1672..d87b945706d7 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -41,6 +41,7 @@ func (err ErrEmailCharIsNotSupported) Error() string { } // ErrEmailInvalid represents an error where the email address does not comply with RFC 5322 +// or has a leading '-' character type ErrEmailInvalid struct { Email string } @@ -134,9 +135,7 @@ func ValidateEmail(email string) error { return ErrEmailCharIsNotSupported{email} } - if !(email[0] >= 'a' && email[0] <= 'z') && - !(email[0] >= 'A' && email[0] <= 'Z') && - !(email[0] >= '0' && email[0] <= '9') { + if email[0] == '-' { return ErrEmailInvalid{email} } diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 471598c89764..b9acaa1113ae 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -281,23 +281,25 @@ func TestEmailAddressValidate(t *testing.T) { `first~last@iana.org`: nil, `first;last@iana.org`: user_model.ErrEmailCharIsNotSupported{`first;last@iana.org`}, ".233@qq.com": user_model.ErrEmailInvalid{".233@qq.com"}, - "!233@qq.com": user_model.ErrEmailInvalid{"!233@qq.com"}, - "#233@qq.com": user_model.ErrEmailInvalid{"#233@qq.com"}, - "$233@qq.com": user_model.ErrEmailInvalid{"$233@qq.com"}, - "%233@qq.com": user_model.ErrEmailInvalid{"%233@qq.com"}, - "&233@qq.com": user_model.ErrEmailInvalid{"&233@qq.com"}, - "'233@qq.com": user_model.ErrEmailInvalid{"'233@qq.com"}, - "*233@qq.com": user_model.ErrEmailInvalid{"*233@qq.com"}, - "+233@qq.com": user_model.ErrEmailInvalid{"+233@qq.com"}, - "/233@qq.com": user_model.ErrEmailInvalid{"/233@qq.com"}, - "=233@qq.com": user_model.ErrEmailInvalid{"=233@qq.com"}, - "?233@qq.com": user_model.ErrEmailInvalid{"?233@qq.com"}, - "^233@qq.com": user_model.ErrEmailInvalid{"^233@qq.com"}, - "`233@qq.com": user_model.ErrEmailInvalid{"`233@qq.com"}, - "{233@qq.com": user_model.ErrEmailInvalid{"{233@qq.com"}, - "|233@qq.com": user_model.ErrEmailInvalid{"|233@qq.com"}, - "}233@qq.com": user_model.ErrEmailInvalid{"}233@qq.com"}, - "~233@qq.com": user_model.ErrEmailInvalid{"~233@qq.com"}, + "!233@qq.com": nil, + "#233@qq.com": nil, + "$233@qq.com": nil, + "%233@qq.com": nil, + "&233@qq.com": nil, + "'233@qq.com": nil, + "*233@qq.com": nil, + "+233@qq.com": nil, + "-233@qq.com": user_model.ErrEmailInvalid{"-233@qq.com"}, + "/233@qq.com": nil, + "=233@qq.com": nil, + "?233@qq.com": nil, + "^233@qq.com": nil, + "_233@qq.com": nil, + "`233@qq.com": nil, + "{233@qq.com": nil, + "|233@qq.com": nil, + "}233@qq.com": nil, + "~233@qq.com": nil, ";233@qq.com": user_model.ErrEmailCharIsNotSupported{";233@qq.com"}, "Foo ": user_model.ErrEmailCharIsNotSupported{"Foo "}, string([]byte{0xE2, 0x84, 0xAA}): user_model.ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})}, diff --git a/modules/base/tool.go b/modules/base/tool.go index a981fd6c57dc..f1e4a3bf9783 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -241,15 +241,6 @@ func Int64sToStrings(ints []int64) []string { return strs } -// Int64sToMap converts a slice of int64 to a int64 map. -func Int64sToMap(ints []int64) map[int64]bool { - m := make(map[int64]bool) - for _, i := range ints { - m[i] = true - } - return m -} - // Int64sContains returns if a int64 in a slice of int64 func Int64sContains(intsSlice []int64, a int64) bool { for _, c := range intsSlice { diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 6685168bacd8..87de898e0b80 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -214,16 +214,7 @@ func TestInt64sToStrings(t *testing.T) { ) } -func TestInt64sToMap(t *testing.T) { - assert.Equal(t, map[int64]bool{}, Int64sToMap([]int64{})) - assert.Equal(t, - map[int64]bool{1: true, 4: true, 16: true}, - Int64sToMap([]int64{1, 4, 16}), - ) -} - func TestInt64sContains(t *testing.T) { - assert.Equal(t, map[int64]bool{}, Int64sToMap([]int64{})) assert.True(t, Int64sContains([]int64{6, 44324, 4324, 32, 1, 2323}, 1)) assert.True(t, Int64sContains([]int64{2323}, 2323)) assert.False(t, Int64sContains([]int64{6, 44324, 4324, 32, 1, 2323}, 232)) diff --git a/modules/container/map.go b/modules/container/map.go deleted file mode 100644 index 3519de09512e..000000000000 --- a/modules/container/map.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package container - -// KeysInt64 returns keys slice for a map with int64 key -func KeysInt64(m map[int64]struct{}) []int64 { - keys := make([]int64, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - return keys -} diff --git a/modules/container/set.go b/modules/container/set.go new file mode 100644 index 000000000000..4b4c74525d1f --- /dev/null +++ b/modules/container/set.go @@ -0,0 +1,57 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package container + +type Set[T comparable] map[T]struct{} + +// SetOf creates a set and adds the specified elements to it. +func SetOf[T comparable](values ...T) Set[T] { + s := make(Set[T], len(values)) + s.AddMultiple(values...) + return s +} + +// Add adds the specified element to a set. +// Returns true if the element is added; false if the element is already present. +func (s Set[T]) Add(value T) bool { + if _, has := s[value]; !has { + s[value] = struct{}{} + return true + } + return false +} + +// AddMultiple adds the specified elements to a set. +func (s Set[T]) AddMultiple(values ...T) { + for _, value := range values { + s.Add(value) + } +} + +// Contains determines whether a set contains the specified element. +// Returns true if the set contains the specified element; otherwise, false. +func (s Set[T]) Contains(value T) bool { + _, has := s[value] + return has +} + +// Remove removes the specified element. +// Returns true if the element is successfully found and removed; otherwise, false. +func (s Set[T]) Remove(value T) bool { + if _, has := s[value]; has { + delete(s, value) + return true + } + return false +} + +// Values gets a list of all elements in the set. +func (s Set[T]) Values() []T { + keys := make([]T, 0, len(s)) + for k := range s { + keys = append(keys, k) + } + return keys +} diff --git a/modules/container/set_test.go b/modules/container/set_test.go new file mode 100644 index 000000000000..6654763e56f3 --- /dev/null +++ b/modules/container/set_test.go @@ -0,0 +1,37 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package container + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSet(t *testing.T) { + s := make(Set[string]) + + assert.True(t, s.Add("key1")) + assert.False(t, s.Add("key1")) + assert.True(t, s.Add("key2")) + + assert.True(t, s.Contains("key1")) + assert.True(t, s.Contains("key2")) + assert.False(t, s.Contains("key3")) + + assert.True(t, s.Remove("key2")) + assert.False(t, s.Contains("key2")) + + assert.False(t, s.Remove("key3")) + + s.AddMultiple("key4", "key5") + assert.True(t, s.Contains("key4")) + assert.True(t, s.Contains("key5")) + + s = SetOf("key6", "key7") + assert.False(t, s.Contains("key1")) + assert.True(t, s.Contains("key6")) + assert.True(t, s.Contains("key7")) +} diff --git a/modules/context/repo.go b/modules/context/repo.go index 1742683d3cf1..1a0263a3307a 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -451,11 +451,20 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { owner, err = user_model.GetUserByName(ctx, userName) if err != nil { if user_model.IsErrUserNotExist(err) { + // go-get does not support redirects + // https://github.com/golang/go/issues/19760 if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return } - ctx.NotFound("GetUserByName", nil) + + if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil { + RedirectToUser(ctx, userName, redirectUserID) + } else if user_model.IsErrUserRedirectNotExist(err) { + ctx.NotFound("GetUserByName", nil) + } else { + ctx.ServerError("LookupUserRedirect", err) + } } else { ctx.ServerError("GetUserByName", err) } diff --git a/modules/convert/convert.go b/modules/convert/convert.go index af759cb9388b..187c67fa7680 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -412,7 +412,7 @@ func ToLFSLock(l *git_model.LFSLock) *api.LFSLock { Path: l.Path, LockedAt: l.Created.Round(time.Second), Owner: &api.LFSLockOwner{ - Name: u.DisplayName(), + Name: u.Name, }, } } diff --git a/modules/doctor/authorizedkeys.go b/modules/doctor/authorizedkeys.go index 34dfe939d3e4..d4ceef87c0f3 100644 --- a/modules/doctor/authorizedkeys.go +++ b/modules/doctor/authorizedkeys.go @@ -14,6 +14,7 @@ import ( "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -40,7 +41,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e } defer f.Close() - linesInAuthorizedKeys := map[string]bool{} + linesInAuthorizedKeys := make(container.Set[string]) scanner := bufio.NewScanner(f) for scanner.Scan() { @@ -48,7 +49,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e if strings.HasPrefix(line, tplCommentPrefix) { continue } - linesInAuthorizedKeys[line] = true + linesInAuthorizedKeys.Add(line) } f.Close() @@ -64,7 +65,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e if strings.HasPrefix(line, tplCommentPrefix) { continue } - if ok := linesInAuthorizedKeys[line]; ok { + if linesInAuthorizedKeys.Contains(line) { continue } if !autofix { diff --git a/modules/doctor/heads.go b/modules/doctor/heads.go new file mode 100644 index 000000000000..ec14aa4eadb1 --- /dev/null +++ b/modules/doctor/heads.go @@ -0,0 +1,91 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package doctor + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) error { + numRepos := 0 + numHeadsBroken := 0 + numDefaultBranchesBroken := 0 + numReposUpdated := 0 + err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { + numRepos++ + runOpts := &git.RunOpts{Dir: repo.RepoPath()} + + _, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse", "--", repo.DefaultBranch).RunStdString(runOpts) + + head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts) + + // what we expect: default branch is valid, and HEAD points to it + if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch { + return nil + } + + if headErr != nil { + numHeadsBroken++ + } + if defaultBranchErr != nil { + numDefaultBranchesBroken++ + } + + // if default branch is broken, let the user fix that in the UI + if defaultBranchErr != nil { + logger.Warn("Default branch for %s/%s doesn't point to a valid commit", repo.OwnerName, repo.Name) + return nil + } + + // if we're not autofixing, that's all we can do + if !autofix { + return nil + } + + // otherwise, let's try fixing HEAD + err := git.NewCommand(ctx, "symbolic-ref", "--", "HEAD", repo.DefaultBranch).Run(runOpts) + if err != nil { + logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err) + return nil + } + numReposUpdated++ + return nil + }) + if err != nil { + logger.Critical("Error when fixing repo HEADs: %v", err) + } + + if autofix { + logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated) + } else { + if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 { + logger.Info("All %d repos have their HEADs in the correct state") + } else { + if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 { + logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos) + } else if numHeadsBroken != 0 && numDefaultBranchesBroken == 0 { + logger.Warn("HEADs are broken for %d/%d repos", numHeadsBroken, numRepos) + } else { + logger.Critical("Out of %d repos, HEADS are broken for %d and default branches are broken for %d", numRepos, numHeadsBroken, numDefaultBranchesBroken) + } + } + } + + return err +} + +func init() { + Register(&Check{ + Title: "Synchronize repo HEADs", + Name: "synchronize-repo-heads", + IsDefault: true, + Run: synchronizeRepoHeads, + Priority: 7, + }) +} diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 80f160270847..469932525fb5 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -14,6 +14,8 @@ import ( "sort" "strings" + "code.gitea.io/gitea/modules/container" + "github.com/djherbis/buffer" "github.com/djherbis/nio/v3" ) @@ -339,7 +341,7 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st lastEmptyParent := head.ID.String() commitSinceLastEmptyParent := uint64(0) commitSinceNextRestart := uint64(0) - parentRemaining := map[string]bool{} + parentRemaining := make(container.Set[string]) changed := make([]bool, len(paths)) @@ -365,7 +367,7 @@ heaploop: if current == nil { break heaploop } - delete(parentRemaining, current.CommitID) + parentRemaining.Remove(current.CommitID) if current.Paths != nil { for i, found := range current.Paths { if !found { @@ -410,14 +412,12 @@ heaploop: } } g = NewLogNameStatusRepoParser(ctx, repo.Path, lastEmptyParent, treepath, remainingPaths...) - parentRemaining = map[string]bool{} + parentRemaining = make(container.Set[string]) nextRestart = (remaining * 3) / 4 continue heaploop } } - for _, parent := range current.ParentIDs { - parentRemaining[parent] = true - } + parentRemaining.AddMultiple(current.ParentIDs...) } g.Close() diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index c0c91c6fc618..cb44c58cec7e 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -13,6 +13,8 @@ import ( "strconv" "strings" "time" + + "code.gitea.io/gitea/modules/container" ) // CodeActivityStats represents git statistics data @@ -80,7 +82,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.Additions = 0 stats.Deletions = 0 authors := make(map[string]*CodeActivityAuthor) - files := make(map[string]bool) + files := make(container.Set[string]) var author string p := 0 for scanner.Scan() { @@ -119,9 +121,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.Deletions += c } } - if _, ok := files[parts[2]]; !ok { - files[parts[2]] = true - } + files.Add(parts[2]) } } } diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index a4c0fb5aa644..3b33852cb58d 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/container" api "code.gitea.io/gitea/modules/structs" "gitea.com/go-chi/binding" @@ -43,7 +44,7 @@ func validateYaml(template *api.IssueTemplate) error { if len(template.Fields) == 0 { return fmt.Errorf("'body' is required") } - ids := map[string]struct{}{} + ids := make(container.Set[string]) for idx, field := range template.Fields { if err := validateID(field, idx, ids); err != nil { return err @@ -125,7 +126,7 @@ func validateRequired(field *api.IssueFormField, idx int) error { return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required") } -func validateID(field *api.IssueFormField, idx int, ids map[string]struct{}) error { +func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error { if field.Type == api.IssueFormFieldTypeMarkdown { // The ID is not required for a markdown field return nil @@ -139,10 +140,9 @@ func validateID(field *api.IssueFormField, idx int, ids map[string]struct{}) err if binding.AlphaDashPattern.MatchString(field.ID) { return position.Errorf("'id' should contain only alphanumeric, '-' and '_'") } - if _, ok := ids[field.ID]; ok { + if !ids.Add(field.ID) { return position.Errorf("'id' should be unique") } - ids[field.ID] = struct{}{} return nil } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 24f1ab7a0167..8417019ddbad 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -10,6 +10,7 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/setting" @@ -198,7 +199,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } type prefixedIDs struct { - values map[string]bool + values container.Set[string] } // Generate generates a new element id. @@ -219,14 +220,12 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { if !bytes.HasPrefix(result, []byte("user-content-")) { result = append([]byte("user-content-"), result...) } - if _, ok := p.values[util.BytesToReadOnlyString(result)]; !ok { - p.values[util.BytesToReadOnlyString(result)] = true + if p.values.Add(util.BytesToReadOnlyString(result)) { return result } for i := 1; ; i++ { newResult := fmt.Sprintf("%s-%d", result, i) - if _, ok := p.values[newResult]; !ok { - p.values[newResult] = true + if p.values.Add(newResult) { return []byte(newResult) } } @@ -234,12 +233,12 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { // Put puts a given element id to the used ids table. func (p *prefixedIDs) Put(value []byte) { - p.values[util.BytesToReadOnlyString(value)] = true + p.values.Add(util.BytesToReadOnlyString(value)) } func newPrefixedIDs() *prefixedIDs { return &prefixedIDs{ - values: map[string]bool{}, + values: make(container.Set[string]), } } diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 5e5196a70af5..4d96a6b0edd4 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -10,6 +10,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/base" @@ -123,14 +124,14 @@ func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err) return } - toNotify := make(map[int64]struct{}, 32) + toNotify := make(container.Set[int64], 32) repoWatchers, err := repo_model.GetRepoWatchersIDs(db.DefaultContext, pr.Issue.RepoID) if err != nil { log.Error("GetRepoWatchersIDs: %v", err) return } for _, id := range repoWatchers { - toNotify[id] = struct{}{} + toNotify.Add(id) } issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(pr.IssueID) if err != nil { @@ -138,11 +139,11 @@ func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest return } for _, id := range issueParticipants { - toNotify[id] = struct{}{} + toNotify.Add(id) } delete(toNotify, pr.Issue.PosterID) for _, mention := range mentions { - toNotify[mention.ID] = struct{}{} + toNotify.Add(mention.ID) } for receiverID := range toNotify { _ = ns.issueQueue.Push(issueNotificationOpts{ diff --git a/modules/private/internal.go b/modules/private/internal.go index 2ea516ba80e1..21e5c9a27999 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -10,6 +10,8 @@ import ( "fmt" "net" "net/http" + "os" + "strings" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" @@ -18,13 +20,14 @@ import ( "code.gitea.io/gitea/modules/setting" ) -func newRequest(ctx context.Context, url, method string) *httplib.Request { +func newRequest(ctx context.Context, url, method, sourceIP string) *httplib.Request { if setting.InternalToken == "" { log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) } return httplib.NewRequest(url, method). SetContext(ctx). + Header("X-Real-IP", sourceIP). Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) } @@ -42,8 +45,16 @@ func decodeJSONError(resp *http.Response) *Response { return &res } +func getClientIP() string { + sshConnEnv := strings.TrimSpace(os.Getenv("SSH_CONNECTION")) + if len(sshConnEnv) == 0 { + return "127.0.0.1" + } + return strings.Fields(sshConnEnv)[0] +} + func newInternalRequest(ctx context.Context, url, method string) *httplib.Request { - req := newRequest(ctx, url, method).SetTLSClientConfig(&tls.Config{ + req := newRequest(ctx, url, method, getClientIP()).SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true, ServerName: setting.Domain, }) diff --git a/modules/public/public.go b/modules/public/public.go index 7804e945e798..ac1d80c860f4 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -83,11 +84,11 @@ func AssetsHandlerFunc(opts *Options) http.HandlerFunc { } // parseAcceptEncoding parse Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5 as compress methods -func parseAcceptEncoding(val string) map[string]bool { +func parseAcceptEncoding(val string) container.Set[string] { parts := strings.Split(val, ";") - types := make(map[string]bool) + types := make(container.Set[string]) for _, v := range strings.Split(parts[0], ",") { - types[strings.TrimSpace(v)] = true + types.Add(strings.TrimSpace(v)) } return types } diff --git a/modules/public/public_test.go b/modules/public/public_test.go index 430e7345646a..8b58d6af336c 100644 --- a/modules/public/public_test.go +++ b/modules/public/public_test.go @@ -7,28 +7,23 @@ package public import ( "testing" + "code.gitea.io/gitea/modules/container" + "github.com/stretchr/testify/assert" ) func TestParseAcceptEncoding(t *testing.T) { kases := []struct { Header string - Expected map[string]bool + Expected container.Set[string] }{ { - Header: "deflate, gzip;q=1.0, *;q=0.5", - Expected: map[string]bool{ - "deflate": true, - "gzip": true, - }, + Header: "deflate, gzip;q=1.0, *;q=0.5", + Expected: container.SetOf("deflate", "gzip"), }, { - Header: " gzip, deflate, br", - Expected: map[string]bool{ - "deflate": true, - "gzip": true, - "br": true, - }, + Header: " gzip, deflate, br", + Expected: container.SetOf("deflate", "gzip", "br"), }, } diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go index 9666880adfe1..10120bf85ded 100644 --- a/modules/public/serve_static.go +++ b/modules/public/serve_static.go @@ -60,7 +60,7 @@ func AssetIsDir(name string) (bool, error) { // serveContent serve http content func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) { encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding")) - if encodings["gzip"] { + if encodings.Contains("gzip") { if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok { rdGzip := bytes.NewReader(cf.GzipBytes()) // all static files are managed by Gitea, so we can make sure every file has the correct ext name diff --git a/modules/queue/unique_queue_channel.go b/modules/queue/unique_queue_channel.go index 6e8d37a20cc4..d1bf7239eb94 100644 --- a/modules/queue/unique_queue_channel.go +++ b/modules/queue/unique_queue_channel.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" ) @@ -33,7 +34,7 @@ type ChannelUniqueQueueConfiguration ChannelQueueConfiguration type ChannelUniqueQueue struct { *WorkerPool lock sync.Mutex - table map[string]bool + table container.Set[string] shutdownCtx context.Context shutdownCtxCancel context.CancelFunc terminateCtx context.Context @@ -58,7 +59,7 @@ func NewChannelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue shutdownCtx, shutdownCtxCancel := context.WithCancel(terminateCtx) queue := &ChannelUniqueQueue{ - table: map[string]bool{}, + table: make(container.Set[string]), shutdownCtx: shutdownCtx, shutdownCtxCancel: shutdownCtxCancel, terminateCtx: terminateCtx, @@ -73,7 +74,7 @@ func NewChannelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue bs, _ := json.Marshal(datum) queue.lock.Lock() - delete(queue.table, string(bs)) + queue.table.Remove(string(bs)) queue.lock.Unlock() if u := handle(datum); u != nil { @@ -127,16 +128,15 @@ func (q *ChannelUniqueQueue) PushFunc(data Data, fn func() error) error { q.lock.Unlock() } }() - if _, ok := q.table[string(bs)]; ok { + if !q.table.Add(string(bs)) { return ErrAlreadyInQueue } // FIXME: We probably need to implement some sort of limit here // If the downstream queue blocks this table will grow without limit - q.table[string(bs)] = true if fn != nil { err := fn() if err != nil { - delete(q.table, string(bs)) + q.table.Remove(string(bs)) return err } } @@ -155,8 +155,7 @@ func (q *ChannelUniqueQueue) Has(data Data) (bool, error) { q.lock.Lock() defer q.lock.Unlock() - _, has := q.table[string(bs)] - return has, nil + return q.table.Contains(string(bs)), nil } // Flush flushes the channel with a timeout - the Flush worker will be registered as a flush worker with the manager diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 48c3edf60f3d..b01be322d2f6 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -275,7 +276,7 @@ func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository) return pullMirrorReleaseSync(repo, gitRepo) } - existingRelTags := make(map[string]struct{}) + existingRelTags := make(container.Set[string]) opts := repo_model.FindReleasesOptions{ IncludeDrafts: true, IncludeTags: true, @@ -303,14 +304,14 @@ func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository) return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err) } } else { - existingRelTags[strings.ToLower(rel.TagName)] = struct{}{} + existingRelTags.Add(strings.ToLower(rel.TagName)) } } } _, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error { tagName := strings.TrimPrefix(refname, git.TagPrefix) - if _, ok := existingRelTags[strings.ToLower(tagName)]; ok { + if existingRelTags.Contains(strings.ToLower(tagName)) { return nil } diff --git a/modules/setting/queue.go b/modules/setting/queue.go index cb86cbdfe05c..d3bb33b24839 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" ini "gopkg.in/ini.v1" @@ -109,8 +110,8 @@ func NewQueueService() { // Now handle the old issue_indexer configuration // FIXME: DEPRECATED to be removed in v1.18.0 section := Cfg.Section("queue.issue_indexer") - directlySet := toDirectlySetKeysMap(section) - if !directlySet["TYPE"] && defaultType == "" { + directlySet := toDirectlySetKeysSet(section) + if !directlySet.Contains("TYPE") && defaultType == "" { switch typ := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_TYPE").MustString(""); typ { case "levelqueue": _, _ = section.NewKey("TYPE", "level") @@ -124,25 +125,25 @@ func NewQueueService() { log.Fatal("Unsupported indexer queue type: %v", typ) } } - if !directlySet["LENGTH"] { + if !directlySet.Contains("LENGTH") { length := Cfg.Section("indexer").Key("UPDATE_BUFFER_LEN").MustInt(0) if length != 0 { _, _ = section.NewKey("LENGTH", strconv.Itoa(length)) } } - if !directlySet["BATCH_LENGTH"] { + if !directlySet.Contains("BATCH_LENGTH") { fallback := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(0) if fallback != 0 { _, _ = section.NewKey("BATCH_LENGTH", strconv.Itoa(fallback)) } } - if !directlySet["DATADIR"] { + if !directlySet.Contains("DATADIR") { queueDir := filepath.ToSlash(Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_DIR").MustString("")) if queueDir != "" { _, _ = section.NewKey("DATADIR", queueDir) } } - if !directlySet["CONN_STR"] { + if !directlySet.Contains("CONN_STR") { connStr := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_CONN_STR").MustString("") if connStr != "" { _, _ = section.NewKey("CONN_STR", connStr) @@ -178,19 +179,19 @@ func handleOldLengthConfiguration(queueName, oldSection, oldKey string, defaultV } section := Cfg.Section("queue." + queueName) - directlySet := toDirectlySetKeysMap(section) - if !directlySet["LENGTH"] { + directlySet := toDirectlySetKeysSet(section) + if !directlySet.Contains("LENGTH") { _, _ = section.NewKey("LENGTH", strconv.Itoa(value)) } } -// toDirectlySetKeysMap returns a bool map of keys directly set by this section +// toDirectlySetKeysSet returns a set of keys directly set by this section // Note: we cannot use section.HasKey(...) as that will immediately set the Key if a parent section has the Key // but this section does not. -func toDirectlySetKeysMap(section *ini.Section) map[string]bool { - sectionMap := map[string]bool{} +func toDirectlySetKeysSet(section *ini.Section) container.Set[string] { + sections := make(container.Set[string]) for _, key := range section.Keys() { - sectionMap[key.Name()] = true + sections.Add(key.Name()) } - return sectionMap + return sections } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 6233437bf5aa..007e3ef61f88 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -21,6 +21,7 @@ import ( "text/template" "time" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" @@ -234,7 +235,7 @@ var ( DefaultTheme string Themes []string Reactions []string - ReactionsMap map[string]bool `ini:"-"` + ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string CustomEmojisMap map[string]string `ini:"-"` SearchRepoDescription bool @@ -1100,9 +1101,9 @@ func loadFromConf(allowEmpty bool, extraConfig string) { newMarkup() - UI.ReactionsMap = make(map[string]bool) + UI.ReactionsLookup = make(container.Set[string]) for _, reaction := range UI.Reactions { - UI.ReactionsMap[reaction] = true + UI.ReactionsLookup.Add(reaction) } UI.CustomEmojisMap = make(map[string]string) for _, emoji := range UI.CustomEmojis { diff --git a/modules/sync/status_pool.go b/modules/sync/status_pool.go index acbd93ab1783..99e5ce9cb3a0 100644 --- a/modules/sync/status_pool.go +++ b/modules/sync/status_pool.go @@ -6,6 +6,8 @@ package sync import ( "sync" + + "code.gitea.io/gitea/modules/container" ) // StatusTable is a table maintains true/false values. @@ -14,13 +16,13 @@ import ( // in different goroutines. type StatusTable struct { lock sync.RWMutex - pool map[string]struct{} + pool container.Set[string] } // NewStatusTable initializes and returns a new StatusTable object. func NewStatusTable() *StatusTable { return &StatusTable{ - pool: make(map[string]struct{}), + pool: make(container.Set[string]), } } @@ -28,32 +30,29 @@ func NewStatusTable() *StatusTable { // Returns whether set value was set to true func (p *StatusTable) StartIfNotRunning(name string) bool { p.lock.Lock() - _, ok := p.pool[name] - if !ok { - p.pool[name] = struct{}{} - } + added := p.pool.Add(name) p.lock.Unlock() - return !ok + return added } // Start sets value of given name to true in the pool. func (p *StatusTable) Start(name string) { p.lock.Lock() - p.pool[name] = struct{}{} + p.pool.Add(name) p.lock.Unlock() } // Stop sets value of given name to false in the pool. func (p *StatusTable) Stop(name string) { p.lock.Lock() - delete(p.pool, name) + p.pool.Remove(name) p.lock.Unlock() } // IsRunning checks if value of given name is set to true in the pool. func (p *StatusTable) IsRunning(name string) bool { p.lock.RLock() - _, ok := p.pool[name] + exists := p.pool.Contains(name) p.lock.RUnlock() - return ok + return exists } diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 133f7d8962bf..6c01a06f8565 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -146,7 +146,7 @@ sqlite_helper=Localização do ficheiro da base de dados em SQLite3.
Insira u reinstall_error=Está a tentar instalar numa base de dados do Gitea já existente reinstall_confirm_message=Reinstalar com uma base de dados do Gitea já existente pode causar múltiplos problemas. Na maioria dos casos deve usar o seu "app.ini" existente para correr o Gitea. Se souber o que está a fazer, confirme o seguinte: reinstall_confirm_check_1=Os dados encriptados pela chave secreta (SECRET_KEY) no ficheiro app.ini poderão ser perdidos: utilizadores poderão não ser capazes de iniciar a sessão com autenticação em dois passos (2FA) ou com chaves de utilização única (OTP) e as réplicas poderão deixar de funcionar em condições. Ao marcar esta opção estará a confirmar que o ficheiro app.ini vigente contém a SECRET_KEY certa. -reinstall_confirm_check_2=Os repositórios e as configurações poderão ter de voltar a ser sincronizados. Ao marcar esta opção estará a confirmar que vai voltar a sincronizar os automatismos para os repositórios e o ficheiro authorized_keys manualmente. Estará também a confirmar que vai assegurar que as configurações do repositório e das réplicas estão em condições. +reinstall_confirm_check_2=Os repositórios e as configurações poderão ter de voltar a ser sincronizados. Ao marcar esta opção estará a confirmar que vai voltar a sincronizar manualmente os automatismos para os repositórios e o ficheiro authorized_keys. Estará também a confirmar que vai assegurar que as configurações do repositório e das réplicas estão em condições. reinstall_confirm_check_3=Você confirma que tem a certeza absoluta de que este Gitea está a correr com a localização certa do ficheiro app.ini e que tem a certeza de que tem de voltar a instalar. Você confirma que tomou conhecimento dos riscos acima descritos. err_empty_db_path=A localização da base de dados SQLite3 não pode estar vazia. no_admin_and_disable_registration=Não pode desabilitar a auto-inscrição de utilizadores sem criar uma conta de administrador. @@ -268,8 +268,11 @@ users=Utilizadores organizations=Organizações search=Procurar code=Código +search.type.tooltip=Tipo de pesquisa search.fuzzy=Aproximada +search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa search.match=Fiel +search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador. repo_no_results=Não foram encontrados quaisquer repositórios correspondentes. user_no_results=Não foram encontrados quaisquer utilizadores correspondentes. @@ -507,6 +510,7 @@ activity=Trabalho público followers=Seguidores starred=Repositórios favoritos watched=Repositórios sob vigilância +code=Código projects=Planeamentos following=Que segue follow=Seguir @@ -1763,8 +1767,11 @@ activity.git_stats_deletion_n=%d eliminações search=Procurar search.search_repo=Procurar repositório +search.type.tooltip=Tipo de pesquisa search.fuzzy=Aproximada +search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa search.match=Fiel +search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa search.results=Resultados da procura de "%s" em %s search.code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa. search.code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador. @@ -1781,16 +1788,16 @@ settings.collaboration.undefined=Não definido settings.hooks=Automatismos web settings.githooks=Automatismos do Git settings.basic_settings=Configurações básicas -settings.mirror_settings=Configurações da réplica +settings.mirror_settings=Configuração de réplicas settings.mirror_settings.docs=Configure o seu repositório para puxar e/ou enviar automaticamente as modificações de/para outro repositório. Ramos, etiquetas e cometimentos serão sincronizados automaticamente. Como é que eu faço uma réplica de outro repositório? settings.mirror_settings.mirrored_repository=Repositório replicado settings.mirror_settings.direction=Sentido settings.mirror_settings.direction.pull=Puxada settings.mirror_settings.direction.push=Envio settings.mirror_settings.last_update=Última modificação -settings.mirror_settings.push_mirror.none=Não foram configuradas quaisquer réplicas de envio +settings.mirror_settings.push_mirror.none=Não foram configuradas quaiquer réplicas deste repositório settings.mirror_settings.push_mirror.remote_url=URL do repositório remoto Git -settings.mirror_settings.push_mirror.add=Adicionar réplica de envio +settings.mirror_settings.push_mirror.add=Adicionar réplica deste repositório settings.sync_mirror=Sincronizar agora settings.mirror_sync_in_progress=A sincronização da réplica está em andamento. Volte a verificar daqui a um minuto. settings.site=Sítio web @@ -2310,6 +2317,7 @@ create_org=Criar organização repo_updated=Modificado people=Pessoas teams=Equipas +code=Código lower_members=membros lower_repositories=repositórios create_new_team=Nova equipa @@ -2854,7 +2862,7 @@ config.git_max_diff_line_characters=Número máximos de caracteres diff (por lin config.git_max_diff_files=Número máximo de ficheiros diff a serem apresentados config.git_gc_args=Argumentos da recolha de lixo config.git_migrate_timeout=Prazo da migração -config.git_mirror_timeout=Tempo limite da réplica +config.git_mirror_timeout=Prazo para sincronização da réplica config.git_clone_timeout=Prazo da operação de clonagem config.git_pull_timeout=Prazo da operação de puxar config.git_gc_timeout=Prazo da operação de recolha de lixo diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 0889006dd757..a54add0621ed 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -196,7 +196,7 @@ func Routes(ctx gocontext.Context) *web.Route { r.Put("/symbolpackage", nuget.UploadSymbolPackage) r.Delete("/{id}/{version}", nuget.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) - r.Get("/symbols/{filename}/{guid:[0-9a-f]{32}}FFFFFFFF/{filename2}", nuget.DownloadSymbolFile) + r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile) }, reqPackageAccess(perm.AccessModeRead)) }) r.Group("/npm", func() { diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index c57f80ac2f06..dd078d6ad34d 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" conan_model "code.gitea.io/gitea/models/packages/conan" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -33,20 +34,18 @@ const ( packageReferenceKey = "PackageReference" ) -type stringSet map[string]struct{} - var ( - recipeFileList = stringSet{ - conanfileFile: struct{}{}, - "conanmanifest.txt": struct{}{}, - "conan_sources.tgz": struct{}{}, - "conan_export.tgz": struct{}{}, - } - packageFileList = stringSet{ - conaninfoFile: struct{}{}, - "conanmanifest.txt": struct{}{}, - "conan_package.tgz": struct{}{}, - } + recipeFileList = container.SetOf( + conanfileFile, + "conanmanifest.txt", + "conan_sources.tgz", + "conan_export.tgz", + ) + packageFileList = container.SetOf( + conaninfoFile, + "conanmanifest.txt", + "conan_package.tgz", + ) ) func jsonResponse(ctx *context.Context, status int, obj interface{}) { @@ -268,7 +267,7 @@ func PackageUploadURLs(ctx *context.Context) { ) } -func serveUploadURLs(ctx *context.Context, fileFilter stringSet, uploadURL string) { +func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) { defer ctx.Req.Body.Close() var files map[string]int64 @@ -279,7 +278,7 @@ func serveUploadURLs(ctx *context.Context, fileFilter stringSet, uploadURL strin urls := make(map[string]string) for file := range files { - if _, ok := fileFilter[file]; ok { + if fileFilter.Contains(file) { urls[file] = fmt.Sprintf("%s/%s", uploadURL, file) } } @@ -301,12 +300,12 @@ func UploadPackageFile(ctx *context.Context) { uploadFile(ctx, packageFileList, pref.AsKey()) } -func uploadFile(ctx *context.Context, fileFilter stringSet, fileKey string) { +func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) filename := ctx.Params("filename") - if _, ok := fileFilter[filename]; !ok { + if !fileFilter.Contains(filename) { apiError(ctx, http.StatusBadRequest, nil) return } @@ -442,11 +441,11 @@ func DownloadPackageFile(ctx *context.Context) { downloadFile(ctx, packageFileList, pref.AsKey()) } -func downloadFile(ctx *context.Context, fileFilter stringSet, fileKey string) { +func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) filename := ctx.Params("filename") - if _, ok := fileFilter[filename]; !ok { + if !fileFilter.Contains(filename) { apiError(ctx, http.StatusBadRequest, nil) return } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index eadf7486a5d2..3c61ae28bb18 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -353,7 +353,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package // DownloadSymbolFile https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request func DownloadSymbolFile(ctx *context.Context) { filename := ctx.Params("filename") - guid := ctx.Params("guid") + guid := ctx.Params("guid")[:32] filename2 := ctx.Params("filename2") if filename != filename2 { diff --git a/routers/private/internal.go b/routers/private/internal.go index e9cc20a77dc6..f8e451e80f07 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/web" "gitea.com/go-chi/binding" + chi_middleware "github.com/go-chi/chi/v5/middleware" ) // CheckInternalToken check internal token is set @@ -57,6 +58,9 @@ func Routes() *web.Route { r := web.NewRoute() r.Use(context.PrivateContexter()) r.Use(CheckInternalToken) + // Log the real ip address of the request from SSH is really helpful for diagnosing sometimes. + // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. + r.Use(chi_middleware.RealIP) r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go new file mode 100644 index 000000000000..c7a9c3100fac --- /dev/null +++ b/routers/web/admin/applications.go @@ -0,0 +1,93 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package admin + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + user_setting "code.gitea.io/gitea/routers/web/user/setting" +) + +var ( + tplSettingsApplications base.TplName = "admin/applications/list" + tplSettingsOauth2ApplicationEdit base.TplName = "admin/applications/oauth2_edit" +) + +func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers { + return &user_setting.OAuth2CommonHandlers{ + OwnerID: 0, + BasePathList: fmt.Sprintf("%s/admin/applications", setting.AppSubURL), + BasePathEditPrefix: fmt.Sprintf("%s/admin/applications/oauth2", setting.AppSubURL), + TplAppEdit: tplSettingsOauth2ApplicationEdit, + } +} + +// Applications render org applications page (for org, at the moment, there are only OAuth2 applications) +func Applications(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.applications") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminApplications"] = true + + apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, 0) + if err != nil { + ctx.ServerError("GetOAuth2ApplicationsByUserID", err) + return + } + ctx.Data["Applications"] = apps + + ctx.HTML(http.StatusOK, tplSettingsApplications) +} + +// ApplicationsPost response for adding an oauth2 application +func ApplicationsPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.applications") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminApplications"] = true + + oa := newOAuth2CommonHandlers() + oa.AddApp(ctx) +} + +// EditApplication displays the given application +func EditApplication(ctx *context.Context) { + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminApplications"] = true + + oa := newOAuth2CommonHandlers() + oa.EditShow(ctx) +} + +// EditApplicationPost response for editing oauth2 application +func EditApplicationPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.applications") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminApplications"] = true + + oa := newOAuth2CommonHandlers() + oa.EditSave(ctx) +} + +// ApplicationsRegenerateSecret handles the post request for regenerating the secret +func ApplicationsRegenerateSecret(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminApplications"] = true + + oa := newOAuth2CommonHandlers() + oa.RegenerateSecret(ctx) +} + +// DeleteApplication deletes the given oauth2 application +func DeleteApplication(ctx *context.Context) { + oa := newOAuth2CommonHandlers() + oa.DeleteApp(ctx) +} + +// TODO: revokes the grant with the given id diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index e6112b4276dc..c172215b903d 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -380,10 +380,13 @@ func AuthorizeOAuth(ctx *context.Context) { return } - user, err := user_model.GetUserByID(app.UID) - if err != nil { - ctx.ServerError("GetUserByID", err) - return + var user *user_model.User + if app.UID != 0 { + user, err = user_model.GetUserByID(app.UID) + if err != nil { + ctx.ServerError("GetUserByID", err) + return + } } if !app.ContainsRedirectURI(form.RedirectURI) { @@ -475,7 +478,11 @@ func AuthorizeOAuth(ctx *context.Context) { ctx.Data["State"] = form.State ctx.Data["Scope"] = form.Scope ctx.Data["Nonce"] = form.Nonce - ctx.Data["ApplicationUserLinkHTML"] = "@" + html.EscapeString(user.Name) + "" + if user != nil { + ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`@%s`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)) + } else { + ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`%s`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)) + } ctx.Data["ApplicationRedirectDomainHTML"] = "" + html.EscapeString(form.RedirectURI) + "" // TODO document SESSION <=> FORM err = ctx.Session.Set("client_id", app.ClientID) diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index 7f2ed8cb2681..316cbcd95f3b 100644 --- a/routers/web/repo/activity.go +++ b/routers/web/repo/activity.go @@ -47,8 +47,8 @@ func Activity(ctx *context.Context) { ctx.Data["Period"] = "weekly" timeFrom = timeUntil.Add(-time.Hour * 168) } - ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006") - ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006") + ctx.Data["DateFrom"] = timeFrom.UTC().Format(time.RFC3339) + ctx.Data["DateUntil"] = timeUntil.UTC().Format(time.RFC3339) ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) var err error diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 5dab770d5530..38ad593c17ad 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" @@ -947,10 +948,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if err != nil { return nil, nil, 0, 0 } - labelIDMark := base.Int64sToMap(labelIDs) + labelIDMark := make(container.Set[int64]) + labelIDMark.AddMultiple(labelIDs...) for i := range labels { - if labelIDMark[labels[i].ID] { + if labelIDMark.Contains(labels[i].ID) { labels[i].IsChecked = true hasSelected = true } @@ -1293,9 +1295,9 @@ func ViewIssue(ctx *context.Context) { // Metas. // Check labels. - labelIDMark := make(map[int64]bool) - for i := range issue.Labels { - labelIDMark[issue.Labels[i].ID] = true + labelIDMark := make(container.Set[int64]) + for _, label := range issue.Labels { + labelIDMark.Add(label.ID) } labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { @@ -1317,7 +1319,7 @@ func ViewIssue(ctx *context.Context) { hasSelected := false for i := range labels { - if labelIDMark[labels[i].ID] { + if labelIDMark.Contains(labels[i].ID) { labels[i].IsChecked = true hasSelected = true } diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go index baec48bfea77..633b8ab1a513 100644 --- a/routers/web/repo/lfs.go +++ b/routers/web/repo/lfs.go @@ -18,6 +18,7 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/pipeline" @@ -176,14 +177,12 @@ func LFSLocks(ctx *context.Context) { return } - filemap := make(map[string]bool, len(filelist)) - for _, name := range filelist { - filemap[name] = true - } + fileset := make(container.Set[string], len(filelist)) + fileset.AddMultiple(filelist...) linkable := make([]bool, len(lfsLocks)) for i, lock := range lfsLocks { - linkable[i] = filemap[lock.Path] + linkable[i] = fileset.Contains(lock.Path) } ctx.Data["Linkable"] = linkable diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 768a30ec2153..a43840467dc8 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -28,6 +28,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" @@ -455,7 +456,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st ctx.ServerError("GetTreePathLock", err) return } - ctx.Data["LFSLockOwner"] = u.DisplayName() + ctx.Data["LFSLockOwner"] = u.Name ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") } @@ -811,16 +812,14 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri defer cancel() } - selected := map[string]bool{} - for _, pth := range ctx.FormStrings("f[]") { - selected[pth] = true - } + selected := make(container.Set[string]) + selected.AddMultiple(ctx.FormStrings("f[]")...) entries := allEntries if len(selected) > 0 { entries = make(git.Entries, 0, len(selected)) for _, entry := range allEntries { - if selected[entry.Name()] { + if selected.Contains(entry.Name()) { entries = append(entries, entry) } } diff --git a/routers/web/web.go b/routers/web/web.go index c74343c8cf71..c01a2bce40a0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -569,6 +569,23 @@ func RegisterRoutes(m *web.Route) { m.Post("/delete", admin.DeleteNotices) m.Post("/empty", admin.EmptyNotices) }) + + m.Group("/applications", func() { + m.Get("", admin.Applications) + m.Post("/oauth2", bindIgnErr(forms.EditOAuth2ApplicationForm{}), admin.ApplicationsPost) + m.Group("/oauth2/{id}", func() { + m.Combo("").Get(admin.EditApplication).Post(bindIgnErr(forms.EditOAuth2ApplicationForm{}), admin.EditApplicationPost) + m.Post("/regenerate_secret", admin.ApplicationsRegenerateSecret) + m.Post("/delete", admin.DeleteApplication) + }) + }, func(ctx *context.Context) { + if !setting.OAuth2.Enable { + ctx.Error(http.StatusForbidden) + return + } + }) + }, func(ctx *context.Context) { + ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable }, adminReq) // ***** END: Admin ***** diff --git a/services/issue/commit.go b/services/issue/commit.go index 0d04de81bcd8..c8cfa6cc8a6c 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -18,6 +18,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/repository" @@ -111,7 +112,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm Action references.XRefAction } - refMarked := make(map[markKey]bool) + refMarked := make(container.Set[markKey]) var refRepo *repo_model.Repository var refIssue *issues_model.Issue var err error @@ -144,10 +145,9 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm } key := markKey{ID: refIssue.ID, Action: ref.Action} - if refMarked[key] { + if !refMarked.Add(key) { continue } - refMarked[key] = true // FIXME: this kind of condition is all over the code, it should be consolidated in a single place canclose := perm.IsAdmin() || perm.IsOwner() || perm.CanWriteIssuesOrPulls(refIssue.IsPull) || refIssue.PosterID == doer.ID diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go index 2dab673b4e78..af07821c29fb 100644 --- a/services/mailer/mail_comment.go +++ b/services/mailer/mail_comment.go @@ -10,6 +10,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -46,8 +47,8 @@ func MailMentionsComment(ctx context.Context, pr *issues_model.PullRequest, c *i return nil } - visited := make(map[int64]bool, len(mentions)+1) - visited[c.Poster.ID] = true + visited := make(container.Set[int64], len(mentions)+1) + visited.Add(c.Poster.ID) if err = mailIssueCommentBatch( &mailCommentContext{ Context: ctx, diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index ec6ddcf14e67..15bfa4af4162 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -89,11 +90,11 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo unfiltered = append(ids, unfiltered...) } - visited := make(map[int64]bool, len(unfiltered)+len(mentions)+1) + visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1) // Avoid mailing the doer if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn { - visited[ctx.Doer.ID] = true + visited.Add(ctx.Doer.ID) } // =========== Mentions =========== @@ -106,9 +107,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo if err != nil { return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err) } - for _, i := range ids { - visited[i] = true - } + visited.AddMultiple(ids...) unfilteredUsers, err := user_model.GetMaileableUsersByIDs(unfiltered, false) if err != nil { @@ -121,7 +120,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo return nil } -func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, visited map[int64]bool, fromMention bool) error { +func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, visited container.Set[int64], fromMention bool) error { checkUnit := unit.TypeIssues if ctx.Issue.IsPull { checkUnit = unit.TypePullRequests @@ -142,13 +141,10 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi } // if we have already visited this user we exclude them - if _, ok := visited[user.ID]; ok { + if !visited.Add(user.ID) { continue } - // now mark them as visited - visited[user.ID] = true - // test if this user is allowed to see the issue/pull if !access_model.CheckRepoUnitUser(ctx, ctx.Issue.Repo, user, checkUnit) { continue diff --git a/services/pull/merge.go b/services/pull/merge.go index 4cd4e3bd7e05..6f3df6ab2a5a 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -28,6 +28,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/references" @@ -165,9 +166,10 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") }() - // TODO: make it able to do this in a database session - mergeCtx := context.Background() - pr.MergedCommitID, err = rawMerge(mergeCtx, pr, doer, mergeStyle, expectedHeadCommitID, message) + // Run the merge in the hammer context to prevent cancellation + hammerCtx := graceful.GetManager().HammerContext() + + pr.MergedCommitID, err = rawMerge(hammerCtx, pr, doer, mergeStyle, expectedHeadCommitID, message) if err != nil { return err } @@ -176,18 +178,18 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U pr.Merger = doer pr.MergerID = doer.ID - if _, err := pr.SetMerged(ctx); err != nil { + if _, err := pr.SetMerged(hammerCtx); err != nil { log.Error("setMerged [%d]: %v", pr.ID, err) } - if err := pr.LoadIssueCtx(ctx); err != nil { + if err := pr.LoadIssueCtx(hammerCtx); err != nil { log.Error("loadIssue [%d]: %v", pr.ID, err) } - if err := pr.Issue.LoadRepo(ctx); err != nil { + if err := pr.Issue.LoadRepo(hammerCtx); err != nil { log.Error("loadRepo for issue [%d]: %v", pr.ID, err) } - if err := pr.Issue.Repo.GetOwner(ctx); err != nil { + if err := pr.Issue.Repo.GetOwner(hammerCtx); err != nil { log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err) } @@ -197,17 +199,17 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) // Resolve cross references - refs, err := pr.ResolveCrossReferences(ctx) + refs, err := pr.ResolveCrossReferences(hammerCtx) if err != nil { log.Error("ResolveCrossReferences: %v", err) return nil } for _, ref := range refs { - if err = ref.LoadIssueCtx(ctx); err != nil { + if err = ref.LoadIssueCtx(hammerCtx); err != nil { return err } - if err = ref.Issue.LoadRepo(ctx); err != nil { + if err = ref.Issue.LoadRepo(hammerCtx); err != nil { return err } close := ref.RefAction == references.XRefActionCloses diff --git a/services/pull/patch.go b/services/pull/patch.go index 32895b2e784f..dafd57706915 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -409,7 +410,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * const appliedPatchPrefix = "Applied patch to '" const withConflicts = "' with conflicts." - conflictMap := map[string]bool{} + conflicts := make(container.Set[string]) // Now scan the output from the command scanner := bufio.NewScanner(stderrReader) @@ -418,7 +419,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * if strings.HasPrefix(line, prefix) { conflict = true filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) - conflictMap[filepath] = true + conflicts.Add(filepath) } else if is3way && line == threewayFailed { conflict = true } else if strings.HasPrefix(line, errorPrefix) { @@ -427,7 +428,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * if strings.HasSuffix(line, suffix) { filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix)) if filepath != "" { - conflictMap[filepath] = true + conflicts.Add(filepath) } break } @@ -436,18 +437,18 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * conflict = true filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix) if filepath != "" { - conflictMap[filepath] = true + conflicts.Add(filepath) } } // only list 10 conflicted files - if len(conflictMap) >= 10 { + if len(conflicts) >= 10 { break } } - if len(conflictMap) > 0 { - pr.ConflictedFiles = make([]string, 0, len(conflictMap)) - for key := range conflictMap { + if len(conflicts) > 0 { + pr.ConflictedFiles = make([]string, 0, len(conflicts)) + for key := range conflicts { pr.ConflictedFiles = append(pr.ConflictedFiles, key) } } diff --git a/services/pull/pull.go b/services/pull/pull.go index 103fdc340d22..9de7cb5d4f8e 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -20,6 +20,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" @@ -640,7 +641,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ posterSig := pr.Issue.Poster.NewGitSig().String() - authorsMap := map[string]bool{} + uniqueAuthors := make(container.Set[string]) authors := make([]string, 0, len(commits)) stringBuilder := strings.Builder{} @@ -687,9 +688,8 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } authorString := commit.Author.String() - if !authorsMap[authorString] && authorString != posterSig { + if uniqueAuthors.Add(authorString) && authorString != posterSig { authors = append(authors, authorString) - authorsMap[authorString] = true } } @@ -709,9 +709,8 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } for _, commit := range commits { authorString := commit.Author.String() - if !authorsMap[authorString] && authorString != posterSig { + if uniqueAuthors.Add(authorString) && authorString != posterSig { authors = append(authors, authorString) - authorsMap[authorString] = true } } skip += limit diff --git a/services/release/release.go b/services/release/release.go index ae610b0e3c17..187ebeb486d6 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -15,6 +15,7 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" @@ -209,7 +210,7 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod return fmt.Errorf("AddReleaseAttachments: %v", err) } - deletedUUIDsMap := make(map[string]bool) + deletedUUIDs := make(container.Set[string]) if len(delAttachmentUUIDs) > 0 { // Check attachments attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs) @@ -220,7 +221,7 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod if attach.ReleaseID != rel.ID { return errors.New("delete attachement of release permission denied") } - deletedUUIDsMap[attach.UUID] = true + deletedUUIDs.Add(attach.UUID) } if _, err := repo_model.DeleteAttachments(ctx, attachments, false); err != nil { @@ -245,7 +246,7 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod } for uuid, newName := range editAttachments { - if !deletedUUIDsMap[uuid] { + if !deletedUUIDs.Contains(uuid) { if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ UUID: uuid, Name: newName, diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 74876d8e763c..9e04c1597763 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" @@ -257,12 +258,12 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad if len(repos) == len(repoNamesToCheck) { return nil } - repoNames := make(map[string]bool, len(repos)) + repoNames := make(container.Set[string], len(repos)) for _, repo := range repos { - repoNames[repo.LowerName] = true + repoNames.Add(repo.LowerName) } for _, repoName := range repoNamesToCheck { - if _, ok := repoNames[repoName]; !ok { + if !repoNames.Contains(repoName) { unadopted.add(filepath.Join(userName, repoName)) } } diff --git a/templates/admin/applications/list.tmpl b/templates/admin/applications/list.tmpl new file mode 100644 index 000000000000..6d627129df0e --- /dev/null +++ b/templates/admin/applications/list.tmpl @@ -0,0 +1,14 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+
+ {{template "base/alert" .}} +

+ {{.locale.Tr "settings.applications"}} +

+ {{template "user/settings/applications_oauth2_list" .}} +
+
+
+{{template "base/footer" .}} diff --git a/templates/admin/applications/oauth2_edit.tmpl b/templates/admin/applications/oauth2_edit.tmpl new file mode 100644 index 000000000000..84d821eccace --- /dev/null +++ b/templates/admin/applications/oauth2_edit.tmpl @@ -0,0 +1,7 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} + + {{template "user/settings/applications_oauth2_edit_form" .}} +
+{{template "base/footer" .}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 0db1aab07929..b138eb79ba4e 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -26,6 +26,11 @@ {{.locale.Tr "admin.emails"}} + {{if .EnableOAuth2}} + + {{.locale.Tr "settings.applications"}} + + {{end}} {{.locale.Tr "admin.config"}} diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index c2f6c2d982e0..cc6ca95edbdb 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -2,7 +2,7 @@
{{template "repo/header" .}}
-

{{.DateFrom}} - {{.DateUntil}} +

-
diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl index 0ba32c550f0a..682614dee58b 100644 --- a/templates/user/auth/grant.tmpl +++ b/templates/user/auth/grant.tmpl @@ -9,7 +9,7 @@ {{template "base/alert" .}}

{{.locale.Tr "auth.authorize_application_description"}}
- {{.locale.Tr "auth.authorize_application_created_by" .ApplicationUserLinkHTML | Str2html}} + {{.locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML | Str2html}}

diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index 9d53311d3564..8d5a5c7c8237 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -280,7 +280,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) - req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, symbolID, symbolFilename)) + req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFffff/%s", url, symbolFilename, symbolID, symbolFilename)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go index 0860f47533c8..2186933bd9bb 100644 --- a/tests/integration/api_repo_lfs_locks_test.go +++ b/tests/integration/api_repo_lfs_locks_test.go @@ -112,6 +112,7 @@ func TestAPILFSLocksLogged(t *testing.T) { if len(test.addTime) > 0 { var lfsLock api.LFSLockResponse DecodeJSON(t, resp, &lfsLock) + assert.Equal(t, test.user.Name, lfsLock.Lock.Owner.Name) assert.EqualValues(t, lfsLock.Lock.LockedAt.Format(time.RFC3339), lfsLock.Lock.LockedAt.Format(time.RFC3339Nano)) // locked at should be rounded to second for _, id := range test.addTime { resultsTests[id].locksTimes = append(resultsTests[id].locksTimes, time.Now()) @@ -129,7 +130,7 @@ func TestAPILFSLocksLogged(t *testing.T) { DecodeJSON(t, resp, &lfsLocks) assert.Len(t, lfsLocks.Locks, test.totalCount) for i, lock := range lfsLocks.Locks { - assert.EqualValues(t, test.locksOwners[i].DisplayName(), lock.Owner.Name) + assert.EqualValues(t, test.locksOwners[i].Name, lock.Owner.Name) assert.WithinDuration(t, test.locksTimes[i], lock.LockedAt, 10*time.Second) assert.EqualValues(t, lock.LockedAt.Format(time.RFC3339), lock.LockedAt.Format(time.RFC3339Nano)) // locked at should be rounded to second } @@ -143,7 +144,7 @@ func TestAPILFSLocksLogged(t *testing.T) { assert.Len(t, lfsLocksVerify.Ours, test.oursCount) assert.Len(t, lfsLocksVerify.Theirs, test.theirsCount) for _, lock := range lfsLocksVerify.Ours { - assert.EqualValues(t, test.user.DisplayName(), lock.Owner.Name) + assert.EqualValues(t, test.user.Name, lock.Owner.Name) deleteTests = append(deleteTests, struct { user *user_model.User repo *repo_model.Repository @@ -165,7 +166,7 @@ func TestAPILFSLocksLogged(t *testing.T) { var lfsLockRep api.LFSLockResponse DecodeJSON(t, resp, &lfsLockRep) assert.Equal(t, test.lockID, lfsLockRep.Lock.ID) - assert.Equal(t, test.user.DisplayName(), lfsLockRep.Lock.Owner.Name) + assert.Equal(t, test.user.Name, lfsLockRep.Lock.Owner.Name) } // check that we don't have any lock diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 7fa26c81474b..9621acbbccad 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -12,29 +12,59 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -const defaultAuthorize = "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate" - -func TestNoClientID(t *testing.T) { +func TestAuthorizeNoClientID(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/login/oauth/authorize") ctx := loginUser(t, "user2") - ctx.MakeRequest(t, req, http.StatusBadRequest) + resp := ctx.MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "Client ID not registered") +} + +func TestAuthorizeUnregisteredRedirect(t *testing.T) { + defer tests.PrepareTestEnv(t)() + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=UNREGISTERED&response_type=code&state=thestate") + ctx := loginUser(t, "user1") + resp := ctx.MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI") +} + +func TestAuthorizeUnsupportedResponseType(t *testing.T) { + defer tests.PrepareTestEnv(t)() + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate") + ctx := loginUser(t, "user1") + resp := ctx.MakeRequest(t, req, http.StatusSeeOther) + u, err := resp.Result().Location() + assert.NoError(t, err) + assert.Equal(t, "unsupported_response_type", u.Query().Get("error")) + assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description")) +} + +func TestAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) { + defer tests.PrepareTestEnv(t)() + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=UNEXPECTED") + ctx := loginUser(t, "user1") + resp := ctx.MakeRequest(t, req, http.StatusSeeOther) + u, err := resp.Result().Location() + assert.NoError(t, err) + assert.Equal(t, "invalid_request", u.Query().Get("error")) + assert.Equal(t, "unsupported code challenge method", u.Query().Get("error_description")) } -func TestLoginRedirect(t *testing.T) { +func TestAuthorizeLoginRedirect(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequest(t, "GET", "/login/oauth/authorize") assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login") } -func TestShowAuthorize(t *testing.T) { +func TestAuthorizeShow(t *testing.T) { defer tests.PrepareTestEnv(t)() - req := NewRequest(t, "GET", defaultAuthorize) + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate") ctx := loginUser(t, "user4") resp := ctx.MakeRequest(t, req, http.StatusOK) @@ -43,15 +73,17 @@ func TestShowAuthorize(t *testing.T) { htmlDoc.GetCSRF() } -func TestRedirectWithExistingGrant(t *testing.T) { +func TestAuthorizeRedirectWithExistingGrant(t *testing.T) { defer tests.PrepareTestEnv(t)() - req := NewRequest(t, "GET", defaultAuthorize) + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https%3A%2F%2Fexample.com%2Fxyzzy&response_type=code&state=thestate") ctx := loginUser(t, "user1") resp := ctx.MakeRequest(t, req, http.StatusSeeOther) u, err := resp.Result().Location() assert.NoError(t, err) assert.Equal(t, "thestate", u.Query().Get("state")) assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code")) + u.RawQuery = "" + assert.Equal(t, "https://example.com/xyzzy", u.String()) } func TestAccessTokenExchange(t *testing.T) { @@ -62,7 +94,7 @@ func TestAccessTokenExchange(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) resp := MakeRequest(t, req, http.StatusOK) type response struct { @@ -78,7 +110,7 @@ func TestAccessTokenExchange(t *testing.T) { assert.True(t, len(parsed.RefreshToken) > 10) } -func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { +func TestAccessTokenExchangeJSON(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", @@ -86,7 +118,7 @@ func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) resp := MakeRequest(t, req, http.StatusOK) type response struct { @@ -102,16 +134,20 @@ func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { assert.True(t, len(parsed.RefreshToken) > 10) } -func TestAccessTokenExchangeJSON(t *testing.T) { +func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { defer tests.PrepareTestEnv(t)() - req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{ + req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", }) - MakeRequest(t, req, http.StatusBadRequest) + resp := MakeRequest(t, req, http.StatusBadRequest) + parsedError := new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription) } func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { @@ -123,9 +159,14 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) - MakeRequest(t, req, http.StatusBadRequest) + resp := MakeRequest(t, req, http.StatusBadRequest) + parsedError := new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "invalid_client", string(parsedError.ErrorCode)) + assert.Equal(t, "cannot load client with client id: '???'", parsedError.ErrorDescription) + // invalid client secret req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", @@ -133,9 +174,14 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { "client_secret": "???", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) - MakeRequest(t, req, http.StatusBadRequest) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "invalid client secret", parsedError.ErrorDescription) + // invalid redirect uri req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", @@ -143,9 +189,14 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "???", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) - MakeRequest(t, req, http.StatusBadRequest) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "unexpected redirect URI", parsedError.ErrorDescription) + // invalid authorization code req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", @@ -153,9 +204,14 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "???", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) - MakeRequest(t, req, http.StatusBadRequest) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "client is not authorized", parsedError.ErrorDescription) + // invalid grant_type req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "???", @@ -163,9 +219,13 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) - MakeRequest(t, req, http.StatusBadRequest) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unsupported_grant_type", string(parsedError.ErrorCode)) + assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription) } func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { @@ -174,7 +234,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { "grant_type": "authorization_code", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9") resp := MakeRequest(t, req, http.StatusOK) @@ -195,19 +255,54 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { "grant_type": "authorization_code", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OmJsYWJsYQ==") - MakeRequest(t, req, http.StatusBadRequest) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError := new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "invalid client secret", parsedError.ErrorDescription) // missing header req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "authorization_code", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", + }) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "invalid_client", string(parsedError.ErrorCode)) + assert.Equal(t, "cannot load client with client id: ''", parsedError.ErrorDescription) + + // client_id inconsistent with Authorization header + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + "grant_type": "authorization_code", + "redirect_uri": "a", + "code": "authcode", + "client_id": "inconsistent", + }) + req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9") + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "invalid_request", string(parsedError.ErrorCode)) + assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription) + + // client_secret inconsistent with Authorization header + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + "grant_type": "authorization_code", + "redirect_uri": "a", + "code": "authcode", + "client_secret": "inconsistent", }) - MakeRequest(t, req, http.StatusBadRequest) + req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9") + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "invalid_request", string(parsedError.ErrorCode)) + assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription) } func TestRefreshTokenInvalidation(t *testing.T) { @@ -218,7 +313,7 @@ func TestRefreshTokenInvalidation(t *testing.T) { "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "redirect_uri": "a", "code": "authcode", - "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally + "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", }) resp := MakeRequest(t, req, http.StatusOK) type response struct { @@ -256,6 +351,11 @@ func TestRefreshTokenInvalidation(t *testing.T) { refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) MakeRequest(t, refreshReq, http.StatusOK) + // repeat request should fail refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) - MakeRequest(t, refreshReq, http.StatusBadRequest) + resp = MakeRequest(t, refreshReq, http.StatusBadRequest) + parsedError := new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "token was already used", parsedError.ErrorDescription) } diff --git a/web_src/js/features/formatting.js b/web_src/js/features/formatting.js index a7ee7ec3cf36..5f4633bba212 100644 --- a/web_src/js/features/formatting.js +++ b/web_src/js/features/formatting.js @@ -1,6 +1,7 @@ import {prettyNumber} from '../utils.js'; const {lang} = document.documentElement; +const dateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'long', day: 'numeric'}); export function initFormattingReplacements() { // replace english formatted numbers with locale-specific separators @@ -11,4 +12,10 @@ export function initFormattingReplacements() { el.textContent = formatted; } } + + // for each tag, if it has the data-format="date" attribute, format + // the text according to the user's chosen locale + for (const timeElement of document.querySelectorAll('time[data-format="date"]')) { + timeElement.textContent = dateFormatter.format(new Date(timeElement.dateTime)); + } } diff --git a/web_src/less/_base.less b/web_src/less/_base.less index fdc235164e64..c66cabd8a196 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -1,12 +1,16 @@ :root { - /* documented customizable variables */ + /* fonts */ --fonts-proportional: -apple-system, "Segoe UI", system-ui, "Roboto", "Helvetica Neue", "Arial"; --fonts-monospace: "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace, var(--fonts-emoji); --fonts-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla"; - /* other variables */ + /* backgrounds */ + --checkbox-mask-checked: url('data:image/svg+xml;utf8,'); + --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,'); + /* non-color variables */ --border-radius: .28571429rem; --opacity-disabled: .55; --height-loading: 12rem; + /* base colors */ --color-primary: #4183c4; --color-primary-dark-1: #3876b3; --color-primary-dark-2: #31699f; @@ -61,7 +65,7 @@ /* console colors */ --color-console-fg: #ffffff; --color-console-bg: #171717; - /* colors */ + /* named colors */ --color-red: #db2828; --color-orange: #f2711c; --color-yellow: #fbbd08; @@ -113,7 +117,6 @@ --color-info-border: #a9d5de; --color-info-bg: #f8ffff; --color-info-text: #276f86; - /* target-based colors */ --color-body: #ffffff; --color-text-dark: #080808; --color-text: #212121; @@ -159,12 +162,9 @@ --color-tooltip-text: #ffffff; --color-header-bar: #ffffff; --color-label-active-bg: #d0d0d0; - /* accent */ --color-small-accent: var(--color-primary-light-6); --color-accent: var(--color-primary-light-4); - /* backgrounds */ - --checkbox-mask-checked: url('data:image/svg+xml;utf8,'); - --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,'); + --color-active-line: #fffbdd; } :root * { @@ -351,6 +351,10 @@ a.commit-statuses-trigger { border-bottom: none !important; } +.ui.dividing.header { + border-bottom-color: var(--color-secondary); +} + .page-content { margin-top: 15px; } @@ -1374,7 +1378,7 @@ footer { max-width: calc(100vw - 1rem) !important; .links > * { - border-left: 1px solid var(--color-secondary); + border-left: 1px solid var(--color-secondary-dark-1); padding-left: 8px; margin-left: 5px; @@ -1712,7 +1716,7 @@ a.ui.label:hover { .lines-code.active, .lines-code .active { - background: #fffbdd !important; + background: var(--color-active-line) !important; } .blame .lines-num { @@ -2114,6 +2118,10 @@ table th[data-sortt-desc] { vertical-align: -.15em; } +.minicolors-panel { + background: var(--color-secondary-dark-1) !important; +} + .labelspage { list-style: none; padding-top: 0; @@ -2215,6 +2223,10 @@ table th[data-sortt-desc] { margin-top: inherit; } +.ui.header .sub.header { + color: var(--color-text-light-1); +} + .flash-error details code, .flash-warning details code { display: block; diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index f30bafa4cc70..c5d2a5f50ac4 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -988,6 +988,12 @@ .comment-form-reply .footer { padding-bottom: 1em; } + + @media @mediaSm { + .ui.segments { + margin-left: -2rem; + } + } } .ui.comments { @@ -1165,6 +1171,10 @@ box-shadow: none; } } + + @media @mediaSm { + padding: 1rem 0 !important; // Important is required here to override existing fomantic styles. + } } .ui.depending { @@ -1934,16 +1944,16 @@ } .dot { - width: 9px; - height: 9px; - background-color: #ddd; + width: 10px; + height: 10px; + background-color: var(--color-secondary-dark-3); z-index: 9; position: absolute; display: block; - left: -5px; + left: -6px; top: 40px; - border-radius: 6px; - border: 1px solid #ffffff; + border-radius: 100%; + border: 2.5px solid var(--color-body); } } } diff --git a/web_src/less/animations.less b/web_src/less/animations.less index ea31d53bfea7..6d32625704d7 100644 --- a/web_src/less/animations.less +++ b/web_src/less/animations.less @@ -24,7 +24,7 @@ animation: isloadingspin 500ms infinite linear; border-width: 4px; border-style: solid; - border-color: #ececec #ececec #666 #666; + border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8); border-radius: 100%; } diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index 3f3d4feae28d..12dba7926626 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -136,6 +136,7 @@ --color-label-active-bg: #4c525e; --color-small-accent: var(--color-primary-light-5); --color-accent: var(--color-primary-light-3); + --color-active-line: #534d1b; } ::-webkit-calendar-picker-indicator { @@ -231,10 +232,6 @@ a.ui.basic.green.label:hover { background-color: #a0cc75; } -.repository .diff-stats li { - border-color: var(--color-secondary); -} - .ui.red.button, .ui.red.buttons .button { background-color: #7d3434; @@ -245,24 +242,6 @@ a.ui.basic.green.label:hover { background-color: #984646; } -.lines-code.active, -.lines-code .active { - background: #534d1b !important; -} - -.ui.header .sub.header { - color: var(--color-secondary-dark-6); -} - -.ui.dividing.header { - border-bottom: 1px solid var(--color-secondary); -} - -.minicolors-panel { - background: var(--color-secondary) !important; - border-color: #6a737d !important; -} - /* invert emojis that are hard to read otherwise */ .emoji[aria-label="check mark"], .emoji[aria-label="currency exchange"], @@ -287,36 +266,10 @@ a.ui.basic.green.label:hover { filter: invert(100%) hue-rotate(180deg); } -.edit-diff > div > .ui.table { - border-left-color: var(--color-secondary) !important; - border-right-color: var(--color-secondary) !important; -} - -footer .container .links > * { - border-left-color: #888; -} - -.repository.release #release-list > li .detail .dot { - background-color: #505667; - border-color: #383c4a; -} - -.tribute-container { - box-shadow: 0 .25rem .5rem rgba(0, 0, 0, .6); -} - -.repository .repo-header .ui.huge.breadcrumb.repo-title .repo-header-icon .avatar { - color: #2a2e3a; -} - img[src$="/img/matrix.svg"] { filter: invert(80%); } -.is-loading::after { - border-color: #4a4c58 #4a4c58 #d7d7da #d7d7da; -} - .markup-block-error { border: 1px solid rgba(121, 71, 66, .5) !important; border-bottom: none !important;