diff --git a/cmd/serv.go b/cmd/serv.go index c834ca298a..b106d40d28 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -296,6 +296,15 @@ func runServ(c *cli.Context) error { gitcmd = exec.CommandContext(ctx, verb, repoPath) } + // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when + // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. + if _, err := os.Stat(setting.RepoRootPath); err != nil { + if os.IsNotExist(err) { + return fail("Incorrect configuration.", + "Directory `[repository]` `ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository]` `ROOT` an absolute value.") + } + } + gitcmd.Dir = setting.RepoRootPath gitcmd.Stdout = os.Stdout gitcmd.Stdin = os.Stdin diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 822be00bae..4b5c2b1022 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -237,7 +237,7 @@ RUN_MODE = ; prod ;; PPROF_DATA_PATH, use an absolute path when you start gitea as service ;PPROF_DATA_PATH = data/tmp/pprof ;; -;; Landing page, can be "home", "explore", "organizations" or "login" +;; Landing page, can be "home", "explore", "organizations", "login", or any URL such as "/org/repo" or even "https://anotherwebsite.com" ;; The "login" choice is not a security measure but just a UI flow change, use REQUIRE_SIGNIN_VIEW to force users to log in. ;LANDING_PAGE = home ;; @@ -879,7 +879,7 @@ PATH = ;DISABLE_STARS = false ;; ;; The default branch name of new repositories -;DEFAULT_BRANCH = master +;DEFAULT_BRANCH = main ;; ;; Allow adoption of unadopted repositories ;ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES = false diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index b3c015cb88..9d70269bf2 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -75,7 +75,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `PREFIX_ARCHIVE_FILES`: **true**: Prefix archive files by placing them in a directory named after the repository. - `DISABLE_MIGRATIONS`: **false**: Disable migrating feature. - `DISABLE_STARS`: **false**: Disable stars feature. -- `DEFAULT_BRANCH`: **master**: Default branch name of all repositories. +- `DEFAULT_BRANCH`: **main**: Default branch name of all repositories. - `ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to adopt unadopted repositories - `ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to delete unadopted repositories @@ -300,8 +300,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `ENABLE_GZIP`: **false**: Enable gzip compression for runtime-generated content, static resources excluded. - `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)__` - `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start Gitea as service -- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login\]. - +- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login, **custom**\]. Where custom would instead be any URL such as "/org/repo" or even `https://anotherwebsite.com` - `LFS_START_SERVER`: **false**: Enables Git LFS support. - `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)/lfs**: Default LFS content path. (if it is on local storage.) **DEPRECATED** use settings in `[lfs]`. - `LFS_JWT_SECRET`: **\**: LFS authentication secret, change this a unique string. diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md index dee1dbb6d6..bdde5bd8c4 100644 --- a/docs/content/doc/advanced/logging-documentation.en-us.md +++ b/docs/content/doc/advanced/logging-documentation.en-us.md @@ -287,6 +287,7 @@ MODE = console LEVEL = debug ; please set the level to debug when we are debugging a problem ROUTER = console COLORIZE = false ; this can be true if you can strip out the ansi coloring +ENABLE_SSH_LOG = true ; shows logs related to git over SSH. ``` Sometimes it will be helpful get some specific `TRACE` level logging restricted @@ -445,7 +446,7 @@ Gitea includes built-in log rotation, which should be enough for most deployment - Disable built-in log rotation by setting `LOG_ROTATE` to `false` in your `app.ini`. - Install `logrotate`. - Configure `logrotate` to match your deployment requirements, see `man 8 logrotate` for configuration syntax details. In the `postrotate/endscript` block send Gitea a `USR1` signal via `kill -USR1` or `kill -10` to the `gitea` process itself, or run `gitea manager logging release-and-reopen` (with the appropriate environment). Ensure that your configurations apply to all files emitted by Gitea loggers as described in the above sections. -- Always do `logrotate /etc/logrotate.conf --debug` to test your configurations. +- Always do `logrotate /etc/logrotate.conf --debug` to test your configurations. - If you are using docker and are running from outside of the container you can use `docker exec -u $OS_USER $CONTAINER_NAME sh -c 'gitea manager logging release-and-reopen'` or `docker exec $CONTAINER_NAME sh -c '/bin/s6-svc -1 /etc/s6/gitea/'` or send `USR1` directly to the Gitea process itself. The next `logrotate` jobs will include your configurations, so no restart is needed. You can also immediately reload `logrotate` with `logrotate /etc/logrotate.conf --force`. diff --git a/integrations/api_issue_milestone_test.go b/integrations/api_issue_milestone_test.go index 60a6329424..a7f89721a5 100644 --- a/integrations/api_issue_milestone_test.go +++ b/integrations/api_issue_milestone_test.go @@ -9,7 +9,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -21,7 +21,7 @@ import ( func TestAPIIssuesMilestone(t *testing.T) { defer prepareTestEnv(t)() - milestone := unittest.AssertExistsAndLoadBean(t, &models.Milestone{ID: 1}).(*models.Milestone) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: milestone.RepoID}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) assert.Equal(t, int64(1), int64(milestone.NumIssues)) diff --git a/integrations/api_issue_reaction_test.go b/integrations/api_issue_reaction_test.go index aa6f46f8bd..4a063c8c68 100644 --- a/integrations/api_issue_reaction_test.go +++ b/integrations/api_issue_reaction_test.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/convert" @@ -23,7 +24,7 @@ func TestAPIIssuesReactions(t *testing.T) { defer prepareTestEnv(t)() issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) session := loginUser(t, owner.Name) @@ -82,7 +83,7 @@ func TestAPICommentReactions(t *testing.T) { comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment) _ = comment.LoadIssue() issue := comment.Issue - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) session := loginUser(t, owner.Name) diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go index 3f62e042cb..90098b9236 100644 --- a/integrations/api_issue_stopwatch_test.go +++ b/integrations/api_issue_stopwatch_test.go @@ -9,6 +9,7 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -45,7 +46,7 @@ func TestAPIStopStopWatches(t *testing.T) { defer prepareTestEnv(t)() issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) @@ -61,7 +62,7 @@ func TestAPICancelStopWatches(t *testing.T) { defer prepareTestEnv(t)() issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) @@ -77,7 +78,7 @@ func TestAPIStartStopWatches(t *testing.T) { defer prepareTestEnv(t)() issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go index 3957c10233..5ed5a0ad99 100644 --- a/integrations/api_issue_test.go +++ b/integrations/api_issue_test.go @@ -168,12 +168,11 @@ func TestAPIEditIssue(t *testing.T) { func TestAPISearchIssues(t *testing.T) { defer prepareTestEnv(t)() - session := loginUser(t, "user2") - token := getTokenForLoggedInUser(t, session) + token := getUserToken(t, "user2") link, _ := url.Parse("/api/v1/repos/issues/search") - req := NewRequest(t, "GET", link.String()) - resp := session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", link.String()+"?token="+token) + resp := MakeRequest(t, req, http.StatusOK) var apiIssues []*api.Issue DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 10) @@ -181,7 +180,7 @@ func TestAPISearchIssues(t *testing.T) { query := url.Values{"token": {token}} link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 10) @@ -189,9 +188,10 @@ func TestAPISearchIssues(t *testing.T) { before := time.Unix(999307200, 0).Format(time.RFC3339) query.Add("since", since) query.Add("before", before) + query.Add("token", token) link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 8) query.Del("since") @@ -200,14 +200,14 @@ func TestAPISearchIssues(t *testing.T) { query.Add("state", "closed") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) query.Set("state", "all") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.EqualValues(t, "15", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 10) // there are more but 10 is page item limit @@ -215,49 +215,49 @@ func TestAPISearchIssues(t *testing.T) { query.Add("limit", "20") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 15) - query = url.Values{"assigned": {"true"}, "state": {"all"}} + query = url.Values{"assigned": {"true"}, "state": {"all"}, "token": {token}} link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 1) - query = url.Values{"milestones": {"milestone1"}, "state": {"all"}} + query = url.Values{"milestones": {"milestone1"}, "state": {"all"}, "token": {token}} link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 1) - query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}} + query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}, "token": {token}} link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) - query = url.Values{"owner": {"user2"}} // user + query = url.Values{"owner": {"user2"}, "token": {token}} // user link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 6) - query = url.Values{"owner": {"user3"}} // organization + query = url.Values{"owner": {"user3"}, "token": {token}} // organization link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 3) - query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team + query = url.Values{"owner": {"user3"}, "team": {"team1"}, "token": {token}} // organization + team link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) } @@ -265,12 +265,11 @@ func TestAPISearchIssues(t *testing.T) { func TestAPISearchIssuesWithLabels(t *testing.T) { defer prepareTestEnv(t)() - session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session) + token := getUserToken(t, "user1") link, _ := url.Parse("/api/v1/repos/issues/search") - req := NewRequest(t, "GET", link.String()) - resp := session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", link.String()+"?token="+token) + resp := MakeRequest(t, req, http.StatusOK) var apiIssues []*api.Issue DecodeJSON(t, resp, &apiIssues) @@ -280,14 +279,14 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { query.Add("token", token) link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 10) query.Add("labels", "label1") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) @@ -295,7 +294,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { query.Set("labels", "label1,label2") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) @@ -303,7 +302,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { query.Set("labels", "orglabel4") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 1) @@ -312,7 +311,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { query.Add("state", "all") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) @@ -320,7 +319,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { query.Set("labels", "label1,orglabel4") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) } diff --git a/integrations/api_issue_tracked_time_test.go b/integrations/api_issue_tracked_time_test.go index 335fd2929a..b6f7091013 100644 --- a/integrations/api_issue_tracked_time_test.go +++ b/integrations/api_issue_tracked_time_test.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" @@ -23,7 +24,7 @@ func TestAPIGetTrackedTimes(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) - assert.NoError(t, issue2.LoadRepo()) + assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) session := loginUser(t, user2.Name) token := getTokenForLoggedInUser(t, session) @@ -65,7 +66,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) { time6 := unittest.AssertExistsAndLoadBean(t, &models.TrackedTime{ID: 6}).(*models.TrackedTime) issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) - assert.NoError(t, issue2.LoadRepo()) + assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) session := loginUser(t, user2.Name) @@ -99,7 +100,7 @@ func TestAPIAddTrackedTimes(t *testing.T) { defer prepareTestEnv(t)() issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) - assert.NoError(t, issue2.LoadRepo()) + assert.NoError(t, issue2.LoadRepo(db.DefaultContext)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) diff --git a/integrations/api_org_test.go b/integrations/api_org_test.go index e33c010e88..a3c1827e70 100644 --- a/integrations/api_org_test.go +++ b/integrations/api_org_test.go @@ -20,9 +20,8 @@ import ( func TestAPIOrgCreate(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { - session := loginUser(t, "user1") + token := getUserToken(t, "user1") - token := getTokenForLoggedInUser(t, session) org := api.CreateOrgOption{ UserName: "user1_org", FullName: "User1's organization", @@ -32,7 +31,7 @@ func TestAPIOrgCreate(t *testing.T) { Visibility: "limited", } req := NewRequestWithJSON(t, "POST", "/api/v1/orgs?token="+token, &org) - resp := session.MakeRequest(t, req, http.StatusCreated) + resp := MakeRequest(t, req, http.StatusCreated) var apiOrg api.Organization DecodeJSON(t, resp, &apiOrg) @@ -50,13 +49,13 @@ func TestAPIOrgCreate(t *testing.T) { FullName: org.FullName, }) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName) - resp = session.MakeRequest(t, req, http.StatusOK) + req = NewRequestf(t, "GET", "/api/v1/orgs/%s?token=%s", org.UserName, token) + resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiOrg) assert.EqualValues(t, org.UserName, apiOrg.UserName) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName) - resp = session.MakeRequest(t, req, http.StatusOK) + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token=%s", org.UserName, token) + resp = MakeRequest(t, req, http.StatusOK) var repos []*api.Repository DecodeJSON(t, resp, &repos) @@ -64,8 +63,8 @@ func TestAPIOrgCreate(t *testing.T) { assert.False(t, repo.Private) } - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName) - resp = session.MakeRequest(t, req, http.StatusOK) + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members?token=%s", org.UserName, token) + resp = MakeRequest(t, req, http.StatusOK) // user1 on this org is public var users []*api.User diff --git a/integrations/api_packages_test.go b/integrations/api_packages_test.go index 263e7cea53..b3f6e88d9f 100644 --- a/integrations/api_packages_test.go +++ b/integrations/api_packages_test.go @@ -9,11 +9,15 @@ import ( "fmt" "net/http" "testing" + "time" - "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + container_model "code.gitea.io/gitea/models/packages/container" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" + packages_service "code.gitea.io/gitea/services/packages" "github.com/stretchr/testify/assert" ) @@ -43,7 +47,7 @@ func TestPackageAPI(t *testing.T) { DecodeJSON(t, resp, &apiPackages) assert.Len(t, apiPackages, 1) - assert.Equal(t, string(packages.TypeGeneric), apiPackages[0].Type) + assert.Equal(t, string(packages_model.TypeGeneric), apiPackages[0].Type) assert.Equal(t, packageName, apiPackages[0].Name) assert.Equal(t, packageVersion, apiPackages[0].Version) assert.NotNil(t, apiPackages[0].Creator) @@ -62,7 +66,7 @@ func TestPackageAPI(t *testing.T) { var p *api.Package DecodeJSON(t, resp, &p) - assert.Equal(t, string(packages.TypeGeneric), p.Type) + assert.Equal(t, string(packages_model.TypeGeneric), p.Type) assert.Equal(t, packageName, p.Name) assert.Equal(t, packageVersion, p.Version) assert.NotNil(t, p.Creator) @@ -100,3 +104,26 @@ func TestPackageAPI(t *testing.T) { MakeRequest(t, req, http.StatusNoContent) }) } + +func TestPackageCleanup(t *testing.T) { + defer prepareTestEnv(t)() + + time.Sleep(time.Second) + + pbs, err := packages_model.FindExpiredUnreferencedBlobs(db.DefaultContext, time.Duration(0)) + assert.NoError(t, err) + assert.NotEmpty(t, pbs) + + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, 2, packages_model.TypeContainer, "test", container_model.UploadVersion) + assert.NoError(t, err) + + err = packages_service.Cleanup(nil, time.Duration(0)) + assert.NoError(t, err) + + pbs, err = packages_model.FindExpiredUnreferencedBlobs(db.DefaultContext, time.Duration(0)) + assert.NoError(t, err) + assert.Empty(t, pbs) + + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, 2, packages_model.TypeContainer, "test", container_model.UploadVersion) + assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) +} diff --git a/integrations/api_releases_test.go b/integrations/api_releases_test.go index 80f53717b6..ebb76cc163 100644 --- a/integrations/api_releases_test.go +++ b/integrations/api_releases_test.go @@ -25,12 +25,11 @@ func TestAPIListReleases(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - session := loginUser(t, user2.LowerName) - token := getTokenForLoggedInUser(t, session) + token := getUserToken(t, user2.LowerName) link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name)) link.RawQuery = url.Values{"token": {token}}.Encode() - resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) + resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) var apiReleases []*api.Release DecodeJSON(t, resp, &apiReleases) if assert.Len(t, apiReleases, 3) { @@ -53,13 +52,11 @@ func TestAPIListReleases(t *testing.T) { // test filter testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) { - link.RawQuery = query.Encode() if auth { query.Set("token", token) - resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) - } else { - resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) } + link.RawQuery = query.Encode() + resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) DecodeJSON(t, resp, &apiReleases) assert.Len(t, apiReleases, expectedLength, msgAndArgs) } diff --git a/integrations/api_repo_topic_test.go b/integrations/api_repo_topic_test.go index b7f9a5a5a6..04295724a7 100644 --- a/integrations/api_repo_topic_test.go +++ b/integrations/api_repo_topic_test.go @@ -59,36 +59,34 @@ func TestAPIRepoTopic(t *testing.T) { repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) // Get user2's token - session := loginUser(t, user2.Name) - token2 := getTokenForLoggedInUser(t, session) + token2 := getUserToken(t, user2.Name) // Test read topics using login url := fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name) - req := NewRequest(t, "GET", url) - res := session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", url+"?token="+token2) + res := MakeRequest(t, req, http.StatusOK) var topics *api.TopicName DecodeJSON(t, res, &topics) assert.ElementsMatch(t, []string{"topicname1", "topicname2"}, topics.TopicNames) // Log out user2 - session = emptyTestSession(t) url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user2.Name, repo2.Name, token2) // Test delete a topic req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2) - session.MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusNoContent) // Test add an existing topic req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Golang", token2) - session.MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusNoContent) // Test add a topic req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "topicName3", token2) - session.MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusNoContent) // Test read topics using token req = NewRequest(t, "GET", url) - res = session.MakeRequest(t, req, http.StatusOK) + res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.ElementsMatch(t, []string{"topicname2", "golang", "topicname3"}, topics.TopicNames) @@ -97,9 +95,9 @@ func TestAPIRepoTopic(t *testing.T) { req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{ Topics: newTopics, }) - session.MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", url) - res = session.MakeRequest(t, req, http.StatusOK) + res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames) @@ -108,9 +106,9 @@ func TestAPIRepoTopic(t *testing.T) { req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{ Topics: newTopics, }) - session.MakeRequest(t, req, http.StatusUnprocessableEntity) + MakeRequest(t, req, http.StatusUnprocessableEntity) req = NewRequest(t, "GET", url) - res = session.MakeRequest(t, req, http.StatusOK) + res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames) @@ -119,9 +117,9 @@ func TestAPIRepoTopic(t *testing.T) { req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{ Topics: newTopics, }) - session.MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", url) - res = session.MakeRequest(t, req, http.StatusOK) + res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.Len(t, topics.TopicNames, 25) @@ -130,29 +128,27 @@ func TestAPIRepoTopic(t *testing.T) { req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{ Topics: newTopics, }) - session.MakeRequest(t, req, http.StatusUnprocessableEntity) + MakeRequest(t, req, http.StatusUnprocessableEntity) // Test add a topic when there is already maximum req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "t26", token2) - session.MakeRequest(t, req, http.StatusUnprocessableEntity) + MakeRequest(t, req, http.StatusUnprocessableEntity) // Test delete a topic that repo doesn't have req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2) - session.MakeRequest(t, req, http.StatusNotFound) + MakeRequest(t, req, http.StatusNotFound) // Get user4's token - session = loginUser(t, user4.Name) - token4 := getTokenForLoggedInUser(t, session) - session = emptyTestSession(t) + token4 := getUserToken(t, user4.Name) // Test read topics with write access url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user3.Name, repo3.Name, token4) req = NewRequest(t, "GET", url) - res = session.MakeRequest(t, req, http.StatusOK) + res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.Empty(t, topics.TopicNames) // Test add a topic to repo with write access (requires repo admin access) req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user3.Name, repo3.Name, "topicName", token4) - session.MakeRequest(t, req, http.StatusForbidden) + MakeRequest(t, req, http.StatusForbidden) } diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go index 7f6d1b81d4..daf1efa2be 100644 --- a/integrations/api_team_test.go +++ b/integrations/api_team_test.go @@ -224,11 +224,9 @@ func TestAPITeamSearch(t *testing.T) { var results TeamSearchResults - session := loginUser(t, user.Name) - csrf := GetCSRF(t, session, "/"+org.Name) - req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "_team") - req.Header.Add("X-Csrf-Token", csrf) - resp := session.MakeRequest(t, req, http.StatusOK) + token := getUserToken(t, user.Name) + req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "_team", token) + resp := MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &results) assert.NotEmpty(t, results.Data) assert.Len(t, results.Data, 1) @@ -236,9 +234,8 @@ func TestAPITeamSearch(t *testing.T) { // no access if not organization member user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) - session = loginUser(t, user5.Name) - csrf = GetCSRF(t, session, "/"+org.Name) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "team") - req.Header.Add("X-Csrf-Token", csrf) - session.MakeRequest(t, req, http.StatusForbidden) + token5 := getUserToken(t, user5.Name) + + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "team", token5) + MakeRequest(t, req, http.StatusForbidden) } diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go index 69f4ff2249..62e70d4c3d 100644 --- a/integrations/api_user_heatmap_test.go +++ b/integrations/api_user_heatmap_test.go @@ -20,15 +20,15 @@ func TestUserHeatmap(t *testing.T) { defer prepareTestEnv(t)() adminUsername := "user1" normalUsername := "user2" - session := loginUser(t, adminUsername) + token := getUserToken(t, adminUsername) fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local) timeutil.Set(fakeNow) defer timeutil.Unset() - urlStr := fmt.Sprintf("/api/v1/users/%s/heatmap", normalUsername) + urlStr := fmt.Sprintf("/api/v1/users/%s/heatmap?token=%s", normalUsername, token) req := NewRequest(t, "GET", urlStr) - resp := session.MakeRequest(t, req, http.StatusOK) + resp := MakeRequest(t, req, http.StatusOK) var heatmap []*models.UserHeatmapData DecodeJSON(t, resp, &heatmap) var dummyheatmap []*models.UserHeatmapData diff --git a/integrations/csrf_test.go b/integrations/csrf_test.go new file mode 100644 index 0000000000..5bfc97bbd1 --- /dev/null +++ b/integrations/csrf_test.go @@ -0,0 +1,52 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "strings" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestCsrfProtection(t *testing.T) { + defer prepareTestEnv(t)() + + // test web form csrf via form + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + session := loginUser(t, user.Name) + req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ + "_csrf": "fake_csrf", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + resp := session.MakeRequest(t, req, http.StatusSeeOther) + loc := resp.Header().Get("Location") + assert.Equal(t, setting.AppSubURL+"/", loc) + resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Equal(t, "Bad Request: invalid CSRF token", + strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), + ) + + // test web form csrf via header. TODO: should use an UI api to test + req = NewRequest(t, "POST", "/user/settings") + req.Header.Add("X-Csrf-Token", "fake_csrf") + session.MakeRequest(t, req, http.StatusSeeOther) + + resp = session.MakeRequest(t, req, http.StatusSeeOther) + loc = resp.Header().Get("Location") + assert.Equal(t, setting.AppSubURL+"/", loc) + resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + assert.Equal(t, "Bad Request: invalid CSRF token", + strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), + ) +} diff --git a/integrations/integration_test.go b/integrations/integration_test.go index 9e0445cae7..8d2bfe9383 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -165,6 +165,7 @@ func initIntegrationTest() { setting.SetCustomPathAndConf("", "", "") setting.LoadForTest() + setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" _ = util.RemoveAll(models.LocalCopyPath()) git.CheckLFSVersion() setting.InitDBConfig() @@ -359,6 +360,10 @@ func emptyTestSession(t testing.TB) *TestSession { return &TestSession{jar: jar} } +func getUserToken(t testing.TB, userName string) string { + return getTokenForLoggedInUser(t, loginUser(t, userName)) +} + func loginUser(t testing.TB, userName string) *TestSession { t.Helper() if session, ok := loginSessionCache[userName]; ok { diff --git a/integrations/issue_test.go b/integrations/issue_test.go index 6a9b48e5a4..c6b801c9a6 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -7,6 +7,7 @@ package integrations import ( "fmt" "net/http" + "net/url" "path" "strconv" "strings" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "github.com/PuerkitoBio/goquery" @@ -347,3 +349,211 @@ func TestIssueRedirect(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusSeeOther) assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp)) } + +func TestSearchIssues(t *testing.T) { + defer prepareTestEnv(t)() + + session := loginUser(t, "user2") + + link, _ := url.Parse("/issues/search") + req := NewRequest(t, "GET", link.String()) + resp := session.MakeRequest(t, req, http.StatusOK) + var apiIssues []*api.Issue + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 10) + + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 10) + + since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 + before := time.Unix(999307200, 0).Format(time.RFC3339) + query := url.Values{} + query.Add("since", since) + query.Add("before", before) + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 8) + query.Del("since") + query.Del("before") + + query.Add("state", "closed") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) + + query.Set("state", "all") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.EqualValues(t, "15", resp.Header().Get("X-Total-Count")) + assert.Len(t, apiIssues, 10) // there are more but 10 is page item limit + + query.Add("limit", "20") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 15) + + query = url.Values{"assigned": {"true"}, "state": {"all"}} + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 1) + + query = url.Values{"milestones": {"milestone1"}, "state": {"all"}} + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 1) + + query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}} + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) + + query = url.Values{"owner": {"user2"}} // user + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 6) + + query = url.Values{"owner": {"user3"}} // organization + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 3) + + query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) +} + +func TestSearchIssuesWithLabels(t *testing.T) { + defer prepareTestEnv(t)() + + token := getUserToken(t, "user1") + + link, _ := url.Parse("/api/v1/repos/issues/search?token=" + token) + req := NewRequest(t, "GET", link.String()) + resp := MakeRequest(t, req, http.StatusOK) + var apiIssues []*api.Issue + DecodeJSON(t, resp, &apiIssues) + + assert.Len(t, apiIssues, 10) + + query := url.Values{ + "token": []string{token}, + } + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 10) + + query.Add("labels", "label1") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) + + // multiple labels + query.Set("labels", "label1,label2") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) + + // an org label + query.Set("labels", "orglabel4") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 1) + + // org and repo label + query.Set("labels", "label2,orglabel4") + query.Add("state", "all") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) + + // org and repo label which share the same issue + query.Set("labels", "label1,orglabel4") + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 2) +} + +func TestGetIssueInfo(t *testing.T) { + defer prepareTestEnv(t)() + + issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) + assert.NoError(t, issue.LoadAttributes()) + assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix)) + assert.Equal(t, api.StateOpen, issue.State()) + + session := loginUser(t, owner.Name) + + urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index) + req := NewRequest(t, "GET", urlStr) + resp := session.MakeRequest(t, req, http.StatusOK) + var apiIssue api.Issue + DecodeJSON(t, resp, &apiIssue) + + assert.EqualValues(t, issue.ID, apiIssue.ID) +} + +func TestUpdateIssueDeadline(t *testing.T) { + defer prepareTestEnv(t)() + + issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) + repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User) + assert.NoError(t, issueBefore.LoadAttributes()) + assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix)) + assert.Equal(t, api.StateOpen, issueBefore.State()) + + session := loginUser(t, owner.Name) + + issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index) + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF() + req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{ + "due_date": "2022-04-06T00:00:00.000Z", + }) + + resp = session.MakeRequest(t, req, http.StatusCreated) + var apiIssue api.IssueDeadline + DecodeJSON(t, resp, &apiIssue) + + assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02")) +} diff --git a/integrations/org_test.go b/integrations/org_test.go index 794475a924..227a1b8d40 100644 --- a/integrations/org_test.go +++ b/integrations/org_test.go @@ -10,6 +10,8 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" @@ -173,3 +175,30 @@ func TestOrgRestrictedUser(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName)) restrictedSession.MakeRequest(t, req, http.StatusOK) } + +func TestTeamSearch(t *testing.T) { + defer prepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) + + var results TeamSearchResults + + session := loginUser(t, user.Name) + csrf := GetCSRF(t, session, "/"+org.Name) + req := NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "_team") + req.Header.Add("X-Csrf-Token", csrf) + resp := session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &results) + assert.NotEmpty(t, results.Data) + assert.Len(t, results.Data, 1) + assert.Equal(t, "test_team", results.Data[0].Name) + + // no access if not organization member + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) + session = loginUser(t, user5.Name) + csrf = GetCSRF(t, session, "/"+org.Name) + req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team") + req.Header.Add("X-Csrf-Token", csrf) + session.MakeRequest(t, req, http.StatusNotFound) +} diff --git a/integrations/repo_topic_test.go b/integrations/repo_topic_test.go new file mode 100644 index 0000000000..e049afdd7c --- /dev/null +++ b/integrations/repo_topic_test.go @@ -0,0 +1,47 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "net/url" + "testing" + + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestTopicSearch(t *testing.T) { + defer prepareTestEnv(t)() + searchURL, _ := url.Parse("/explore/topics/search") + var topics struct { + TopicNames []*api.TopicResponse `json:"topics"` + } + + query := url.Values{"page": []string{"1"}, "limit": []string{"4"}} + + searchURL.RawQuery = query.Encode() + res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 4) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + query.Add("q", "topic") + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 2) + + query.Set("q", "database") + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + if assert.Len(t, topics.TopicNames, 1) { + assert.EqualValues(t, 2, topics.TopicNames[0].ID) + assert.EqualValues(t, "database", topics.TopicNames[0].Name) + assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount) + } +} diff --git a/integrations/user_test.go b/integrations/user_test.go index e8fbccd51e..d0523d8b3a 100644 --- a/integrations/user_test.go +++ b/integrations/user_test.go @@ -8,8 +8,11 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/models" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation/i18n" @@ -222,3 +225,26 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) { // t.Log(resp.Body.String()) assert.Equal(t, expected, resp.Body.String()) } + +func TestListStopWatches(t *testing.T) { + defer prepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) + + session := loginUser(t, owner.Name) + req := NewRequestf(t, "GET", "/user/stopwatches") + resp := session.MakeRequest(t, req, http.StatusOK) + var apiWatches []*api.StopWatch + DecodeJSON(t, resp, &apiWatches) + stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch) + issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) + if assert.Len(t, apiWatches, 1) { + assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) + assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) + assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) + assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) + assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) + assert.Greater(t, int64(apiWatches[0].Seconds), int64(0)) + } +} diff --git a/models/action.go b/models/action.go index d3231641d9..3339d3b87a 100644 --- a/models/action.go +++ b/models/action.go @@ -243,7 +243,7 @@ func (a *Action) getCommentLink(ctx context.Context) string { return "#" } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return "#" } diff --git a/models/consistency_test.go b/models/consistency_test.go index d49a0132f0..1593500361 100644 --- a/models/consistency_test.go +++ b/models/consistency_test.go @@ -8,7 +8,10 @@ import ( "testing" "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/models/unittest" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -33,3 +36,70 @@ func TestDeleteOrphanedObjects(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, countBefore, countAfter) } + +func TestNewMilestone(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := &issues_model.Milestone{ + RepoID: 1, + Name: "milestoneName", + Content: "milestoneContent", + } + + assert.NoError(t, issues_model.NewMilestone(milestone)) + unittest.AssertExistsAndLoadBean(t, milestone) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) +} + +func TestChangeMilestoneStatus(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + + assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true)) + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1") + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) + + assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false)) + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0") + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) +} + +func TestDeleteMilestoneByRepoID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1)) + unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1}) + + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID)) +} + +func TestUpdateMilestone(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + milestone.Name = " newMilestoneName " + milestone.Content = "newMilestoneContent" + assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed)) + milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + assert.EqualValues(t, "newMilestoneName", milestone.Name) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} + +func TestUpdateMilestoneCounters(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1}, + "is_closed=0").(*Issue) + + issue.IsClosed = true + issue.ClosedUnix = timeutil.TimeStampNow() + _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + assert.NoError(t, err) + assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) + + issue.IsClosed = false + issue.ClosedUnix = 0 + _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + assert.NoError(t, err) + assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} diff --git a/models/error.go b/models/error.go index 6233b2ea85..6846bf8320 100644 --- a/models/error.go +++ b/models/error.go @@ -1048,33 +1048,6 @@ func (err ErrLabelNotExist) Error() string { return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) } -// _____ .__.__ __ -// / \ |__| | ____ _______/ |_ ____ ____ ____ -// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ -// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ -// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > -// \/ \/ \/ \/ \/ - -// ErrMilestoneNotExist represents a "MilestoneNotExist" kind of error. -type ErrMilestoneNotExist struct { - ID int64 - RepoID int64 - Name string -} - -// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist. -func IsErrMilestoneNotExist(err error) bool { - _, ok := err.(ErrMilestoneNotExist) - return ok -} - -func (err ErrMilestoneNotExist) Error() string { - if len(err.Name) > 0 { - return fmt.Sprintf("milestone does not exist [name: %s, repo_id: %d]", err.Name, err.RepoID) - } - return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID) -} - // ____ ___ .__ .___ // | | \______ | | _________ __| _/ // | | /\____ \| | / _ \__ \ / __ | diff --git a/models/issue.go b/models/issue.go index cf5e4fd8b6..b1fa2d02ad 100644 --- a/models/issue.go +++ b/models/issue.go @@ -16,7 +16,7 @@ import ( admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" - "code.gitea.io/gitea/models/issues" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" project_model "code.gitea.io/gitea/models/project" @@ -46,14 +46,14 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *issues_model.Milestone `xorm:"-"` + Project *project_model.Project `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` @@ -72,7 +72,7 @@ type Issue struct { Attachments []*repo_model.Attachment `xorm:"-"` Comments []*Comment `xorm:"-"` - Reactions issues.ReactionList `xorm:"-"` + Reactions issues_model.ReactionList `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` Assignees []*user_model.User `xorm:"-"` ForeignReference *foreignreference.ForeignReference `xorm:"-"` @@ -124,11 +124,7 @@ func (issue *Issue) IsOverdue() bool { } // LoadRepo loads issue's repository -func (issue *Issue) LoadRepo() error { - return issue.loadRepo(db.DefaultContext) -} - -func (issue *Issue) loadRepo(ctx context.Context) (err error) { +func (issue *Issue) LoadRepo(ctx context.Context) (err error) { if issue.Repo == nil { issue.Repo, err = repo_model.GetRepositoryByIDCtx(ctx, issue.RepoID) if err != nil { @@ -144,7 +140,7 @@ func (issue *Issue) IsTimetrackerEnabled() bool { } func (issue *Issue) isTimetrackerEnabled(ctx context.Context) bool { - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { log.Error(fmt.Sprintf("loadRepo: %v", err)) return false } @@ -244,17 +240,17 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) { if issue.Reactions != nil { return nil } - reactions, _, err := issues.FindReactions(ctx, issues.FindReactionsOptions{ + reactions, _, err := issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ IssueID: issue.ID, }) if err != nil { return err } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } // Load reaction user data - if _, err := issues.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { + if _, err := issues_model.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil { return err } @@ -297,10 +293,10 @@ func (issue *Issue) loadForeignReference(ctx context.Context) (err error) { return nil } -func (issue *Issue) loadMilestone(e db.Engine) (err error) { +func (issue *Issue) loadMilestone(ctx context.Context) (err error) { if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 { - issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) - if err != nil && !IsErrMilestoneNotExist(err) { + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID) + if err != nil && !issues_model.IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err) } } @@ -309,7 +305,7 @@ func (issue *Issue) loadMilestone(e db.Engine) (err error) { func (issue *Issue) loadAttributes(ctx context.Context) (err error) { e := db.GetEngine(ctx) - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return } @@ -321,7 +317,7 @@ func (issue *Issue) loadAttributes(ctx context.Context) (err error) { return } - if err = issue.loadMilestone(e); err != nil { + if err = issue.loadMilestone(ctx); err != nil { return } @@ -372,7 +368,7 @@ func (issue *Issue) LoadAttributes() error { // LoadMilestone load milestone of this issue. func (issue *Issue) LoadMilestone() error { - return issue.loadMilestone(db.GetEngine(db.DefaultContext)) + return issue.loadMilestone(db.DefaultContext) } // GetIsRead load the `IsRead` field of the issue @@ -391,7 +387,7 @@ func (issue *Issue) GetIsRead(userID int64) error { // APIURL returns the absolute APIURL to this issue. func (issue *Issue) APIURL() string { if issue.Repo == nil { - err := issue.LoadRepo() + err := issue.LoadRepo(db.DefaultContext) if err != nil { log.Error("Issue[%d].APIURL(): %v", issue.ID, err) return "" @@ -491,7 +487,7 @@ func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { } defer committer.Close() - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } else if err = issue.loadPullRequest(db.GetEngine(ctx)); err != nil { return err @@ -539,7 +535,7 @@ func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (e } defer committer.Close() - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } @@ -607,7 +603,8 @@ func (issue *Issue) ReadBy(userID int64) error { return setIssueNotificationStatusReadIfUnread(db.GetEngine(db.DefaultContext), userID, issue.ID) } -func updateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { +// UpdateIssueCols updates cols of issue +func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { return err } @@ -658,7 +655,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use issue.ClosedUnix = 0 } - if err := updateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { return nil, err } @@ -674,7 +671,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use // Update issue count of milestone if issue.MilestoneID > 0 { - if err := updateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return nil, err } } @@ -691,7 +688,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use cmtType = CommentTypeMergePull } - return createComment(ctx, &CreateCommentOptions{ + return CreateCommentCtx(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, Repo: issue.Repo, @@ -707,7 +704,7 @@ func ChangeIssueStatus(issue *Issue, doer *user_model.User, isClosed bool) (*Com } defer committer.Close() - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return nil, err } if err := issue.loadPoster(db.GetEngine(ctx)); err != nil { @@ -734,11 +731,11 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err } defer committer.Close() - if err = updateIssueCols(ctx, issue, "name"); err != nil { + if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %v", err) } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %v", err) } @@ -750,7 +747,7 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err OldTitle: oldTitle, NewTitle: issue.Title, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return fmt.Errorf("createComment: %v", err) } if err = issue.addCrossReferences(ctx, doer, true); err != nil { @@ -768,11 +765,11 @@ func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err err } defer committer.Close() - if err = updateIssueCols(ctx, issue, "ref"); err != nil { + if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { return fmt.Errorf("updateIssueCols: %v", err) } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %v", err) } oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) @@ -786,7 +783,7 @@ func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err err OldRef: oldRefFriendly, NewRef: newRefFriendly, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return fmt.Errorf("createComment: %v", err) } @@ -811,7 +808,7 @@ func AddDeletePRBranchComment(doer *user_model.User, repo *repo_model.Repository Issue: issue, OldRef: branchName, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return err } @@ -846,12 +843,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er } defer committer.Close() - hasContentHistory, err := issues.HasIssueContentHistory(ctx, issue.ID, 0) + hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, issue.ID, 0) if err != nil { return fmt.Errorf("HasIssueContentHistory: %v", err) } if !hasContentHistory { - if err = issues.SaveIssueContentHistory(db.GetEngine(ctx), issue.PosterID, issue.ID, 0, + if err = issues_model.SaveIssueContentHistory(db.GetEngine(ctx), issue.PosterID, issue.ID, 0, issue.CreatedUnix, issue.Content, true); err != nil { return fmt.Errorf("SaveIssueContentHistory: %v", err) } @@ -859,11 +856,11 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er issue.Content = content - if err = updateIssueCols(ctx, issue, "content"); err != nil { + if err = UpdateIssueCols(ctx, issue, "content"); err != nil { return fmt.Errorf("UpdateIssueCols: %v", err) } - if err = issues.SaveIssueContentHistory(db.GetEngine(ctx), doer.ID, issue.ID, 0, + if err = issues_model.SaveIssueContentHistory(db.GetEngine(ctx), doer.ID, issue.ID, 0, timeutil.TimeStampNow(), issue.Content, false); err != nil { return fmt.Errorf("SaveIssueContentHistory: %v", err) } @@ -943,8 +940,8 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) if opts.Issue.MilestoneID > 0 { - milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID) - if err != nil && !IsErrMilestoneNotExist(err) { + milestone, err := issues_model.GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) + if err != nil && !issues_model.IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByID: %v", err) } @@ -968,7 +965,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) } if opts.Issue.MilestoneID > 0 { - if err := updateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { + if err := issues_model.UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { return err } @@ -980,7 +977,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) OldMilestoneID: 0, MilestoneID: opts.Issue.MilestoneID, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return err } } @@ -1270,7 +1267,7 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64 } } -func (opts *IssuesOptions) setupSession(sess *xorm.Session) { +func (opts *IssuesOptions) setupSessionWithLimit(sess *xorm.Session) { if opts.Page >= 0 && opts.PageSize > 0 { var start int if opts.Page == 0 { @@ -1280,7 +1277,10 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { } sess.Limit(opts.PageSize, start) } + opts.setupSessionNoLimit(sess) +} +func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { if len(opts.IssueIDs) > 0 { sess.In("issue.id", opts.IssueIDs) } @@ -1446,7 +1446,7 @@ func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) { sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSession(sess) + opts.setupSessionNoLimit(sess) countsSlice := make([]*struct { RepoID int64 @@ -1456,7 +1456,7 @@ func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) { Select("issue.repo_id AS repo_id, COUNT(*) AS count"). Table("issue"). Find(&countsSlice); err != nil { - return nil, err + return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) } countMap := make(map[int64]int64, len(countsSlice)) @@ -1473,14 +1473,14 @@ func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]i sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSession(sess) + opts.setupSessionNoLimit(sess) accessCond := accessibleRepositoryCondition(user) if err := sess.Where(accessCond). Distinct("issue.repo_id"). Table("issue"). Find(&repoIDs); err != nil { - return nil, err + return nil, fmt.Errorf("unable to GetRepoIDsForIssuesOptions: %w", err) } return repoIDs, nil @@ -1491,17 +1491,16 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) { e := db.GetEngine(db.DefaultContext) sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSession(sess) + opts.setupSessionWithLimit(sess) sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID) issues := make([]*Issue, 0, opts.ListOptions.PageSize) if err := sess.Find(&issues); err != nil { - return nil, fmt.Errorf("Find: %v", err) + return nil, fmt.Errorf("unable to query Issues: %w", err) } - sess.Close() if err := IssueList(issues).LoadAttributes(); err != nil { - return nil, fmt.Errorf("LoadAttributes: %v", err) + return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) } return issues, nil @@ -1512,18 +1511,17 @@ func CountIssues(opts *IssuesOptions) (int64, error) { e := db.GetEngine(db.DefaultContext) countsSlice := make([]*struct { - RepoID int64 - Count int64 + Count int64 }, 0, 1) sess := e.Select("COUNT(issue.id) AS count").Table("issue") sess.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSession(sess) + opts.setupSessionNoLimit(sess) if err := sess.Find(&countsSlice); err != nil { - return 0, fmt.Errorf("Find: %v", err) + return 0, fmt.Errorf("unable to CountIssues: %w", err) } - if len(countsSlice) < 1 { - return 0, fmt.Errorf("there is less than one result sql record") + if len(countsSlice) != 1 { + return 0, fmt.Errorf("unable to get one row result when CountIssues, row count=%d", len(countsSlice)) } return countsSlice[0].Count, nil } @@ -1955,7 +1953,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment defer committer.Close() sess := db.GetEngine(ctx) - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return nil, false, fmt.Errorf("loadRepo: %v", err) } @@ -1982,7 +1980,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment OldTitle: currentIssue.Title, NewTitle: issue.Title, } - _, err := createComment(ctx, opts) + _, err := CreateCommentCtx(ctx, opts) if err != nil { return nil, false, fmt.Errorf("createComment: %v", err) } @@ -2014,7 +2012,7 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us defer committer.Close() // Update the deadline - if err = updateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { return err } @@ -2103,14 +2101,14 @@ func deleteIssue(ctx context.Context, issue *Issue) error { // delete all database data still assigned to this issue if err := deleteInIssue(e, issue.ID, - &issues.ContentHistory{}, + &issues_model.ContentHistory{}, &Comment{}, &IssueLabel{}, &IssueDependency{}, &IssueAssignees{}, &IssueUser{}, &Notification{}, - &issues.Reaction{}, + &issues_model.Reaction{}, &IssueWatch{}, &Stopwatch{}, &TrackedTime{}, @@ -2241,7 +2239,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u if len(mentions) == 0 { return } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return } @@ -2400,7 +2398,7 @@ func deleteIssuesByRepoID(sess db.Engine, repoID int64) (attachmentPaths []strin // Delete content histories if _, err = sess.In("issue_id", deleteCond). - Delete(&issues.ContentHistory{}); err != nil { + Delete(&issues_model.ContentHistory{}); err != nil { return } @@ -2428,7 +2426,7 @@ func deleteIssuesByRepoID(sess db.Engine, repoID int64) (attachmentPaths []strin } if _, err = sess.In("issue_id", deleteCond). - Delete(&issues.Reaction{}); err != nil { + Delete(&issues_model.Reaction{}); err != nil { return } diff --git a/models/issue_assignees.go b/models/issue_assignees.go index a91793f2f1..0f1f7b6576 100644 --- a/models/issue_assignees.go +++ b/models/issue_assignees.go @@ -120,7 +120,7 @@ func toggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.Use } // Repo infos - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return false, nil, fmt.Errorf("loadRepo: %v", err) } @@ -133,7 +133,7 @@ func toggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.Use AssigneeID: assigneeID, } // Comment - comment, err = createComment(ctx, opts) + comment, err = CreateCommentCtx(ctx, opts) if err != nil { return false, nil, fmt.Errorf("createComment: %v", err) } diff --git a/models/issue_comment.go b/models/issue_comment.go index 332ae7bdc5..39c2818eed 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -15,7 +15,7 @@ import ( "unicode/utf8" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/issues" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" @@ -209,8 +209,8 @@ type Comment struct { Project *project_model.Project `xorm:"-"` OldMilestoneID int64 MilestoneID int64 - OldMilestone *Milestone `xorm:"-"` - Milestone *Milestone `xorm:"-"` + OldMilestone *issues_model.Milestone `xorm:"-"` + Milestone *issues_model.Milestone `xorm:"-"` TimeID int64 Time *TrackedTime `xorm:"-"` AssigneeID int64 @@ -243,8 +243,8 @@ type Comment struct { // Reference issue in commit message CommitSHA string `xorm:"VARCHAR(40)"` - Attachments []*repo_model.Attachment `xorm:"-"` - Reactions issues.ReactionList `xorm:"-"` + Attachments []*repo_model.Attachment `xorm:"-"` + Reactions issues_model.ReactionList `xorm:"-"` // For view issue page. ShowRole RoleDescriptor `xorm:"-"` @@ -358,7 +358,7 @@ func (c *Comment) HTMLURL() string { log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } - err = c.Issue.loadRepo(db.DefaultContext) + err = c.Issue.LoadRepo(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" @@ -387,7 +387,7 @@ func (c *Comment) APIURL() string { log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } - err = c.Issue.loadRepo(db.DefaultContext) + err = c.Issue.LoadRepo(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" @@ -408,7 +408,7 @@ func (c *Comment) IssueURL() string { return "" } - err = c.Issue.loadRepo(db.DefaultContext) + err = c.Issue.LoadRepo(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" @@ -424,7 +424,7 @@ func (c *Comment) PRURL() string { return "" } - err = c.Issue.loadRepo(db.DefaultContext) + err = c.Issue.LoadRepo(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" @@ -495,7 +495,7 @@ func (c *Comment) LoadProject() error { // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone func (c *Comment) LoadMilestone() error { if c.OldMilestoneID > 0 { - var oldMilestone Milestone + var oldMilestone issues_model.Milestone has, err := db.GetEngine(db.DefaultContext).ID(c.OldMilestoneID).Get(&oldMilestone) if err != nil { return err @@ -505,7 +505,7 @@ func (c *Comment) LoadMilestone() error { } if c.MilestoneID > 0 { - var milestone Milestone + var milestone issues_model.Milestone has, err := db.GetEngine(db.DefaultContext).ID(c.MilestoneID).Get(&milestone) if err != nil { return err @@ -574,7 +574,7 @@ func (c *Comment) LoadAssigneeUserAndTeam() error { return err } - if err = c.Issue.LoadRepo(); err != nil { + if err = c.Issue.LoadRepo(db.DefaultContext); err != nil { return err } @@ -635,7 +635,7 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository if c.Reactions != nil { return nil } - c.Reactions, _, err = issues.FindReactions(ctx, issues.FindReactionsOptions{ + c.Reactions, _, err = issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{ IssueID: c.IssueID, CommentID: c.ID, }) @@ -717,7 +717,7 @@ func (c *Comment) CodeCommentURL() string { log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } - err = c.Issue.loadRepo(db.DefaultContext) + err = c.Issue.LoadRepo(db.DefaultContext) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" @@ -761,7 +761,8 @@ func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { return err } -func createComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { +// CreateCommentCtx creates comment with context +func CreateCommentCtx(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { e := db.GetEngine(ctx) var LabelID int64 if opts.Label != nil { @@ -863,7 +864,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } } // update the issue's updated_unix column - return updateIssueCols(ctx, opts.Issue, "updated_unix") + return UpdateIssueCols(ctx, opts.Issue, "updated_unix") } func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { @@ -884,7 +885,7 @@ func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Is content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02") } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return nil, err } @@ -895,7 +896,7 @@ func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Is Issue: issue, Content: content, } - comment, err := createComment(ctx, opts) + comment, err := CreateCommentCtx(ctx, opts) if err != nil { return nil, err } @@ -908,7 +909,7 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is if !add { cType = CommentTypeRemoveDependency } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return } @@ -920,7 +921,7 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is Issue: issue, DependentIssueID: dependentIssue.ID, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return } @@ -931,7 +932,7 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is Issue: dependentIssue, DependentIssueID: issue.ID, } - _, err = createComment(ctx, opts) + _, err = CreateCommentCtx(ctx, opts) return } @@ -981,7 +982,7 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { } defer committer.Close() - comment, err = createComment(ctx, opts) + comment, err = CreateCommentCtx(ctx, opts) if err != nil { return nil, err } @@ -1159,7 +1160,7 @@ func deleteComment(ctx context.Context, comment *Comment) error { return err } - if _, err := e.Delete(&issues.ContentHistory{ + if _, err := e.Delete(&issues_model.ContentHistory{ CommentID: comment.ID, }); err != nil { return err @@ -1178,7 +1179,7 @@ func deleteComment(ctx context.Context, comment *Comment) error { return err } - return issues.DeleteReaction(ctx, &issues.ReactionOptions{CommentID: comment.ID}) + return issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{CommentID: comment.ID}) } // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS @@ -1230,7 +1231,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return nil, err } diff --git a/models/issue_comment_list.go b/models/issue_comment_list.go index 2107f4790f..4133fc8761 100644 --- a/models/issue_comment_list.go +++ b/models/issue_comment_list.go @@ -8,6 +8,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + 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" @@ -139,7 +140,7 @@ func (comments CommentList) loadMilestones(e db.Engine) error { return nil } - milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { limit := defaultMaxInSize @@ -182,7 +183,7 @@ func (comments CommentList) loadOldMilestones(e db.Engine) error { return nil } - milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { limit := defaultMaxInSize @@ -387,7 +388,7 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { if comment.DependentIssue == nil { comment.DependentIssue = issues[comment.DependentIssueID] if comment.DependentIssue != nil { - if err := comment.DependentIssue.loadRepo(ctx); err != nil { + if err := comment.DependentIssue.LoadRepo(ctx); err != nil { return err } } diff --git a/models/issue_label.go b/models/issue_label.go index 453a0b14a9..016109e80f 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -586,7 +586,7 @@ func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m return err } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return } @@ -598,7 +598,7 @@ func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m Label: label, Content: "1", } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return err } @@ -618,7 +618,7 @@ func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error defer committer.Close() sess := db.GetEngine(ctx) - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } @@ -642,7 +642,7 @@ func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { e := db.GetEngine(ctx) - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } for _, label := range labels { @@ -691,7 +691,7 @@ func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use return nil } - if err = issue.loadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return } @@ -702,7 +702,7 @@ func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use Issue: issue, Label: label, } - if _, err = createComment(ctx, opts); err != nil { + if _, err = CreateCommentCtx(ctx, opts); err != nil { return err } diff --git a/models/issue_list.go b/models/issue_list.go index 8d7a3121b0..3116b49d8a 100644 --- a/models/issue_list.go +++ b/models/issue_list.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + 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" @@ -199,7 +200,7 @@ func (issues IssueList) loadMilestones(e db.Engine) error { return nil } - milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) + milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { limit := defaultMaxInSize diff --git a/models/issue_lock.go b/models/issue_lock.go index 20e94c7b21..a122f618d0 100644 --- a/models/issue_lock.go +++ b/models/issue_lock.go @@ -46,7 +46,7 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error { } defer committer.Close() - if err := updateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { + if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { return err } @@ -57,7 +57,7 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error { Type: commentType, Content: opts.Reason, } - if _, err := createComment(ctx, opt); err != nil { + if _, err := CreateCommentCtx(ctx, opt); err != nil { return err } diff --git a/models/issue_project.go b/models/issue_project.go index 526ac95152..0e993b39c5 100644 --- a/models/issue_project.go +++ b/models/issue_project.go @@ -129,12 +129,12 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U return err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } if oldProjectID > 0 || newProjectID > 0 { - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeProject, Doer: doer, Repo: issue.Repo, diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 3be9ad4e3f..80dd44642e 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -157,11 +157,11 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss return err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, @@ -178,7 +178,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss // CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { e := db.GetEngine(ctx) - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } @@ -208,11 +208,11 @@ func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss return err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, @@ -249,11 +249,11 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e return err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, diff --git a/models/issue_test.go b/models/issue_test.go index 71e0e8a2f7..7893df8a7f 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -132,7 +133,7 @@ func TestUpdateIssueCols(t *testing.T) { issue.Content = "This should have no effect" now := time.Now().Unix() - assert.NoError(t, updateIssueCols(db.DefaultContext, issue, "name")) + assert.NoError(t, UpdateIssueCols(db.DefaultContext, issue, "name")) then := time.Now().Unix() updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue) @@ -568,3 +569,23 @@ func TestIssueForeignReference(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, found.Index, issue.Index) } + +func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + miles := issues_model.MilestoneList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone), + } + + assert.NoError(t, miles.LoadTotalTrackedTimes()) + + assert.Equal(t, int64(3682), miles[0].TotalTrackedTime) +} + +func TestLoadTotalTrackedTime(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) + + assert.NoError(t, milestone.LoadTotalTrackedTime()) + + assert.Equal(t, int64(3682), milestone.TotalTrackedTime) +} diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go index 2d7bef19e1..e675c79193 100644 --- a/models/issue_tracked_time.go +++ b/models/issue_tracked_time.go @@ -53,7 +53,7 @@ func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { if err != nil { return } - err = t.Issue.loadRepo(ctx) + err = t.Issue.LoadRepo(ctx) if err != nil { return } @@ -170,11 +170,11 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim return nil, err } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return nil, err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Issue: issue, Repo: issue.Repo, Doer: user, @@ -254,10 +254,10 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { return ErrNotExist{} } - if err := issue.loadRepo(ctx); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Issue: issue, Repo: issue.Repo, Doer: user, @@ -286,7 +286,7 @@ func DeleteTime(t *TrackedTime) error { return err } - if _, err := createComment(ctx, &CreateCommentOptions{ + if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Issue: t.Issue, Repo: t.Issue.Repo, Doer: t.User, diff --git a/models/issue_xref.go b/models/issue_xref.go index 7b2f097c1c..405f1dae22 100644 --- a/models/issue_xref.go +++ b/models/issue_xref.go @@ -129,7 +129,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe RefAction: xref.Action, RefIsPull: ctx.OrigIssue.IsPull, } - _, err := createComment(stdCtx, opts) + _, err := CreateCommentCtx(stdCtx, opts) if err != nil { return err } @@ -150,7 +150,7 @@ func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferen for _, ref := range allrefs { if ref.Owner == "" && ref.Name == "" { // Issues in the same repository - if err := ctx.OrigIssue.loadRepo(stdCtx); err != nil { + if err := ctx.OrigIssue.LoadRepo(stdCtx); err != nil { return nil, err } refRepo = ctx.OrigIssue.Repo @@ -204,7 +204,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe if has, _ := e.Get(refIssue); !has { return nil, references.XRefActionNone, nil } - if err := refIssue.loadRepo(stdCtx); err != nil { + if err := refIssue.LoadRepo(stdCtx); err != nil { return nil, references.XRefActionNone, err } @@ -282,7 +282,7 @@ func (comment *Comment) LoadRefIssue() (err error) { } comment.RefIssue, err = GetIssueByID(comment.RefIssueID) if err == nil { - err = comment.RefIssue.loadRepo(db.DefaultContext) + err = comment.RefIssue.LoadRepo(db.DefaultContext) } return } diff --git a/models/issues/main_test.go b/models/issues/main_test.go index 1c786d005f..bec6176abd 100644 --- a/models/issues/main_test.go +++ b/models/issues/main_test.go @@ -22,5 +22,6 @@ func TestMain(m *testing.M) { "reaction.yml", "user.yml", "repository.yml", + "milestone.yml", ) } diff --git a/models/issue_milestone.go b/models/issues/milestone.go similarity index 90% rename from models/issue_milestone.go rename to models/issues/milestone.go index a321718513..07c38754d4 100644 --- a/models/issue_milestone.go +++ b/models/issues/milestone.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "context" @@ -12,7 +12,6 @@ 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/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -20,6 +19,26 @@ import ( "xorm.io/builder" ) +// ErrMilestoneNotExist represents a "MilestoneNotExist" kind of error. +type ErrMilestoneNotExist struct { + ID int64 + RepoID int64 + Name string +} + +// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist. +func IsErrMilestoneNotExist(err error) bool { + _, ok := err.(ErrMilestoneNotExist) + return ok +} + +func (err ErrMilestoneNotExist) Error() string { + if len(err.Name) > 0 { + return fmt.Sprintf("milestone does not exist [name: %s, repo_id: %d]", err.Name, err.RepoID) + } + return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID) +} + // Milestone represents a milestone of repository. type Milestone struct { ID int64 `xorm:"pk autoincr"` @@ -105,9 +124,10 @@ func NewMilestone(m *Milestone) (err error) { return committer.Commit() } -func getMilestoneByRepoID(e db.Engine, repoID, id int64) (*Milestone, error) { +// GetMilestoneByRepoID returns the milestone in a repository. +func GetMilestoneByRepoID(ctx context.Context, repoID, id int64) (*Milestone, error) { m := new(Milestone) - has, err := e.ID(id).Where("repo_id=?", repoID).Get(m) + has, err := db.GetEngine(ctx).ID(id).Where("repo_id=?", repoID).Get(m) if err != nil { return nil, err } else if !has { @@ -116,11 +136,6 @@ func getMilestoneByRepoID(e db.Engine, repoID, id int64) (*Milestone, error) { return m, nil } -// GetMilestoneByRepoID returns the milestone in a repository. -func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) { - return getMilestoneByRepoID(db.GetEngine(db.DefaultContext), repoID, id) -} - // GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo func GetMilestoneByRepoIDANDName(repoID int64, name string) (*Milestone, error) { var mile Milestone @@ -166,11 +181,11 @@ func updateMilestone(ctx context.Context, m *Milestone) error { if err != nil { return err } - return updateMilestoneCounters(ctx, m.ID) + return UpdateMilestoneCounters(ctx, m.ID) } -// updateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness -func updateMilestoneCounters(ctx context.Context, id int64) error { +// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness +func UpdateMilestoneCounters(ctx context.Context, id int64) error { e := db.GetEngine(ctx) _, err := e.ID(id). SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( @@ -250,65 +265,9 @@ func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) err return updateRepoMilestoneNum(ctx, m.RepoID) } -func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *Issue, oldMilestoneID int64) error { - if err := updateIssueCols(ctx, issue, "milestone_id"); err != nil { - return err - } - - if oldMilestoneID > 0 { - if err := updateMilestoneCounters(ctx, oldMilestoneID); err != nil { - return err - } - } - - if issue.MilestoneID > 0 { - if err := updateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return err - } - } - - if oldMilestoneID > 0 || issue.MilestoneID > 0 { - if err := issue.loadRepo(ctx); err != nil { - return err - } - - opts := &CreateCommentOptions{ - Type: CommentTypeMilestone, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldMilestoneID: oldMilestoneID, - MilestoneID: issue.MilestoneID, - } - if _, err := createComment(ctx, opts); err != nil { - return err - } - } - - return nil -} - -// ChangeMilestoneAssign changes assignment of milestone for issue. -func ChangeMilestoneAssign(issue *Issue, doer *user_model.User, oldMilestoneID int64) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = changeMilestoneAssign(ctx, doer, issue, oldMilestoneID); err != nil { - return err - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - return nil -} - // DeleteMilestoneByRepoID deletes a milestone from a repository. func DeleteMilestoneByRepoID(repoID, id int64) error { - m, err := GetMilestoneByRepoID(repoID, id) + m, err := GetMilestoneByRepoID(db.DefaultContext, repoID, id) if err != nil { if IsErrMilestoneNotExist(err) { return nil diff --git a/models/issue_milestone_test.go b/models/issues/milestone_test.go similarity index 67% rename from models/issue_milestone_test.go rename to models/issues/milestone_test.go index 6593f78fa1..09f51de45c 100644 --- a/models/issue_milestone_test.go +++ b/models/issues/milestone_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package issues import ( "sort" @@ -11,10 +11,8 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" "xorm.io/builder" @@ -25,28 +23,15 @@ func TestMilestone_State(t *testing.T) { assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State()) } -func TestNewMilestone(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := &Milestone{ - RepoID: 1, - Name: "milestoneName", - Content: "milestoneContent", - } - - assert.NoError(t, NewMilestone(milestone)) - unittest.AssertExistsAndLoadBean(t, milestone) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &Milestone{}) -} - func TestGetMilestoneByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - milestone, err := GetMilestoneByRepoID(1, 1) + milestone, err := GetMilestoneByRepoID(db.DefaultContext, 1, 1) assert.NoError(t, err) assert.EqualValues(t, 1, milestone.ID) assert.EqualValues(t, 1, milestone.RepoID) - _, err = GetMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID) + _, err = GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) assert.True(t, IsErrMilestoneNotExist(err)) } @@ -160,18 +145,6 @@ func TestGetMilestones(t *testing.T) { }) } -func TestUpdateMilestone(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - milestone := unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) - milestone.Name = " newMilestoneName " - milestone.Content = "newMilestoneContent" - assert.NoError(t, UpdateMilestone(milestone, milestone.IsClosed)) - milestone = unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) - assert.EqualValues(t, "newMilestoneName", milestone.Name) - unittest.CheckConsistencyFor(t, &Milestone{}) -} - func TestCountRepoMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { @@ -206,78 +179,6 @@ func TestCountRepoClosedMilestones(t *testing.T) { assert.EqualValues(t, 0, count) } -func TestChangeMilestoneStatus(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) - - assert.NoError(t, ChangeMilestoneStatus(milestone, true)) - unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=1") - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &Milestone{}) - - assert.NoError(t, ChangeMilestoneStatus(milestone, false)) - unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=0") - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &Milestone{}) -} - -func TestUpdateMilestoneCounters(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1}, - "is_closed=0").(*Issue) - - issue.IsClosed = true - issue.ClosedUnix = timeutil.TimeStampNow() - _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) - assert.NoError(t, err) - assert.NoError(t, updateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) - unittest.CheckConsistencyFor(t, &Milestone{}) - - issue.IsClosed = false - issue.ClosedUnix = 0 - _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) - assert.NoError(t, err) - assert.NoError(t, updateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) - unittest.CheckConsistencyFor(t, &Milestone{}) -} - -func TestChangeMilestoneAssign(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) - assert.NotNil(t, issue) - assert.NotNil(t, doer) - - oldMilestoneID := issue.MilestoneID - issue.MilestoneID = 2 - assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID)) - unittest.AssertExistsAndLoadBean(t, &Comment{ - IssueID: issue.ID, - Type: CommentTypeMilestone, - MilestoneID: issue.MilestoneID, - OldMilestoneID: oldMilestoneID, - }) - unittest.CheckConsistencyFor(t, &Milestone{}, &Issue{}) -} - -func TestDeleteMilestoneByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, DeleteMilestoneByRepoID(1, 1)) - unittest.AssertNotExistsBean(t, &Milestone{ID: 1}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1}) - - assert.NoError(t, DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID)) -} - -func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - miles := MilestoneList{ - unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone), - } - - assert.NoError(t, miles.LoadTotalTrackedTimes()) - - assert.Equal(t, int64(3682), miles[0].TotalTrackedTime) -} - func TestCountMilestonesByRepoIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) milestonesCount := func(repoID int64) (int, int) { @@ -343,15 +244,6 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { }) } -func TestLoadTotalTrackedTime(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - milestone := unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) - - assert.NoError(t, milestone.LoadTotalTrackedTime()) - - assert.Equal(t, int64(3682), milestone.TotalTrackedTime) -} - func TestGetMilestonesStats(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/main_test.go b/models/main_test.go index 08cb2386c0..00da59cbe6 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -7,6 +7,7 @@ package models import ( "testing" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -29,7 +30,7 @@ func TestFixturesAreConsistent(t *testing.T) { &repo_model.Repository{}, &Issue{}, &PullRequest{}, - &Milestone{}, + &issues_model.Milestone{}, &Label{}, &organization.Team{}, &Action{}) diff --git a/models/migrate.go b/models/migrate.go index 2470fd4336..7b12bc9c93 100644 --- a/models/migrate.go +++ b/models/migrate.go @@ -8,13 +8,14 @@ import ( "context" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/structs" "xorm.io/builder" ) // InsertMilestones creates milestones of repository. -func InsertMilestones(ms ...*Milestone) (err error) { +func InsertMilestones(ms ...*issues_model.Milestone) (err error) { if len(ms) == 0 { return nil } diff --git a/models/migrate_test.go b/models/migrate_test.go index 6da434d76a..ce28b3ca7c 100644 --- a/models/migrate_test.go +++ b/models/migrate_test.go @@ -8,6 +8,7 @@ import ( "strconv" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" @@ -22,7 +23,7 @@ func TestMigrate_InsertMilestones(t *testing.T) { reponame := "repo1" repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository) name := "milestonetest1" - ms := &Milestone{ + ms := &issues_model.Milestone{ RepoID: repo.ID, Name: name, } @@ -32,7 +33,7 @@ func TestMigrate_InsertMilestones(t *testing.T) { repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}).(*repo_model.Repository) assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) - unittest.CheckConsistencyFor(t, &Milestone{}) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } func assertCreateIssues(t *testing.T, isPull bool) { @@ -41,7 +42,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label) - milestone := unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone) assert.EqualValues(t, milestone.ID, 1) reaction := &issues_model.Reaction{ Type: "heart", @@ -90,7 +91,7 @@ func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { func TestMigrate_InsertIssueComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - _ = issue.LoadRepo() + _ = issue.LoadRepo(db.DefaultContext) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User) reaction := &issues_model.Reaction{ Type: "heart", diff --git a/models/notification.go b/models/notification.go index 9d0dc38aa4..a1248c240b 100644 --- a/models/notification.go +++ b/models/notification.go @@ -255,7 +255,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n } } - err = issue.loadRepo(ctx) + err = issue.LoadRepo(ctx) if err != nil { return err } diff --git a/models/packages/conan/references.go b/models/packages/conan/references.go index 4b7b201430..e47e689af7 100644 --- a/models/packages/conan/references.go +++ b/models/packages/conan/references.go @@ -65,14 +65,14 @@ func findPropertyValues(ctx context.Context, propertyName string, ownerID int64, in2 := builder. Select("package_file.id"). From("package_file"). - Join("INNER", "package_version", "package_version.id = package_file.version_id"). - Join("INNER", "package", "package.id = package_version.package_id"). + InnerJoin("package_version", "package_version.id = package_file.version_id"). + InnerJoin("package", "package.id = package_version.package_id"). Where(cond) query := builder. Select("package_property.value, MAX(package_file.created_unix) AS created_unix"). From("package_property"). - Join("INNER", "package_file", "package_file.id = package_property.ref_id"). + InnerJoin("package_file", "package_file.id = package_property.ref_id"). Where(builder.Eq{"package_property.name": propertyName}.And(builder.In("package_property.ref_id", in2))). GroupBy("package_property.value"). OrderBy("created_unix DESC") diff --git a/models/packages/conan/search.go b/models/packages/conan/search.go index c274a7ce02..6a2cfa38f5 100644 --- a/models/packages/conan/search.go +++ b/models/packages/conan/search.go @@ -74,8 +74,8 @@ func SearchRecipes(ctx context.Context, opts *RecipeSearchOptions) ([]string, er query := builder. Select("package.name, package_version.version, package_file.id"). From("package_file"). - Join("INNER", "package_version", "package_version.id = package_file.version_id"). - Join("INNER", "package", "package.id = package_version.package_id"). + InnerJoin("package_version", "package_version.id = package_file.version_id"). + InnerJoin("package", "package.id = package_version.package_id"). Where(cond) results := make([]struct { diff --git a/models/packages/package.go b/models/packages/package.go index 05170ab3f4..373bd86d9f 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -190,13 +190,15 @@ func GetPackagesByType(ctx context.Context, ownerID int64, packageType Type) ([] // DeletePackagesIfUnreferenced deletes a package if there are no associated versions func DeletePackagesIfUnreferenced(ctx context.Context) error { in := builder. - Select("package_version.package_id"). + Select("package.id"). From("package"). - Join("LEFT", "package_version", "package_version.package_id = package.id"). + LeftJoin("package_version", "package_version.package_id = package.id"). Where(builder.Expr("package_version.id IS NULL")) _, err := db.GetEngine(ctx). - Where(builder.In("package.id", in)). + // double select workaround for MySQL + // https://stackoverflow.com/questions/4471277/mysql-delete-from-with-subquery-as-condition + Where(builder.In("package.id", builder.Select("id").From(in, "temp"))). Delete(&Package{}) return err diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go index d9a8314c88..8c701d4285 100644 --- a/models/packages/package_blob.go +++ b/models/packages/package_blob.go @@ -67,7 +67,7 @@ func FindExpiredUnreferencedBlobs(ctx context.Context, olderThan time.Duration) pbs := make([]*PackageBlob, 0, 10) return pbs, db.GetEngine(ctx). Table("package_blob"). - Join("LEFT OUTER", "package_file", "package_file.blob_id = package_blob.id"). + Join("LEFT", "package_file", "package_file.blob_id = package_blob.id"). Where("package_file.id IS NULL AND package_blob.created_unix < ?", time.Now().Add(-olderThan).Unix()). Find(&pbs) } diff --git a/models/packages/package_file.go b/models/packages/package_file.go index df36467548..8f304ce8ac 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -147,7 +147,7 @@ func (opts *PackageFileSearchOptions) toConds() builder.Cond { in := builder. Select("package_version.id"). From("package_version"). - Join("INNER", "package", "package.id = package_version.package_id"). + InnerJoin("package", "package.id = package_version.package_id"). Where(versionCond) cond = cond.And(builder.In("package_file.version_id", in)) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index f7c6d4dc58..78e76c5054 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -12,16 +12,13 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -var ( - // ErrDuplicatePackageVersion indicates a duplicated package version error - ErrDuplicatePackageVersion = errors.New("Package version does exist already") - // ErrPackageVersionNotExist indicates a package version not exist error - ErrPackageVersionNotExist = errors.New("Package version does not exist") -) +// ErrDuplicatePackageVersion indicates a duplicated package version error +var ErrDuplicatePackageVersion = errors.New("Package version already exists") func init() { db.RegisterModel(new(PackageVersion)) @@ -99,75 +96,49 @@ func GetInternalVersionByNameAndVersion(ctx context.Context, ownerID int64, pack } func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string, isInternal bool) (*PackageVersion, error) { - var cond builder.Cond = builder.Eq{ - "package.owner_id": ownerID, - "package.type": packageType, - "package.lower_name": strings.ToLower(name), - "package_version.is_internal": isInternal, - } - pv := &PackageVersion{ - LowerVersion: strings.ToLower(version), - } - has, err := db.GetEngine(ctx). - Join("INNER", "package", "package.id = package_version.package_id"). - Where(cond). - Get(pv) + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + Name: SearchValue{ + ExactMatch: true, + Value: name, + }, + Version: SearchValue{ + ExactMatch: true, + Value: version, + }, + IsInternal: isInternal, + Paginator: db.NewAbsoluteListOptions(0, 1), + }) if err != nil { return nil, err } - if !has { + if len(pvs) == 0 { return nil, ErrPackageNotExist } - - return pv, nil + return pvs[0], nil } // GetVersionsByPackageType gets all versions of a specific type func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) { - var cond builder.Cond = builder.Eq{ - "package.owner_id": ownerID, - "package.type": packageType, - "package_version.is_internal": false, - } - - pvs := make([]*PackageVersion, 0, 10) - return pvs, db.GetEngine(ctx). - Where(cond). - Join("INNER", "package", "package.id = package_version.package_id"). - Find(&pvs) + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + }) + return pvs, err } // GetVersionsByPackageName gets all versions of a specific package func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Type, name string) ([]*PackageVersion, error) { - var cond builder.Cond = builder.Eq{ - "package.owner_id": ownerID, - "package.type": packageType, - "package.lower_name": strings.ToLower(name), - "package_version.is_internal": false, - } - - pvs := make([]*PackageVersion, 0, 10) - return pvs, db.GetEngine(ctx). - Where(cond). - Join("INNER", "package", "package.id = package_version.package_id"). - Find(&pvs) -} - -// GetVersionsByFilename gets all versions which are linked to a filename -func GetVersionsByFilename(ctx context.Context, ownerID int64, packageType Type, filename string) ([]*PackageVersion, error) { - var cond builder.Cond = builder.Eq{ - "package.owner_id": ownerID, - "package.type": packageType, - "package_file.lower_name": strings.ToLower(filename), - "package_version.is_internal": false, - } - - pvs := make([]*PackageVersion, 0, 10) - return pvs, db.GetEngine(ctx). - Where(cond). - Join("INNER", "package_file", "package_file.version_id = package_version.id"). - Join("INNER", "package", "package.id = package_version.package_id"). - Find(&pvs) + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + Name: SearchValue{ + ExactMatch: true, + Value: name, + }, + }) + return pvs, err } // DeleteVersionByID deletes a version by id @@ -183,21 +154,32 @@ func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error }) } +// SearchValue describes a value to search +// If ExactMatch is true, the field must match the value otherwise a LIKE search is performed. +type SearchValue struct { + Value string + ExactMatch bool +} + // PackageSearchOptions are options for SearchXXX methods +// Besides IsInternal are all fields optional and are not used if they have their default value (nil, "", 0) type PackageSearchOptions struct { - OwnerID int64 - RepoID int64 - Type string - PackageID int64 - QueryName string - QueryVersion string - Properties map[string]string - Sort string + OwnerID int64 + RepoID int64 + Type Type + PackageID int64 + Name SearchValue // only results with the specific name are found + Version SearchValue // only results with the specific version are found + Properties map[string]string // only results are found which contain all listed version properties with the specific value + IsInternal bool + HasFileWithName string // only results are found which are associated with a file with the specific name + HasFiles util.OptionalBool // only results are found which have associated files + Sort string db.Paginator } func (opts *PackageSearchOptions) toConds() builder.Cond { - var cond builder.Cond = builder.Eq{"package_version.is_internal": false} + var cond builder.Cond = builder.Eq{"package_version.is_internal": opts.IsInternal} if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID}) @@ -211,11 +193,19 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { if opts.PackageID != 0 { cond = cond.And(builder.Eq{"package.id": opts.PackageID}) } - if opts.QueryName != "" { - cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.QueryName)}) + if opts.Name.Value != "" { + if opts.Name.ExactMatch { + cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)}) + } else { + cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)}) + } } - if opts.QueryVersion != "" { - cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.QueryVersion)}) + if opts.Version.Value != "" { + if opts.Version.ExactMatch { + cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Version.Value)}) + } else { + cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.Version.Value)}) + } } if len(opts.Properties) != 0 { @@ -238,6 +228,22 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { }) } + if opts.HasFileWithName != "" { + fileCond := builder.Expr("package_file.version_id = package_version.id").And(builder.Eq{"package_file.lower_name": strings.ToLower(opts.HasFileWithName)}) + + cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond))) + } + + if !opts.HasFiles.IsNone() { + var filesCond builder.Cond = builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id"))) + + if opts.HasFiles.IsFalse() { + filesCond = builder.Not{filesCond} + } + + cond = cond.And(filesCond) + } + return cond } @@ -297,20 +303,3 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P count, err := sess.FindAndCount(&pvs) return pvs, count, err } - -// FindVersionsByPropertyNameAndValue gets all package versions which are associated with a specific property + value -func FindVersionsByPropertyNameAndValue(ctx context.Context, packageID int64, name, value string) ([]*PackageVersion, error) { - var cond builder.Cond = builder.Eq{ - "package_property.ref_type": PropertyTypeVersion, - "package_property.name": name, - "package_property.value": value, - "package_version.package_id": packageID, - "package_version.is_internal": false, - } - - pvs := make([]*PackageVersion, 0, 5) - return pvs, db.GetEngine(ctx). - Where(cond). - Join("INNER", "package_property", "package_property.ref_id = package_version.id"). - Find(&pvs) -} diff --git a/models/pull.go b/models/pull.go index 6abd9f04b2..439005deb4 100644 --- a/models/pull.go +++ b/models/pull.go @@ -407,7 +407,7 @@ func (pr *PullRequest) SetMerged() (bool, error) { return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) } - if err := pr.Issue.loadRepo(ctx); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { return false, err } diff --git a/models/repo.go b/models/repo.go index 628a102ee2..5073d1ceb9 100644 --- a/models/repo.go +++ b/models/repo.go @@ -19,6 +19,7 @@ import ( admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" project_model "code.gitea.io/gitea/models/project" @@ -698,7 +699,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &webhook.HookTask{RepoID: repoID}, &LFSLock{RepoID: repoID}, &repo_model.LanguageStat{RepoID: repoID}, - &Milestone{RepoID: repoID}, + &issues_model.Milestone{RepoID: repoID}, &repo_model.Mirror{RepoID: repoID}, &Notification{RepoID: repoID}, &ProtectedBranch{RepoID: repoID}, @@ -945,10 +946,6 @@ func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error { var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)" -func milestoneStatsCorrectNumIssues(ctx context.Context, id int64) error { - return updateMilestoneCounters(ctx, id) -} - func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { e := db.GetEngine(ctx) results, err := e.Query(milestoneStatsQueryNumIssues+" AND `milestone`.repo_id = ?", true, id) @@ -957,7 +954,7 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { } for _, result := range results { id, _ := strconv.ParseInt(string(result["id"]), 10, 64) - err = milestoneStatsCorrectNumIssues(ctx, id) + err = issues_model.UpdateMilestoneCounters(ctx, id) if err != nil { return err } @@ -1049,7 +1046,7 @@ func CheckRepoStats(ctx context.Context) error { // Milestone.Num{,Closed}Issues { statsQuery(milestoneStatsQueryNumIssues, true), - milestoneStatsCorrectNumIssues, + issues_model.UpdateMilestoneCounters, "milestone count 'num_closed_issues' and 'num_issues'", }, // User.NumRepos diff --git a/models/review.go b/models/review.go index f360d459cf..51818bc722 100644 --- a/models/review.go +++ b/models/review.go @@ -427,7 +427,7 @@ func SubmitReview(doer *user_model.User, issue *Issue, reviewType ReviewType, co } } - comm, err := createComment(ctx, &CreateCommentOptions{ + comm, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeReview, Doer: doer, Content: review.Content, @@ -662,7 +662,7 @@ func AddReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, return nil, err } - comment, err := createComment(ctx, &CreateCommentOptions{ + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeReviewRequest, Doer: doer, Repo: issue.Repo, @@ -717,7 +717,7 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen } } - comment, err := createComment(ctx, &CreateCommentOptions{ + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeReviewRequest, Doer: doer, Repo: issue.Repo, @@ -776,7 +776,7 @@ func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_ } } - comment, err := createComment(ctx, &CreateCommentOptions{ + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeReviewRequest, Doer: doer, Repo: issue.Repo, @@ -786,7 +786,7 @@ func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_ ReviewID: review.ID, }) if err != nil { - return nil, fmt.Errorf("createComment(): %v", err) + return nil, fmt.Errorf("CreateCommentCtx(): %v", err) } return comment, committer.Commit() @@ -837,7 +837,7 @@ func RemoveTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *us return nil, committer.Commit() } - comment, err := createComment(ctx, &CreateCommentOptions{ + comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ Type: CommentTypeReviewRequest, Doer: doer, Repo: issue.Repo, @@ -846,7 +846,7 @@ func RemoveTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *us AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID }) if err != nil { - return nil, fmt.Errorf("createComment(): %v", err) + return nil, fmt.Errorf("CreateCommentCtx(): %v", err) } return comment, committer.Commit() @@ -887,7 +887,7 @@ func CanMarkConversation(issue *Issue, doer *user_model.User) (permResult bool, } if doer.ID != issue.PosterID { - if err = issue.LoadRepo(); err != nil { + if err = issue.LoadRepo(db.DefaultContext); err != nil { return false, err } diff --git a/models/statistic.go b/models/statistic.go index a63cc2878b..87c1bd6d75 100644 --- a/models/statistic.go +++ b/models/statistic.go @@ -8,6 +8,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" @@ -102,7 +103,7 @@ func GetStatistic() (stats Statistic) { stats.Counter.Release, _ = e.Count(new(Release)) stats.Counter.AuthSource = auth.CountSources() stats.Counter.Webhook, _ = e.Count(new(webhook.Webhook)) - stats.Counter.Milestone, _ = e.Count(new(Milestone)) + stats.Counter.Milestone, _ = e.Count(new(issues_model.Milestone)) stats.Counter.Label, _ = e.Count(new(Label)) stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask)) stats.Counter.Team, _ = e.Count(new(organization.Team)) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index b6924d4706..4f1bd63f29 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -69,6 +69,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string, fixtureFiles ...string) { setting.SSH.Port = 3000 setting.SSH.Domain = "try.gitea.io" setting.Database.UseSQLite3 = true + setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos") if err != nil { fatalTestError("TempDir: %v\n", err) diff --git a/modules/context/api.go b/modules/context/api.go index ae516503e4..e5c2eeda0a 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -8,7 +8,6 @@ package context import ( "context" "fmt" - "html" "net/http" "net/url" "strings" @@ -20,8 +19,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" auth_service "code.gitea.io/gitea/services/auth" - - "gitea.com/go-chi/session" ) // APIContext is a specific context for API service @@ -191,33 +188,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { } } -// SetTotalCountHeader set "X-Total-Count" header -func (ctx *APIContext) SetTotalCountHeader(total int64) { - ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) - ctx.AppendAccessControlExposeHeaders("X-Total-Count") -} - -// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header -func (ctx *APIContext) AppendAccessControlExposeHeaders(names ...string) { - val := ctx.RespHeader().Get("Access-Control-Expose-Headers") - if len(val) != 0 { - ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) - } else { - ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) - } -} - -// RequireCSRF requires a validated a CSRF token -func (ctx *APIContext) RequireCSRF() { - headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName()) - formValueToken := ctx.Req.FormValue(ctx.csrf.GetFormName()) - if len(headerToken) > 0 || len(formValueToken) > 0 { - Validate(ctx.Context, ctx.csrf) - } else { - ctx.Context.Error(http.StatusUnauthorized, "Missing CSRF token.") - } -} - // CheckForOTP validates OTP func (ctx *APIContext) CheckForOTP() { if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) { @@ -269,17 +239,14 @@ func APIAuth(authMethod auth_service.Method) func(*APIContext) { // APIContexter returns apicontext as middleware func APIContexter() func(http.Handler) http.Handler { - csrfOpts := getCsrfOpts() - return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { locale := middleware.Locale(w, req) ctx := APIContext{ Context: &Context{ - Resp: NewResponse(w), - Data: map[string]interface{}{}, - Locale: locale, - Session: session.GetSession(req), + Resp: NewResponse(w), + Data: map[string]interface{}{}, + Locale: locale, Repo: &Repository{ PullRequest: &PullRequest{}, }, @@ -289,7 +256,6 @@ func APIContexter() func(http.Handler) http.Handler { } ctx.Req = WithAPIContext(WithContext(req, ctx.Context), &ctx) - ctx.csrf = Csrfer(csrfOpts, ctx.Context) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { @@ -301,7 +267,6 @@ func APIContexter() func(http.Handler) http.Handler { ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) - ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken()) ctx.Data["Context"] = &ctx next.ServeHTTP(ctx.Resp, ctx.Req) diff --git a/modules/context/auth.go b/modules/context/auth.go index 1a46ab586a..09c2295455 100644 --- a/modules/context/auth.go +++ b/modules/context/auth.go @@ -63,7 +63,7 @@ func Toggle(options *ToggleOptions) func(ctx *Context) { } if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" { - Validate(ctx, ctx.csrf) + ctx.csrf.Validate(ctx) if ctx.Written() { return } diff --git a/modules/context/context.go b/modules/context/context.go index f73b5f19c0..17e1673a36 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "html" "html/template" "io" @@ -21,6 +22,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -57,7 +59,7 @@ type Context struct { Render Render translation.Locale Cache cache.Cache - csrf CSRF + csrf CSRFProtector Flash *middleware.Flash Session session.Store @@ -577,6 +579,22 @@ func (ctx *Context) Value(key interface{}) interface{} { return ctx.Req.Context().Value(key) } +// SetTotalCountHeader set "X-Total-Count" header +func (ctx *Context) SetTotalCountHeader(total int64) { + ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) + ctx.AppendAccessControlExposeHeaders("X-Total-Count") +} + +// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header +func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) { + val := ctx.RespHeader().Get("Access-Control-Expose-Headers") + if len(val) != 0 { + ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) + } else { + ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) + } +} + // Handler represents a custom handler type Handler func(*Context) @@ -648,7 +666,9 @@ func Auth(authMethod auth.Method) func(*Context) { func Contexter() func(next http.Handler) http.Handler { rnd := templates.HTMLRenderer() csrfOpts := getCsrfOpts() - + if !setting.IsProd { + CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose + } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { locale := middleware.Locale(resp, req) @@ -679,7 +699,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Data["Context"] = &ctx ctx.Req = WithContext(req, &ctx) - ctx.csrf = Csrfer(csrfOpts, &ctx) + ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx) // Get flash. flashCookie := ctx.GetCookie("macaron_flash") @@ -737,7 +757,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) - ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken()) + ctx.Data["CsrfToken"] = ctx.csrf.GetToken() ctx.Data["CsrfTokenHtml"] = template.HTML(``) // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these @@ -780,3 +800,21 @@ func Contexter() func(next http.Handler) http.Handler { }) } } + +// SearchOrderByMap represents all possible search order +var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ + "asc": { + "alpha": db.SearchOrderByAlphabetically, + "created": db.SearchOrderByOldest, + "updated": db.SearchOrderByLeastUpdated, + "size": db.SearchOrderBySize, + "id": db.SearchOrderByID, + }, + "desc": { + "alpha": db.SearchOrderByAlphabeticallyReverse, + "created": db.SearchOrderByNewest, + "updated": db.SearchOrderByRecentUpdated, + "size": db.SearchOrderBySizeReverse, + "id": db.SearchOrderByIDReverse, + }, +} diff --git a/modules/context/csrf.go b/modules/context/csrf.go index 4fc9270504..df775048cb 100644 --- a/modules/context/csrf.go +++ b/modules/context/csrf.go @@ -31,29 +31,19 @@ import ( "code.gitea.io/gitea/modules/web/middleware" ) -// CSRF represents a CSRF service and is used to get the current token and validate a suspect token. -type CSRF interface { - // Return HTTP header to search for token. +// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. +type CSRFProtector interface { + // GetHeaderName returns HTTP header to search for token. GetHeaderName() string - // Return form value to search for token. + // GetFormName returns form value to search for token. GetFormName() string - // Return cookie name to search for token. - GetCookieName() string - // Return cookie path - GetCookiePath() string - // Return the flag value used for the csrf token. - GetCookieHTTPOnly() bool - // Return cookie domain - GetCookieDomain() string - // Return the token. + // GetToken returns the token. GetToken() string - // Validate by token. - ValidToken(t string) bool - // Error replies to the request with a custom function when ValidToken fails. - Error(w http.ResponseWriter) + // Validate validates the token in http context. + Validate(ctx *Context) } -type csrf struct { +type csrfProtector struct { // Header name value for setting and getting csrf token. Header string // Form name value for setting and getting csrf token. @@ -72,56 +62,24 @@ type csrf struct { ID string // Secret used along with the unique id above to generate the Token. Secret string - // ErrorFunc is the custom function that replies to the request when ValidToken fails. - ErrorFunc func(w http.ResponseWriter) } // GetHeaderName returns the name of the HTTP header for csrf token. -func (c *csrf) GetHeaderName() string { +func (c *csrfProtector) GetHeaderName() string { return c.Header } // GetFormName returns the name of the form value for csrf token. -func (c *csrf) GetFormName() string { +func (c *csrfProtector) GetFormName() string { return c.Form } -// GetCookieName returns the name of the cookie for csrf token. -func (c *csrf) GetCookieName() string { - return c.Cookie -} - -// GetCookiePath returns the path of the cookie for csrf token. -func (c *csrf) GetCookiePath() string { - return c.CookiePath -} - -// GetCookieHTTPOnly returns the flag value used for the csrf token. -func (c *csrf) GetCookieHTTPOnly() bool { - return c.CookieHTTPOnly -} - -// GetCookieDomain returns the flag value used for the csrf token. -func (c *csrf) GetCookieDomain() string { - return c.CookieDomain -} - // GetToken returns the current token. This is typically used // to populate a hidden form in an HTML template. -func (c *csrf) GetToken() string { +func (c *csrfProtector) GetToken() string { return c.Token } -// ValidToken validates the passed token against the existing Secret and ID. -func (c *csrf) ValidToken(t string) bool { - return ValidToken(t, c.Secret, c.ID, "POST") -} - -// Error replies to the request when ValidToken fails. -func (c *csrf) Error(w http.ResponseWriter) { - c.ErrorFunc(w) -} - // CsrfOptions maintains options to manage behavior of Generate. type CsrfOptions struct { // The global secret value used to generate Tokens. @@ -143,7 +101,7 @@ type CsrfOptions struct { SessionKey string // oldSessionKey saves old value corresponding to SessionKey. oldSessionKey string - // If true, send token via X-CSRFToken header. + // If true, send token via X-Csrf-Token header. SetHeader bool // If true, send token via _csrf cookie. SetCookie bool @@ -151,20 +109,12 @@ type CsrfOptions struct { Secure bool // Disallow Origin appear in request header. Origin bool - // The function called when Validate fails. - ErrorFunc func(w http.ResponseWriter) - // Cookie life time. Default is 0 + // Cookie lifetime. Default is 0 CookieLifeTime int } -func prepareOptions(options []CsrfOptions) CsrfOptions { - var opt CsrfOptions - if len(options) > 0 { - opt = options[0] - } - - // Defaults. - if len(opt.Secret) == 0 { +func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions { + if opt.Secret == "" { randBytes, err := util.CryptoRandomBytes(8) if err != nil { // this panic can be handled by the recover() in http handlers @@ -172,36 +122,30 @@ func prepareOptions(options []CsrfOptions) CsrfOptions { } opt.Secret = base32.StdEncoding.EncodeToString(randBytes) } - if len(opt.Header) == 0 { - opt.Header = "X-CSRFToken" + if opt.Header == "" { + opt.Header = "X-Csrf-Token" } - if len(opt.Form) == 0 { + if opt.Form == "" { opt.Form = "_csrf" } - if len(opt.Cookie) == 0 { + if opt.Cookie == "" { opt.Cookie = "_csrf" } - if len(opt.CookiePath) == 0 { + if opt.CookiePath == "" { opt.CookiePath = "/" } - if len(opt.SessionKey) == 0 { + if opt.SessionKey == "" { opt.SessionKey = "uid" } opt.oldSessionKey = "_old_" + opt.SessionKey - if opt.ErrorFunc == nil { - opt.ErrorFunc = func(w http.ResponseWriter) { - http.Error(w, "Invalid csrf token.", http.StatusBadRequest) - } - } - return opt } -// Csrfer maps CSRF to each request. If this request is a Get request, it will generate a new token. +// PrepareCSRFProtector returns a CSRFProtector to be used for every request. // Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie. -func Csrfer(opt CsrfOptions, ctx *Context) CSRF { - opt = prepareOptions([]CsrfOptions{opt}) - x := &csrf{ +func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector { + opt = prepareDefaultCsrfOptions(opt) + x := &csrfProtector{ Secret: opt.Secret, Header: opt.Header, Form: opt.Form, @@ -209,7 +153,6 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF { CookieDomain: opt.CookieDomain, CookiePath: opt.CookiePath, CookieHTTPOnly: opt.CookieHTTPOnly, - ErrorFunc: opt.ErrorFunc, } if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 { @@ -229,28 +172,31 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF { } } - needsNew := false oldUID := ctx.Session.Get(opt.oldSessionKey) - if oldUID == nil || oldUID.(string) != x.ID { - needsNew = true + uidChanged := oldUID == nil || oldUID.(string) != x.ID + cookieToken := ctx.GetCookie(opt.Cookie) + + needsNew := true + if uidChanged { _ = ctx.Session.Set(opt.oldSessionKey, x.ID) - } else { - // If cookie present, map existing token, else generate a new one. - if val := ctx.GetCookie(opt.Cookie); len(val) > 0 { - // FIXME: test coverage. - x.Token = val - } else { - needsNew = true + } else if cookieToken != "" { + // If cookie token presents, re-use existing unexpired token, else generate a new one. + if issueTime, ok := ParseCsrfToken(cookieToken); ok { + dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. + if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { + x.Token = cookieToken + needsNew = false + } } } if needsNew { // FIXME: actionId. - x.Token = GenerateToken(x.Secret, x.ID, "POST") + x.Token = GenerateCsrfToken(x.Secret, x.ID, "POST", time.Now()) if opt.SetCookie { var expires interface{} if opt.CookieLifeTime == 0 { - expires = time.Now().AddDate(0, 0, 1) + expires = time.Now().Add(CsrfTokenTimeout) } middleware.SetCookie(ctx.Resp, opt.Cookie, x.Token, opt.CookieLifeTime, @@ -270,47 +216,31 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF { return x } -// Validate should be used as a per route middleware. It attempts to get a token from a "X-CSRFToken" -// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated -// using ValidToken. If this validation fails, custom Error is sent in the reply. -// If neither a header or form value is found, http.StatusBadRequest is sent. -func Validate(ctx *Context, x CSRF) { - if token := ctx.Req.Header.Get(x.GetHeaderName()); len(token) > 0 { - if !x.ValidToken(token) { - // Delete the cookie - middleware.SetCookie(ctx.Resp, x.GetCookieName(), "", - -1, - x.GetCookiePath(), - x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too? - if middleware.IsAPIPath(ctx.Req) { - x.Error(ctx.Resp) - return - } +func (c *csrfProtector) validateToken(ctx *Context, token string) { + if !ValidCsrfToken(token, c.Secret, c.ID, "POST", time.Now()) { + middleware.DeleteCSRFCookie(ctx.Resp) + if middleware.IsAPIPath(ctx.Req) { + // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. + http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) + } else { ctx.Flash.Error(ctx.Tr("error.invalid_csrf")) ctx.Redirect(setting.AppSubURL + "/") } - return } - if token := ctx.Req.FormValue(x.GetFormName()); len(token) > 0 { - if !x.ValidToken(token) { - // Delete the cookie - middleware.SetCookie(ctx.Resp, x.GetCookieName(), "", - -1, - x.GetCookiePath(), - x.GetCookieDomain()) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too? - if middleware.IsAPIPath(ctx.Req) { - x.Error(ctx.Resp) - return - } - ctx.Flash.Error(ctx.Tr("error.invalid_csrf")) - ctx.Redirect(setting.AppSubURL + "/") - } - return - } - if middleware.IsAPIPath(ctx.Req) { - http.Error(ctx.Resp, "Bad Request: no CSRF token present", http.StatusBadRequest) - return - } - ctx.Flash.Error(ctx.Tr("error.missing_csrf")) - ctx.Redirect(setting.AppSubURL + "/") +} + +// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token" +// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated. +// If this validation fails, custom Error is sent in the reply. +// If neither a header nor form value is found, http.StatusBadRequest is sent. +func (c *csrfProtector) Validate(ctx *Context) { + if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" { + c.validateToken(ctx, token) + return + } + if token := ctx.Req.FormValue(c.GetFormName()); token != "" { + c.validateToken(ctx, token) + return + } + c.validateToken(ctx, "") // no csrf token, use an empty token to respond error } diff --git a/routers/api/v1/utils/utils.go b/modules/context/utils.go similarity index 66% rename from routers/api/v1/utils/utils.go rename to modules/context/utils.go index 7564857115..aea51cc5d6 100644 --- a/routers/api/v1/utils/utils.go +++ b/modules/context/utils.go @@ -2,20 +2,16 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package utils +package context import ( "net/url" "strings" "time" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/convert" ) // GetQueryBeforeSince return parsed time (unix format) from URL query's before and since -func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) { +func GetQueryBeforeSince(ctx *Context) (before, since int64, err error) { qCreatedBefore, err := prepareQueryArg(ctx, "before") if err != nil { return 0, 0, err @@ -53,16 +49,8 @@ func parseTime(value string) (int64, error) { } // prepareQueryArg unescape and trim a query arg -func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) { +func prepareQueryArg(ctx *Context, name string) (value string, err error) { value, err = url.PathUnescape(ctx.FormString(name)) value = strings.TrimSpace(value) return } - -// GetListOptions returns list options using the page and limit parameters -func GetListOptions(ctx *context.APIContext) db.ListOptions { - return db.ListOptions{ - Page: ctx.FormInt("page"), - PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), - } -} diff --git a/modules/context/xsrf.go b/modules/context/xsrf.go index 10e63a4180..e3ecc82f6d 100644 --- a/modules/context/xsrf.go +++ b/modules/context/xsrf.go @@ -28,69 +28,69 @@ import ( "time" ) -// Timeout represents the duration that XSRF tokens are valid. +// CsrfTokenTimeout represents the duration that XSRF tokens are valid. // It is exported so clients may set cookie timeouts that match generated tokens. -const Timeout = 24 * time.Hour +const CsrfTokenTimeout = 24 * time.Hour -// clean sanitizes a string for inclusion in a token by replacing all ":"s. -func clean(s string) string { - return strings.ReplaceAll(s, ":", "_") -} +// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout +var CsrfTokenRegenerationInterval = 10 * time.Minute -// GenerateToken returns a URL-safe secure XSRF token that expires in 24 hours. -// +var csrfTokenSep = []byte(":") + +// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours. // key is a secret key for your application. // userID is a unique identifier for the user. // actionID is the action the user is taking (e.g. POSTing to a particular path). -func GenerateToken(key, userID, actionID string) string { - return generateTokenAtTime(key, userID, actionID, time.Now()) -} - -// generateTokenAtTime is like Generate, but returns a token that expires 24 hours from now. -func generateTokenAtTime(key, userID, actionID string, now time.Time) string { +func GenerateCsrfToken(key, userID, actionID string, now time.Time) string { + nowUnixNano := now.UnixNano() + nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10) h := hmac.New(sha1.New, []byte(key)) - fmt.Fprintf(h, "%s:%s:%d", clean(userID), clean(actionID), now.UnixNano()) - tok := fmt.Sprintf("%s:%d", h.Sum(nil), now.UnixNano()) + h.Write([]byte(strings.ReplaceAll(userID, ":", "_"))) + h.Write(csrfTokenSep) + h.Write([]byte(strings.ReplaceAll(actionID, ":", "_"))) + h.Write(csrfTokenSep) + h.Write([]byte(nowUnixNanoStr)) + tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr) return base64.RawURLEncoding.EncodeToString([]byte(tok)) } -// ValidToken returns true if token is a valid, unexpired token returned by Generate. -func ValidToken(token, key, userID, actionID string) bool { - return validTokenAtTime(token, key, userID, actionID, time.Now()) -} - -// validTokenAtTime is like Valid, but it uses now to check if the token is expired. -func validTokenAtTime(token, key, userID, actionID string, now time.Time) bool { - // Decode the token. +func ParseCsrfToken(token string) (issueTime time.Time, ok bool) { data, err := base64.RawURLEncoding.DecodeString(token) if err != nil { - return false + return time.Time{}, false } - // Extract the issue time of the token. - sep := bytes.LastIndex(data, []byte{':'}) - if sep < 0 { - return false + pos := bytes.LastIndex(data, csrfTokenSep) + if pos == -1 { + return time.Time{}, false } - nanos, err := strconv.ParseInt(string(data[sep+1:]), 10, 64) + nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64) if err != nil { + return time.Time{}, false + } + return time.Unix(0, nanos), true +} + +// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate. +func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool { + issueTime, ok := ParseCsrfToken(token) + if !ok { return false } - issueTime := time.Unix(0, nanos) // Check that the token is not expired. - if now.Sub(issueTime) >= Timeout { + if now.Sub(issueTime) >= CsrfTokenTimeout { return false } // Check that the token is not from the future. - // Allow 1 minute grace period in case the token is being verified on a + // Allow 1-minute grace period in case the token is being verified on a // machine whose clock is behind the machine that issued the token. if issueTime.After(now.Add(1 * time.Minute)) { return false } - expected := generateTokenAtTime(key, userID, actionID, issueTime) + expected := GenerateCsrfToken(key, userID, actionID, issueTime) // Check that the token matches the expected value. // Use constant time comparison to avoid timing attacks. diff --git a/modules/context/xsrf_test.go b/modules/context/xsrf_test.go index c0c711bf07..ef42d61d5a 100644 --- a/modules/context/xsrf_test.go +++ b/modules/context/xsrf_test.go @@ -37,18 +37,18 @@ var ( func Test_ValidToken(t *testing.T) { t.Run("Validate token", func(t *testing.T) { - tok := generateTokenAtTime(key, userID, actionID, now) - assert.True(t, validTokenAtTime(tok, key, userID, actionID, oneMinuteFromNow)) - assert.True(t, validTokenAtTime(tok, key, userID, actionID, now.Add(Timeout-1*time.Nanosecond))) - assert.True(t, validTokenAtTime(tok, key, userID, actionID, now.Add(-1*time.Minute))) + tok := GenerateCsrfToken(key, userID, actionID, now) + assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow)) + assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond))) + assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute))) }) } // Test_SeparatorReplacement tests that separators are being correctly substituted func Test_SeparatorReplacement(t *testing.T) { t.Run("Test two separator replacements", func(t *testing.T) { - assert.NotEqual(t, generateTokenAtTime("foo:bar", "baz", "wah", now), - generateTokenAtTime("foo", "bar:baz", "wah", now)) + assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now), + GenerateCsrfToken("foo", "bar:baz", "wah", now)) }) } @@ -61,13 +61,13 @@ func Test_InvalidToken(t *testing.T) { {"Bad key", "foobar", userID, actionID, oneMinuteFromNow}, {"Bad userID", key, "foobar", actionID, oneMinuteFromNow}, {"Bad actionID", key, userID, "foobar", oneMinuteFromNow}, - {"Expired", key, userID, actionID, now.Add(Timeout)}, + {"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)}, {"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)}, } - tok := generateTokenAtTime(key, userID, actionID, now) + tok := GenerateCsrfToken(key, userID, actionID, now) for _, itt := range invalidTokenTests { - assert.False(t, validTokenAtTime(tok, itt.key, itt.userID, itt.actionID, itt.t)) + assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t)) } }) } @@ -84,7 +84,7 @@ func Test_ValidateBadData(t *testing.T) { } for _, bdt := range badDataTests { - assert.False(t, validTokenAtTime(bdt.tok, key, userID, actionID, oneMinuteFromNow)) + assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow)) } }) } diff --git a/modules/convert/issue.go b/modules/convert/issue.go index 4972b070a7..6cdb10f7da 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -29,7 +30,7 @@ func ToAPIIssue(issue *models.Issue) *api.Issue { if err := issue.LoadPoster(); err != nil { return &api.Issue{} } - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(db.DefaultContext); err != nil { return &api.Issue{} } if err := issue.Repo.GetOwner(db.DefaultContext); err != nil { @@ -214,7 +215,7 @@ func ToLabelList(labels []*models.Label, repo *repo_model.Repository, org *user_ } // ToAPIMilestone converts Milestone into API Format -func ToAPIMilestone(m *models.Milestone) *api.Milestone { +func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone { apiMilestone := &api.Milestone{ ID: m.ID, State: m.State(), diff --git a/modules/convert/issue_test.go b/modules/convert/issue_test.go index fb75413eab..b237c18f69 100644 --- a/modules/convert/issue_test.go +++ b/modules/convert/issue_test.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -32,7 +33,7 @@ func TestLabel_ToLabel(t *testing.T) { } func TestMilestone_APIFormat(t *testing.T) { - milestone := &models.Milestone{ + milestone := &issues_model.Milestone{ ID: 3, RepoID: 4, Name: "milestoneName", diff --git a/modules/convert/pull.go b/modules/convert/pull.go index cea1028e16..9c53afe8f3 100644 --- a/modules/convert/pull.go +++ b/modules/convert/pull.go @@ -27,7 +27,7 @@ func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_mo err error ) - if err = pr.Issue.LoadRepo(); err != nil { + if err = pr.Issue.LoadRepo(ctx); err != nil { log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err) return nil } diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index bab28db475..547498a9dc 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models" + "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/graceful" @@ -37,7 +38,7 @@ func (a *actionNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_mo log.Error("issue.LoadPoster: %v", err) return } - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(db.DefaultContext); err != nil { log.Error("issue.LoadRepo: %v", err) return } @@ -130,7 +131,7 @@ func (a *actionNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions log.Error("pull.LoadIssue: %v", err) return } - if err := pull.Issue.LoadRepo(); err != nil { + if err := pull.Issue.LoadRepo(db.DefaultContext); err != nil { log.Error("pull.Issue.LoadRepo: %v", err) return } diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index b74482fbf7..6636d18b99 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -161,7 +161,7 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *m log.Error("comment.LoadIssue: %v", err) return } - if err = comment.Issue.LoadRepo(); err != nil { + if err = comment.Issue.LoadRepo(ctx); err != nil { log.Error("comment.Issue.LoadRepo: %v", err) return } diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 94d4d180be..d24440d585 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" @@ -46,7 +47,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *m return } - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) return } @@ -282,7 +283,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue * } func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(db.DefaultContext); err != nil { log.Error("issue.LoadRepo: %v", err) return } @@ -311,7 +312,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest, mention log.Error("pull.LoadIssue: %v", err) return } - if err := pull.Issue.LoadRepo(); err != nil { + if err := pull.Issue.LoadRepo(ctx); err != nil { log.Error("pull.Issue.LoadRepo: %v", err) return } @@ -507,7 +508,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue * var err error - if err = issue.LoadRepo(); err != nil { + if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) return } @@ -634,7 +635,7 @@ func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *use return } - if err := pr.Issue.LoadRepo(); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { log.Error("pr.Issue.LoadRepo: %v", err) return } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index f4a2f4ad66..96b37f6980 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -154,7 +154,7 @@ var ( PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, - DefaultBranch: "master", + DefaultBranch: "main", // Repository editor settings Editor: struct { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index ed91382de3..5e317b39ea 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -106,6 +106,7 @@ var ( StaticCacheTime time.Duration EnableGzip bool LandingPageURL LandingPage + LandingPageCustom string UnixSocketPermission uint32 EnablePprof bool PprofDataPath string @@ -776,15 +777,19 @@ func loadFromConf(allowEmpty bool, extraConfig string) { PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath) } - switch sec.Key("LANDING_PAGE").MustString("home") { + landingPage := sec.Key("LANDING_PAGE").MustString("home") + switch landingPage { case "explore": LandingPageURL = LandingPageExplore case "organizations": LandingPageURL = LandingPageOrganizations case "login": LandingPageURL = LandingPageLogin - default: + case "": + case "home": LandingPageURL = LandingPageHome + default: + LandingPageURL = LandingPage(landingPage) } if len(SSH.Domain) == 0 { diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go index 80fe302137..b5904d6713 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -98,17 +98,6 @@ func DeleteRedirectToCookie(resp http.ResponseWriter) { SameSite(setting.SessionConfig.SameSite)) } -// DeleteSesionConfigPathCookie convenience function to delete SessionConfigPath cookies consistently -func DeleteSesionConfigPathCookie(resp http.ResponseWriter, name string) { - SetCookie(resp, name, "", - -1, - setting.SessionConfig.CookiePath, - setting.SessionConfig.Domain, - setting.SessionConfig.Secure, - true, - SameSite(setting.SessionConfig.SameSite)) -} - // DeleteCSRFCookie convenience function to delete SessionConfigPath cookies consistently func DeleteCSRFCookie(resp http.ResponseWriter) { SetCookie(resp, setting.CSRFCookieName, "", @@ -117,7 +106,7 @@ func DeleteCSRFCookie(resp http.ResponseWriter) { setting.SessionConfig.Domain) // FIXME: Do we need to set the Secure, httpOnly and SameSite values too? } -// SetCookie set the cookies +// SetCookie set the cookies. (name, value, lifetime, path, domain, secure, httponly, expires, {sameSite, ...}) // TODO: Copied from gitea.com/macaron/macaron and should be improved after macaron removed. func SetCookie(resp http.ResponseWriter, name, value string, others ...interface{}) { cookie := http.Cookie{} diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index a416aabc47..ccc792756d 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -653,7 +653,6 @@ ssh_invalid_token_signature=Der gegebene SSH-Schlüssel, Signatur oder Token sti ssh_token_required=Du musst eine Signatur für den Token unten angeben ssh_token=Token ssh_token_help=Du kannst eine Signatur wie folgt generieren: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /pfad_zu_deinem_oeffentlichen_Schluessel ssh_token_signature=SSH Textsignatur (armored signature) key_signature_ssh_placeholder=Beginnt mit „-----BEGIN PGP SIGNATURE-----“ verify_ssh_key_success=Der SSH-Key "%s" wurde verifiziert. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 4b906a290c..14d7c9fd1d 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -658,7 +658,6 @@ ssh_invalid_token_signature=Το παρεχόμενο κλειδί SSH, υπογ ssh_token_required=Πρέπει να δώσετε μια υπογραφή για το παρακάτω διακριτικό ssh_token=Διακριτικό ssh_token_help=Μπορείτε να δημιουργήσετε μια υπογραφή χρησιμοποιώντας: -ssh_token_code=echo -n "%s" "ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey ssh_token_signature=Θωρακισμένη υπογραφή SSH key_signature_ssh_placeholder=Ξεκινά με '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=Το SSH κλειδί '%s' επαληθεύτηκε. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 40d4c1c940..5662ed2c40 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -664,7 +664,6 @@ ssh_invalid_token_signature = The provided SSH key, signature or token do not ma ssh_token_required = You must provide a signature for the below token ssh_token = Token ssh_token_help = You can generate a signature using: -ssh_token_code = echo -n "%s" | ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey ssh_token_signature = Armored SSH signature key_signature_ssh_placeholder = Begins with '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success = SSH key '%s' has been verified. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 72f37480d4..d187c43a3d 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -656,7 +656,6 @@ ssh_invalid_token_signature=La clave SSH proporcionada, la firma o el token no c ssh_token_required=Debe proporcionar una firma para el token de abajo ssh_token=Token ssh_token_help=Puede generar una firma de la siguiente manera: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /ruta_a_su_clave_publico ssh_token_signature=Firma SSH armadura key_signature_ssh_placeholder=Comienza con '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=La clave SSH '%s' ha sido verificada. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 400af5a7f4..db05158ca1 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -488,7 +488,9 @@ auth_failed=認証に失敗しました: %v still_own_repo=あなたのアカウントは1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。 still_has_org=あなたのアカウントは1つ以上の組織に参加しています。 先にそれらから脱退してください。 +still_own_packages=あなたのアカウントは1つ以上のパッケージを所有しています。 先にそれらを削除してください。 org_still_own_repo=組織はまだ1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。 +org_still_own_packages=組織はまだ1つ以上のパッケージを所有しています。 先にそれらを削除してください。 target_branch_not_exist=ターゲットのブランチが存在していません。 @@ -662,7 +664,6 @@ ssh_invalid_token_signature=入力されたSSH 鍵、署名、トークンが合 ssh_token_required=以下のトークンの署名を入力する必要があります ssh_token=トークン ssh_token_help=署名はこの方法で生成できます: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey ssh_token_signature=Armor形式のSSH署名 key_signature_ssh_placeholder=先頭は '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=SSH 鍵 '%s' を確認しました。 @@ -1793,6 +1794,7 @@ settings.pulls.allow_manual_merge=プルリクエストを手動マージ済み settings.pulls.enable_autodetect_manual_merge=手動マージの自動検出を有効にする (注意: 特殊なケースでは判定ミスが発生する場合があります) settings.pulls.allow_rebase_update=リベースでプルリクエストのブランチの更新を可能にする settings.pulls.default_delete_branch_after_merge=デフォルトでプルリクエストのブランチをマージ後に削除する +settings.packages_desc=リポジトリパッケージレジストリを有効にする settings.projects_desc=リポジトリプロジェクトを有効にする settings.admin_settings=管理者用設定 settings.admin_enable_health_check=リポジトリのヘルスチェックを有効にする (git fsck) @@ -1950,6 +1952,8 @@ settings.event_pull_request_review=プルリクエストのレビュー settings.event_pull_request_review_desc=プルリクエストの承認・拒否、またはレビューコメントが付いたとき。 settings.event_pull_request_sync=プルリクエストの同期 settings.event_pull_request_sync_desc=プルリクエストが同期されたとき。 +settings.event_package=パッケージ +settings.event_package_desc=リポジトリにパッケージが作成または削除されたとき。 settings.branch_filter=ブランチ フィルター settings.branch_filter_desc=プッシュ、ブランチ作成、ブランチ削除のイベントを通知するブランチを、globパターンで指定するホワイトリストです。 空か*のときは、すべてのブランチのイベントを通知します。 文法については github.com/gobwas/glob を参照してください。 例: master{master,release*} settings.active=有効 @@ -2431,6 +2435,7 @@ dashboard.resync_all_hooks=すべてのリポジトリの pre-receive, update, p dashboard.reinit_missing_repos=レコードが存在するが見当たらないすべてのGitリポジトリを再初期化する dashboard.sync_external_users=外部ユーザーデータの同期 dashboard.cleanup_hook_task_table=hook_taskテーブルのクリーンアップ +dashboard.cleanup_packages=期限切れパッケージのクリーンアップ dashboard.server_uptime=サーバーの稼働時間 dashboard.current_goroutine=現在のGoroutine数 dashboard.current_memory_usage=現在のメモリ使用量 @@ -2500,6 +2505,7 @@ users.update_profile=ユーザーアカウントを更新 users.delete_account=ユーザーアカウントを削除 users.still_own_repo=このユーザーはまだ1つ以上のリポジトリを所有しています。 先にそれらのリポジトリを削除するか移転してください。 users.still_has_org=このユーザーは組織のメンバーになっています。 先に組織からこのユーザーを削除してください。 +users.still_own_packages=このユーザーはまだ1つ以上のパッケージを所有しています。最初にそれらのパッケージを削除してください。 users.deletion_success=ユーザーアカウントを削除しました。 users.reset_2fa=2要素認証をリセット users.list_status_filter.menu_text=フィルター @@ -2546,6 +2552,16 @@ repos.forks=フォーク repos.issues=イシュー repos.size=サイズ +packages.package_manage_panel=パッケージ管理 +packages.total_size=合計サイズ: %s +packages.owner=オーナー +packages.creator=作成者 +packages.name=名前 +packages.version=バージョン +packages.type=タイプ +packages.repository=リポジトリ +packages.size=サイズ +packages.published=配布 defaulthooks=デフォルトWebhook defaulthooks.desc=Webhookは、特定のGiteaイベントトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義されたWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくはwebhooks guideをご覧下さい。 @@ -2814,6 +2830,8 @@ monitor.next=次回 monitor.previous=前回 monitor.execute_times=実行回数 monitor.process=実行中のプロセス +monitor.stacktrace=スタックトレース +monitor.goroutines=%d 件のGoroutines monitor.desc=説明 monitor.start=開始日時 monitor.execute_time=実行時間 @@ -2985,4 +3003,91 @@ error.no_unit_allowed_repo=このリポジトリのどのセクションにも error.unit_not_allowed=このセクションへのアクセスが許可されていません。 [packages] +title=パッケージ +desc=リポジトリ パッケージを管理します。 +empty=パッケージはまだありません。 +empty.documentation=パッケージレジストリの詳細については、 ドキュメント を参照してください。 +filter.type=タイプ +filter.type.all=すべて +filter.no_result=フィルタの結果、空になりました。 +filter.container.tagged=タグあり +filter.container.untagged=タグなし +published_by=%[1]sに%[3]sが配布 +published_by_in=%[1]sに%[3]s%[5]sで配布 +installation=インストール方法 +about=このパッケージについて +requirements=要求事項 +dependencies=依存関係 +keywords=キーワード +details=詳細 +details.author=著作者 +details.project_site=プロジェクトサイト +details.license=ライセンス +assets=アセット +versions=バージョン +versions.on=on +versions.view_all=すべて表示 +dependency.id=ID +dependency.version=バージョン +composer.registry=あなたの ~/.composer/config.json ファイルに、このレジストリをセットアップします: +composer.install=Composer を使用してパッケージをインストールするには、次のコマンドを実行します: +composer.documentation=Composer レジストリの詳細については、 ドキュメント を参照してください。 +composer.dependencies=依存関係 +composer.dependencies.development=開発用依存関係 +conan.details.repository=リポジトリ +conan.registry=このレジストリをコマンドラインからセットアップします: +conan.install=Conan を使用してパッケージをインストールするには、次のコマンドを実行します: +conan.documentation=Conan レジストリの詳細については、 ドキュメント を参照してください。 +container.details.type=イメージタイプ +container.details.platform=プラットフォーム +container.details.repository_site=リポジトリサイト +container.details.documentation_site=ドキュメントサイト +container.pull=コマンドラインでイメージを取得します: +container.documentation=Container レジストリの詳細については、 ドキュメント を参照してください。 +container.multi_arch=OS / アーキテクチャ +container.layers=イメージレイヤー +container.labels=ラベル +container.labels.key=キー +container.labels.value=値 +generic.download=コマンドラインでパッケージをダウンロードします: +generic.documentation=汎用 レジストリの詳細については、ドキュメント を参照してください。 +maven.registry=あなたのプロジェクトの pom.xml ファイルに、このレジストリをセットアップします: +maven.install=パッケージを使用するため pom.xml ファイル内の dependencies ブロックに以下を含めます: +maven.install2=コマンドラインで実行します: +maven.download=依存関係をダウンロードするには、コマンドラインでこれを実行します: +maven.documentation=Mavenレジストリの詳細については、ドキュメント を参照してください。 +nuget.registry=このレジストリをコマンドラインからセットアップします: +nuget.install=NuGet を使用してパッケージをインストールするには、次のコマンドを実行します: +nuget.documentation=NuGetレジストリの詳細については、 ドキュメント を参照してください。 +nuget.dependency.framework=ターゲットフレームワーク +npm.registry=あなたのプロジェクトの .npmrc ファイルに、このレジストリをセットアップします: +npm.install=npm を使用してパッケージをインストールするには、次のコマンドを実行します: +npm.install2=または package.json ファイルに追加します: +npm.documentation=Npm レジストリの詳細については、 ドキュメント を参照してください。 +npm.dependencies=依存関係 +npm.dependencies.development=開発用依存関係 +npm.dependencies.peer=Peer依存関係 +npm.dependencies.optional=オプションの依存関係 +npm.details.tag=タグ +pypi.requires=必要なPython +pypi.install=pip を使用してパッケージをインストールするには、次のコマンドを実行します: +pypi.documentation=PyPI レジストリの詳細については、ドキュメント を参照してください。 +rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します: +rubygems.install2=または Gemfile に追加します: +rubygems.dependencies.runtime=実行用依存関係 +rubygems.dependencies.development=開発用依存関係 +rubygems.required.ruby=必要なRubyバージョン +rubygems.required.rubygems=必要なRubyGemバージョン +rubygems.documentation=RubyGemsレジストリの詳細については、ドキュメント を参照してください。 +settings.link=このパッケージをリポジトリにリンク +settings.link.description=パッケージをリポジトリにリンクすると、リポジトリのパッケージリストに表示されるようになります。 +settings.link.select=リポジトリを選択 +settings.link.button=リポジトリのリンクを更新 +settings.link.success=リポジトリのリンクが正常に更新されました。 +settings.link.error=リポジトリのリンクの更新に失敗しました。 +settings.delete=パッケージ削除 +settings.delete.description=パッケージの削除は恒久的で元に戻すことはできません。 +settings.delete.notice=%s (%s) を削除しようとしています。この操作は元に戻せません。よろしいですか? +settings.delete.success=パッケージを削除しました。 +settings.delete.error=パッケージの削除に失敗しました。 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 6304ebab35..16eb1866a5 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -644,7 +644,6 @@ ssh_invalid_token_signature=Norādītā SSH atslēga, paraksts un talons neatbil ssh_token_required=Jānorāda paraksts zemāk esošajam talonam ssh_token=Talons ssh_token_help=Parakstu ir iespējams uzģenerēt izmantojot komandu: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /ceļš/uz/atslēgu ssh_token_signature=Aizsargāts SSH paraksts key_signature_ssh_placeholder=Sākas ar '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=SSH atslēga '%s' veiksmīgi pārbaudīta. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 16a346ce68..cfdb177765 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -625,7 +625,6 @@ ssh_key_verify=Weryfikuj ssh_token_required=Musisz podać podpis poniższego tokenu ssh_token=Token ssh_token_help=Możesz wygenerować podpis używając: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /ścieżka_do_twojego_klucza_publicznego ssh_token_signature=Wzmocniony podpis SSH key_signature_ssh_placeholder=Zaczyna się od '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=Klucz SSH '%s' został zweryfikowany. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index eeb141850c..942b7ab96e 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -144,6 +144,9 @@ path=Caminho sqlite_helper=Caminho do arquivo do banco de dados SQLite3.
Informe um caminho absoluto se você executar o Gitea como um serviço. reinstall_error=Você está tentando instalar em um banco de dados existente do Gitea reinstall_confirm_message=Reinstalar com um banco de dados Gitea existente pode causar vários problemas. Na maioria dos casos, você deve usar seu "app.ini" existente para executar o Gitea. Se você sabe o que está fazendo, confirme o seguinte: +reinstall_confirm_check_1=Os dados criptografados pelo SECRET_KEY no app.ini poderão ser perdidos: os usuários podem não conseguir fazer login com 2FA/OTP & espelhos podem não funcionar corretamente. Ao marcar esta caixa você confirma que o atual arquivo app.ini contém o SECRET_KEY correto. +reinstall_confirm_check_2=Os repositórios e configurações podem precisar ser re-sincronizados. Marcando esta caixa você confirma que irá sincronizar novamente os hooks para os repositórios e o arquivo authorized_keys manualmente. Você confirma que irá garantir que as configurações de repositório e espelhamento estão corretas. +reinstall_confirm_check_3=Você confirma que este Gitea está realmente executando com a localização correta do app.ini e que você tem certeza de que precisa reinstalar. Você confirma que tomou conhecimento dos riscos acima descritos. err_empty_db_path=O caminho do banco de dados SQLite3 não pode ser em branco. no_admin_and_disable_registration=Você não pode desabilitar o auto-cadastro do usuário sem criar uma conta de administrador. err_empty_admin_password=A senha do administrador não pode ser em branco. @@ -243,6 +246,7 @@ view_home=Ver %s search_repos=Encontre um repositório… filter=Outros filtros filter_by_team_repositories=Filtrar por repositórios da equipe +feed_of=Feed de "%s" show_archived=Arquivado show_both_archived_unarchived=Mostrando arquivados e não arquivados @@ -374,6 +378,7 @@ issue.action.push_1=@%[1]s fez o push de %[3]d commit para %[2]s issue.action.push_n=@%[1]s fez o push de %[3]d commits para %[2]s issue.action.close=@%[1]s fechou #%[2]d. issue.action.reopen=@%[1]s reabriu #%[2]d. +issue.action.merge=@%[1]s aplicou o merge #%[2]d em %[3]s. issue.action.approve=@%[1]s aprovou este pull request. issue.action.reject=@%[1]s solicitou alterações neste pull request. issue.action.review=@%[1]s fez um comentário neste pull request. @@ -549,9 +554,12 @@ ui=Tema hidden_comment_types=Tipos de comentários ocultos comment_type_group_reference=Referência comment_type_group_label=Rótulo +comment_type_group_milestone=Marco comment_type_group_title=Título comment_type_group_branch=Branch comment_type_group_deadline=Prazo final +comment_type_group_dependency=Dependência +comment_type_group_lock=Status de Bloqueio comment_type_group_review_request=Revisar solicitação comment_type_group_pull_request_push=Commits adicionados comment_type_group_project=Projeto @@ -647,10 +655,10 @@ verify_gpg_key_success=A chave GPG '%s' foi validada. ssh_key_verified=Chave validada ssh_key_verified_long=A chave foi validada com um token e pode ser usada para validar commits que correspondam a qualquer dos endereços de e-mail ativados deste usuário. ssh_key_verify=Validar +ssh_invalid_token_signature=A chave, assinatura ou token SSH fornecidos não coincidem, ou então o token expirou. ssh_token_required=Você tem que fornecer uma assinatura para o token abaixo ssh_token=Token ssh_token_help=Você pode gerar uma assinatura usando: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /caminho_para_a_sua_chave_pública ssh_token_signature=Assinatura SSH blindada key_signature_ssh_placeholder=Começa com '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=A chave SSH '%s' foi validada. @@ -835,6 +843,7 @@ auto_init=Inicializar o repositório (adicionando .gitignore, licença e LEIA-ME trust_model_helper=Selecione o modelo de confiança para verificação de assinatura. As opções possíveis são: trust_model_helper_collaborator=Colaborador: Confiar em assinaturas de colaboradores trust_model_helper_committer=Committer: Confiar em assinaturas que correspondem aos committers +trust_model_helper_collaborator_committer=Colaborador+Committer: Confiar em assinaturas dos colaboradores que correspondem ao committer trust_model_helper_default=Padrão: Usar o modelo de confiança padrão para esta instalação create_repo=Criar repositório default_branch=Branch Padrão @@ -891,6 +900,7 @@ desc.archived=Arquivado template.items=Itens do modelo template.git_content=Conteúdo Git (Branch padrão) template.git_hooks=Hooks do Git +template.git_hooks_tooltip=Atualmente você não pode modificar ou remover os Git Hooks adicionados. Selecione isso apenas se você confia no repositório modelo. template.webhooks=Webhooks template.topics=Tópicos template.avatar=Avatar @@ -1022,6 +1032,7 @@ line_unicode=`Esta linha possui caracteres unicode ocultos` escape_control_characters=Escapar unescape_control_characters=Desescapar +file_copy_permalink=Copiar Link Permanente video_not_supported_in_browser=Seu navegador não suporta a tag 'video' do HTML5. audio_not_supported_in_browser=Seu navegador não suporta a tag 'audio' do HTML5. stored_lfs=Armazenado com Git LFS @@ -1127,6 +1138,7 @@ commit.revert-header=Reverter: %s commit.revert-content=Selecione a branch para reverter para: commit.cherry-pick=Cherry-pick commit.cherry-pick-header=Cherry-pick: %s +commit.cherry-pick-content=Selecione o branch para receber o cherry-pick: ext_issues=Acesso a Issues Externos ext_issues.desc=Link para o issue tracker externo. @@ -1271,6 +1283,11 @@ issues.action_milestone_no_select=Sem marco issues.action_assignee=Responsável issues.action_assignee_no_select=Sem responsável issues.opened_by=aberto por %[3]s %[1]s +pulls.merged_by=por %[3]s foi aplicado em %[1]s +pulls.merged_by_fake=por %[2]s foi aplicado %[1]s +issues.closed_by=por %[3]s foi fechada %[1]s +issues.opened_by_fake=%[1]s abertas por %[2]s +issues.closed_by_fake=por %[2]s foi fechada %[1]s issues.previous=Anterior issues.next=Próximo issues.open_title=Aberto @@ -1285,10 +1302,14 @@ issues.context.edit=Editar issues.context.delete=Excluir issues.no_content=Ainda não há conteúdo. issues.close_issue=Fechar +issues.pull_merged_at=`aplicou o merge do commit %[2]s em %[3]s %[4]s` +issues.manually_pull_merged_at=`aplicou o merge do commit %[2]s em %[3]s manualmente %[4]s` issues.close_comment_issue=Comentar e fechar issues.reopen_issue=Reabrir issues.reopen_comment_issue=Comentar e reabrir issues.create_comment=Comentar +issues.closed_at=`fechou esta issue %[2]s` +issues.reopened_at=`reabriu esta issue %[2]s` issues.commit_ref_at=`citou esta issue em um commit %[2]s` issues.ref_issue_from=`referenciado esta issue %[4]s %[2]s` issues.ref_pull_from=`referenciado este pull request %[4]s %[2]s` @@ -1399,6 +1420,8 @@ issues.dependency.remove=Remover issues.dependency.remove_info=Remover esta dependência issues.dependency.added_dependency=`adicionou uma nova dependência %s` issues.dependency.removed_dependency=`removeu uma dependência %s` +issues.dependency.pr_closing_blockedby=Fechamento deste pull request está bloqueado pelas seguintes issues +issues.dependency.issue_closing_blockedby=Fechamento desta issue está bloqueado pelas seguintes issues issues.dependency.issue_close_blocks=Esta issue bloqueia o fechamento das seguintes issues issues.dependency.pr_close_blocks=Este pull request bloqueia o fechamento das seguintes issues issues.dependency.issue_close_blocked=Você precisa fechar todas as issues que bloqueiam esta issue antes de poder fechá-la. @@ -1424,6 +1447,10 @@ issues.review.dismissed_label=Rejeitada issues.review.left_comment=deixou um comentário issues.review.content.empty=Você precisa deixar um comentário indicando as alterações solicitadas. issues.review.reject=alterações solicitadas %s +issues.review.wait=foi solicitada para revisão %s +issues.review.add_review_request=solicitou revisão de %s %s +issues.review.remove_review_request=removeu a solicitação de revisão para %s %s +issues.review.remove_review_request_self=recusou revisar %s issues.review.pending=Pendente issues.review.review=Revisão issues.review.reviewers=Revisores @@ -1433,6 +1460,7 @@ issues.review.hide_outdated=Ocultar desatualizado issues.review.show_resolved=Mostrar resolvidas issues.review.hide_resolved=Ocultar resolvidas issues.review.resolve_conversation=Resolver conversa +issues.review.un_resolve_conversation=Conversa não resolvida issues.review.resolved_by=marcou esta conversa como resolvida issues.assignee.error=Nem todos os responsáveis foram adicionados devido a um erro inesperado. issues.reference_issue.body=Conteúdo @@ -1450,7 +1478,7 @@ pulls.desc=Habilitar pull requests e revisões de código. pulls.new=Novo pull request pulls.view=Ver Pull Request pulls.compare_changes=Novo pull request -pulls.compare_changes_desc=Selecione a branch de destino (push) e a branch de origem (pull) para o merge. +pulls.compare_changes_desc=Selecione o branch de destino (push) e o branch de origem (pull) para o merge. pulls.compare_base=merge em pulls.compare_compare=pull de pulls.switch_comparison_type=Mudar tipo de comparação @@ -1472,6 +1500,7 @@ pulls.cant_reopen_deleted_branch=Este pull request não pode ser reaberto porque pulls.merged=Merge aplicado pulls.merged_as=O pull request teve merge aplicado como %[2]s. pulls.manually_merged=Merge aplicado manualmente +pulls.manually_merged_as=O pull request foi aplicado manualmente como %[2]s. pulls.is_closed=O pull request foi fechado. pulls.has_merged=O merge deste pull request foi aplicado. pulls.title_wip_desc=`Inicie o título com o prefixo %s para prevenir o merge do pull request até que o mesmo esteja pronto.` @@ -1482,6 +1511,7 @@ pulls.remove_prefix=Remover o prefixo %s pulls.data_broken=Este pull request está quebrado devido a falta de informação do fork. pulls.files_conflicted=Este pull request tem alterações conflitantes com o branch de destino. pulls.is_checking=Verificação de conflitos do merge está em andamento. Tente novamente em alguns momentos. +pulls.is_empty=Este branch é igual ao branch de destino. pulls.required_status_check_failed=Algumas verificações necessárias não foram bem sucedidas. pulls.required_status_check_missing=Estão faltando algumas verificações necessárias. pulls.required_status_check_administrator=Como administrador, você ainda pode aplicar o merge deste pull request. @@ -1510,17 +1540,24 @@ pulls.no_merge_wip=O merge deste pull request não pode ser aplicado porque est pulls.no_merge_not_ready=Este pull request não está pronto para ser realizado o merge, verifique o status da revisão e as verificações de status. pulls.no_merge_access=Você não está autorizado para realizar o merge deste pull request. pulls.merge_pull_request=Criar commit de merge +pulls.rebase_merge_pull_request=Rebase e fast-forward +pulls.rebase_merge_commit_pull_request=Rebase e criar commit de merge pulls.squash_merge_pull_request=Criar commit de squash pulls.merge_manually=Merge feito manualmente pulls.merge_commit_id=A ID de merge commit pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado pulls.invalid_merge_option=Você não pode usar esta opção de merge neste pull request. +pulls.merge_conflict=O merge falhou: Houve um conflito ao fazer merge. Dica: Tente uma estratégia diferente pulls.merge_conflict_summary=Mensagem de erro +pulls.rebase_conflict=O merge falhou: Houve um conflito durante o rebase do commit %[1]s. Dica: Tente uma estratégia diferente pulls.rebase_conflict_summary=Mensagem de Erro ; %[2]s
%[3]s
pulls.unrelated_histories=Merge falhou: O merge do principal e da base não compartilham uma história comum. Dica: Tente uma estratégia diferente pulls.merge_out_of_date=Merge falhou: durante a geração do merge, a base não foi atualizada. Dica: Tente novamente. +pulls.head_out_of_date=O merge falhou: Enquanto gerava o merge, a head foi atualizada. Dica: Tente novamente. +pulls.push_rejected=O merge falhou: O push foi rejeitado. Revise os Git Hooks para este repositório. pulls.push_rejected_summary=Mensagem completa da rejeição +pulls.push_rejected_no_message=O merge falhou: O push foi rejeitado mas não houve mensagem remota.
Revise os Git Hooks para este repositório pulls.open_unmerged_pull_exists=`Não é possível executar uma operação de reabertura pois há um pull request pendente (#%d) com propriedades idênticas.` pulls.status_checking=Algumas verificações estão pendentes pulls.status_checks_success=Todas as verificações foram bem sucedidas @@ -1529,13 +1566,16 @@ pulls.status_checks_failure=Algumas verificações falharam pulls.status_checks_error=Algumas verificações reportaram erros pulls.status_checks_requested=Obrigatário pulls.status_checks_details=Detalhes -pulls.update_branch_success=Atualização da branch foi bem-sucedida -pulls.update_not_allowed=Você não tem permissão para atualizar a branch -pulls.outdated_with_base_branch=Esta branch está desatualizado com a branch base +pulls.update_branch=Atualizar branch por merge +pulls.update_branch_rebase=Atualizar branch por rebase +pulls.update_branch_success=Atualização do branch foi bem-sucedida +pulls.update_not_allowed=Você não tem permissão para atualizar o branch +pulls.outdated_with_base_branch=Este branch está desatualizado com o branch base pulls.closed_at=`fechou este pull request %[2]s` pulls.reopened_at=`reabriu este pull request %[2]s` pulls.merge_instruction_hint=`Você também pode ver as instruções para a linha de comandos.` +pulls.merge_instruction_step1_desc=No repositório do seu projeto, crie um novo branch e teste as alterações. pulls.merge_instruction_step2_desc=Faça merge das alterações e atualize no Gitea. milestones.new=Novo marco @@ -1582,6 +1622,7 @@ signing.wont_sign.basesigned=O merge não será assinada porque o commit base n signing.wont_sign.headsigned=O merge não será assinado porque o commit principal não foi assinado signing.wont_sign.commitssigned=O merge não será assinado pois todos os commits associados não foram assinados signing.wont_sign.approved=O merge não será assinado pois o PR não foi aprovado +signing.wont_sign.not_signed_in=Você não está logado ext_wiki=Acesso a Wiki Externo ext_wiki.desc=Link para uma wiki externa. @@ -1662,9 +1703,9 @@ activity.git_stats_pushed_1=realizou push de activity.git_stats_pushed_n=realizaram push de activity.git_stats_commit_1=%d commit activity.git_stats_commit_n=%d commits -activity.git_stats_push_to_branch=para a %s e -activity.git_stats_push_to_all_branches=para todas as branches. -activity.git_stats_on_default_branch=Na %s, +activity.git_stats_push_to_branch=para o %s e +activity.git_stats_push_to_all_branches=para todos os branches. +activity.git_stats_on_default_branch=No %s, activity.git_stats_file_1=%d arquivo activity.git_stats_file_n=%d arquivos activity.git_stats_files_changed_1=foi modificado @@ -1702,6 +1743,9 @@ settings.mirror_settings.direction=Sentido settings.mirror_settings.direction.pull=Pull settings.mirror_settings.direction.push=Push settings.mirror_settings.last_update=Última atualização +settings.mirror_settings.push_mirror.none=Nenhum espelhamento de push configurado +settings.mirror_settings.push_mirror.remote_url=URL do repositório do Git remoto +settings.mirror_settings.push_mirror.add=Adicionar Espelho de Push settings.sync_mirror=Sincronizar agora settings.mirror_sync_in_progress=Sincronização do espelhamento está em andamento. Verifique novamente em um minuto. settings.email_notifications.enable=Habilitar notificações de e-mail @@ -1738,6 +1782,9 @@ settings.pulls.allow_merge_commits=Habilitar commit no merge settings.pulls.allow_rebase_merge=Habilitar Rebasing em commits via merge settings.pulls.allow_rebase_merge_commit=Habilitar Rebasing com commits explícitos no merge (--no-ff) settings.pulls.allow_squash_commits=Habilitar Squashing em commits via merge +settings.pulls.allow_manual_merge=Habilitar Marcar PR como aplicado manualmente +settings.pulls.enable_autodetect_manual_merge=Habilitar a detecção automática de merge manual (Nota: Em alguns casos especiais, podem ocorrer julgamentos errados) +settings.pulls.default_delete_branch_after_merge=Excluir o branch de pull request após o merge por padrão settings.packages_desc=Habilitar Registro de Pacotes de Repositório settings.projects_desc=Habilitar Projetos do Repositório settings.admin_settings=Configurações do administrador @@ -1780,8 +1827,16 @@ settings.transfer_succeed=O repositório foi transferido. settings.signing_settings=Configurações de Verificação de Assinatura settings.trust_model=Modelo de Confiança na Assinatura settings.trust_model.default=Modelo Padrão de Confiança +settings.trust_model.default.desc=Use o modelo de confiança de repositório padrão para esta instalação. settings.trust_model.collaborator=Colaborador settings.trust_model.collaborator.long=Colaborador: Confiar em assinaturas feitas por colaboradores +settings.trust_model.collaborator.desc=Assinaturas válidas dos colaboradores deste repositório serão marcadas como "confiáveis" - (quer correspondam ao autor do commit ou não). Caso contrário, assinaturas válidas serão marcadas como "não confiáveis" se a assinatura corresponder ao autor do submissão e "não corresponde" se não corresponder. +settings.trust_model.committer=Committer +settings.trust_model.committer.long=Committer: Confiar nas assinaturas que correspondam aos committers (isso corresponde ao GitHub e forçará commits assinados pelo Gitea a ter o Gitea como o committer) +settings.trust_model.committer.desc=Assinaturas válidas só serão marcadas como "confiáveis" se corresponderem ao committer, caso contrário serão marcadas como "não correspondidas". Isso forçará o Gitea a ser o commiter nos commits assinados, com o autor real marcado como Co-authored-by: e Co-commited-by: no final do commit. A chave padrão do Gitea tem que corresponder a um usuário no banco de dados. +settings.trust_model.collaboratorcommitter=Colaborador+Commiter +settings.trust_model.collaboratorcommitter.long=Colaborador+Committer: Confiar na assinatura dos colaboradores que correspondem ao autor do commit +settings.trust_model.collaboratorcommitter.desc=Assinaturas válidas dos colaboradores deste repositório serão marcadas como "confiáveis" se corresponderem ao autor do commit. Caso contrário, as assinaturas válidas serão marcadas como "não confiáveis" se a assinatura corresponder ao autor do commit e "não corresponde" caso contrário. Isso forçará o Gitea a ser marcado como o autor do commit nos commits assinados com o autor marcado como Co-Authored-By: e o Committed-By: resumo do commit. A chave padrão do Gitea tem que corresponder a um usuário no banco de dados. settings.wiki_delete=Excluir dados da wiki settings.wiki_delete_desc=A exclusão de dados da wiki é permanente e não pode ser desfeita. settings.wiki_delete_notices_1=- Isso excluirá e desabilitará permanentemente a wiki do repositório %s. @@ -1829,6 +1884,8 @@ settings.webhook.headers=Cabeçalhos settings.webhook.payload=Conteúdo settings.webhook.body=Corpo settings.webhook.replay.description=Executar novamente esse webhook. +settings.webhook.delivery.success=Um evento foi adicionado à fila de envio. Pode levar alguns segundos até que ele apareça no histórico de envio. +settings.githooks_desc=Hooks do Git são executados pelo próprio Git. Você pode editar arquivos de hook abaixo para configurar operações personalizadas. settings.githook_edit_desc=Se o hook não estiver ativo, o conteúdo de exemplo será apresentado. Deixar o conteúdo em branco irá desabilitar esse hook. settings.githook_name=Nome do Hook settings.githook_content=Conteúdo do Hook @@ -1853,6 +1910,7 @@ settings.event_create_desc=Branch ou tag criado. settings.event_delete=Excluir settings.event_delete_desc=Branch ou tag deletado. settings.event_fork=Fork +settings.event_fork_desc=Feito fork do repositório. settings.event_release=Versão settings.event_release_desc=Versão publicada, atualizada ou excluída em um repositório. settings.event_push=Push @@ -1863,14 +1921,31 @@ settings.event_header_issue=Eventos da Issue settings.event_issues=Issues settings.event_issues_desc=Issue aberta, fechada, reaberta ou editada. settings.event_issue_assign=Issue Atribuída +settings.event_issue_assign_desc=Issue atribuída ou não atribuída. settings.event_issue_label=Issue Rotulada settings.event_issue_label_desc=Rótulos da issue atualizados ou removidos. +settings.event_issue_milestone=Marco Atribuído à Issue +settings.event_issue_milestone_desc=Marco atribuído ou desatribuído à Issue. settings.event_issue_comment=Comentário da issue settings.event_issue_comment_desc=Comentário da issue criado, editado ou excluído. settings.event_header_pull_request=Eventos de Pull Request settings.event_pull_request=Pull request +settings.event_pull_request_desc=Pull request aberto, fechado, reaberto ou editado. +settings.event_pull_request_assign=Pull Request Atribuído +settings.event_pull_request_assign_desc=Pull request atribuído ou desatribuído. +settings.event_pull_request_label=Pull Request Rotulado +settings.event_pull_request_label_desc=Rótulos do pull request atualizados ou limpos. +settings.event_pull_request_milestone=Marco Atribuído ao Pull Request +settings.event_pull_request_milestone_desc=Marco atribuído ou desatribuído ao pull request. +settings.event_pull_request_comment=Comentário no Pull Request +settings.event_pull_request_comment_desc=Comentário criado, editado ou excluído no pull request. +settings.event_pull_request_review=Pull Request Revisado +settings.event_pull_request_review_desc=Pull request aprovado, rejeitado ou revisão comentada. +settings.event_pull_request_sync=Pull Request Sincronizado +settings.event_pull_request_sync_desc=Pull request sincronizado. settings.event_package=Pacote settings.branch_filter=Filtro de branch +settings.branch_filter_desc=Lista dos branches a serem considerados nos eventos push, criação de branch e exclusão de branch, especificados como padrão glob. Se estiver vazio ou for *, eventos para todos os branches serão relatados. Veja github.com/gobwas/glob documentação da sintaxe. Exemplos: master, {master,release*}. settings.active=Ativo settings.active_helper=Informações sobre eventos disparados serão enviadas para esta URL do webhook. settings.add_hook_success=O webhook foi adicionado. @@ -1882,6 +1957,7 @@ settings.hook_type=Tipo de Hook settings.slack_token=Token settings.slack_domain=Domínio settings.slack_channel=Canal +settings.add_web_hook_desc=Integrar %s no seu repositório. settings.web_hook_name_gitea=Gitea settings.web_hook_name_gogs=Gogs settings.web_hook_name_slack=Slack @@ -1917,8 +1993,8 @@ settings.protected_branch=Proteção de Branch settings.protected_branch_can_push=Permitir push? settings.protected_branch_can_push_yes=Você pode fazer push settings.protected_branch_can_push_no=Você não pode fazer push -settings.branch_protection=Proteção de branch para '%s' -settings.protect_this_branch=Habilitar proteção de branch +settings.branch_protection=Proteção de Branch para '%s' +settings.protect_this_branch=Habilitar Proteção de Branch settings.protect_this_branch_desc=Previne a exclusão e restringe o merge e push para o branch. settings.protect_disable_push=Desabilitar push settings.protect_disable_push_desc=Nenhum push será permitido neste branch. @@ -1926,6 +2002,7 @@ settings.protect_enable_push=Habilitar push settings.protect_enable_push_desc=Qualquer pessoa com acesso de escrita terá permissão para realizar push neste branch (mas não forçar o push). settings.protect_whitelist_committers=Lista permitida para push settings.protect_whitelist_committers_desc=Somente usuários ou equipes da lista permitida serão autorizados realizar push neste branch (mas não forçar o push). +settings.protect_whitelist_deploy_keys=Dar permissão às chaves de deploy com acesso de gravação para push. settings.protect_whitelist_users=Usuários com permissão para realizar push: settings.protect_whitelist_search_users=Pesquisar usuários... settings.protect_whitelist_teams=Equipes com permissão para realizar push: @@ -1935,6 +2012,7 @@ settings.protect_merge_whitelist_committers_desc=Permitir que determinados usuá settings.protect_merge_whitelist_users=Usuários com permissão para aplicar merge: settings.protect_merge_whitelist_teams=Equipes com permissão para aplicar merge: settings.protect_check_status_contexts=Habilitar verificação de status +settings.protect_check_status_contexts_desc=Exigir que as verificações de status passem antes de fazer merge. Escolha quais verificações de status devem passar antes que os branches possam ter o merge aplicado em um branch que corresponda a esta regra. Quando habilitado, os commits devem primeiro ser enviados para outro branch, então faça merge ou push diretamente para um branch que corresponde a esta regra após a verificação de status ter passado. Se nenhum contexto for selecionado, o último commit deve ser bem sucedido, independentemente do contexto. settings.protect_check_status_contexts_list=Verificações de status encontradas na última semana para este repositório settings.protect_required_approvals=Aprovações necessárias: settings.protect_required_approvals_desc=Permite apenas realizar merge do pull request com avaliações positivas suficientes. @@ -1945,6 +2023,8 @@ settings.protect_approvals_whitelist_teams=Equipes com permissão de revisão: settings.dismiss_stale_approvals=Descartar aprovações obsoletas settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas. settings.require_signed_commits=Exibir commits assinados +settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis. +settings.protect_protected_file_patterns=Padrões de arquivos protegidos (separados usando ponto e vírgula '\;'): settings.protect_unprotected_file_patterns=Padrões de arquivos desprotegidos (separados usando ponto e vírgula '\;'): settings.protect_unprotected_file_patterns_desc=Arquivos não protegidos que podem ser alterados diretamente se o usuário tiver acesso de gravação, ignorando as restrições de push. Vários padrões podem ser separados usando ponto e vírgula ('\;'). Veja github.com/gobwas/glob documentação para sintaxe de padrões. Exemplos: .drone.yml, /docs/**/*.txt. settings.add_protected_branch=Habilitar proteção @@ -1955,7 +2035,12 @@ settings.protected_branch_deletion=Desabilitar proteção de branch settings.protected_branch_deletion_desc=Desabilitar a proteção de branch permite que os usuários com permissão de escrita realizem push. Continuar? settings.block_rejected_reviews=Bloquear merge em revisões rejeitadas settings.block_rejected_reviews_desc=O merge não será possível quando são solicitadas alterações pelos revisores oficiais, mesmo que haja aprovação suficiente. +settings.block_on_official_review_requests=Bloquear merge em solicitações de revisão oficiais +settings.block_on_official_review_requests_desc=O merge não será possível quando tiver pedidos de revisão oficiais, mesmo que haja aprovações suficientes. +settings.block_outdated_branch=Bloquear o merge se o pull request estiver desatualizado +settings.block_outdated_branch_desc=O merge não será possível quando o branch de topo estiver atrás do branch base. settings.default_branch_desc=Selecione um branch padrão para pull requests e commits de código: +settings.default_merge_style_desc=Estilo de merge padrão para pull requests: settings.choose_branch=Escolha um branch... settings.no_protected_branch=Não há branches protegidos. settings.edit_protected_branch=Editar @@ -2075,6 +2160,7 @@ diff.has_escaped=Essa linha tem caracteres Unicode ocultos releases.desc=Acompanhe as versões e downloads do projeto. release.releases=Versões +release.detail=Detalhes da versão release.tags=Tags release.new_release=Nova versão release.draft=Rascunho @@ -2083,6 +2169,7 @@ release.stable=Estável release.compare=Comparar release.edit=editar release.ahead.commits=%d commits +release.ahead.target=para %s desde esta versão release.source_code=Código fonte release.new_subheader=Lançamentos organizam versões do projeto. release.edit_subheader=Lançamentos organizam versões do projeto. @@ -2113,28 +2200,29 @@ release.download_count=Downloads: %s release.add_tag_msg=Use o título e o conteúdo do lançamento como mensagem da tag. release.add_tag=Criar apenas a tag -branch.name=Nome da branch +branch.name=Nome do Branch branch.search=Pesquisar branches -branch.already_exists=Uma branch com o nome %s já existe. +branch.already_exists=Um branch com o nome %s já existe. branch.delete_head=Excluir -branch.delete=Excluir branch '%s' +branch.delete=Excluir Branch '%s' branch.delete_html=Excluir Branch -branch.delete_desc=A exclusão de uma branch é permanente. Isto NÃO PODERÁ ser desfeito. Continuar? -branch.deletion_success=A branch '%s' foi excluída. -branch.deletion_failed=Falha ao excluir a branch %s. -branch.delete_branch_has_new_commits=A branch %s não pode ser excluída porque há novos commits após o merge. +branch.delete_desc=A exclusão de um branch é permanente. Isto NÃO PODERÁ ser desfeito. Continuar? +branch.deletion_success=O branch '%s' foi excluído. +branch.deletion_failed=Falha ao excluir o branch %s. +branch.delete_branch_has_new_commits=O branch %s não pode ser excluído porque há novos commits após o merge. branch.create_branch=Criar branch %s branch.create_from=de %s -branch.create_success=A branch '%s' foi criada. +branch.create_success=O branch '%s' foi criado. branch.branch_already_exists=Branch '%s' já existe neste repositório. -branch.branch_name_conflict=O nome da branch '%s' está em conflito com a branch '%s'. -branch.tag_collision=A branch '%s' não pode ser criada como tag com o mesmo nome já existente neste repositório. +branch.branch_name_conflict=O nome do branch '%s' está em conflito com o branch '%s'. +branch.tag_collision=O branch '%s' não pode ser criado como tag com o mesmo nome já existente neste repositório. branch.deleted_by=Excluído por %s -branch.restore_success=A branch '%s' foi restaurada. -branch.restore_failed=Falha ao restaurar a branch %s. -branch.protected_deletion_failed=A branch '%s' está protegida. Ela não pode ser excluída. -branch.restore=Restaurar branch '%s' -branch.download=Baixar branch '%s' +branch.restore_success=O branch '%s' foi restaurado. +branch.restore_failed=Falha ao restaurar o branch %s. +branch.protected_deletion_failed=O branch '%s' está protegido. Ele não pode ser excluído. +branch.default_deletion_failed=Branch '%s' é o branch padrão. Não pode ser excluído. +branch.restore=Restaurar Branch '%s' +branch.download=Baixar Branch '%s' branch.included_desc=Este branch faz parte do branch padrão branch.included=Incluído branch.create_new_branch=Criar branch a partir do branch: @@ -2156,6 +2244,9 @@ topic.done=Feito topic.count_prompt=Você não pode selecionar mais de 25 tópicos topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres. +error.csv.too_large=Não é possível renderizar este arquivo porque ele é muito grande. +error.csv.unexpected=Não é possível renderizar este arquivo porque ele contém um caractere inesperado na linha %d e coluna %d. +error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d. [org] org_name_holder=Nome da organização @@ -2193,6 +2284,7 @@ settings.repoadminchangeteam=O administrador do repositório pode adicionar e re settings.visibility=Visibilidade settings.visibility.public=Pública settings.visibility.limited=Limitada (Visível apenas para usuários registrados) +settings.visibility.limited_shortname=Limitado settings.visibility.private=Privada (Visível apenas para membros da organização) settings.visibility.private_shortname=Privado @@ -2288,6 +2380,7 @@ first_page=Primeira last_page=Última total=Total: %d +dashboard.new_version_hint=Gitea %s está disponível, você está executando %s. Verifique o blog para mais detalhes. dashboard.statistic=Resumo dashboard.operations=Operações de manutenção dashboard.system_status=Status do sistema @@ -2297,13 +2390,28 @@ dashboard.operation_switch=Trocar dashboard.operation_run=Executar dashboard.clean_unbind_oauth=Limpar conexões OAuth não vinculadas dashboard.clean_unbind_oauth_success=Todas as conexões de OAuth não vinculadas foram excluídas. +dashboard.task.started=Tarefa Iniciada: %[1]s dashboard.task.process=Tarefa: %[1]s +dashboard.task.cancelled=Tarefa: %[1]s cancelada: %[3]s +dashboard.task.error=Erro na Tarefa: %[1]: %[3]s +dashboard.task.finished=Tarefa: %[1]s iniciada por %[2]s foi finalizada dashboard.task.unknown=Tarefa desconhecida: %[1]s +dashboard.cron.started=Cron Iniciado: %[1]s +dashboard.cron.process=Cron: %[1]s +dashboard.cron.cancelled=Cron: %s cancelado: %[3]s +dashboard.cron.error=Erro no Cron: %s: %[3]s +dashboard.cron.finished=Cron: %[1]s terminou dashboard.delete_inactive_accounts=Excluir todas as contas não ativadas dashboard.delete_inactive_accounts.started=A tarefa de apagar todas as contas não ativadas foi iniciada. +dashboard.delete_repo_archives=Excluir todos os arquivos dos repositórios (ZIP, TAR.GZ, etc..) +dashboard.delete_repo_archives.started=A tarefa de remover todos os arquivos foi iniciada. dashboard.delete_missing_repos=Excluir todos os repositórios que não possuem seus arquivos Git +dashboard.delete_missing_repos.started=Foi iniciada a tarefa de excluir todos os repositórios que não têm arquivos Git. dashboard.delete_generated_repository_avatars=Excluir avatares gerados do repositório dashboard.update_mirrors=Atualizar espelhamentos +dashboard.repo_health_check=Verificar estado de saúde de todos os repositórios +dashboard.check_repo_stats=Verificar estatísticas de todos os repositórios +dashboard.archive_cleanup=Apagar arquivos antigos de repositório dashboard.deleted_branches_cleanup=Realizar limpeza de branches apagados dashboard.git_gc_repos=Coleta de lixo em todos os repositórios dashboard.resync_all_sshkeys.desc=(Não necessário para o servidor SSH embutido.) @@ -2342,6 +2450,9 @@ dashboard.total_gc_time=Pausa total do GC dashboard.total_gc_pause=Pausa total do GC dashboard.last_gc_pause=Última pausa do GC dashboard.gc_times=Nº de execuções do GC +dashboard.delete_old_actions=Excluir todas as ações antigas do banco de dados +dashboard.delete_old_actions.started=A exclusão de todas as ações antigas do banco de dados foi iniciada. +dashboard.update_checker=Verificador de atualização dashboard.delete_old_system_notices=Excluir todos os avisos de sistema antigos do banco de dados users.user_manage_panel=Gerenciamento de conta de usuário @@ -2413,6 +2524,7 @@ orgs.members=Membros orgs.new_orga=Nova organização repos.repo_manage_panel=Gerenciamento do repositório +repos.unadopted=Repositórios Não Adotados repos.owner=Proprietário repos.name=Nome repos.private=Privado @@ -2432,7 +2544,15 @@ packages.type=Tipo packages.repository=Repositório packages.size=Tamanho +defaulthooks=Webhooks Padrões +defaulthooks.desc=Webhooks automaticamente fazem requisições HTTP POST para um servidor quando acionados por determinados eventos do Gitea. Webhooks definidos aqui são os padrões e serão copiados para todos os novos repositórios. Leia mais no guia de webhooks. +defaulthooks.add_webhook=Adicionar Webhook Padrão +defaulthooks.update_webhook=Atualizar Webhook Padrão +systemhooks=Webhooks do Sistema +systemhooks.desc=Webhooks automaticamente fazem requisições HTTP POST para um servidor quando acionados por determinados eventos do Gitea. Webhooks definidos aqui agirão em todos os repositórios do sistema, então, por favor, considere quaisquer implicações de desempenho que isso possa ter. Leia mais no guia de webhooks. +systemhooks.add_webhook=Adicionar Webhook do Sistema +systemhooks.update_webhook=Atualizar Webhook do Sistema auths.auth_manage_panel=Gerenciamento de fonte de autenticação auths.new=Adicionar fonte de autenticação @@ -2480,6 +2600,7 @@ auths.disable_helo=Desativar HELO auths.pam_service_name=Nome de Serviço PAM auths.pam_email_domain=Domínio de e-mail do PAM (opcional) auths.oauth2_provider=Provedor OAuth2 +auths.oauth2_icon_url=URL do Ícone auths.oauth2_clientID=ID do cliente (chave) auths.oauth2_clientSecret=Senha do cliente auths.openIdConnectAutoDiscoveryURL=URL do OpenID Connect Auto Discovery @@ -2506,6 +2627,7 @@ auths.tips.oauth2.general=Autenticação OAuth2 auths.tips.oauth2.general.tip=Ao cadastrar uma nova autenticação OAuth2, o retorno de chamada/redirecionamento URL deve ser: /user/oauth2//callback auths.tip.oauth2_provider=Provedor OAuth2 auths.tip.bitbucket=Cadastrar um novo consumidor de OAuth em https://bitbucket.org/account/user/ e adicionar a permissão 'Account' - 'Read' +auths.tip.nextcloud=Registre um novo consumidor OAuth em sua instância usando o seguinte menu "Configurações -> Segurança -> Cliente OAuth 2.0" auths.tip.dropbox=Criar um novo aplicativo em https://www.dropbox.com/developers/apps auths.tip.facebook=Cadastrar um novo aplicativo em https://developers.facebook.com/apps e adicionar o produto "Facebook Login" auths.tip.github=Cadastrar um novo aplicativo de OAuth na https://github.com/settings/applications/new @@ -2515,6 +2637,8 @@ auths.tip.openid_connect=Use o OpenID Connect Discovery URL (/.well-kn auths.tip.twitter=Vá em https://dev.twitter.com/apps, crie um aplicativo e certifique-se de que está habilitada a opção “Allow this application to be used to Sign in with Twitter“ auths.tip.discord=Cadastrar um novo aplicativo em https://discordapp.com/developers/applications/me auths.tip.gitea=Cadastrar um novo aplicativo OAuth2. Guia pode ser encontrado em https://docs.gitea.io/en-us/oauth2-provider/ +auths.tip.yandex=Crie um novo aplicativo em https://oauth.yandex.com/client/new. Selecione as seguintes permissões da seção "Yandex.Passport API": "Access to email address", "Access to user avatar" and "Access to username, first name and surname, gender" +auths.tip.mastodon=Insira a URL da instância personalizada do mastodon que você deseja usar para autenticar (ou use o padrão) auths.edit=Editar fonte de autenticação auths.activated=Esta fonte de autenticação está ativada auths.new_success=A autenticação '%s' foi adicionada. @@ -2670,6 +2794,7 @@ monitor.next=Próxima vez monitor.previous=Vez anterior monitor.execute_times=Execuções monitor.process=Processos em execução +monitor.stacktrace=Stacktraces monitor.goroutines=%d Goroutines monitor.desc=Descrição monitor.start=Hora de início @@ -2783,6 +2908,7 @@ publish_release=`lançou a versão "%[4]s" em %[3]s#%[2]s` review_dismissed_reason=Motivo: create_branch=criou o branch %[3]s em %[4]s +starred_repo=favoritou %[2]s watched_repo=começou a observar %[2]s [tool] diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 834cd47716..ff6dc76c40 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -663,7 +663,6 @@ ssh_invalid_token_signature=A chave SSH, assinatura ou código fornecidos não c ssh_token_required=Tem que fornecer uma assinatura para o código abaixo ssh_token=Código ssh_token_help=Pode gerar uma assinatura usando o seguinte comando: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /caminho_para_a_sua_chave_pública ssh_token_signature=Assinatura SSH blindada (com armadura ASCII) key_signature_ssh_placeholder=Começa com '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=A chave SSH '%s' foi validada. @@ -3024,44 +3023,44 @@ dependency.id=ID dependency.version=Versão composer.registry=Configure este registo no seu ficheiro ~/.composer/config.json: composer.install=Para instalar o pacote usando o Composer, execute o seguinte comando: -composer.documentation=Para obter mais informações sobre o registo do Composer, consulte a documentação. +composer.documentation=Para obter mais informações sobre o registo do Composer, consulte a documentação. composer.dependencies=Dependências composer.dependencies.development=Dependências de desenvolvimento conan.details.repository=Repositório conan.registry=Configurar este registo usando a linha de comandos: conan.install=Para instalar o pacote usando o Conan, execute o seguinte comando: -conan.documentation=Para obter mais informações sobre o registo do Conan, consulte a documentação. +conan.documentation=Para obter mais informações sobre o registo do Conan, consulte a documentação. container.details.type=Tipo de imagem container.details.platform=Plataforma container.details.repository_site=Página web do repositório container.details.documentation_site=Página web da documentação container.pull=Puxar a imagem usando a linha de comandos: -container.documentation=Para obter mais informações sobre o registo do Contentor, consulte a documentação. +container.documentation=Para obter mais informações sobre o registo do Container, consulte a documentação. container.multi_arch=S.O. / Arquit. container.labels=Rótulos container.labels.key=Chave container.labels.value=Valor generic.download=Descarregar pacote usando a linha de comandos: -generic.documentation=Para obter mais informações sobre o registo genérico, consulte a documentação. +generic.documentation=Para obter mais informações sobre o registo genérico, consulte a documentação. maven.registry=Configure este registo no seu ficheiro pom.xml do projecto: maven.install=Para usar este pacote, inclua no bloco dependencies do ficheiro pom.xml o seguinte: maven.install2=Executar usando a linha de comandos: maven.download=Para descarregar a dependência, execute na linha de comandos: -maven.documentation=Para obter mais informações sobre o registo Maven, consulte a documentação. +maven.documentation=Para obter mais informações sobre o registo do Maven, consulte a documentação. nuget.registry=Configurar este registo usando a linha de comandos: nuget.install=Para instalar o pacote usando NuGet, execute o seguinte comando: -nuget.documentation=Para obter mais informações sobre o registo Nuget, consulte a documentação. +nuget.documentation=Para obter mais informações sobre o registo do Nuget, consulte a documentação. npm.install=Para instalar o pacote usando o npm, execute o seguinte comando: -npm.documentation=Para obter mais informações sobre o registo npm, consulte a documentação. +npm.documentation=Para obter mais informações sobre o registo do npm, consulte a documentação. npm.dependencies=Dependências npm.dependencies.development=Dependências de desenvolvimento npm.dependencies.optional=Dependências opcionais pypi.requires=Requer Python pypi.install=Para instalar o pacote usando o pip, execute o seguinte comando: -pypi.documentation=Para obter mais informações sobre o registo do PyPI, consulte a documentação. +pypi.documentation=Para obter mais informações sobre o registo do PyPI, consulte a documentação. rubygems.install=Para instalar o pacote usando o gem, execute o seguinte comando: rubygems.dependencies.development=Dependências de desenvolvimento -rubygems.documentation=Para obter mais informações sobre o registo do RubyGems, consulte a documentação. +rubygems.documentation=Para obter mais informações sobre o registo do RubyGems, consulte a documentação. settings.link=Vincular este pacote a um repositório settings.link.description=Se você vincular um pacote a um repositório, o pacote será listado na lista de pacotes do repositório. settings.link.select=Escolha o repositório diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 7ca953d6a4..c4acfe1ae6 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -632,7 +632,6 @@ ssh_invalid_token_signature=Предоставленный SSH ключ, под ssh_token_required=Вы должны предоставить подпись для токена ниже ssh_token=Токен ssh_token_help=Вы можете сгенерировать подпись с помощью: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey ssh_token_signature=Бронированная SSH подпись key_signature_ssh_placeholder=Начинается с '-----BEGIN SSH SIGNATURE-----' verify_ssh_key_success=SSH ключ '%s' проверен. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index caebde87a9..64f806e00e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -664,7 +664,6 @@ ssh_invalid_token_signature=提供的 SSH 密钥、签名或令牌不匹配或 ssh_token_required=您必须为下面的令牌提供签名 ssh_token=令牌 ssh_token_help=您可以使用以下方式生成签名: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign-n gitea -f /path_to_your_pubkey ssh_token_signature=增强 SSH 签名 key_signature_ssh_placeholder=以 '-----BEGIN SSH SIGNATURE -----' 开头 verify_ssh_key_success=SSH 密钥 '%s' 已被验证。 @@ -3032,39 +3031,39 @@ dependency.id=ID dependency.version=版本 composer.registry=在您的 ~/.composer/config.json 文件中设置此注册中心: composer.install=要使用 Composer 安装软件包,请运行以下命令: -composer.documentation=关于 Composer 注册中心的更多信息,请参阅 文档。 +composer.documentation=关于 Composer 注册中心的更多信息,请参阅 文档 。 composer.dependencies=依赖 composer.dependencies.development=开发依赖 conan.details.repository=仓库 conan.registry=从命令行设置此注册中心: conan.install=要使用 Conan 安装软件包,请运行以下命令: -conan.documentation=关于 Conan 注册中心的更多信息,请参阅 文档。 +conan.documentation=关于 Conan 注册中心的更多信息,请参阅 文档。 container.details.type=镜像类型 container.details.platform=平台 container.details.repository_site=仓库站点 container.details.documentation_site=文档网站 container.pull=从命令行拉取镜像: -container.documentation=关于 Container 注册中心的更多信息,请参阅 文档 。 +container.documentation=关于 Container 注册中心的更多信息,请参阅 文档。 container.multi_arch=OS / Arch container.layers=镜像层 container.labels=标签 container.labels.key=键 container.labels.value=值 generic.download=从命令行下载软件包: -generic.documentation=关于通用注册中心的更多信息,请参阅 文档 。 +generic.documentation=关于通用注册中心的更多信息,请参阅 文档。 maven.registry=在您项目的 pom.xml 文件中设置此注册中心: maven.install=要使用这个软件包,在 pom.xml 文件中的 依赖项 块中包含以下内容: maven.install2=通过命令行运行: maven.download=要下载依赖项,请通过命令行运行: -maven.documentation=关于 Maven 注册中心的更多信息,请参阅 文档。 +maven.documentation=关于 Maven 注册中心的更多信息,请参阅 文档。 nuget.registry=从命令行设置此注册中心: nuget.install=要使用 Nuget 安装软件包,请运行以下命令: -nuget.documentation=关于 Nuget 注册中心的更多信息,请参阅 文档。 +nuget.documentation=关于 Nuget 注册中心的更多信息,请参阅 文档。 nuget.dependency.framework=目标框架 npm.registry=在您项目的 .npmrc 文件中设置此注册中心: npm.install=要使用 npm 安装软件包,请运行以下命令: npm.install2=或将其添加到 package.json 文件: -npm.documentation=关于 npm 注册中心的更多信息,请参阅 文档。 +npm.documentation=关于 npm 注册中心的更多信息,请参阅 文档。 npm.dependencies=依赖项 npm.dependencies.development=开发依赖 npm.dependencies.peer=Peer 依赖 @@ -3072,14 +3071,14 @@ npm.dependencies.optional=可选依赖 npm.details.tag=标签 pypi.requires=需要 Python pypi.install=要使用 pip 安装软件包,请运行以下命令: -pypi.documentation=关于 PyPI 注册中心的更多信息,请参阅 文档。 +pypi.documentation=关于 PyPI 注册中心的信息,请参阅 文档。 rubygems.install=要使用 gem 安装软件包,请运行以下命令: rubygems.install2=或将它添加到 Gemfile: rubygems.dependencies.runtime=运行时依赖 rubygems.dependencies.development=开发依赖 rubygems.required.ruby=需要 Ruby 版本 rubygems.required.rubygems=需要 RubyGem 版本 -rubygems.documentation=关于 RubyGems 注册中心的更多信息,请参阅 文档 。 +rubygems.documentation=关于 RubyGems 注册中心的更多信息,请参阅 文档。 settings.link=将此软件包链接到仓库 settings.link.description=如果您将一个软件包与一个代码库链接起来,软件包将显示在代码库的软件包列表中。 settings.link.select=选择仓库 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index ffb0cf1fbc..7193429309 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -662,7 +662,6 @@ ssh_invalid_token_signature=提供的 SSH 金鑰、簽署、Token 不符合或 T ssh_token_required=您必須為下列的 Token 提供簽署 ssh_token=Token ssh_token_help=您可以使用以下方法產生簽署: -ssh_token_code=echo -n "%s" | ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey ssh_token_signature=Armored SSH 簽署 key_signature_ssh_placeholder=以「-----BEGIN SSH SIGNATURE-----」開頭 verify_ssh_key_success=已驗證 SSH 金鑰「%s」。 diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 22a452325e..23de28c7f9 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -63,8 +63,8 @@ func SearchPackages(ctx *context.Context) { opts := &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, - Type: string(packages_model.TypeComposer), - QueryName: ctx.FormTrim("q"), + Type: packages_model.TypeComposer, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, Paginator: &paginator, } if ctx.FormTrim("type") != "" { diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 50151ee5ea..d127134d44 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -256,7 +256,12 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo } defer committer.Close() - pvs, err := packages_model.FindVersionsByPropertyNameAndValue(ctx, pv.PackageID, npm_module.TagProperty, tag) + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + PackageID: pv.PackageID, + Properties: map[string]string{ + npm_module.TagProperty: tag, + }, + }) if err != nil { return err } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 3af7155fae..013c0c1e33 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -39,9 +39,9 @@ func ServiceIndex(ctx *context.Context) { // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages func SearchService(ctx *context.Context) { pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: string(packages_model.TypeNuGet), - QueryName: ctx.FormTrim("q"), + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeNuGet, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, Paginator: db.NewAbsoluteListOptions( ctx.FormInt("skip"), ctx.FormInt("take"), diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index a5a9b779ab..6fdd03e8ea 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -41,7 +41,7 @@ func EnumeratePackages(ctx *context.Context) { func EnumeratePackagesLatest(ctx *context.Context) { pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, - Type: string(packages_model.TypeRubyGems), + Type: packages_model.TypeRubyGems, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -96,7 +96,7 @@ func ServePackageSpecification(ctx *context.Context) { return } - pvs, err := packages_model.GetVersionsByFilename(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, filename[:len(filename)-10]+"gem") + pvs, err := getVersionsByFilename(ctx, filename[:len(filename)-10]+"gem") if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -158,7 +158,7 @@ func ServePackageSpecification(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) { filename := ctx.Params("filename") - pvs, err := packages_model.GetVersionsByFilename(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, filename) + pvs, err := getVersionsByFilename(ctx, filename) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -283,3 +283,12 @@ func DeletePackage(ctx *context.Context) { apiError(ctx, http.StatusInternalServerError, err) } } + +func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) { + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeRubyGems, + HasFileWithName: filename, + }) + return pvs, err +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2c29263890..a430eb453a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -216,7 +216,6 @@ func reqToken() func(ctx *context.APIContext) { return } if ctx.IsSigned { - ctx.RequireCSRF() return } ctx.Error(http.StatusUnauthorized, "reqToken", "token is required") @@ -584,8 +583,7 @@ func bind(obj interface{}) http.HandlerFunc { func buildAuthGroup() *auth.Group { group := auth.NewGroup( &auth.OAuth2{}, - &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API - auth.SharedSession, // FIXME: this should be removed once all UI don't reference API/v1, see https://github.com/go-gitea/gitea/pull/16052 + &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API ) if setting.Service.EnableReverseProxyAuth { group.Add(&auth.ReverseProxy{}) @@ -596,11 +594,9 @@ func buildAuthGroup() *auth.Group { } // Routes registers all v1 APIs routes to web application. -func Routes(sessioner func(http.Handler) http.Handler) *web.Route { +func Routes() *web.Route { m := web.NewRoute() - m.Use(sessioner) - m.Use(securityHeaders()) if setting.CORSConfig.Enabled { m.Use(cors.Handler(cors.Options{ @@ -609,7 +605,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route { // setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, - AllowedHeaders: []string{"Authorization", "X-CSRFToken", "X-Gitea-OTP"}, + AllowedHeaders: []string{"Authorization", "X-Gitea-OTP"}, MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go index 1821c30377..c707cf4524 100644 --- a/routers/api/v1/notify/notifications.go +++ b/routers/api/v1/notify/notifications.go @@ -26,7 +26,7 @@ func NewAvailable(ctx *context.APIContext) { } func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions { - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return nil diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 8952241222..b445e8e2f8 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -56,8 +56,8 @@ func ListPackages(ctx *context.APIContext) { pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, - Type: packageType, - QueryName: query, + Type: packages.Type(packageType), + Name: packages.SearchValue{Value: query}, Paginator: &listOptions, }) if err != nil { diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 05c9c27144..083fe8f0b9 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -111,7 +112,7 @@ func SearchIssues(ctx *context.APIContext) { // "200": // "$ref": "#/responses/IssueList" - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return @@ -359,7 +360,7 @@ func ListIssues(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/IssueList" - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return @@ -405,12 +406,12 @@ func ListIssues(ctx *context.APIContext) { for i := range part { // uses names and fall back to ids // non existent milestones are discarded - mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i]) + mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i]) if err == nil { mileIDs = append(mileIDs, mile.ID) continue } - if !models.IsErrMilestoneNotExist(err) { + if !issues_model.IsErrMilestoneNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err) return } @@ -418,12 +419,12 @@ func ListIssues(ctx *context.APIContext) { if err != nil { continue } - mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id) + mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id) if err == nil { mileIDs = append(mileIDs, mile.ID) continue } - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { continue } ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index d63a71afc2..ef91a2481c 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -58,7 +58,7 @@ func ListIssueComments(ctx *context.APIContext) { // "200": // "$ref": "#/responses/CommentList" - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return @@ -150,7 +150,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { // "200": // "$ref": "#/responses/TimelineList" - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return @@ -253,7 +253,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { // "200": // "$ref": "#/responses/CommentList" - before, since, err := utils.GetQueryBeforeSince(ctx) + before, since, err := context.GetQueryBeforeSince(ctx.Context) if err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 19732c101f..e42dc60a94 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) { opts.UserID = user.ID } - if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { + if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } @@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { } var err error - if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { + if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } @@ -597,7 +597,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { } var err error - if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { + if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 3b4b85158c..ce6aa7f46f 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -10,7 +10,7 @@ import ( "strconv" "time" - "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -57,7 +57,7 @@ func ListMilestones(ctx *context.APIContext) { // "200": // "$ref": "#/responses/MilestoneList" - milestones, total, err := models.GetMilestones(models.GetMilestonesOption{ + milestones, total, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ ListOptions: utils.GetListOptions(ctx), RepoID: ctx.Repo.Repository.ID, State: api.StateType(ctx.FormString("state")), @@ -146,7 +146,7 @@ func CreateMilestone(ctx *context.APIContext) { form.Deadline = &defaultDeadline } - milestone := &models.Milestone{ + milestone := &issues_model.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Description, @@ -158,7 +158,7 @@ func CreateMilestone(ctx *context.APIContext) { milestone.ClosedDateUnix = timeutil.TimeStampNow() } - if err := models.NewMilestone(milestone); err != nil { + if err := issues_model.NewMilestone(milestone); err != nil { ctx.Error(http.StatusInternalServerError, "NewMilestone", err) return } @@ -218,7 +218,7 @@ func EditMilestone(ctx *context.APIContext) { milestone.IsClosed = *form.State == string(api.StateClosed) } - if err := models.UpdateMilestone(milestone, oldIsClosed); err != nil { + if err := issues_model.UpdateMilestone(milestone, oldIsClosed); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateMilestone", err) return } @@ -255,7 +255,7 @@ func DeleteMilestone(ctx *context.APIContext) { return } - if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, m.ID); err != nil { + if err := issues_model.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, m.ID); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteMilestoneByRepoID", err) return } @@ -263,23 +263,23 @@ func DeleteMilestone(ctx *context.APIContext) { } // getMilestoneByIDOrName get milestone by ID and if not available by name -func getMilestoneByIDOrName(ctx *context.APIContext) *models.Milestone { +func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { mile := ctx.Params(":id") mileID, _ := strconv.ParseInt(mile, 0, 64) if mileID != 0 { - milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, mileID) + milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, mileID) if err == nil { return milestone - } else if !models.IsErrMilestoneNotExist(err) { + } else if !issues_model.IsErrMilestoneNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) return nil } } - milestone, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, mile) + milestone, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, mile) if err != nil { - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound() return nil } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index bb922ddb23..94262f81d1 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -14,6 +14,7 @@ import ( "time" "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -342,9 +343,9 @@ func CreatePullRequest(ctx *context.APIContext) { } if form.Milestone > 0 { - milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, form.Milestone) + milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone) if err != nil { - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 331b78e1d8..3b36f28326 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -75,7 +75,7 @@ func ListPullReviews(ctx *context.APIContext) { return } - if err = pr.Issue.LoadRepo(); err != nil { + if err = pr.Issue.LoadRepo(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "LoadRepo", err) return } @@ -322,7 +322,7 @@ func CreatePullReview(ctx *context.APIContext) { return } - if err := pr.Issue.LoadRepo(); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) return } @@ -657,7 +657,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions return } - if err := pr.Issue.LoadRepo(); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index aca1338a27..f645502590 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -31,23 +31,6 @@ import ( repo_service "code.gitea.io/gitea/services/repository" ) -var searchOrderByMap = map[string]map[string]db.SearchOrderBy{ - "asc": { - "alpha": db.SearchOrderByAlphabetically, - "created": db.SearchOrderByOldest, - "updated": db.SearchOrderByLeastUpdated, - "size": db.SearchOrderBySize, - "id": db.SearchOrderByID, - }, - "desc": { - "alpha": db.SearchOrderByAlphabeticallyReverse, - "created": db.SearchOrderByNewest, - "updated": db.SearchOrderByRecentUpdated, - "size": db.SearchOrderBySizeReverse, - "id": db.SearchOrderByIDReverse, - }, -} - // Search repositories via options func Search(ctx *context.APIContext) { // swagger:operation GET /repos/search repository repoSearch @@ -193,7 +176,7 @@ func Search(ctx *context.APIContext) { if len(sortOrder) == 0 { sortOrder = "asc" } - if searchModeMap, ok := searchOrderByMap[sortOrder]; ok { + if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { if orderBy, ok := searchModeMap[sortMode]; ok { opts.OrderBy = orderBy } else { diff --git a/routers/api/v1/utils/page.go b/routers/api/v1/utils/page.go new file mode 100644 index 0000000000..608bec7395 --- /dev/null +++ b/routers/api/v1/utils/page.go @@ -0,0 +1,19 @@ +// Copyright 2017 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 utils + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" +) + +// GetListOptions returns list options using the page and limit parameters +func GetListOptions(ctx *context.APIContext) db.ListOptions { + return db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } +} diff --git a/routers/init.go b/routers/init.go index 62a9e4002b..88c393736e 100644 --- a/routers/init.go +++ b/routers/init.go @@ -48,8 +48,6 @@ import ( "code.gitea.io/gitea/services/repository/archiver" "code.gitea.io/gitea/services/task" "code.gitea.io/gitea/services/webhook" - - "gitea.com/go-chi/session" ) func mustInit(fn func() error) { @@ -174,20 +172,8 @@ func NormalRoutes() *web.Route { r.Use(middle) } - sessioner := session.Sessioner(session.Options{ - Provider: setting.SessionConfig.Provider, - ProviderConfig: setting.SessionConfig.ProviderConfig, - CookieName: setting.SessionConfig.CookieName, - CookiePath: setting.SessionConfig.CookiePath, - Gclifetime: setting.SessionConfig.Gclifetime, - Maxlifetime: setting.SessionConfig.Maxlifetime, - Secure: setting.SessionConfig.Secure, - SameSite: setting.SessionConfig.SameSite, - Domain: setting.SessionConfig.Domain, - }) - - r.Mount("/", web_routers.Routes(sessioner)) - r.Mount("/api/v1", apiv1.Routes(sessioner)) + r.Mount("/", web_routers.Routes()) + r.Mount("/api/v1", apiv1.Routes()) r.Mount("/api/internal", private.Routes()) if setting.Packages.Enabled { r.Mount("/api/packages", packages_router.Routes()) diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 22be37526f..79bf025dd2 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -31,9 +31,9 @@ func Packages(ctx *context.Context) { sort := ctx.FormTrim("sort") pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - QueryName: query, - Type: packageType, - Sort: sort, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, + Sort: sort, Paginator: &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 1e894262d6..e8250616ab 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -345,7 +345,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) } - // Clear whatever CSRF has right now, force to generate a new one + // Clear whatever CSRF cookie has right now, force to generate a new one middleware.DeleteCSRFCookie(ctx.Resp) // Register last login diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index a0cbaa0385..4c3e3c3ace 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1010,7 +1010,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model log.Error("Error storing session: %v", err) } - // Clear whatever CSRF has right now, force to generate a new one + // Clear whatever CSRF cookie has right now, force to generate a new one middleware.DeleteCSRFCookie(ctx.Resp) // Register last login diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go new file mode 100644 index 0000000000..39b87f2498 --- /dev/null +++ b/routers/web/explore/topic.go @@ -0,0 +1,42 @@ +// 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 explore + +import ( + "net/http" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + api "code.gitea.io/gitea/modules/structs" +) + +// TopicSearch search for creating topic +func TopicSearch(ctx *context.Context) { + opts := &repo_model.FindTopicOptions{ + Keyword: ctx.FormString("q"), + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }, + } + + topics, total, err := repo_model.FindTopics(opts) + if err != nil { + ctx.Error(http.StatusInternalServerError) + return + } + + topicResponses := make([]*api.TopicResponse, len(topics)) + for i, topic := range topics { + topicResponses[i] = convert.ToTopicResponse(topic) + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "topics": topicResponses, + }) +} diff --git a/routers/web/misc/markdown.go b/routers/web/misc/markdown.go new file mode 100644 index 0000000000..b37aaf10ff --- /dev/null +++ b/routers/web/misc/markdown.go @@ -0,0 +1,98 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 misc + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "mvdan.cc/xurls/v2" +) + +// Markdown render markdown document to HTML +func Markdown(ctx *context.Context) { + // swagger:operation POST /markdown miscellaneous renderMarkdown + // --- + // summary: Render a markdown document as HTML + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MarkdownOption" + // consumes: + // - application/json + // produces: + // - text/html + // responses: + // "200": + // "$ref": "#/responses/MarkdownRender" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.MarkdownOption) + + if ctx.HasAPIError() { + ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + return + } + + if len(form.Text) == 0 { + _, _ = ctx.Write([]byte("")) + return + } + + switch form.Mode { + case "comment": + fallthrough + case "gfm": + urlPrefix := form.Context + meta := map[string]string{} + if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { + // check if urlPrefix is already set to a URL + linkRegex, _ := xurls.StrictMatchingScheme("https?://") + m := linkRegex.FindStringIndex(urlPrefix) + if m == nil { + urlPrefix = util.URLJoin(setting.AppURL, form.Context) + } + } + if ctx.Repo != nil && ctx.Repo.Repository != nil { + // "gfm" = Github Flavored Markdown - set this to render as a document + if form.Mode == "gfm" { + meta = ctx.Repo.Repository.ComposeDocumentMetas() + } else { + meta = ctx.Repo.Repository.ComposeMetas() + } + } + if form.Mode == "gfm" { + meta["mode"] = "document" + } + + if err := markdown.Render(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: urlPrefix, + Metas: meta, + IsWiki: form.Wiki, + }, strings.NewReader(form.Text), ctx.Resp); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + default: + if err := markdown.RenderRaw(&markup.RenderContext{ + Ctx: ctx, + URLPrefix: form.Context, + }, strings.NewReader(form.Text), ctx.Resp); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } +} diff --git a/routers/api/v1/misc/swagger.go b/routers/web/misc/swagger.go similarity index 100% rename from routers/api/v1/misc/swagger.go rename to routers/web/misc/swagger.go diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 034a8ce978..31bfaea92f 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" @@ -20,7 +21,9 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/forms" @@ -329,6 +332,51 @@ func TeamRepositories(ctx *context.Context) { ctx.HTML(http.StatusOK, tplTeamRepositories) } +// SearchTeam api for searching teams +func SearchTeam(ctx *context.Context) { + listOptions := db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } + + opts := &organization.SearchTeamOptions{ + UserID: ctx.Doer.ID, + Keyword: ctx.FormTrim("q"), + OrgID: ctx.Org.Organization.ID, + IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), + ListOptions: listOptions, + } + + teams, maxResults, err := organization.SearchTeam(opts) + if err != nil { + log.Error("SearchTeam failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": "SearchTeam internal failure", + }) + return + } + + apiTeams := make([]*api.Team, len(teams)) + for i := range teams { + if err := teams[i].GetUnits(); err != nil { + log.Error("Team GetUnits failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": "SearchTeam failed to get units", + }) + return + } + apiTeams[i] = convert.ToTeam(teams[i]) + } + + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + "data": apiTeams, + }) +} + // EditTeam render team edit page func EditTeam(ctx *context.Context) { ctx.Data["Title"] = ctx.Org.Organization.FullName diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a1a7200ba4..3ca193a15e 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -16,6 +16,7 @@ import ( "path" "strconv" "strings" + "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" @@ -36,6 +37,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates/vars" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -401,7 +403,7 @@ func Issues(ctx *context.Context) { var err error // Get milestones - ctx.Data["Milestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["Milestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: ctx.Repo.Repository.ID, State: api.StateType(ctx.FormString("state")), }) @@ -418,7 +420,7 @@ func Issues(ctx *context.Context) { // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) { var err error - ctx.Data["OpenMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: repo.ID, State: api.StateOpen, }) @@ -426,7 +428,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R ctx.ServerError("GetMilestones", err) return } - ctx.Data["ClosedMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{ + ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: repo.ID, State: api.StateClosed, }) @@ -804,7 +806,7 @@ func NewIssue(ctx *context.Context) { milestoneID := ctx.FormInt64("milestone") if milestoneID > 0 { - milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID) + milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { log.Error("GetMilestoneByID: %d: %v", milestoneID, err) } else { @@ -913,7 +915,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull // Check milestone. milestoneID := form.MilestoneID if milestoneID > 0 { - milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID) + milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { ctx.ServerError("GetMilestoneByID", err) return nil, nil, 0, 0 @@ -1323,7 +1325,7 @@ func ViewIssue(ctx *context.Context) { ctx.ServerError("GetIssueByID", err) return } - if err = otherIssue.LoadRepo(); err != nil { + if err = otherIssue.LoadRepo(ctx); err != nil { ctx.ServerError("LoadRepo", err) return } @@ -1403,7 +1405,7 @@ func ViewIssue(ctx *context.Context) { ctx.ServerError("LoadMilestone", err) return } - ghostMilestone := &models.Milestone{ + ghostMilestone := &issues_model.Milestone{ ID: -1, Name: ctx.Tr("repo.issues.deleted_milestone"), } @@ -1762,6 +1764,20 @@ func getActionIssues(ctx *context.Context) []*models.Issue { return issues } +// GetIssueInfo get an issue of a repository +func GetIssueInfo(ctx *context.Context) { + issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.Error(http.StatusNotFound) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) + } + return + } + ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue)) +} + // UpdateIssueTitle change issue's title func UpdateIssueTitle(ctx *context.Context) { issue := GetActionIssue(ctx) @@ -1856,6 +1872,40 @@ func UpdateIssueContent(ctx *context.Context) { }) } +// UpdateIssueDeadline updates an issue deadline +func UpdateIssueDeadline(ctx *context.Context) { + form := web.GetForm(ctx).(*api.EditDeadlineOption) + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound("GetIssueByIndex", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) + } + return + } + + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.Error(http.StatusForbidden, "", "Not repo writer") + return + } + + var deadlineUnix timeutil.TimeStamp + var deadline time.Time + if form.Deadline != nil && !form.Deadline.IsZero() { + deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), + 23, 59, 59, 0, time.Local) + deadlineUnix = timeutil.TimeStamp(deadline.Unix()) + } + + if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) + return + } + + ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) +} + // UpdateIssueMilestone change issue's milestone func UpdateIssueMilestone(ctx *context.Context) { issues := getActionIssues(ctx) @@ -1944,7 +1994,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { } for _, issue := range issues { - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(ctx); err != nil { ctx.ServerError("issue.LoadRepo", err) return } @@ -2052,6 +2102,338 @@ func UpdatePullReviewRequest(ctx *context.Context) { }) } +// SearchIssues searches for issues across the repositories that the user has access to +func SearchIssues(ctx *context.Context) { + before, since, err := context.GetQueryBeforeSince(ctx) + if err != nil { + ctx.Error(http.StatusUnprocessableEntity, err.Error()) + return + } + + var isClosed util.OptionalBool + switch ctx.FormString("state") { + case "closed": + isClosed = util.OptionalBoolTrue + case "all": + isClosed = util.OptionalBoolNone + default: + isClosed = util.OptionalBoolFalse + } + + // find repos user can access (for issue search) + opts := &models.SearchRepoOptions{ + Private: false, + AllPublic: true, + TopicOnly: false, + Collaborate: util.OptionalBoolNone, + // This needs to be a column that is not nil in fixtures or + // MySQL will return different results when sorting by null in some cases + OrderBy: db.SearchOrderByAlphabetically, + Actor: ctx.Doer, + } + if ctx.IsSigned { + opts.Private = true + opts.AllLimited = true + } + if ctx.FormString("owner") != "" { + owner, err := user_model.GetUserByName(ctx.FormString("owner")) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) + } else { + ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + } + return + } + opts.OwnerID = owner.ID + opts.AllLimited = false + opts.AllPublic = false + opts.Collaborate = util.OptionalBoolFalse + } + if ctx.FormString("team") != "" { + if ctx.FormString("owner") == "" { + ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") + return + } + team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team")) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) + } else { + ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + } + return + } + opts.TeamID = team.ID + } + + repoIDs, _, err := models.SearchRepositoryIDs(opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error()) + return + } + + var issues []*models.Issue + var filteredCount int64 + + keyword := ctx.FormTrim("q") + if strings.IndexByte(keyword, 0) >= 0 { + keyword = "" + } + var issueIDs []int64 + if len(keyword) > 0 && len(repoIDs) > 0 { + if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword); err != nil { + ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error()) + return + } + } + + var isPull util.OptionalBool + switch ctx.FormString("type") { + case "pulls": + isPull = util.OptionalBoolTrue + case "issues": + isPull = util.OptionalBoolFalse + default: + isPull = util.OptionalBoolNone + } + + labels := ctx.FormTrim("labels") + var includedLabelNames []string + if len(labels) > 0 { + includedLabelNames = strings.Split(labels, ",") + } + + milestones := ctx.FormTrim("milestones") + var includedMilestones []string + if len(milestones) > 0 { + includedMilestones = strings.Split(milestones, ",") + } + + // this api is also used in UI, + // so the default limit is set to fit UI needs + limit := ctx.FormInt("limit") + if limit == 0 { + limit = setting.UI.IssuePagingNum + } else if limit > setting.API.MaxResponseItems { + limit = setting.API.MaxResponseItems + } + + // Only fetch the issues if we either don't have a keyword or the search returned issues + // This would otherwise return all issues if no issues were found by the search. + if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 { + issuesOpt := &models.IssuesOptions{ + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: limit, + }, + RepoIDs: repoIDs, + IsClosed: isClosed, + IssueIDs: issueIDs, + IncludedLabelNames: includedLabelNames, + IncludeMilestones: includedMilestones, + SortType: "priorityrepo", + PriorityRepoID: ctx.FormInt64("priority_repo_id"), + IsPull: isPull, + UpdatedBeforeUnix: before, + UpdatedAfterUnix: since, + } + + ctxUserID := int64(0) + if ctx.IsSigned { + ctxUserID = ctx.Doer.ID + } + + // Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested + if ctx.FormBool("created") { + issuesOpt.PosterID = ctxUserID + } + if ctx.FormBool("assigned") { + issuesOpt.AssigneeID = ctxUserID + } + if ctx.FormBool("mentioned") { + issuesOpt.MentionedID = ctxUserID + } + if ctx.FormBool("review_requested") { + issuesOpt.ReviewRequestedID = ctxUserID + } + + if issues, err = models.Issues(issuesOpt); err != nil { + ctx.Error(http.StatusInternalServerError, "Issues", err.Error()) + return + } + + issuesOpt.ListOptions = db.ListOptions{ + Page: -1, + } + if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error()) + return + } + } + + ctx.SetTotalCountHeader(filteredCount) + ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) +} + +func getUserIDForFilter(ctx *context.Context, queryName string) int64 { + userName := ctx.FormString(queryName) + if len(userName) == 0 { + return 0 + } + + user, err := user_model.GetUserByName(userName) + if user_model.IsErrUserNotExist(err) { + ctx.NotFound("", err) + return 0 + } + + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return 0 + } + + return user.ID +} + +// ListIssues list the issues of a repository +func ListIssues(ctx *context.Context) { + before, since, err := context.GetQueryBeforeSince(ctx) + if err != nil { + ctx.Error(http.StatusUnprocessableEntity, err.Error()) + return + } + + var isClosed util.OptionalBool + switch ctx.FormString("state") { + case "closed": + isClosed = util.OptionalBoolTrue + case "all": + isClosed = util.OptionalBoolNone + default: + isClosed = util.OptionalBoolFalse + } + + var issues []*models.Issue + var filteredCount int64 + + keyword := ctx.FormTrim("q") + if strings.IndexByte(keyword, 0) >= 0 { + keyword = "" + } + var issueIDs []int64 + var labelIDs []int64 + if len(keyword) > 0 { + issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } + + if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { + labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } + + var mileIDs []int64 + if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { + for i := range part { + // uses names and fall back to ids + // non existent milestones are discarded + mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i]) + if err == nil { + mileIDs = append(mileIDs, mile.ID) + continue + } + if !issues_model.IsErrMilestoneNotExist(err) { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + id, err := strconv.ParseInt(part[i], 10, 64) + if err != nil { + continue + } + mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id) + if err == nil { + mileIDs = append(mileIDs, mile.ID) + continue + } + if issues_model.IsErrMilestoneNotExist(err) { + continue + } + ctx.Error(http.StatusInternalServerError, err.Error()) + } + } + + listOptions := db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } + + var isPull util.OptionalBool + switch ctx.FormString("type") { + case "pulls": + isPull = util.OptionalBoolTrue + case "issues": + isPull = util.OptionalBoolFalse + default: + isPull = util.OptionalBoolNone + } + + // FIXME: we should be more efficient here + createdByID := getUserIDForFilter(ctx, "created_by") + if ctx.Written() { + return + } + assignedByID := getUserIDForFilter(ctx, "assigned_by") + if ctx.Written() { + return + } + mentionedByID := getUserIDForFilter(ctx, "mentioned_by") + if ctx.Written() { + return + } + + // Only fetch the issues if we either don't have a keyword or the search returned issues + // This would otherwise return all issues if no issues were found by the search. + if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { + issuesOpt := &models.IssuesOptions{ + ListOptions: listOptions, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + IsClosed: isClosed, + IssueIDs: issueIDs, + LabelIDs: labelIDs, + MilestoneIDs: mileIDs, + IsPull: isPull, + UpdatedBeforeUnix: before, + UpdatedAfterUnix: since, + PosterID: createdByID, + AssigneeID: assignedByID, + MentionedID: mentionedByID, + } + + if issues, err = models.Issues(issuesOpt); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + issuesOpt.ListOptions = db.ListOptions{ + Page: -1, + } + if filteredCount, err = models.CountIssues(issuesOpt); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + } + + ctx.SetTotalCountHeader(filteredCount) + ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) +} + // UpdateIssueStatus change issue's status func UpdateIssueStatus(ctx *context.Context) { issues := getActionIssues(ctx) diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index d43cb373a2..ec713238c6 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -29,7 +29,7 @@ func AddDependency(ctx *context.Context) { depID := ctx.FormInt64("newDependency") - if err = issue.LoadRepo(); err != nil { + if err = issue.LoadRepo(ctx); err != nil { ctx.ServerError("LoadRepo", err) return } @@ -88,7 +88,7 @@ func RemoveDependency(ctx *context.Context) { depID := ctx.FormInt64("removeDependencyID") - if err = issue.LoadRepo(); err != nil { + if err = issue.LoadRepo(ctx); err != nil { ctx.ServerError("LoadRepo", err) return } diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index 8e8f9555a3..7ef80e7725 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -64,18 +64,18 @@ func CancelStopwatch(c *context.Context) { } // GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context -func GetActiveStopwatch(c *context.Context) { - if strings.HasPrefix(c.Req.URL.Path, "/api") { +func GetActiveStopwatch(ctx *context.Context) { + if strings.HasPrefix(ctx.Req.URL.Path, "/api") { return } - if !c.IsSigned { + if !ctx.IsSigned { return } - _, sw, err := models.HasUserStopwatch(c.Doer.ID) + _, sw, err := models.HasUserStopwatch(ctx.Doer.ID) if err != nil { - c.ServerError("HasUserStopwatch", err) + ctx.ServerError("HasUserStopwatch", err) return } @@ -85,15 +85,15 @@ func GetActiveStopwatch(c *context.Context) { issue, err := models.GetIssueByID(sw.IssueID) if err != nil || issue == nil { - c.ServerError("GetIssueByID", err) + ctx.ServerError("GetIssueByID", err) return } - if err = issue.LoadRepo(); err != nil { - c.ServerError("LoadRepo", err) + if err = issue.LoadRepo(ctx); err != nil { + ctx.ServerError("LoadRepo", err) return } - c.Data["ActiveStopwatch"] = StopwatchTmplInfo{ + ctx.Data["ActiveStopwatch"] = StopwatchTmplInfo{ issue.Link(), issue.Repo.FullName(), issue.Index, diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index df5fd411b4..1e75bd79fb 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -9,8 +9,8 @@ import ( "net/url" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/markup" @@ -38,7 +38,7 @@ func Milestones(ctx *context.Context) { ctx.Data["PageIsMilestones"] = true isShowClosed := ctx.FormString("state") == "closed" - stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID})) + stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID})) if err != nil { ctx.ServerError("MilestoneStats", err) return @@ -60,7 +60,7 @@ func Milestones(ctx *context.Context) { state = structs.StateClosed } - miles, total, err := models.GetMilestones(models.GetMilestonesOption{ + miles, total, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -143,7 +143,7 @@ func NewMilestonePost(ctx *context.Context) { } deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) - if err = models.NewMilestone(&models.Milestone{ + if err = issues_model.NewMilestone(&issues_model.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Content, @@ -163,9 +163,9 @@ func EditMilestone(ctx *context.Context) { ctx.Data["PageIsMilestones"] = true ctx.Data["PageIsEditMilestone"] = true - m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetMilestoneByRepoID", err) @@ -203,9 +203,9 @@ func EditMilestonePost(ctx *context.Context) { } deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) - m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetMilestoneByRepoID", err) @@ -215,7 +215,7 @@ func EditMilestonePost(ctx *context.Context) { m.Name = form.Title m.Content = form.Content m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix()) - if err = models.UpdateMilestone(m, m.IsClosed); err != nil { + if err = issues_model.UpdateMilestone(m, m.IsClosed); err != nil { ctx.ServerError("UpdateMilestone", err) return } @@ -237,8 +237,8 @@ func ChangeMilestoneStatus(ctx *context.Context) { } id := ctx.ParamsInt64(":id") - if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { - if models.IsErrMilestoneNotExist(err) { + if err := issues_model.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", err) } else { ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err) @@ -250,7 +250,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { // DeleteMilestone delete a milestone func DeleteMilestone(ctx *context.Context) { - if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { + if err := issues_model.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success")) @@ -264,9 +264,9 @@ func DeleteMilestone(ctx *context.Context) { // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone func MilestoneIssuesAndPulls(ctx *context.Context) { milestoneID := ctx.ParamsInt64(":id") - milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID) + milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { - if models.IsErrMilestoneNotExist(err) { + if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("GetMilestoneByID", err) return } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index f796bb0de5..b4db2d5787 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -32,10 +32,10 @@ func Packages(ctx *context.Context) { PageSize: setting.UI.PackagesPagingNum, Page: page, }, - OwnerID: ctx.ContextUser.ID, - RepoID: ctx.Repo.Repository.ID, - QueryName: query, - Type: packageType, + OwnerID: ctx.ContextUser.ID, + RepoID: ctx.Repo.Repository.ID, + Type: packages.Type(packageType), + Name: packages.SearchValue{Value: query}, }) if err != nil { ctx.ServerError("SearchLatestVersions", err) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index b324ae4d2e..113e2d8421 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -266,7 +266,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue { ctx.ServerError("LoadPoster", err) return nil } - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(ctx); err != nil { ctx.ServerError("LoadRepo", err) return nil } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 989c1a565e..60298121df 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -20,11 +20,14 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" repo_service "code.gitea.io/gitea/services/repository" @@ -503,3 +506,112 @@ func InitiateDownload(ctx *context.Context) { "complete": completed, }) } + +// SearchRepo repositories via options +func SearchRepo(ctx *context.Context) { + opts := &models.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }, + Actor: ctx.Doer, + Keyword: ctx.FormTrim("q"), + OwnerID: ctx.FormInt64("uid"), + PriorityOwnerID: ctx.FormInt64("priority_owner_id"), + TeamID: ctx.FormInt64("team_id"), + TopicOnly: ctx.FormBool("topic"), + Collaborate: util.OptionalBoolNone, + Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), + Template: util.OptionalBoolNone, + StarredByID: ctx.FormInt64("starredBy"), + IncludeDescription: ctx.FormBool("includeDesc"), + } + + if ctx.FormString("template") != "" { + opts.Template = util.OptionalBoolOf(ctx.FormBool("template")) + } + + if ctx.FormBool("exclusive") { + opts.Collaborate = util.OptionalBoolFalse + } + + mode := ctx.FormString("mode") + switch mode { + case "source": + opts.Fork = util.OptionalBoolFalse + opts.Mirror = util.OptionalBoolFalse + case "fork": + opts.Fork = util.OptionalBoolTrue + case "mirror": + opts.Mirror = util.OptionalBoolTrue + case "collaborative": + opts.Mirror = util.OptionalBoolFalse + opts.Collaborate = util.OptionalBoolTrue + case "": + default: + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode)) + return + } + + if ctx.FormString("archived") != "" { + opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived")) + } + + if ctx.FormString("is_private") != "" { + opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private")) + } + + sortMode := ctx.FormString("sort") + if len(sortMode) > 0 { + sortOrder := ctx.FormString("order") + if len(sortOrder) == 0 { + sortOrder = "asc" + } + if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { + if orderBy, ok := searchModeMap[sortMode]; ok { + opts.OrderBy = orderBy + } else { + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode)) + return + } + } else { + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder)) + return + } + } + + var err error + repos, count, err := models.SearchRepository(opts) + if err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + return + } + + results := make([]*api.Repository, len(repos)) + for i, repo := range repos { + if err = repo.GetOwner(ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + return + } + accessMode, err := models.AccessLevel(ctx.Doer, repo) + if err != nil { + ctx.JSON(http.StatusInternalServerError, api.SearchError{ + OK: false, + Error: err.Error(), + }) + } + results[i] = convert.ToRepo(repo, accessMode) + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, api.SearchResults{ + OK: true, + Data: results, + }) +} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 156cf0fa64..33492aa209 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -219,13 +220,13 @@ func Milestones(ctx *context.Context) { } } - counts, err := models.CountMilestonesByRepoCondAndKw(userRepoCond, keyword, isShowClosed) + counts, err := issues_model.CountMilestonesByRepoCondAndKw(userRepoCond, keyword, isShowClosed) if err != nil { ctx.ServerError("CountMilestonesByRepoIDs", err) return } - milestones, err := models.SearchMilestones(repoCond, page, isShowClosed, sortType, keyword) + milestones, err := issues_model.SearchMilestones(repoCond, page, isShowClosed, sortType, keyword) if err != nil { ctx.ServerError("SearchMilestones", err) return @@ -271,17 +272,17 @@ func Milestones(ctx *context.Context) { i++ } - milestoneStats, err := models.GetMilestonesStatsByRepoCondAndKw(repoCond, keyword) + milestoneStats, err := issues_model.GetMilestonesStatsByRepoCondAndKw(repoCond, keyword) if err != nil { ctx.ServerError("GetMilestoneStats", err) return } - var totalMilestoneStats *models.MilestonesStats + var totalMilestoneStats *issues_model.MilestonesStats if len(repoIDs) == 0 { totalMilestoneStats = milestoneStats } else { - totalMilestoneStats, err = models.GetMilestonesStatsByRepoCondAndKw(userRepoCond, keyword) + totalMilestoneStats, err = issues_model.GetMilestonesStatsByRepoCondAndKw(userRepoCond, keyword) if err != nil { ctx.ServerError("GetMilestoneStats", err) return diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 04e987924d..f7848de90a 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" ) const ( @@ -191,3 +192,8 @@ func NotificationPurgePost(c *context.Context) { c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) } + +// NewAvailable returns the notification counts +func NewAvailable(ctx *context.APIContext) { + ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)}) +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index edbb4aadf6..04b4e1e8ec 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -43,9 +43,9 @@ func ListPackages(ctx *context.Context) { PageSize: setting.UI.PackagesPagingNum, Page: page, }, - OwnerID: ctx.ContextUser.ID, - Type: packageType, - QueryName: query, + OwnerID: ctx.ContextUser.ID, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, }) if err != nil { ctx.ServerError("SearchLatestVersions", err) @@ -219,9 +219,12 @@ func ListPackageVersions(ctx *context.Context) { } default: pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - Paginator: pagination, - PackageID: p.ID, - QueryVersion: query, + Paginator: pagination, + PackageID: p.ID, + Version: packages_model.SearchValue{ + ExactMatch: false, + Value: query, + }, }) if err != nil { ctx.ServerError("SearchVersions", err) @@ -256,7 +259,8 @@ func PackageSettings(ctx *context.Context) { ctx.Data["PackageDescriptor"] = pd repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{ - Actor: pd.Owner, + Actor: pd.Owner, + Private: true, }) ctx.Data["Repos"] = repos ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() diff --git a/routers/web/user/search.go b/routers/web/user/search.go new file mode 100644 index 0000000000..328c7bade4 --- /dev/null +++ b/routers/web/user/search.go @@ -0,0 +1,44 @@ +// 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 user + +import ( + "net/http" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" +) + +// Search search users +func Search(ctx *context.Context) { + listOptions := db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } + + users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{ + Actor: ctx.Doer, + Keyword: ctx.FormTrim("q"), + UID: ctx.FormInt64("uid"), + Type: user_model.UserTypeIndividual, + ListOptions: listOptions, + }) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + + ctx.SetTotalCountHeader(maxResults) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + "data": convert.ToUsers(ctx.Doer, users), + }) +} diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go new file mode 100644 index 0000000000..4b16c9aeda --- /dev/null +++ b/routers/web/user/stop_watch.go @@ -0,0 +1,41 @@ +// 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 user + +import ( + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" +) + +// GetStopwatches get all stopwatches +func GetStopwatches(ctx *context.Context) { + sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + count, err := models.CountUserStopwatches(ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + apiSWs, err := convert.ToStopWatches(sws) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSWs) +} diff --git a/routers/web/web.go b/routers/web/web.go index 3bdedab854..190ab099e0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -20,17 +20,18 @@ import ( "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/routing" - "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/web/admin" "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/routers/web/dev" "code.gitea.io/gitea/routers/web/events" "code.gitea.io/gitea/routers/web/explore" "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/routers/web/org" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/user" @@ -45,6 +46,7 @@ import ( _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters "gitea.com/go-chi/captcha" + "gitea.com/go-chi/session" "github.com/NYTimes/gziphandler" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" @@ -84,7 +86,7 @@ func buildAuthGroup() *auth_service.Group { group := auth_service.NewGroup( &auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth realted routers &auth_service.Basic{}, // FIXME: this should be removed and only applied in download and git/lfs routers - auth_service.SharedSession, + &auth_service.Session{}, ) if setting.Service.EnableReverseProxyAuth { group.Add(&auth_service.ReverseProxy{}) @@ -95,7 +97,7 @@ func buildAuthGroup() *auth_service.Group { } // Routes returns all web routes -func Routes(sessioner func(http.Handler) http.Handler) *web.Route { +func Routes() *web.Route { routes := web.NewRoute() routes.Use(web.WrapWithPrefix(public.AssetsURLPathPrefix, public.AssetsHandlerFunc(&public.Options{ @@ -104,6 +106,17 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route { CorsHandler: CorsHandler(), }), "AssetsHandler")) + sessioner := session.Sessioner(session.Options{ + Provider: setting.SessionConfig.Provider, + ProviderConfig: setting.SessionConfig.ProviderConfig, + CookieName: setting.SessionConfig.CookieName, + CookiePath: setting.SessionConfig.CookiePath, + Gclifetime: setting.SessionConfig.Gclifetime, + Maxlifetime: setting.SessionConfig.Maxlifetime, + Secure: setting.SessionConfig.Secure, + SameSite: setting.SessionConfig.SameSite, + Domain: setting.SessionConfig.Domain, + }) routes.Use(sessioner) routes.Use(Recovery()) @@ -289,8 +302,13 @@ func RegisterRoutes(m *web.Route) { m.Get("/users", explore.Users) m.Get("/organizations", explore.Organizations) m.Get("/code", explore.Code) + m.Get("/topics/search", explore.TopicSearch) }, ignExploreSignIn) - m.Get("/issues", reqSignIn, user.Issues) + m.Group("/issues", func() { + m.Get("", user.Issues) + m.Get("/search", repo.SearchIssues) + }, reqSignIn) + m.Get("/pulls", reqSignIn, user.Pulls) m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones) @@ -421,6 +439,8 @@ func RegisterRoutes(m *web.Route) { m.Post("/forgot_password", auth.ForgotPasswdPost) m.Post("/logout", auth.SignOut) m.Get("/task/{task}", user.TaskStatus) + m.Get("/stopwatches", user.GetStopwatches, reqSignIn) + m.Get("/search", user.Search, ignExploreSignIn) }) // ***** END: User ***** @@ -605,6 +625,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/{org}", func() { m.Get("/teams/new", org.NewTeam) m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost) + m.Get("/teams/-/search", org.SearchTeam) m.Get("/teams/{team}/edit", org.EditTeam) m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost) m.Post("/teams/{team}/delete", org.DeleteTeam) @@ -669,6 +690,7 @@ func RegisterRoutes(m *web.Route) { m.Combo("/{repoid}").Get(repo.Fork). Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost) }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) + m.Get("/search", repo.SearchRepo) }, reqSignIn) m.Group("/{username}/-", func() { @@ -811,13 +833,16 @@ func RegisterRoutes(m *web.Route) { Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost) m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) }) + m.Get("/search", repo.ListIssues) }, context.RepoMustNotBeArchived(), reqRepoIssueReader) // FIXME: should use different URLs but mostly same logic for comments of issue and pull request. // So they can apply their own enable/disable logic on routers. m.Group("/{type:issues|pulls}", func() { m.Group("/{index}", func() { + m.Get("/info", repo.GetIssueInfo) m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) + m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Post("/watch", repo.IssueWatch) m.Post("/ref", repo.UpdateIssueRef) m.Group("/dependency", func() { @@ -865,6 +890,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/comments/{id}", func() { m.Get("/attachments", repo.GetCommentAttachments) }) + m.Post("/markdown", bindIgnErr(structs.MarkdownOption{}), misc.Markdown) m.Group("/labels", func() { m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel) m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel) @@ -1195,6 +1221,7 @@ func RegisterRoutes(m *web.Route) { m.Get("", user.Notifications) m.Post("/status", user.NotificationStatusPost) m.Post("/purge", user.NotificationPurgePost) + m.Get("/new", user.NewAvailable) }, reqSignIn) if setting.API.EnableSwagger { diff --git a/services/auth/auth.go b/services/auth/auth.go index 15df47da33..3a5bb9d27e 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -20,16 +20,6 @@ import ( "code.gitea.io/gitea/modules/web/middleware" ) -// The purpose of the following three function variables is to let the linter know that -// those functions are not dead code and are actually being used -var ( - _ = handleSignIn - - // SharedSession the session auth should only be used by web, but now both web and API/v1 - // will use it. We can remove this after Web removed dependent API/v1 - SharedSession = &Session{} -) - // Init should be called exactly once when the application starts to allow plugins // to allocate necessary resources func Init() { diff --git a/services/issue/label.go b/services/issue/label.go index 9971be326c..e72e1cb521 100644 --- a/services/issue/label.go +++ b/services/issue/label.go @@ -6,6 +6,7 @@ package issue import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/notification" ) @@ -43,7 +44,7 @@ func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Labe // RemoveLabel removes a label from issue by given ID. func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error { - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(db.DefaultContext); err != nil { return err } diff --git a/services/issue/milestone.go b/services/issue/milestone.go index 999da50844..287f8ae285 100644 --- a/services/issue/milestone.go +++ b/services/issue/milestone.go @@ -5,15 +5,68 @@ package issue import ( + "context" + "fmt" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/notification" ) +func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *models.Issue, oldMilestoneID int64) error { + if err := models.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil { + return err + } + + if oldMilestoneID > 0 { + if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil { + return err + } + } + + if issue.MilestoneID > 0 { + if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return err + } + } + + if oldMilestoneID > 0 || issue.MilestoneID > 0 { + if err := issue.LoadRepo(ctx); err != nil { + return err + } + + opts := &models.CreateCommentOptions{ + Type: models.CommentTypeMilestone, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldMilestoneID: oldMilestoneID, + MilestoneID: issue.MilestoneID, + } + if _, err := models.CreateCommentCtx(ctx, opts); err != nil { + return err + } + } + + return nil +} + // ChangeMilestoneAssign changes assignment of milestone for issue. func ChangeMilestoneAssign(issue *models.Issue, doer *user_model.User, oldMilestoneID int64) (err error) { - if err = models.ChangeMilestoneAssign(issue, doer, oldMilestoneID); err != nil { - return + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = changeMilestoneAssign(ctx, doer, issue, oldMilestoneID); err != nil { + return err + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) } notification.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID) diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go new file mode 100644 index 0000000000..80e37a8acd --- /dev/null +++ b/services/issue/milestone_test.go @@ -0,0 +1,35 @@ +// Copyright 2019 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 issue + +import ( + "testing" + + "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestChangeMilestoneAssign(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: 1}).(*models.Issue) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + assert.NotNil(t, issue) + assert.NotNil(t, doer) + + oldMilestoneID := issue.MilestoneID + issue.MilestoneID = 2 + assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID)) + unittest.AssertExistsAndLoadBean(t, &models.Comment{ + IssueID: issue.ID, + Type: models.CommentTypeMilestone, + MilestoneID: issue.MilestoneID, + OldMilestoneID: oldMilestoneID, + }) + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &models.Issue{}) +} diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 1e90deb4c4..a5b60f71ec 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/models" + "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/base" @@ -404,7 +405,7 @@ func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content s return nil } - if err := issue.LoadRepo(); err != nil { + if err := issue.LoadRepo(db.DefaultContext); err != nil { log.Error("Unable to load repo [%d] for issue #%d [%d]. Error: %v", issue.RepoID, issue.Index, issue.ID, err) return err } diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index 7f0fb9a3e1..c24edf50c9 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -40,7 +40,7 @@ const ( // 2. Users who are not in 1. but get mentioned in current issue/comment. func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_model.User) error { // Required by the mail composer; make sure to load these before calling the async function - if err := ctx.Issue.LoadRepo(); err != nil { + if err := ctx.Issue.LoadRepo(ctx); err != nil { return fmt.Errorf("LoadRepo(): %v", err) } if err := ctx.Issue.LoadPoster(); err != nil { diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index c06bae1b98..baf426146a 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -14,6 +14,7 @@ import ( texttmpl "text/template" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -57,7 +58,7 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer}).(*repo_model.Repository) issue = unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue) - assert.NoError(t, issue.LoadRepo()) + assert.NoError(t, issue.LoadRepo(db.DefaultContext)) comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment) return } @@ -180,7 +181,7 @@ func TestTemplateSelection(t *testing.T) { func TestTemplateServices(t *testing.T) { doer, _, issue, comment := prepareMailerTest(t) - assert.NoError(t, issue.LoadRepo()) + assert.NoError(t, issue.LoadRepo(db.DefaultContext)) expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *user_model.User, actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string, diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 9d2a7eb41b..8529f24895 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -79,7 +79,7 @@ func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { case "comment": return db.MaxBatchInsertSize(new(models.Comment)) case "milestone": - return db.MaxBatchInsertSize(new(models.Milestone)) + return db.MaxBatchInsertSize(new(issues_model.Milestone)) case "label": return db.MaxBatchInsertSize(new(models.Label)) case "release": @@ -164,7 +164,7 @@ func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { // CreateMilestones creates milestones func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error { - mss := make([]*models.Milestone, 0, len(milestones)) + mss := make([]*issues_model.Milestone, 0, len(milestones)) for _, milestone := range milestones { var deadline timeutil.TimeStamp if milestone.Deadline != nil { @@ -187,7 +187,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err milestone.Updated = &milestone.Created } - ms := models.Milestone{ + ms := issues_model.Milestone{ RepoID: g.repo.ID, Name: milestone.Title, Content: milestone.Description, diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index ad5caa4279..51c7ad9717 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models" "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/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -65,14 +66,14 @@ func TestGiteaUploadRepo(t *testing.T) { assert.True(t, repo.HasWiki()) assert.EqualValues(t, repo_model.RepositoryReady, repo.Status) - milestones, _, err := models.GetMilestones(models.GetMilestonesOption{ + milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: repo.ID, State: structs.StateOpen, }) assert.NoError(t, err) assert.Len(t, milestones, 1) - milestones, _, err = models.GetMilestones(models.GetMilestonesOption{ + milestones, _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ RepoID: repo.ID, State: structs.StateClosed, }) diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index 91992a4d7f..390a0b7b05 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -10,6 +10,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" + "code.gitea.io/gitea/modules/util" ) // Cleanup removes expired container data @@ -43,10 +44,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e return err } - versions := make(map[int64]struct{}) for _, pf := range pfs { - versions[pf.VersionID] = struct{}{} - if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { return err } @@ -55,19 +53,26 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e } } - for versionID := range versions { - has, err := packages_model.HasVersionFileReferences(ctx, versionID) - if err != nil { + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + Type: packages_model.TypeContainer, + Version: packages_model.SearchValue{ + ExactMatch: true, + Value: container_model.UploadVersion, + }, + IsInternal: true, + HasFiles: util.OptionalBoolFalse, + }) + if err != nil { + return err + } + + for _, pv := range pvs { + if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil { return err } - if !has { - if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, versionID); err != nil { - return err - } - if err := packages_model.DeleteVersionByID(ctx, versionID); err != nil { - return err - } + if err := packages_model.DeleteVersionByID(ctx, pv.ID); err != nil { + return err } } diff --git a/services/packages/packages.go b/services/packages/packages.go index 7f90f80baf..7f25fce5b8 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -336,7 +336,7 @@ func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) erro return packages_model.DeleteFileByID(ctx, pf.ID) } -// Cleanup removes old unreferenced package blobs +// Cleanup removes expired package data func Cleanup(unused context.Context, olderThan time.Duration) error { ctx, committer, err := db.TxContext() if err != nil { @@ -345,24 +345,20 @@ func Cleanup(unused context.Context, olderThan time.Duration) error { defer committer.Close() if err := container_service.Cleanup(ctx, olderThan); err != nil { - log.Error("hier") return err } if err := packages_model.DeletePackagesIfUnreferenced(ctx); err != nil { - log.Error("hier2") return err } pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan) if err != nil { - log.Error("hier3") return err } for _, pb := range pbs { if err := packages_model.DeleteBlobByID(ctx, pb.ID); err != nil { - log.Error("hier4") return err } } @@ -403,10 +399,9 @@ func GetFileStreamByPackageVersionAndFileID(ctx context.Context, owner *user_mod pv, err := packages_model.GetVersionByID(ctx, versionID) if err != nil { - if err == packages_model.ErrPackageVersionNotExist { - return nil, nil, packages_model.ErrPackageNotExist + if err != packages_model.ErrPackageNotExist { + log.Error("Error getting package version: %v", err) } - log.Error("Error getting package version: %v", err) return nil, nil, err } diff --git a/services/pull/merge.go b/services/pull/merge.go index 6ecb3cf08e..0c615d93c8 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -76,7 +76,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b log.Error("loadIssue [%d]: %v", pr.ID, err) } - if err := pr.Issue.LoadRepo(); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { log.Error("loadRepo for issue [%d]: %v", pr.ID, err) } if err := pr.Issue.Repo.GetOwner(ctx); err != nil { @@ -99,7 +99,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b if err = ref.LoadIssue(); err != nil { return err } - if err = ref.Issue.LoadRepo(); err != nil { + if err = ref.Issue.LoadRepo(ctx); err != nil { return err } close := ref.RefAction == references.XRefActionCloses diff --git a/services/pull/review.go b/services/pull/review.go index cb21d736cf..e7e6f3135b 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -44,7 +44,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. // Comments that are replies don't require a review header to show up in the issue view if !isReview && existsReview { - if err = issue.LoadRepo(); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return nil, err } diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index f115a5f499..b7135de59f 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -162,7 +162,7 @@
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index cb7234b3b0..7f7fb7e329 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -11,7 +11,7 @@
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 5e3351c865..8135348b9b 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -31,13 +31,13 @@
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 820055c136..46a2a3969b 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -195,7 +195,7 @@
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index aed155fdbf..e673add812 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -429,7 +429,7 @@ {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}