diff --git a/Makefile b/Makefile index d7ddd197b..4f4802a02 100644 --- a/Makefile +++ b/Makefile @@ -183,15 +183,15 @@ test-pgsql: integrations.test generate-ini .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test - GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.bench . + GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-mysql bench-mysql: integrations.test generate-ini - GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/mysql.ini ./integrations.test -test.bench . + GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/mysql.ini ./integrations.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-pgsql bench-pgsql: integrations.test generate-ini - GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/pgsql.ini ./integrations.test -test.bench . + GITEA_ROOT=${CURDIR} GITEA_CONF=integrations/pgsql.ini ./integrations.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: integration-test-coverage diff --git a/cmd/web.go b/cmd/web.go index 55546ea48..473688a8b 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -51,6 +51,26 @@ and it takes care of all the other things for you`, }, } +func runHTTPRedirector() { + source := fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.PortToRedirect) + dest := strings.TrimSuffix(setting.AppURL, "/") + log.Info("Redirecting: %s to %s", source, dest) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + target := dest + r.URL.Path + if len(r.URL.RawQuery) > 0 { + target += "?" + r.URL.RawQuery + } + http.Redirect(w, r, target, http.StatusTemporaryRedirect) + }) + + var err = runHTTP(source, context2.ClearHandler(handler)) + + if err != nil { + log.Fatal(4, "Failed to start port redirection: %v", err) + } +} + func runWeb(ctx *cli.Context) error { if ctx.IsSet("config") { setting.CustomConf = ctx.String("config") @@ -124,6 +144,9 @@ func runWeb(ctx *cli.Context) error { case setting.HTTP: err = runHTTP(listenAddr, context2.ClearHandler(m)) case setting.HTTPS: + if setting.RedirectOtherPort { + go runHTTPRedirector() + } err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m)) case setting.FCGI: listener, err := net.Listen("tcp", listenAddr) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index b1994f9a1..1edbfbd48 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -16,6 +16,8 @@ SCRIPT_TYPE = bash ANSI_CHARSET = ; Force every new repository to be private FORCE_PRIVATE = false +; Default private when create a new repository, could be: last, private, public. Default is last which means last user repo visiblity. +DEFAULT_PRIVATE = last ; Global maximum creation limit of repository per user, -1 means no limit MAX_CREATION_LIMIT = -1 ; Mirror sync queue length, increase if mirror syncing starts hanging @@ -107,6 +109,12 @@ ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ; Listen address. Either a IPv4/IPv6 address or the path to a unix socket. HTTP_ADDR = 0.0.0.0 HTTP_PORT = 3000 +; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server +; will be started on PORT_TO_REDIRECT and redirect request to the main +; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for +; PORT_TO_REDIRECT. +REDIRECT_OTHER_PORT = false +PORT_TO_REDIRECT = 80 ; Permission for unix socket UNIX_SOCKET_PERMISSION = 666 ; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. 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 3aafb8e8f..dcaaea0d1 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -39,6 +39,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `SCRIPT_TYPE`: The script type your server supports, usually this is `bash`, but some customers report that they only have `sh`. - `ANSI_CHARSET`: The default charset for an unrecognized charset. - `FORCE_PRIVATE`: Force every new repository to be private. +- `DEFAULT_PRIVATE`: Default private when create a new repository, could be: `last`, `private` and `public`. Default is last which means last user repo visiblity. - `MAX_CREATION_LIMIT`: Global maximum creation limit of repositories per user, `-1` means no limit. - `PULL_REQUEST_QUEUE_LENGTH`:exclamation:: Length of pull request patch test queue, make it as large as possible. - `MIRROR_QUEUE_LENGTH`: Patch test queue length, increase if pull request patch testing starts hanging. Defaults to 1000. @@ -86,6 +87,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `LFS_START_SERVER`: Enables git-lfs support. `true` or `false`, default is `false`. - `LFS_CONTENT_PATH`: Where your lfs files put on, default is `data/lfs`. - `LFS_JWT_SECRET`: LFS authentication secret, changed this to yourself. +- `REDIRECT_OTHER_PORT`: If true and `PROTOCOL` is https, redirects http requests on another port to `ROOT_URL`, default is `false`. +- `PORT_TO_REDIRECT`: Port used when `REDIRECT_OTHER_PORT` is true, default is `80`. ## Database (`database`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 7ec650ddb..9a11d960c 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -29,6 +29,7 @@ menu: - `SCRIPT_TYPE`: 服务器支持的Shell类型,通常是 `bash`,但有些服务器也有可能是 `sh`。 - `ANSI_CHARSET`: 默认字符编码。 - `FORCE_PRIVATE`: 强制所有git工程必须私有。 +- `DEFAULT_PRIVATE`: 默认创建的git工程为私有。 可以是`last`, `private` 或 `public`。默认值是 `last`表示用户最后创建的Repo的选择。 - `MAX_CREATION_LIMIT`: 全局最大每个用户创建的git工程数目, `-1` 表示没限制。 - `PULL_REQUEST_QUEUE_LENGTH`: 小心:合并请求测试队列的长度,尽量放大。 diff --git a/integrations/benchmarks_test.go b/integrations/benchmarks_test.go new file mode 100644 index 000000000..7c4196f2b --- /dev/null +++ b/integrations/benchmarks_test.go @@ -0,0 +1,113 @@ +// 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 ( + "math/rand" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/sdk/gitea" +) + +func BenchmarkRepo(b *testing.B) { + samples := []struct { + url string + name string + skipShort bool + }{ + {url: "https://github.com/go-gitea/gitea.git", name: "gitea"}, + {url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"}, + {url: "https://github.com/moby/moby.git", name: "moby", skipShort: true}, + {url: "https://github.com/golang/go.git", name: "go", skipShort: true}, + {url: "https://github.com/torvalds/linux.git", name: "linux", skipShort: true}, + } + prepareTestEnv(b) + session := loginUser(b, "user2") + b.ResetTimer() + + for _, s := range samples { + b.Run(s.name, func(b *testing.B) { + if testing.Short() && s.skipShort { + b.Skip("skipping test in short mode.") + } + b.Run("Migrate", func(b *testing.B) { + for i := 0; i < b.N; i++ { + testRepoMigrate(b, session, s.url, s.name) + } + }) + b.Run("Access", func(b *testing.B) { + var branches []*api.Branch + b.Run("APIBranchList", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req := NewRequestf(b, "GET", "/api/v1/repos/%s/%s/branches", "user2", s.name) + resp := session.MakeRequest(b, req, http.StatusOK) + b.StopTimer() + if len(branches) == 0 { + DecodeJSON(b, resp, &branches) //Store for next phase + } + b.StartTimer() + } + }) + branchCount := len(branches) + b.Run("WebViewCommit", func(b *testing.B) { + for i := 0; i < b.N; i++ { + req := NewRequestf(b, "GET", "/%s/%s/commit/%s", "user2", s.name, branches[i%branchCount].Commit.ID) + session.MakeRequest(b, req, http.StatusOK) + } + }) + }) + }) + } +} + +//StringWithCharset random string (from https://www.calhoun.io/creating-random-strings-in-go/) +func StringWithCharset(length int, charset string) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +func BenchmarkRepoBranchCommit(b *testing.B) { + samples := []int64{1, 3, 15, 16} + prepareTestEnv(b) + b.ResetTimer() + + for _, repoID := range samples { + b.StopTimer() + repo := models.AssertExistsAndLoadBean(b, &models.Repository{ID: repoID}).(*models.Repository) + b.StartTimer() + b.Run(repo.Name, func(b *testing.B) { + owner := models.AssertExistsAndLoadBean(b, &models.User{ID: repo.OwnerID}).(*models.User) + session := loginUser(b, owner.LoginName) + b.ResetTimer() + b.Run("Create", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + branchName := StringWithCharset(5+rand.Intn(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + b.StartTimer() + testCreateBranch(b, session, owner.LoginName, repo.Name, "branch/master", branchName, http.StatusFound) + } + }) + b.Run("Access", func(b *testing.B) { + var branches []*api.Branch + req := NewRequestf(b, "GET", "/api/v1/%s/branches", repo.FullName()) + resp := session.MakeRequest(b, req, http.StatusOK) + DecodeJSON(b, resp, &branches) + branchCount := len(branches) + b.ResetTimer() //We measure from here + for i := 0; i < b.N; i++ { + req := NewRequestf(b, "GET", "/%s/%s/commits/%s", owner.Name, repo.Name, branches[i%branchCount]) + session.MakeRequest(b, req, http.StatusOK) + } + }) + }) + } +} + +//TODO list commits /repos/{owner}/{repo}/commits diff --git a/integrations/repo_branch_test.go b/integrations/repo_branch_test.go index c20fd6192..fb33778cd 100644 --- a/integrations/repo_branch_test.go +++ b/integrations/repo_branch_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" ) -func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { +func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { var csrf string if expectedStatus == http.StatusNotFound { csrf = GetCSRF(t, session, path.Join(user, repo, "src/branch/master")) diff --git a/integrations/repo_migrate_test.go b/integrations/repo_migrate_test.go index 9f41cca82..41791c1d0 100644 --- a/integrations/repo_migrate_test.go +++ b/integrations/repo_migrate_test.go @@ -40,29 +40,3 @@ func TestRepoMigrate(t *testing.T) { session := loginUser(t, "user2") testRepoMigrate(t, session, "https://github.com/go-gitea/git.git", "git") } - -func BenchmarkRepoMigrate(b *testing.B) { - samples := []struct { - url string - name string - }{ - {url: "https://github.com/go-gitea/gitea.git", name: "gitea"}, - {url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"}, - {url: "https://github.com/moby/moby.git", name: "moby"}, - {url: "https://github.com/golang/go.git", name: "go"}, - {url: "https://github.com/torvalds/linux.git", name: "linux"}, - } - - prepareTestEnv(b) - session := loginUser(b, "user2") - b.ResetTimer() - - for _, s := range samples { - b.Run(s.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { - testRepoMigrate(b, session, s.url, s.name) - } - - }) - } -} diff --git a/models/attachment.go b/models/attachment.go index e7e0dbf5c..acb1f0716 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -135,19 +135,28 @@ func DeleteAttachment(a *Attachment, remove bool) error { // DeleteAttachments deletes the given attachments and optionally the associated files. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { - for i, a := range attachments { - if remove { + if len(attachments) == 0 { + return 0, nil + } + + var ids = make([]int64, 0, len(attachments)) + for _, a := range attachments { + ids = append(ids, a.ID) + } + + cnt, err := x.In("id", ids).NoAutoCondition().Delete(attachments[0]) + if err != nil { + return 0, err + } + + if remove { + for i, a := range attachments { if err := os.Remove(a.LocalPath()); err != nil { return i, err } } - - if _, err := x.Delete(a); err != nil { - return i, err - } } - - return len(attachments), nil + return int(cnt), nil } // DeleteAttachmentsByIssue deletes all attachments associated with the given issue. diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index 2592ad2de..ff514e706 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -47,6 +47,8 @@ content: content for the fourth issue is_closed: true is_pull: false + created_unix: 946684830 + updated_unix: 978307200 - id: 5 @@ -57,6 +59,9 @@ content: content for the fifth issue is_closed: true is_pull: false + created_unix: 946684840 + updated_unix: 978307200 + - id: 6 repo_id: 3 @@ -68,5 +73,5 @@ is_closed: false is_pull: false num_comments: 0 - created_unix: 946684800 + created_unix: 946684850 updated_unix: 978307200 diff --git a/models/issue.go b/models/issue.go index 26e513b48..5aaae5e2c 100644 --- a/models/issue.go +++ b/models/issue.go @@ -10,14 +10,15 @@ import ( "sort" "strings" - api "code.gitea.io/sdk/gitea" - "github.com/Unknwon/com" - "github.com/go-xorm/xorm" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + api "code.gitea.io/sdk/gitea" + + "github.com/Unknwon/com" + "github.com/go-xorm/builder" + "github.com/go-xorm/xorm" ) // Issue represents an issue or pull request of repository. @@ -1022,12 +1023,11 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) { // IssuesOptions represents options of an issue. type IssuesOptions struct { - RepoID int64 + RepoIDs []int64 // include all repos if empty AssigneeID int64 PosterID int64 MentionedID int64 MilestoneID int64 - RepoIDs []int64 Page int PageSize int IsClosed util.OptionalBool @@ -1073,9 +1073,7 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error { sess.In("issue.id", opts.IssueIDs) } - if opts.RepoID > 0 { - sess.And("issue.repo_id=?", opts.RepoID) - } else if len(opts.RepoIDs) > 0 { + if len(opts.RepoIDs) > 0 { // In case repository IDs are provided but actually no repository has issue. sess.In("issue.repo_id", opts.RepoIDs) } @@ -1339,58 +1337,92 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { return stats, err } +// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. +type UserIssueStatsOptions struct { + UserID int64 + RepoID int64 + UserRepoIDs []int64 + FilterMode int + IsPull bool + IsClosed bool +} + // GetUserIssueStats returns issue statistic information for dashboard by given conditions. -func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPull bool) *IssueStats { +func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { + var err error stats := &IssueStats{} - countSession := func(isClosed, isPull bool, repoID int64, repoIDs []int64) *xorm.Session { - sess := x. - Where("issue.is_closed = ?", isClosed). - And("issue.is_pull = ?", isPull) - - if repoID > 0 { - sess.And("repo_id = ?", repoID) - } else if len(repoIDs) > 0 { - sess.In("repo_id", repoIDs) - } - - return sess + cond := builder.NewCond() + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull}) + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) } - stats.AssignCount, _ = countSession(false, isPull, repoID, nil). - And("assignee_id = ?", uid). - Count(new(Issue)) - - stats.CreateCount, _ = countSession(false, isPull, repoID, nil). - And("poster_id = ?", uid). - Count(new(Issue)) - - stats.YourRepositoriesCount, _ = countSession(false, isPull, repoID, repoIDs). - Count(new(Issue)) - - switch filterMode { + switch opts.FilterMode { case FilterModeAll: - stats.OpenCount, _ = countSession(false, isPull, repoID, repoIDs). + stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) - stats.ClosedCount, _ = countSession(true, isPull, repoID, repoIDs). + if err != nil { + return nil, err + } + stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) + if err != nil { + return nil, err + } case FilterModeAssign: - stats.OpenCount, _ = countSession(false, isPull, repoID, nil). - And("assignee_id = ?", uid). + stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). + And("assignee_id = ?", opts.UserID). Count(new(Issue)) - stats.ClosedCount, _ = countSession(true, isPull, repoID, nil). - And("assignee_id = ?", uid). + if err != nil { + return nil, err + } + stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). + And("assignee_id = ?", opts.UserID). Count(new(Issue)) + if err != nil { + return nil, err + } case FilterModeCreate: - stats.OpenCount, _ = countSession(false, isPull, repoID, nil). - And("poster_id = ?", uid). + stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). + And("poster_id = ?", opts.UserID). Count(new(Issue)) - stats.ClosedCount, _ = countSession(true, isPull, repoID, nil). - And("poster_id = ?", uid). + if err != nil { + return nil, err + } + stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). + And("poster_id = ?", opts.UserID). Count(new(Issue)) + if err != nil { + return nil, err + } } - return stats + cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) + stats.AssignCount, err = x.Where(cond). + And("assignee_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.CreateCount, err = x.Where(cond). + And("poster_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.YourRepositoriesCount, err = x.Where(cond). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). + Count(new(Issue)) + if err != nil { + return nil, err + } + + return stats, nil } // GetRepoIssueStats returns number of open and closed repository issues by given filter mode. diff --git a/models/issue_indexer.go b/models/issue_indexer.go index 3a2ad157c..26d053a5d 100644 --- a/models/issue_indexer.go +++ b/models/issue_indexer.go @@ -42,7 +42,7 @@ func populateIssueIndexer() error { } for _, repo := range repos { issues, err := Issues(&IssuesOptions{ - RepoID: repo.ID, + RepoIDs: []int64{repo.ID}, IsClosed: util.OptionalBoolNone, IsPull: util.OptionalBoolNone, }) diff --git a/models/issue_test.go b/models/issue_test.go index 47bb4feea..851fe684f 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -168,3 +168,114 @@ func TestUpdateIssueCols(t *testing.T) { assert.EqualValues(t, prevContent, updatedIssue.Content) AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } + +func TestIssues(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + for _, test := range []struct { + Opts IssuesOptions + ExpectedIssueIDs []int64 + }{ + { + IssuesOptions{ + AssigneeID: 1, + SortType: "oldest", + }, + []int64{1, 6}, + }, + { + IssuesOptions{ + RepoIDs: []int64{1, 3}, + SortType: "oldest", + Page: 1, + PageSize: 4, + }, + []int64{1, 2, 3, 5}, + }, + { + IssuesOptions{ + Labels: "1,2", + Page: 1, + PageSize: 4, + }, + []int64{5, 2, 1}, + }, + } { + issues, err := Issues(&test.Opts) + assert.NoError(t, err) + if assert.Len(t, issues, len(test.ExpectedIssueIDs)) { + for i, issue := range issues { + assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID) + } + } + } +} + +func TestGetUserIssueStats(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + for _, test := range []struct { + Opts UserIssueStatsOptions + ExpectedIssueStats IssueStats + }{ + { + UserIssueStatsOptions{ + UserID: 1, + RepoID: 1, + FilterMode: FilterModeAll, + }, + IssueStats{ + YourRepositoriesCount: 0, + AssignCount: 1, + CreateCount: 1, + OpenCount: 0, + ClosedCount: 0, + }, + }, + { + UserIssueStatsOptions{ + UserID: 1, + FilterMode: FilterModeAssign, + }, + IssueStats{ + YourRepositoriesCount: 0, + AssignCount: 2, + CreateCount: 2, + OpenCount: 2, + ClosedCount: 0, + }, + }, + { + UserIssueStatsOptions{ + UserID: 1, + FilterMode: FilterModeCreate, + }, + IssueStats{ + YourRepositoriesCount: 0, + AssignCount: 2, + CreateCount: 2, + OpenCount: 2, + ClosedCount: 0, + }, + }, + { + UserIssueStatsOptions{ + UserID: 2, + UserRepoIDs: []int64{1, 2}, + FilterMode: FilterModeAll, + IsClosed: true, + }, + IssueStats{ + YourRepositoriesCount: 2, + AssignCount: 0, + CreateCount: 2, + OpenCount: 1, + ClosedCount: 2, + }, + }, + } { + stats, err := GetUserIssueStats(test.Opts) + if !assert.NoError(t, err) { + continue + } + assert.Equal(t, test.ExpectedIssueStats, *stats) + } +} diff --git a/models/org.go b/models/org.go index b349e4c17..a28a8e28e 100644 --- a/models/org.go +++ b/models/org.go @@ -21,13 +21,13 @@ var ( ) // IsOwnedBy returns true if given user is in the owner team. -func (org *User) IsOwnedBy(uid int64) bool { +func (org *User) IsOwnedBy(uid int64) (bool, error) { return IsOrganizationOwner(org.ID, uid) } // IsOrgMember returns true if given user is member of organization. -func (org *User) IsOrgMember(uid int64) bool { - return org.IsOrganization() && IsOrganizationMember(org.ID, uid) +func (org *User) IsOrgMember(uid int64) (bool, error) { + return IsOrganizationMember(org.ID, uid) } func (org *User) getTeam(e Engine, name string) (*Team, error) { @@ -285,32 +285,32 @@ type OrgUser struct { } // IsOrganizationOwner returns true if given user is in the owner team. -func IsOrganizationOwner(orgID, uid int64) bool { - has, _ := x. +func IsOrganizationOwner(orgID, uid int64) (bool, error) { + return x. Where("is_owner=?", true). And("uid=?", uid). And("org_id=?", orgID). - Get(new(OrgUser)) - return has + Table("org_user"). + Exist() } // IsOrganizationMember returns true if given user is member of organization. -func IsOrganizationMember(orgID, uid int64) bool { - has, _ := x. +func IsOrganizationMember(orgID, uid int64) (bool, error) { + return x. Where("uid=?", uid). And("org_id=?", orgID). - Get(new(OrgUser)) - return has + Table("org_user"). + Exist() } // IsPublicMembership returns true if given user public his/her membership. -func IsPublicMembership(orgID, uid int64) bool { - has, _ := x. +func IsPublicMembership(orgID, uid int64) (bool, error) { + return x. Where("uid=?", uid). And("org_id=?", orgID). And("is_public=?", true). - Get(new(OrgUser)) - return has + Table("org_user"). + Exist() } func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) { @@ -401,8 +401,9 @@ func ChangeOrgUserStatus(orgID, uid int64, public bool) error { // AddOrgUser adds new user to given organization. func AddOrgUser(orgID, uid int64) error { - if IsOrganizationMember(orgID, uid) { - return nil + isAlreadyMember, err := IsOrganizationMember(orgID, uid) + if err != nil || isAlreadyMember { + return err } sess := x.NewSession() @@ -447,7 +448,9 @@ func RemoveOrgUser(orgID, userID int64) error { } // Check if the user to delete is the last member in owner team. - if IsOrganizationOwner(orgID, userID) { + if isOwner, err := IsOrganizationOwner(orgID, userID); err != nil { + return err + } else if isOwner { t, err := org.GetOwnerTeam() if err != nil { return err diff --git a/models/org_team.go b/models/org_team.go index dcbf07383..1e3bc2707 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" "strings" + + "code.gitea.io/gitea/modules/log" ) const ownerTeamName = "Owners" @@ -47,7 +49,12 @@ func (t *Team) IsOwnerTeam() bool { // IsMember returns true if given user is a member of team. func (t *Team) IsMember(userID int64) bool { - return IsTeamMember(t.OrgID, t.ID, userID) + isMember, err := IsTeamMember(t.OrgID, t.ID, userID) + if err != nil { + log.Error(4, "IsMember: %v", err) + return false + } + return isMember } func (t *Team) getRepositories(e Engine) error { @@ -413,17 +420,17 @@ type TeamUser struct { UID int64 `xorm:"UNIQUE(s)"` } -func isTeamMember(e Engine, orgID, teamID, userID int64) bool { - has, _ := e. +func isTeamMember(e Engine, orgID, teamID, userID int64) (bool, error) { + return e. Where("org_id=?", orgID). And("team_id=?", teamID). And("uid=?", userID). - Get(new(TeamUser)) - return has + Table("team_user"). + Exist() } // IsTeamMember returns true if given user is a member of team. -func IsTeamMember(orgID, teamID, userID int64) bool { +func IsTeamMember(orgID, teamID, userID int64) (bool, error) { return isTeamMember(x, orgID, teamID, userID) } @@ -471,8 +478,9 @@ func GetUserTeams(orgID, userID int64) ([]*Team, error) { // AddTeamMember adds new membership of given team to given organization, // the user will have membership to given organization automatically when needed. func AddTeamMember(team *Team, userID int64) error { - if IsTeamMember(team.OrgID, team.ID, userID) { - return nil + isAlreadyMember, err := IsTeamMember(team.OrgID, team.ID, userID) + if err != nil || isAlreadyMember { + return err } if err := AddOrgUser(team.OrgID, userID); err != nil { @@ -529,8 +537,9 @@ func AddTeamMember(team *Team, userID int64) error { } func removeTeamMember(e Engine, team *Team, userID int64) error { - if !isTeamMember(e, team.OrgID, team.ID, userID) { - return nil + isMember, err := isTeamMember(e, team.OrgID, team.ID, userID) + if err != nil || !isMember { + return err } // Check if the user to delete is the last member in owner team. @@ -566,7 +575,7 @@ func removeTeamMember(e Engine, team *Team, userID int64) error { // This must exist. ou := new(OrgUser) - _, err := e. + _, err = e. Where("uid = ?", userID). And("org_id = ?", team.OrgID). Get(ou) diff --git a/models/org_team_test.go b/models/org_team_test.go index 9afd733d8..05429c7cc 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -250,16 +250,21 @@ func TestDeleteTeam(t *testing.T) { func TestIsTeamMember(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) + test := func(orgID, teamID, userID int64, expected bool) { + isMember, err := IsTeamMember(orgID, teamID, userID) + assert.NoError(t, err) + assert.Equal(t, expected, isMember) + } - assert.True(t, IsTeamMember(3, 1, 2)) - assert.False(t, IsTeamMember(3, 1, 4)) - assert.False(t, IsTeamMember(3, 1, NonexistentID)) + test(3, 1, 2, true) + test(3, 1, 4, false) + test(3, 1, NonexistentID, false) - assert.True(t, IsTeamMember(3, 2, 2)) - assert.True(t, IsTeamMember(3, 2, 4)) + test(3, 2, 2, true) + test(3, 2, 4, true) - assert.False(t, IsTeamMember(3, NonexistentID, NonexistentID)) - assert.False(t, IsTeamMember(NonexistentID, NonexistentID, NonexistentID)) + test(3, NonexistentID, NonexistentID, false) + test(NonexistentID, NonexistentID, NonexistentID, false) } func TestGetTeamMembers(t *testing.T) { diff --git a/models/org_test.go b/models/org_test.go index 8f59af074..aef313d05 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -12,28 +12,44 @@ import ( func TestUser_IsOwnedBy(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) - assert.True(t, org.IsOwnedBy(2)) - assert.False(t, org.IsOwnedBy(1)) - assert.False(t, org.IsOwnedBy(3)) - assert.False(t, org.IsOwnedBy(4)) - - nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - assert.False(t, nonOrg.IsOwnedBy(2)) - assert.False(t, nonOrg.IsOwnedBy(3)) + for _, testCase := range []struct { + OrgID int64 + UserID int64 + ExpectedOwner bool + }{ + {3, 2, true}, + {3, 1, false}, + {3, 3, false}, + {3, 4, false}, + {2, 2, false}, // user2 is not an organization + {2, 3, false}, + } { + org := AssertExistsAndLoadBean(t, &User{ID: testCase.OrgID}).(*User) + isOwner, err := org.IsOwnedBy(testCase.UserID) + assert.NoError(t, err) + assert.Equal(t, testCase.ExpectedOwner, isOwner) + } } func TestUser_IsOrgMember(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) - assert.True(t, org.IsOrgMember(2)) - assert.True(t, org.IsOrgMember(4)) - assert.False(t, org.IsOrgMember(1)) - assert.False(t, org.IsOrgMember(3)) - - nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - assert.False(t, nonOrg.IsOrgMember(2)) - assert.False(t, nonOrg.IsOrgMember(3)) + for _, testCase := range []struct { + OrgID int64 + UserID int64 + ExpectedMember bool + }{ + {3, 2, true}, + {3, 4, true}, + {3, 1, false}, + {3, 3, false}, + {2, 2, false}, // user2 is not an organization + {2, 3, false}, + } { + org := AssertExistsAndLoadBean(t, &User{ID: testCase.OrgID}).(*User) + isMember, err := org.IsOrgMember(testCase.UserID) + assert.NoError(t, err) + assert.Equal(t, testCase.ExpectedMember, isMember) + } } func TestUser_GetTeam(t *testing.T) { @@ -257,31 +273,46 @@ func TestDeleteOrganization(t *testing.T) { func TestIsOrganizationOwner(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - assert.True(t, IsOrganizationOwner(3, 2)) - assert.False(t, IsOrganizationOwner(3, 3)) - assert.True(t, IsOrganizationOwner(6, 5)) - assert.False(t, IsOrganizationOwner(6, 4)) - assert.False(t, IsOrganizationOwner(NonexistentID, NonexistentID)) + test := func(orgID, userID int64, expected bool) { + isOwner, err := IsOrganizationOwner(orgID, userID) + assert.NoError(t, err) + assert.EqualValues(t, expected, isOwner) + } + test(3, 2, true) + test(3, 3, false) + test(6, 5, true) + test(6, 4, false) + test(NonexistentID, NonexistentID, false) } func TestIsOrganizationMember(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - assert.True(t, IsOrganizationMember(3, 2)) - assert.False(t, IsOrganizationMember(3, 3)) - assert.True(t, IsOrganizationMember(3, 4)) - assert.True(t, IsOrganizationMember(6, 5)) - assert.False(t, IsOrganizationMember(6, 4)) - assert.False(t, IsOrganizationMember(NonexistentID, NonexistentID)) + test := func(orgID, userID int64, expected bool) { + isMember, err := IsOrganizationMember(orgID, userID) + assert.NoError(t, err) + assert.EqualValues(t, expected, isMember) + } + test(3, 2, true) + test(3, 3, false) + test(3, 4, true) + test(6, 5, true) + test(6, 4, false) + test(NonexistentID, NonexistentID, false) } func TestIsPublicMembership(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - assert.True(t, IsPublicMembership(3, 2)) - assert.False(t, IsPublicMembership(3, 3)) - assert.False(t, IsPublicMembership(3, 4)) - assert.True(t, IsPublicMembership(6, 5)) - assert.False(t, IsPublicMembership(6, 4)) - assert.False(t, IsPublicMembership(NonexistentID, NonexistentID)) + test := func(orgID, userID int64, expected bool) { + isMember, err := IsPublicMembership(orgID, userID) + assert.NoError(t, err) + assert.EqualValues(t, expected, isMember) + } + test(3, 2, true) + test(3, 3, false) + test(3, 4, false) + test(6, 5, true) + test(6, 4, false) + test(NonexistentID, NonexistentID, false) } func TestGetOrgsByUserID(t *testing.T) { diff --git a/models/repo.go b/models/repo.go index e937b36ed..833dd9021 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1497,30 +1497,22 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error // Dummy object. collaboration := &Collaboration{RepoID: repo.ID} for _, c := range collaborators { - collaboration.UserID = c.ID - if c.ID == newOwner.ID || newOwner.IsOrgMember(c.ID) { - if _, err = sess.Delete(collaboration); err != nil { - return fmt.Errorf("remove collaborator '%d': %v", c.ID, err) + if c.ID != newOwner.ID { + isMember, err := newOwner.IsOrgMember(c.ID) + if err != nil { + return fmt.Errorf("IsOrgMember: %v", err) + } else if !isMember { + continue } } + collaboration.UserID = c.ID + if _, err = sess.Delete(collaboration); err != nil { + return fmt.Errorf("remove collaborator '%d': %v", c.ID, err) + } } // Remove old team-repository relations. if owner.IsOrganization() { - if err = owner.getTeams(sess); err != nil { - return fmt.Errorf("getTeams: %v", err) - } - for _, t := range owner.Teams { - if !t.hasRepository(sess, repo.ID) { - continue - } - - t.NumRepos-- - if _, err := sess.ID(t.ID).Cols("num_repos").Update(t); err != nil { - return fmt.Errorf("decrease team repository count '%d': %v", t.ID, err) - } - } - if err = owner.removeOrgRepo(sess, repo.ID); err != nil { return fmt.Errorf("removeOrgRepo: %v", err) } diff --git a/models/repo_test.go b/models/repo_test.go index 34eaa16c0..752ffc2dd 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -153,3 +153,26 @@ func TestRepoLocalCopyPath(t *testing.T) { setting.Repository.Local.LocalCopyPath = tempPath assert.Equal(t, expected, repo.LocalCopyPath()) } + +func TestTransferOwnership(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) + repo.Owner = AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User) + assert.NoError(t, TransferOwnership(doer, "user2", repo)) + + transferredRepo := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) + assert.EqualValues(t, 2, transferredRepo.OwnerID) + + assert.False(t, com.IsExist(RepoPath("user3", "repo3"))) + assert.True(t, com.IsExist(RepoPath("user2", "repo3"))) + AssertExistsAndLoadBean(t, &Action{ + OpType: ActionTransferRepo, + ActUserID: 2, + RepoID: 3, + Content: "user3/repo3", + }) + + CheckConsistencyFor(t, &Repository{}, &User{}, &Team{}) +} diff --git a/models/unit_tests.go b/models/unit_tests.go index 25808a948..8fdeb0b14 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -115,7 +115,7 @@ func loadBeanIfExists(bean interface{}, conditions ...interface{}) (bool, error) } // BeanExists for testing, check if a bean exists -func BeanExists(t *testing.T, bean interface{}, conditions ...interface{}) bool { +func BeanExists(t testing.TB, bean interface{}, conditions ...interface{}) bool { exists, err := loadBeanIfExists(bean, conditions...) assert.NoError(t, err) return exists @@ -123,7 +123,7 @@ func BeanExists(t *testing.T, bean interface{}, conditions ...interface{}) bool // AssertExistsAndLoadBean assert that a bean exists and load it from the test // database -func AssertExistsAndLoadBean(t *testing.T, bean interface{}, conditions ...interface{}) interface{} { +func AssertExistsAndLoadBean(t testing.TB, bean interface{}, conditions ...interface{}) interface{} { exists, err := loadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.True(t, exists, @@ -133,7 +133,7 @@ func AssertExistsAndLoadBean(t *testing.T, bean interface{}, conditions ...inter } // GetCount get the count of a bean -func GetCount(t *testing.T, bean interface{}, conditions ...interface{}) int { +func GetCount(t testing.TB, bean interface{}, conditions ...interface{}) int { sess := x.NewSession() defer sess.Close() whereConditions(sess, conditions) @@ -143,7 +143,7 @@ func GetCount(t *testing.T, bean interface{}, conditions ...interface{}) int { } // AssertNotExistsBean assert that a bean does not exist in the test database -func AssertNotExistsBean(t *testing.T, bean interface{}, conditions ...interface{}) { +func AssertNotExistsBean(t testing.TB, bean interface{}, conditions ...interface{}) { exists, err := loadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) @@ -158,18 +158,18 @@ func AssertExistsIf(t *testing.T, expected bool, bean interface{}, conditions .. } // AssertSuccessfulInsert assert that beans is successfully inserted -func AssertSuccessfulInsert(t *testing.T, beans ...interface{}) { +func AssertSuccessfulInsert(t testing.TB, beans ...interface{}) { _, err := x.Insert(beans...) assert.NoError(t, err) } // AssertCount assert the count of a bean -func AssertCount(t *testing.T, bean interface{}, expected interface{}) { +func AssertCount(t testing.TB, bean interface{}, expected interface{}) { assert.EqualValues(t, expected, GetCount(t, bean)) } // AssertInt64InRange assert value is in range [low, high] -func AssertInt64InRange(t *testing.T, low, high, value int64) { +func AssertInt64InRange(t testing.TB, low, high, value int64) { assert.True(t, value >= low && value <= high, "Expected value in range [%d, %d], found %d", low, high, value) } diff --git a/models/user.go b/models/user.go index fa5dc73de..3839e1459 100644 --- a/models/user.go +++ b/models/user.go @@ -487,12 +487,22 @@ func (u *User) IsOrganization() bool { // IsUserOrgOwner returns true if user is in the owner team of given organization. func (u *User) IsUserOrgOwner(orgID int64) bool { - return IsOrganizationOwner(orgID, u.ID) + isOwner, err := IsOrganizationOwner(orgID, u.ID) + if err != nil { + log.Error(4, "IsOrganizationOwner: %v", err) + return false + } + return isOwner } // IsPublicMember returns true if user public his/her membership in given organization. func (u *User) IsPublicMember(orgID int64) bool { - return IsPublicMembership(orgID, u.ID) + isMember, err := IsPublicMembership(orgID, u.ID) + if err != nil { + log.Error(4, "IsPublicMembership: %v", err) + return false + } + return isMember } func (u *User) getOrganizationCount(e Engine) (int64, error) { diff --git a/modules/context/org.go b/modules/context/org.go index cfe9a2622..29cc67dcc 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -73,14 +73,21 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true } else if ctx.IsSigned { - ctx.Org.IsOwner = org.IsOwnedBy(ctx.User.ID) + ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOwnedBy", err) + return + } + if ctx.Org.IsOwner { ctx.Org.IsMember = true ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true } else { - if org.IsOrgMember(ctx.User.ID) { - ctx.Org.IsMember = true + ctx.Org.IsMember, err = org.IsOrgMember(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOrgMember", err) + return } } } else { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 3144e8183..c701a0882 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -70,6 +70,13 @@ type MarkupParser struct { IsInputFile bool } +// enumerates all the policy repository creating +const ( + RepoCreatingLastUserVisibility = "last" + RepoCreatingPrivate = "private" + RepoCreatingPublic = "public" +) + // settings var ( // AppVer settings @@ -89,6 +96,8 @@ var ( HTTPAddr string HTTPPort string LocalURL string + RedirectOtherPort bool + PortToRedirect string OfflineMode bool DisableRouterLog bool CertFile string @@ -180,6 +189,7 @@ var ( Repository = struct { AnsiCharset string ForcePrivate bool + DefaultPrivate string MaxCreationLimit int MirrorQueueLength int PullRequestQueueLength int @@ -209,6 +219,7 @@ var ( }{ AnsiCharset: "", ForcePrivate: false, + DefaultPrivate: RepoCreatingLastUserVisibility, MaxCreationLimit: -1, MirrorQueueLength: 1000, PullRequestQueueLength: 1000, @@ -732,6 +743,8 @@ func NewContext() { defaultLocalURL += ":" + HTTPPort + "/" } LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) + RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) + PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") OfflineMode = sec.Key("OFFLINE_MODE").MustBool() DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(AppWorkPath) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index dbffa85ea..8d6aecfd4 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -489,6 +489,8 @@ mirror_last_synced=Zuletzt synchronisiert watchers=Beobachter stargazers=In Favoriten von forks=Forks +pick_reaction=Wähle eine Reaktion +reactions_more=und %d weitere form.reach_limit_of_creation=Du hast bereits dein Limit von %d Repositories erreicht. form.name_reserved=Der Repository-Name '%s' ist reserviert. @@ -539,6 +541,7 @@ pulls=Pull-Requests labels=Label milestones=Meilensteine commits=Commits +commit=Committen releases=Releases file_raw=Originalformat file_history=Verlauf @@ -1544,6 +1547,7 @@ no_read=Du hast momentan keine gelesenen Benachrichtigungen. pin=Benachrichtigung pinnen mark_as_read=Als gelesen markieren mark_as_unread=Als ungelesen markieren +mark_all_as_read=Alle als gelesen markieren [gpg] error.extract_sign=Die Signatur konnte nicht extrahiert werden diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 23a62c54e..a8b60356a 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -8,7 +8,7 @@ sign_in=Bejelentkezés sign_in_with=Bejelentkezés a következővel sign_out=Kijelentkezés sign_up=Regisztráció -link_account=Fiók Összekötés +link_account=Fiók kapcsolása link_account_signin_or_signup=Bejelentkezés meglévő felhasználóval, hogy a meglévő fiókodhoz kösd, vagy új fiók létrehozása register=Regisztráció website=Webhely @@ -49,22 +49,22 @@ your_settings=Beállításaid all=Összes sources=Saját -mirrors=Tükör +mirrors=Tükrök collaborative=Közreműködő -forks=Másolat +forks=Másolatok activities=Tevékenységek pull_requests=Egyesítési Kérések issues=Hibajegyek -cancel=Mégsem +cancel=Mégse [install] install=Telepítés title=Kezdeti konfiguráció -docker_helper=Hogyha a Gitea-t Docker-en belül futtatod, akkor kérlek olvasd el az irányelveket mielőtt bármit megváltoztatnál ezen az oldalon. -requite_db_desc=Giteához szükséges MySQL, PostgreSQL, SQLite3 vagy TiDB. -db_title=Adatbázis beállításai +docker_helper=Ha Docker-en belül futtatja a Gitea-t, kérjük olvassa el az irányelveket mielőtt bármit megváltoztatna ezen az oldalon. +requite_db_desc=A Giteához MySQL, PostgreSQL, SQLite3 vagy TiDB adatbázis szükséges. +db_title=Adatbázis beállítások db_type=Adatbázis típusa host=Kiszolgáló user=Felhasználó @@ -103,7 +103,7 @@ optional_title=További beállítások email_title=E-mail szolgáltatás beállításai smtp_host=SMTP kiszolgáló smtp_from=Feladó -smtp_from_helper=E-Mail feladójának a címe az RFC 5322 formátumban. Lehet megadni csak egy e-mail címet, vagy "Név" formátumban. +smtp_from_helper=Az e-mail feladójának a címe, RFC 5322 formátumban, amely lehet csak az e-mail cím, vagy a "Név" formátum is. mailer_user=Feladó Felhasználója mailer_password=Feladó Jelszava register_confirm=Regisztráció megerősítésének engedélyezése @@ -140,7 +140,7 @@ run_user_not_match=A futtató felhasználó nem az aktuális felhasználó: %s - save_config_failed=Hiba történt a konfiguráció mentése közben: %v invalid_admin_setting=Rendszergazda fiók beállítása helytelen: %v install_success=Üdvözlünk! Köszönjük, hogy a Gitea-t választotta és jó szórakozást kívánunk a használatához! -invalid_log_root_path=Napó gyökérkönyvtára helytelen: %v +invalid_log_root_path=Napló gyökérkönyvtára helytelen: %v default_keep_email_private=Email cím ne látszódjon" alapértelmezett értéke default_keep_email_private_popup=Ez az alapértelmezett értéke a felhasználó e-mail cím láthatóságának. Ha értéke igaz, akkor minden új felhasználó e-mail címe rejtve marad, amíg a felhasználó nem módosítja a beállítását. default_allow_create_organization=Felhasználó létrehozhat Szervezeteket" jogosultság alapértelmezett értéke diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f6ed844d4..588a76361 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -177,7 +177,10 @@ func reqOrgMembership() macaron.Handler { return } - if !models.IsOrganizationMember(orgID, ctx.User.ID) { + if isMember, err := models.IsOrganizationMember(orgID, ctx.User.ID); err != nil { + ctx.Error(500, "IsOrganizationMember", err) + return + } else if !isMember { if ctx.Org.Organization != nil { ctx.Error(403, "", "Must be an organization member") } else { @@ -200,7 +203,10 @@ func reqOrgOwnership() macaron.Handler { return } - if !models.IsOrganizationOwner(orgID, ctx.User.ID) { + isOwner, err := models.IsOrganizationOwner(orgID, ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOrganizationOwner", err) + } else if !isOwner { if ctx.Org.Organization != nil { ctx.Error(403, "", "Must be an organization owner") } else { diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 7cae7c19f..0cc531780 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -67,7 +67,15 @@ func ListMembers(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/UserList" - publicOnly := ctx.User == nil || !ctx.Org.Organization.IsOrgMember(ctx.User.ID) + publicOnly := true + if ctx.User != nil { + isMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOrgMember", err) + return + } + publicOnly = !isMember + } listMembers(ctx, publicOnly) } @@ -119,19 +127,30 @@ func IsMember(ctx *context.APIContext) { if ctx.Written() { return } - if ctx.User != nil && ctx.Org.Organization.IsOrgMember(ctx.User.ID) { - if ctx.Org.Organization.IsOrgMember(userToCheck.ID) { - ctx.Status(204) - } else { + if ctx.User != nil { + userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOrgMember", err) + return + } else if userIsMember { + userToCheckIsMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOrgMember", err) + } else if userToCheckIsMember { + ctx.Status(204) + } else { + ctx.Status(404) + } + return + } else if ctx.User.ID == userToCheck.ID { ctx.Status(404) + return } - } else if ctx.User != nil && ctx.User.ID == userToCheck.ID { - ctx.Status(404) - } else { - redirectURL := fmt.Sprintf("%sapi/v1/orgs/%s/public_members/%s", - setting.AppURL, ctx.Org.Organization.Name, userToCheck.Name) - ctx.Redirect(redirectURL, 302) } + + redirectURL := fmt.Sprintf("%sapi/v1/orgs/%s/public_members/%s", + setting.AppURL, ctx.Org.Organization.Name, userToCheck.Name) + ctx.Redirect(redirectURL, 302) } // IsPublicMember check if a user is a public member of an organization diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index eead7dd8f..b999d62aa 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -176,7 +176,11 @@ func GetTeamMembers(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/UserList" - if !models.IsOrganizationMember(ctx.Org.Team.OrgID, ctx.User.ID) { + isMember, err := models.IsOrganizationMember(ctx.Org.Team.OrgID, ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOrganizationMember", err) + return + } else if !isMember { ctx.Status(404) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 90301cc35..ec1b37b91 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -89,7 +89,11 @@ func CreateFork(ctx *context.APIContext, form api.CreateForkOption) { } return } - if !org.IsOrgMember(ctx.User.ID) { + isMember, err := org.IsOrgMember(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOrgMember", err) + return + } else if !isMember { ctx.Status(403) return } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 08815ee07..c2d4819f0 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -56,7 +56,7 @@ func ListIssues(ctx *context.APIContext) { } issues, err := models.Issues(&models.IssuesOptions{ - RepoID: ctx.Repo.Repository.ID, + RepoIDs: []int64{ctx.Repo.Repository.ID}, Page: ctx.QueryInt("page"), PageSize: setting.UI.IssuePagingNum, IsClosed: isClosed, diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index b154d50a0..c9c7aa805 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -108,8 +108,19 @@ func Search(ctx *context.APIContext) { } // Check visibility. - if ctx.IsSigned && (ctx.User.ID == repoOwner.ID || (repoOwner.IsOrganization() && repoOwner.IsOwnedBy(ctx.User.ID))) { - opts.Private = true + if ctx.IsSigned { + if ctx.User.ID == repoOwner.ID { + opts.Private = true + } else if repoOwner.IsOrganization() { + opts.Private, err = repoOwner.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.JSON(500, api.SearchError{ + OK: false, + Error: err.Error(), + }) + return + } + } } } @@ -245,7 +256,11 @@ func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) { return } - if !org.IsOwnedBy(ctx.User.ID) { + isOwner, err := org.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOwnedBy", err) + return + } else if !isOwner { ctx.Error(403, "", "Given user is not owner of organization.") return } @@ -292,7 +307,11 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { if ctxUser.IsOrganization() && !ctx.User.IsAdmin { // Check ownership of organization. - if !ctxUser.IsOwnedBy(ctx.User.ID) { + isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOwnedBy", err) + return + } else if !isOwner { ctx.Error(403, "", "Given user is not owner of organization.") return } @@ -431,9 +450,15 @@ func Delete(ctx *context.APIContext) { owner := ctx.Repo.Owner repo := ctx.Repo.Repository - if owner.IsOrganization() && !owner.IsOwnedBy(ctx.User.ID) { - ctx.Error(403, "", "Given user is not owner of organization.") - return + if owner.IsOrganization() { + isOwner, err := owner.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Error(500, "IsOwnedBy", err) + return + } else if !isOwner { + ctx.Error(403, "", "Given user is not owner of organization.") + return + } } if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil { diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 264d2d080..386866574 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -191,8 +191,8 @@ func Issues(ctx *context.Context) { issues = []*models.Issue{} } else { issues, err = models.Issues(&models.IssuesOptions{ + RepoIDs: []int64{repo.ID}, AssigneeID: assigneeID, - RepoID: repo.ID, PosterID: posterID, MentionedID: mentionedID, MilestoneID: milestoneID, @@ -476,6 +476,26 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) } +// commentTag returns the CommentTag for a comment in/with the given repo, poster and issue +func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) { + if repo.IsOwnedBy(poster.ID) { + return models.CommentTagOwner, nil + } else if repo.Owner.IsOrganization() { + isOwner, err := repo.Owner.IsOwnedBy(poster.ID) + if err != nil { + return models.CommentTagNone, err + } else if isOwner { + return models.CommentTagOwner, nil + } + } + if poster.IsWriterOfRepo(repo) { + return models.CommentTagWriter, nil + } else if poster.ID == issue.PosterID { + return models.CommentTagPoster, nil + } + return models.CommentTagNone, nil +} + // ViewIssue render issue view page func ViewIssue(ctx *context.Context) { ctx.Data["RequireHighlightJS"] = true @@ -648,15 +668,11 @@ func ViewIssue(ctx *context.Context) { continue } - if repo.IsOwnedBy(comment.PosterID) || - (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) { - comment.ShowTag = models.CommentTagOwner - } else if comment.Poster.IsWriterOfRepo(repo) { - comment.ShowTag = models.CommentTagWriter - } else if comment.PosterID == issue.PosterID { - comment.ShowTag = models.CommentTagPoster + comment.ShowTag, err = commentTag(repo, comment.Poster, issue) + if err != nil { + ctx.Handle(500, "commentTag", err) + return } - marked[comment.PosterID] = comment.ShowTag isAdded := false diff --git a/routers/repo/pull.go b/routers/repo/pull.go index b6e4d849e..43a16b107 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -173,7 +173,11 @@ func ForkPost(ctx *context.Context, form auth.CreateRepoForm) { // Check ownership of organization. if ctxUser.IsOrganization() { - if !ctxUser.IsOwnedBy(ctx.User.ID) { + isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOwnedBy", err) + return + } else if !isOwner { ctx.Error(403) return } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 36105bfe1..4cd7c8062 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -74,13 +74,36 @@ func checkContextUser(ctx *context.Context, uid int64) *models.User { } // Check ownership of organization. - if !org.IsOrganization() || !(ctx.User.IsAdmin || org.IsOwnedBy(ctx.User.ID)) { + if !org.IsOrganization() { ctx.Error(403) return nil } + if !ctx.User.IsAdmin { + isOwner, err := org.IsOwnedBy(ctx.User.ID) + if err != nil { + ctx.Handle(500, "IsOwnedBy", err) + return nil + } else if !isOwner { + ctx.Error(403) + return nil + } + } return org } +func getRepoPrivate(ctx *context.Context) bool { + switch strings.ToLower(setting.Repository.DefaultPrivate) { + case setting.RepoCreatingLastUserVisibility: + return ctx.User.LastRepoVisibility + case setting.RepoCreatingPrivate: + return true + case setting.RepoCreatingPublic: + return false + default: + return ctx.User.LastRepoVisibility + } +} + // Create render creating repository page func Create(ctx *context.Context) { if !ctx.User.CanCreateRepo() { @@ -94,7 +117,7 @@ func Create(ctx *context.Context) { ctx.Data["Licenses"] = models.Licenses ctx.Data["Readmes"] = models.Readmes ctx.Data["readme"] = "Default" - ctx.Data["private"] = ctx.User.LastRepoVisibility + ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) @@ -170,7 +193,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { // Migrate render migration of repository page func Migrate(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_migrate") - ctx.Data["private"] = ctx.User.LastRepoVisibility + ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["mirror"] = ctx.Query("mirror") == "1" ctx.Data["LFSActive"] = setting.LFS.StartServer diff --git a/routers/repo/setting.go b/routers/repo/setting.go index d3bb14ea0..814f866d1 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -235,13 +235,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { return } - if ctx.Repo.Owner.IsOrganization() { - if !ctx.Repo.Owner.IsOwnedBy(ctx.User.ID) { - ctx.Error(404) - return - } - } - if !repo.IsMirror { ctx.Error(404) return @@ -269,13 +262,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { return } - if ctx.Repo.Owner.IsOrganization() { - if !ctx.Repo.Owner.IsOwnedBy(ctx.User.ID) { - ctx.Error(404) - return - } - } - newOwner := ctx.Query("new_owner_name") isExist, err := models.IsUserExist(0, newOwner) if err != nil { @@ -308,13 +294,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { return } - if ctx.Repo.Owner.IsOrganization() { - if !ctx.Repo.Owner.IsOwnedBy(ctx.User.ID) { - ctx.Error(404) - return - } - } - if err := models.DeleteRepository(ctx.User, ctx.Repo.Owner.ID, repo.ID); err != nil { ctx.Handle(500, "DeleteRepository", err) return @@ -334,13 +313,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { return } - if ctx.Repo.Owner.IsOrganization() { - if !ctx.Repo.Owner.IsOwnedBy(ctx.User.ID) { - ctx.Error(404) - return - } - } - repo.DeleteWiki() log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) @@ -394,10 +366,16 @@ func CollaborationPost(ctx *context.Context) { } // Check if user is organization member. - if ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOrgMember(u.ID) { - ctx.Flash.Info(ctx.Tr("repo.settings.user_is_org_member")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return + if ctx.Repo.Owner.IsOrganization() { + isMember, err := ctx.Repo.Owner.IsOrgMember(u.ID) + if err != nil { + ctx.Handle(500, "IsOrgMember", err) + return + } else if isMember { + ctx.Flash.Info(ctx.Tr("repo.settings.user_is_org_member")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } } if err = ctx.Repo.Repository.AddCollaborator(u); err != nil { diff --git a/routers/user/home.go b/routers/user/home.go index 756da4f57..01a106813 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -9,13 +9,14 @@ import ( "fmt" "sort" - "github.com/Unknwon/paginater" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + + "github.com/Unknwon/com" + "github.com/Unknwon/paginater" ) const ( @@ -231,21 +232,30 @@ func Issues(ctx *context.Context) { return } } - if len(userRepoIDs) <= 0 { userRepoIDs = []int64{-1} } opts := &models.IssuesOptions{ - RepoID: repoID, IsClosed: util.OptionalBoolOf(isShowClosed), IsPull: util.OptionalBoolOf(isPullList), SortType: sortType, } + if repoID > 0 { + opts.RepoIDs = []int64{repoID} + } + switch filterMode { case models.FilterModeAll: - opts.RepoIDs = userRepoIDs + if repoID > 0 { + if !com.IsSliceContainsInt64(userRepoIDs, repoID) { + // force an empty result + opts.RepoIDs = []int64{-1} + } + } else { + opts.RepoIDs = userRepoIDs + } case models.FilterModeAssign: opts.AssigneeID = ctxUser.ID case models.FilterModeCreate: @@ -308,7 +318,18 @@ func Issues(ctx *context.Context) { issue.Repo = showReposMap[issue.RepoID] } - issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, userRepoIDs, filterMode, isPullList) + issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ + UserID: ctxUser.ID, + RepoID: repoID, + UserRepoIDs: userRepoIDs, + FilterMode: filterMode, + IsPull: isPullList, + IsClosed: isShowClosed, + }) + if err != nil { + ctx.Handle(500, "GetUserIssueStats", err) + return + } var total int if !isShowClosed { diff --git a/vendor/code.gitea.io/git/commit_info.go b/vendor/code.gitea.io/git/commit_info.go index 77fe53bdd..6b42b57c9 100644 --- a/vendor/code.gitea.io/git/commit_info.go +++ b/vendor/code.gitea.io/git/commit_info.go @@ -79,7 +79,7 @@ func targetedSearch(state *getCommitsInfoState, done chan error) { done <- nil return } - command := NewCommand("rev-list", "-1", "HEAD", "--", entryPath) + command := NewCommand("rev-list", "-1", state.headCommit.ID.String(), "--", entryPath) output, err := command.RunInDir(state.headCommit.repo.Path) if err != nil { done <- err @@ -192,7 +192,7 @@ func getCommitsInfo(state *getCommitsInfoState) error { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() - args := []string{"log", getCommitsInfoPretty, "--name-status", "-c"} + args := []string{"log", state.headCommit.ID.String(), getCommitsInfoPretty, "--name-status", "-c"} if len(state.treePath) > 0 { args = append(args, "--", state.treePath) } @@ -207,6 +207,10 @@ func getCommitsInfo(state *getCommitsInfoState) error { if err := cmd.Start(); err != nil { return err } + // it's okay to ignore the error returned by cmd.Wait(); we expect the + // subprocess to sometimes have a non-zero exit status, since we may + // prematurely close stdout, resulting in a broken pipe. + defer cmd.Wait() numThreads := runtime.NumCPU() done := make(chan error, numThreads) @@ -216,6 +220,14 @@ func getCommitsInfo(state *getCommitsInfoState) error { scanner := bufio.NewScanner(readCloser) err = state.processGitLogOutput(scanner) + + // it is important that we close stdout here; if we do not close + // stdout, the subprocess will keep running, and the deffered call + // cmd.Wait() may block for a long time. + if closeErr := readCloser.Close(); closeErr != nil && err == nil { + err = closeErr + } + for i := 0; i < numThreads; i++ { doneErr := <-done if doneErr != nil && err == nil { diff --git a/vendor/vendor.json b/vendor/vendor.json index f2d4a9aab..3689f57b6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -3,10 +3,10 @@ "ignore": "test appengine", "package": [ { - "checksumSHA1": "UnJFMWkh0ulYlOV0etJYgt5SzJY=", + "checksumSHA1": "Em29XiKkOh5rFFXdkCjqqsQ7fe4=", "path": "code.gitea.io/git", - "revision": "4768133d10fa395278f545f3bf3ce44552b30ad6", - "revisionTime": "2017-12-10T10:06:09Z" + "revision": "4ec3654064ef7eef4f05f891073a38039ad8d0f7", + "revisionTime": "2017-12-22T02:43:26Z" }, { "checksumSHA1": "QQ7g7B9+EIzGjO14KCGEs9TNEzM=",