From 5930d09096b6ab8d55aabf490ed4ea27faa7f17f Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 23 Jun 2021 19:43:33 +0200 Subject: [PATCH 01/33] Fix Nits (#16234) * template fix * nits * add doer to "created issue/pull" --- options/locale/locale_en-US.ini | 6 +++--- templates/mail/issue/default.tmpl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ded76272b6..59ee6e48ea 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -348,16 +348,16 @@ issue_assigned.issue = @%[1]s assigned you to the issue %[2]s in repository %[3] issue.x_mentioned_you = @%s mentioned you: issue.action.force_push = %[1]s force-pushed the %[2]s from %[3]s to %[4]s. issue.action.push_1 = @%[1]s pushed 1 commit to %[2]s -issue.action.push_n = @%[1]s pushed %[3]d commits to %s: %[2]s +issue.action.push_n = @%[1]s pushed %[3]d commits to %[2]s issue.action.close = @%[1]s closed #%[2]d. issue.action.reopen = @%[1]s reopened #%[2]d. -issue.action.merge = @%[1]s merged #%[2]d into #%[3]s. +issue.action.merge = @%[1]s merged #%[2]d into %[3]s. issue.action.approve = @%[1]s approved this pull request. issue.action.reject = @%[1]s requested changes on this pull request. issue.action.review = @%[1]s commented on this pull request. issue.action.review_dismissed = @%[1]s dismissed last review from %[2]s for this pull request. issue.action.ready_for_review = @%[1]s marked this pull request ready for review. -issue.action.new = Created #%[2]d. +issue.action.new = @%[1]s created #%[2]d. issue.in_tree_path = In %s: release.new.subject = %s in %s released diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index 61fe02037c..f7257f6e6b 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -33,7 +33,7 @@ {{if eq .Comment.Commits.Len 1}} {{.i18n.Tr "mail.issue.action.push_1" .Doer.Name .Comment.Issue.PullRequest.HeadBranch | Str2html}} {{else}} - {{.i18n.Tr "mail.issue.action.push_1" .Doer.Name .Comment.Issue.PullRequest.HeadBranch .Comment.Commits.Len | Str2html}} + {{.i18n.Tr "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch .Comment.Commits.Len | Str2html}} {{end}} {{end}}

From 383ffcfa34d284e3938517989a036da31ad42215 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 23 Jun 2021 21:38:19 +0200 Subject: [PATCH 02/33] Small refactoring of modules/private (#15947) * Use correct variable name. * doer is never nil here. * Use status code constants. * Replaced generic map with concrete struct. * Fixed windows lint. * Removed unused method. * Changed error codes. Co-authored-by: techknowlogick --- cmd/hook.go | 2 +- models/branches.go | 27 +--- modules/private/hook.go | 2 +- modules/private/serv.go | 1 - routers/private/hook.go | 104 +++++++-------- routers/private/key.go | 19 +-- routers/private/mail.go | 16 +-- routers/private/manager.go | 24 ++-- routers/private/manager_windows.go | 5 +- routers/private/restore_repo.go | 16 +-- routers/private/serv.go | 196 +++++++++++++---------------- routers/web/repo/issue.go | 2 +- services/repository/branch.go | 2 +- 13 files changed, 185 insertions(+), 231 deletions(-) diff --git a/cmd/hook.go b/cmd/hook.go index def3b636eb..312c9a14fc 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -179,7 +179,7 @@ Gitea or set your environment appropriately.`, "") GitObjectDirectory: os.Getenv(private.GitObjectDirectory), GitQuarantinePath: os.Getenv(private.GitQuarantinePath), GitPushOptions: pushOptions(), - ProtectedBranchID: prID, + PullRequestID: prID, IsDeployKey: isDeployKey, } diff --git a/models/branches.go b/models/branches.go index 1ac1fa49e5..d346f19578 100644 --- a/models/branches.go +++ b/models/branches.go @@ -362,11 +362,7 @@ func (repo *Repository) GetBranchProtection(branchName string) (*ProtectedBranch } // IsProtectedBranch checks if branch is protected -func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) { - if doer == nil { - return true, nil - } - +func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) { protectedBranch := &ProtectedBranch{ RepoID: repo.ID, BranchName: branchName, @@ -379,27 +375,6 @@ func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, return has, nil } -// IsProtectedBranchForPush checks if branch is protected for push -func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User) (bool, error) { - if doer == nil { - return true, nil - } - - protectedBranch := &ProtectedBranch{ - RepoID: repo.ID, - BranchName: branchName, - } - - has, err := x.Get(protectedBranch) - if err != nil { - return true, err - } else if has { - return !protectedBranch.CanUserPush(doer.ID), nil - } - - return false, nil -} - // updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with // the users from newWhitelist which have explicit read or write access to the repo. func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { diff --git a/modules/private/hook.go b/modules/private/hook.go index cb8fe25708..82dcaf3fc9 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -54,7 +54,7 @@ type HookOptions struct { GitAlternativeObjectDirectories string GitQuarantinePath string GitPushOptions GitPushOptions - ProtectedBranchID int64 + PullRequestID int64 IsDeployKey bool } diff --git a/modules/private/serv.go b/modules/private/serv.go index e077b00ccc..659af6dff5 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -58,7 +58,6 @@ type ServCommandResults struct { // ErrServCommand is an error returned from ServCommmand. type ErrServCommand struct { Results ServCommandResults - Type string Err string StatusCode int } diff --git a/routers/private/hook.go b/routers/private/hook.go index 83c3f21b8f..17ea4f2437 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -124,8 +124,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -133,8 +133,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { gitRepo, err := git.OpenRepository(repo.RepoPath()) if err != nil { log.Error("Unable to get git repository for: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -164,8 +164,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), }) return } @@ -173,8 +173,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) if err != nil { log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -191,8 +191,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { // 1. Detect and prevent deletion of the branch if newCommitID == git.EmptySHA { log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from deletion", branchName), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from deletion", branchName), }) return } @@ -202,14 +202,14 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) if err != nil { log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Fail to detect force push: %v", err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Fail to detect force push: %v", err), }) return } else if len(output) > 0 { log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from force push", branchName), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from force push", branchName), }) return @@ -222,15 +222,15 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { if err != nil { if !isErrUnverifiedCommit(err) { log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), }) return } unverifiedCommit := err.(*errUnverifiedCommit).sha log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), }) return } @@ -248,8 +248,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { if err != nil { if !models.IsErrFilePathProtected(err) { log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), }) return } @@ -270,49 +270,49 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { // 6. If we're not allowed to push directly if !canPush { // Is this is a merge from the UI/API? - if opts.ProtectedBranchID == 0 { + if opts.PullRequestID == 0 { // 6a. If we're not merging from the UI/API then there are two ways we got here: // // We are changing a protected file and we're not allowed to do that if changedProtectedfiles { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), }) return } // Or we're simply not able to push to this protected branch log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), }) return } // 6b. Merge (from UI or API) // Get the PR, user and permissions for the user in the repository - pr, err := models.GetPullRequestByID(opts.ProtectedBranchID) + pr, err := models.GetPullRequestByID(opts.PullRequestID) if err != nil { - log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err), + log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err), }) return } user, err := models.GetUserByID(opts.UserID) if err != nil { log.Error("Unable to get User id %d Error: %v", opts.UserID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), }) return } perm, err := models.GetUserRepoPermission(repo, user) if err != nil { log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), }) return } @@ -321,16 +321,16 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) if err != nil { log.Error("Error calculating if allowed to merge: %v", err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Error calculating if allowed to merge: %v", err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err), }) return } if !allowedMerge { log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), }) return } @@ -343,8 +343,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { // Now if we're not an admin - we can't overwrite protected files so fail now if changedProtectedfiles { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), }) return } @@ -353,14 +353,14 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { if models.IsErrNotAllowedToMerge(err) { log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.ProtectedBranchID, err.Error()), + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()), }) return } log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.ProtectedBranchID, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err), }) return } @@ -549,8 +549,8 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) if err != nil { log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -561,16 +561,16 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { repo.DefaultBranch = branch gitRepo, err := git.OpenRepository(repo.RepoPath()) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err), }) return } if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { gitRepo.Close() - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -578,10 +578,10 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { gitRepo.Close() if err := repo.UpdateDefaultBranch(); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), }) return } - ctx.PlainText(200, []byte("success")) + ctx.PlainText(http.StatusOK, []byte("success")) } diff --git a/routers/private/key.go b/routers/private/key.go index b90faa22a4..2bb319083f 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/timeutil" ) @@ -18,8 +19,8 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { keyID := ctx.ParamsInt64(":id") repoID := ctx.ParamsInt64(":repoid") if err := models.UpdatePublicKeyUpdated(keyID); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -27,18 +28,18 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) if err != nil { if models.IsErrDeployKeyNotExist(err) { - ctx.PlainText(200, []byte("success")) + ctx.PlainText(http.StatusOK, []byte("success")) return } - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } deployKey.UpdatedUnix = timeutil.TimeStampNow() if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -53,8 +54,8 @@ func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) { publicKey, err := models.SearchPublicKeyByContent(content) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } diff --git a/routers/private/mail.go b/routers/private/mail.go index cda442ea04..d7bd5155f6 100644 --- a/routers/private/mail.go +++ b/routers/private/mail.go @@ -23,8 +23,8 @@ import ( // It doesn't wait before each message will be processed func SendEmail(ctx *context.PrivateContext) { if setting.MailService == nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "Mail service is not enabled.", + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Mail service is not enabled.", }) return } @@ -35,8 +35,8 @@ func SendEmail(ctx *context.PrivateContext) { json := jsoniter.ConfigCompatibleWithStandardLibrary if err := json.NewDecoder(rd).Decode(&mail); err != nil { log.Error("%v", err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -48,8 +48,8 @@ func SendEmail(ctx *context.PrivateContext) { if err != nil { err := fmt.Sprintf("Failed to get user information: %v", err) log.Error(err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, }) return } @@ -68,8 +68,8 @@ func SendEmail(ctx *context.PrivateContext) { if err != nil { err := fmt.Sprintf("Failed to find users: %v", err) log.Error(err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, }) return } diff --git a/routers/private/manager.go b/routers/private/manager.go index 1ccb184363..7d010f3f81 100644 --- a/routers/private/manager.go +++ b/routers/private/manager.go @@ -30,15 +30,15 @@ func FlushQueues(ctx *context.PrivateContext) { log.Error("Flushing request timed-out with error: %v", err) } }() - ctx.JSON(http.StatusAccepted, map[string]interface{}{ - "err": "Flushing", + ctx.JSON(http.StatusAccepted, private.Response{ + Err: "Flushing", }) return } err := queue.GetManager().FlushAll(ctx, opts.Timeout) if err != nil { - ctx.JSON(http.StatusRequestTimeout, map[string]interface{}{ - "err": fmt.Sprintf("%v", err), + ctx.JSON(http.StatusRequestTimeout, private.Response{ + Err: fmt.Sprintf("%v", err), }) } ctx.PlainText(http.StatusOK, []byte("success")) @@ -59,8 +59,8 @@ func ResumeLogging(ctx *context.PrivateContext) { // ReleaseReopenLogging releases and reopens logging files func ReleaseReopenLogging(ctx *context.PrivateContext) { if err := log.ReleaseReopen(); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Error during release and reopen: %v", err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error during release and reopen: %v", err), }) return } @@ -73,8 +73,8 @@ func RemoveLogger(ctx *context.PrivateContext) { name := ctx.Params("name") ok, err := log.GetLogger(group).DelLogger(name) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to remove logger: %s %s %v", group, name, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to remove logger: %s %s %v", group, name, err), }) return } @@ -134,8 +134,8 @@ func AddLogger(ctx *context.PrivateContext) { byteConfig, err := json.Marshal(opts.Config) if err != nil { log.Error("Failed to marshal log configuration: %v %v", opts.Config, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to marshal log configuration: %v %v", opts.Config, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to marshal log configuration: %v %v", opts.Config, err), }) return } @@ -143,8 +143,8 @@ func AddLogger(ctx *context.PrivateContext) { if err := log.NewNamedLogger(opts.Group, bufferLen, opts.Name, opts.Mode, config); err != nil { log.Error("Failed to create new named logger: %s %v", config, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to create new named logger: %s %v", config, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to create new named logger: %s %v", config, err), }) return } diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go index 244dbbe4df..f6c9b7ec8f 100644 --- a/routers/private/manager_windows.go +++ b/routers/private/manager_windows.go @@ -11,12 +11,13 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/private" ) // Restart is not implemented for Windows based servers as they can't fork func Restart(ctx *context.PrivateContext) { - ctx.JSON(http.StatusNotImplemented, map[string]interface{}{ - "err": "windows servers cannot be gracefully restarted - shutdown and restart manually", + ctx.JSON(http.StatusNotImplemented, private.Response{ + Err: "windows servers cannot be gracefully restarted - shutdown and restart manually", }) } diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go index df787e1b33..36d17dd95c 100644 --- a/routers/private/restore_repo.go +++ b/routers/private/restore_repo.go @@ -6,9 +6,11 @@ package private import ( "io/ioutil" + "net/http" myCtx "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/private" jsoniter "github.com/json-iterator/go" ) @@ -17,8 +19,8 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { json := jsoniter.ConfigCompatibleWithStandardLibrary bs, err := ioutil.ReadAll(ctx.Req.Body) if err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -29,8 +31,8 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { Units []string }{} if err = json.Unmarshal(bs, ¶ms); err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -42,10 +44,10 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { params.RepoName, params.Units, ); err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) } else { - ctx.Status(200) + ctx.Status(http.StatusOK) } } diff --git a/routers/private/serv.go b/routers/private/serv.go index 1461194e7f..6e39790eb5 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -23,8 +23,8 @@ import ( func ServNoCommand(ctx *context.PrivateContext) { keyID := ctx.ParamsInt64(":keyid") if keyID <= 0 { - ctx.JSON(http.StatusBadRequest, map[string]interface{}{ - "err": fmt.Sprintf("Bad key id: %d", keyID), + ctx.JSON(http.StatusBadRequest, private.Response{ + Err: fmt.Sprintf("Bad key id: %d", keyID), }) } results := private.KeyAndOwner{} @@ -32,14 +32,14 @@ func ServNoCommand(ctx *context.PrivateContext) { key, err := models.GetPublicKeyByID(keyID) if err != nil { if models.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "err": fmt.Sprintf("Cannot find key: %d", keyID), + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: fmt.Sprintf("Cannot find key: %d", keyID), }) return } log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -49,20 +49,20 @@ func ServNoCommand(ctx *context.PrivateContext) { user, err := models.GetUserByID(key.OwnerID) if err != nil { if models.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "err": fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), }) return } log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Your account is disabled.", + ctx.JSON(http.StatusForbidden, private.Response{ + Err: "Your account is disabled.", }) return } @@ -106,18 +106,16 @@ func ServCommand(ctx *context.PrivateContext) { owner, err := models.GetUserByName(results.OwnerName) if err != nil { log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } if !owner.IsOrganization() && !owner.IsActive { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ForbiddenError", - "err": "Repository cannot be accessed, you could retry it later", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Repository cannot be accessed, you could retry it later", }) return } @@ -132,20 +130,18 @@ func ServCommand(ctx *context.PrivateContext) { if "git-upload-pack" == verb { // User is fetching/cloning a non-existent repository log.Error("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } } } else { log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } @@ -157,20 +153,18 @@ func ServCommand(ctx *context.PrivateContext) { results.RepoID = repo.ID if repo.IsBeingCreated() { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": "Repository is being created, you could retry after it finished", + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: "Repository is being created, you could retry after it finished", }) return } // We can shortcut at this point if the repo is a mirror if mode > models.AccessModeRead && repo.IsMirror { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrMirrorReadOnly", - "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), }) return } @@ -180,18 +174,16 @@ func ServCommand(ctx *context.PrivateContext) { key, err := models.GetPublicKeyByID(keyID) if err != nil { if models.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrKeyNotExist", - "err": fmt.Sprintf("Cannot find key: %d", keyID), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find key: %d", keyID), }) return } log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), }) return } @@ -201,10 +193,9 @@ func ServCommand(ctx *context.PrivateContext) { // If repo doesn't exist, deploy key doesn't make sense if !repoExist && key.Type == models.KeyTypeDeploy { - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), }) return } @@ -221,18 +212,16 @@ func ServCommand(ctx *context.PrivateContext) { deployKey, err = models.GetDeployKeyByRepo(key.ID, repo.ID) if err != nil { if models.IsErrDeployKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrDeployKeyNotExist", - "err": fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), }) return } @@ -252,25 +241,23 @@ func ServCommand(ctx *context.PrivateContext) { user, err = models.GetUserByID(key.OwnerID) if err != nil { if models.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUserNotExist", - "err": fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), }) return } log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), }) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Your account is disabled.", + ctx.JSON(http.StatusForbidden, private.Response{ + Err: "Your account is disabled.", }) return } @@ -283,10 +270,9 @@ func ServCommand(ctx *context.PrivateContext) { // Don't allow pushing if the repo is archived if repoExist && mode > models.AccessModeRead && repo.IsArchived { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrRepoIsArchived", - "err": fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), }) return } @@ -295,10 +281,9 @@ func ServCommand(ctx *context.PrivateContext) { if repoExist && (mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView) { if key.Type == models.KeyTypeDeploy { if deployKey.Mode < mode { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUnauthorized", - "err": fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } @@ -306,10 +291,9 @@ func ServCommand(ctx *context.PrivateContext) { perm, err := models.GetUserRepoPermission(repo, user) if err != nil { log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), }) return } @@ -318,10 +302,9 @@ func ServCommand(ctx *context.PrivateContext) { if userMode < mode { log.Error("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUnauthorized", - "err": fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), }) return } @@ -332,27 +315,24 @@ func ServCommand(ctx *context.PrivateContext) { if !repoExist { owner, err := models.GetUserByName(ownerName) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), }) return } if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "Push to create is not enabled for organizations.", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Push to create is not enabled for organizations.", }) return } if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "Push to create is not enabled for users.", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Push to create is not enabled for users.", }) return } @@ -360,10 +340,9 @@ func ServCommand(ctx *context.PrivateContext) { repo, err = repo_service.PushCreateRepo(user, owner, results.RepoName) if err != nil { log.Error("pushCreateRepo: %v", err) - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } @@ -374,18 +353,16 @@ func ServCommand(ctx *context.PrivateContext) { // Ensure the wiki is enabled before we allow access to it if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil { if models.IsErrUnitTypeNotExist(err) { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "repository wiki is disabled", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "repository wiki is disabled", }) return } log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -393,10 +370,9 @@ func ServCommand(ctx *context.PrivateContext) { // Finally if we're trying to touch the wiki we should init it if err = wiki_service.InitWiki(repo); err != nil { log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), }) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a7951b6bce..9ef5c1d1f0 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1461,7 +1461,7 @@ func ViewIssue(ctx *context.Context) { } if perm.CanWrite(models.UnitTypeCode) { // Check if branch is not protected - if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil { + if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch); err != nil { log.Error("IsProtectedBranch: %v", err) } else if !protected { canDelete = true diff --git a/services/repository/branch.go b/services/repository/branch.go index df07030be3..28d24f121d 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -26,7 +26,7 @@ func DeleteBranch(doer *models.User, repo *models.Repository, gitRepo *git.Repos return ErrBranchIsDefault } - isProtected, err := repo.IsProtectedBranch(branchName, doer) + isProtected, err := repo.IsProtectedBranch(branchName) if err != nil { return err } From 8640717f5fb045ee88eda14c7dbe3731b8c068b2 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 23 Jun 2021 15:57:56 -0400 Subject: [PATCH 03/33] Add docs for windows env vars (#16236) * Add docs for windows env vars Fix #16213 * Fix docs/content/doc/developers/hacking-on-gitea.en-us.md Co-authored-by: John Olheiser Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: John Olheiser --- docs/content/doc/developers/hacking-on-gitea.en-us.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/doc/developers/hacking-on-gitea.en-us.md b/docs/content/doc/developers/hacking-on-gitea.en-us.md index 360a46d20f..96db0a01bb 100644 --- a/docs/content/doc/developers/hacking-on-gitea.en-us.md +++ b/docs/content/doc/developers/hacking-on-gitea.en-us.md @@ -73,6 +73,8 @@ One of these three distributions of Make will run on Windows: - The binary is called `mingw32-make.exe` instead of `make.exe`. Add the `bin` folder to `PATH`. - [Chocolatey package](https://chocolatey.org/packages/make). Run `choco install make` +**Note**: If you are attempting to build using make with Windows Command Prompt, you may run into issues. The above prompts (git bash, or mingw) are recommended, however if you only have command prompt (or potentially powershell) you can set environment variables using the [set](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1) command, e.g. `set TAGS=bindata`. + ## Downloading and cloning the Gitea source code The recommended method of obtaining the source code is by using `git clone`. From 58501a26821160c3258c1ad3501f4c1d8db0a597 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 23 Jun 2021 21:58:44 +0200 Subject: [PATCH 04/33] [API] GET / SET User Settings (#16169) * API: GET/SET User Settings * linter * Apply suggestions from code review * Update modules/structs/user.go * lint * fix swagger * move User2UserSettings to convert * as per @zeripath "preferences" -> "settings" Co-authored-by: techknowlogick Co-authored-by: techknowlogick --- modules/convert/user.go | 15 ++++ modules/structs/user.go | 30 +++++++ routers/api/v1/api.go | 4 + routers/api/v1/swagger/options.go | 3 + routers/api/v1/swagger/user.go | 7 ++ routers/api/v1/user/settings.go | 83 ++++++++++++++++++ templates/swagger/v1_json.tmpl | 140 +++++++++++++++++++++++++++++- 7 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 routers/api/v1/user/settings.go diff --git a/modules/convert/user.go b/modules/convert/user.go index 07a4efd41a..894be3bd44 100644 --- a/modules/convert/user.go +++ b/modules/convert/user.go @@ -76,3 +76,18 @@ func toUser(user *models.User, signed, authed bool) *api.User { } return result } + +// User2UserSettings return UserSettings based on a user +func User2UserSettings(user *models.User) api.UserSettings { + return api.UserSettings{ + FullName: user.FullName, + Website: user.Website, + Location: user.Location, + Language: user.Language, + Description: user.Description, + Theme: user.Theme, + HideEmail: user.KeepEmailPrivate, + HideActivity: user.KeepActivityPrivate, + DiffViewStyle: user.DiffViewStyle, + } +} diff --git a/modules/structs/user.go b/modules/structs/user.go index de2e68c2a2..0d8b0300c3 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -60,3 +60,33 @@ func (u User) MarshalJSON() ([]byte, error) { CompatUserName string `json:"username"` }{shadow(u), u.UserName}) } + +// UserSettings represents user settings +// swagger:model +type UserSettings struct { + FullName string `json:"full_name"` + Website string `json:"website"` + Description string `json:"description"` + Location string `json:"location"` + Language string `json:"language"` + Theme string `json:"theme"` + DiffViewStyle string `json:"diff_view_style"` + // Privacy + HideEmail bool `json:"hide_email"` + HideActivity bool `json:"hide_activity"` +} + +// UserSettingsOptions represents options to change user settings +// swagger:model +type UserSettingsOptions struct { + FullName *string `json:"full_name" binding:"MaxSize(100)"` + Website *string `json:"website" binding:"OmitEmpty;ValidUrl;MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` + Location *string `json:"location" binding:"MaxSize(50)"` + Language *string `json:"language"` + Theme *string `json:"theme"` + DiffViewStyle *string `json:"diff_view_style"` + // Privacy + HideEmail *bool `json:"hide_email"` + HideActivity *bool `json:"hide_activity"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9efc2af244..87db804dcb 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -649,6 +649,10 @@ func Routes() *web.Route { m.Group("/user", func() { m.Get("", user.GetAuthenticatedUser) + m.Group("/settings", func() { + m.Get("", user.GetUserSettings) + m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings) + }, reqToken()) m.Combo("/emails").Get(user.ListEmails). Post(bind(api.CreateEmailOption{}), user.AddEmail). Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail) diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 11158fb86d..b5f34e86a3 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -161,4 +161,7 @@ type swaggerParameterBodies struct { // in:body CreateTagOption api.CreateTagOption + + // in:body + UserSettingsOptions api.UserSettingsOptions } diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go index a2df40e4cf..a4d5201236 100644 --- a/routers/api/v1/swagger/user.go +++ b/routers/api/v1/swagger/user.go @@ -42,3 +42,10 @@ type swaggerResponseUserHeatmapData struct { // in:body Body []models.UserHeatmapData `json:"body"` } + +// UserSettings +// swagger:response UserSettings +type swaggerResponseUserSettings struct { + // in:body + Body []api.UserSettings `json:"body"` +} diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go new file mode 100644 index 0000000000..b4548e7443 --- /dev/null +++ b/routers/api/v1/user/settings.go @@ -0,0 +1,83 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" +) + +// GetUserSettings returns user settings +func GetUserSettings(ctx *context.APIContext) { + // swagger:operation GET /user/settings user getUserSettings + // --- + // summary: Get user settings + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User)) +} + +// UpdateUserSettings returns user settings +func UpdateUserSettings(ctx *context.APIContext) { + // swagger:operation PATCH /user/settings user updateUserSettings + // --- + // summary: Update user settings + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserSettingsOptions" + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + + form := web.GetForm(ctx).(*api.UserSettingsOptions) + + if form.FullName != nil { + ctx.User.FullName = *form.FullName + } + if form.Description != nil { + ctx.User.Description = *form.Description + } + if form.Website != nil { + ctx.User.Website = *form.Website + } + if form.Location != nil { + ctx.User.Location = *form.Location + } + if form.Language != nil { + ctx.User.Language = *form.Language + } + if form.Theme != nil { + ctx.User.Theme = *form.Theme + } + if form.DiffViewStyle != nil { + ctx.User.DiffViewStyle = *form.DiffViewStyle + } + + if form.HideEmail != nil { + ctx.User.KeepEmailPrivate = *form.HideEmail + } + if form.HideActivity != nil { + ctx.User.KeepActivityPrivate = *form.HideActivity + } + + if err := models.UpdateUser(ctx.User); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User)) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 017dc824d7..7425150a98 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -10911,6 +10911,47 @@ } } }, + "/user/settings": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user settings", + "operationId": "getUserSettings", + "responses": { + "200": { + "$ref": "#/responses/UserSettings" + } + } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update user settings", + "operationId": "updateUserSettings", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserSettingsOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserSettings" + } + } + } + }, "/user/starred": { "get": { "produces": [ @@ -16578,6 +16619,94 @@ }, "x-go-package": "code.gitea.io/gitea/models" }, + "UserSettings": { + "description": "UserSettings represents user settings", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "diff_view_style": { + "type": "string", + "x-go-name": "DiffViewStyle" + }, + "full_name": { + "type": "string", + "x-go-name": "FullName" + }, + "hide_activity": { + "type": "boolean", + "x-go-name": "HideActivity" + }, + "hide_email": { + "description": "Privacy", + "type": "boolean", + "x-go-name": "HideEmail" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "location": { + "type": "string", + "x-go-name": "Location" + }, + "theme": { + "type": "string", + "x-go-name": "Theme" + }, + "website": { + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "UserSettingsOptions": { + "description": "UserSettingsOptions represents options to change user settings", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "diff_view_style": { + "type": "string", + "x-go-name": "DiffViewStyle" + }, + "full_name": { + "type": "string", + "x-go-name": "FullName" + }, + "hide_activity": { + "type": "boolean", + "x-go-name": "HideActivity" + }, + "hide_email": { + "description": "Privacy", + "type": "boolean", + "x-go-name": "HideEmail" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "location": { + "type": "string", + "x-go-name": "Location" + }, + "theme": { + "type": "string", + "x-go-name": "Theme" + }, + "website": { + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "WatchInfo": { "description": "WatchInfo represents an API watch status of one repository", "type": "object", @@ -17281,6 +17410,15 @@ } } }, + "UserSettings": { + "description": "UserSettings", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/UserSettings" + } + } + }, "WatchInfo": { "description": "WatchInfo", "schema": { @@ -17335,7 +17473,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateTagOption" + "$ref": "#/definitions/UserSettingsOptions" } }, "redirect": { From f2babf334676f9c16a540153432bc6e4ebf62423 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 24 Jun 2021 04:45:47 +0800 Subject: [PATCH 05/33] Add code block highlight to orgmode back (#14222) Fix missed orgmode code block hightlight Co-authored-by: zeripath --- modules/markup/orgmode/orgmode.go | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 96e67f90cf..851fc97f9a 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -11,9 +11,12 @@ import ( "io" "strings" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/util" + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/lexers" "github.com/niklasfasching/go-org/org" ) @@ -41,6 +44,47 @@ func (Renderer) Extensions() []string { // Render renders orgmode rawbytes to HTML func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { htmlWriter := org.NewHTMLWriter() + htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool) string { + var w strings.Builder + if _, err := w.WriteString(`
`); err != nil {
+			return ""
+		}
+
+		lexer := lexers.Get(lang)
+		if lexer == nil && lang == "" {
+			lexer = lexers.Analyse(source)
+			if lexer == nil {
+				lexer = lexers.Fallback
+			}
+			lang = strings.ToLower(lexer.Config().Name)
+		}
+
+		if lexer == nil {
+			// include language-x class as part of commonmark spec
+			if _, err := w.WriteString(``); err != nil {
+				return ""
+			}
+			if _, err := w.WriteString(html.EscapeString(source)); err != nil {
+				return ""
+			}
+		} else {
+			// include language-x class as part of commonmark spec
+			if _, err := w.WriteString(``); err != nil {
+				return ""
+			}
+			lexer = chroma.Coalesce(lexer)
+
+			if _, err := w.WriteString(highlight.Code(lexer.Config().Filenames[0], source)); err != nil {
+				return ""
+			}
+		}
+
+		if _, err := w.WriteString("
"); err != nil { + return "" + } + + return w.String() + } w := &Writer{ HTMLWriter: htmlWriter, From 08f4b3f31288bc4e12e94f00c7d88583ab04dd2e Mon Sep 17 00:00:00 2001 From: Viktor Yakovchuk Date: Thu, 24 Jun 2021 00:08:26 +0300 Subject: [PATCH 06/33] Fix 500 Error with branch and tag sharing the same name #15592 (#16040) * Fix 500 Error with branch and tag sharing the same name #15592 Fixed 500 error while create Pull request when there are more than one sources (branch, tag) with the same name Fix #15592 Signed-off-by: Viktor Yakovchuk * fix logging Co-authored-by: techknowlogick Co-authored-by: 6543 <6543@obermui.de> --- modules/git/error.go | 17 +++++++++++++++++ modules/git/repo.go | 7 +++++++ services/pull/pull.go | 14 +++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/modules/git/error.go b/modules/git/error.go index 85a938a7da..387dd724e5 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -159,3 +159,20 @@ func (err *ErrPushRejected) GenerateMessage() { } err.Message = strings.TrimSpace(messageBuilder.String()) } + +// ErrMoreThanOne represents an error if pull request fails when there are more than one sources (branch, tag) with the same name +type ErrMoreThanOne struct { + StdOut string + StdErr string + Err error +} + +// IsErrMoreThanOne checks if an error is a ErrMoreThanOne +func IsErrMoreThanOne(err error) bool { + _, ok := err.(*ErrMoreThanOne) + return ok +} + +func (err *ErrMoreThanOne) Error() string { + return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} diff --git a/modules/git/repo.go b/modules/git/repo.go index e06cd43935..43f329f448 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -225,6 +225,13 @@ func Push(repoPath string, opts PushOptions) error { } err.GenerateMessage() return err + } else if strings.Contains(errbuf.String(), "matches more than one") { + err := &ErrMoreThanOne{ + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + return err } } diff --git a/services/pull/pull.go b/services/pull/pull.go index 02c0a7fe7c..7b5dd6a964 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -389,6 +389,10 @@ func checkIfPRContentChanged(pr *models.PullRequest, oldCommitID, newCommitID st // corresponding branches of base repository. // FIXME: Only push branches that are actually updates? func PushToBaseRepo(pr *models.PullRequest) (err error) { + return pushToBaseRepoHelper(pr, "") +} + +func pushToBaseRepoHelper(pr *models.PullRequest, prefixHeadBranch string) (err error) { log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName()) if err := pr.LoadHeadRepo(); err != nil { @@ -414,7 +418,7 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { if err := git.Push(headRepoPath, git.PushOptions{ Remote: baseRepoPath, - Branch: pr.HeadBranch + ":" + gitRefName, + Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName, Force: true, // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/... Env: models.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo), @@ -427,6 +431,14 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { rejectErr := err.(*git.ErrPushRejected) log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err) return err + } else if git.IsErrMoreThanOne(err) { + if prefixHeadBranch != "" { + log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch) + return err + } + log.Info("Retrying to push with refs/heads/%s", pr.HeadBranch) + err = pushToBaseRepoHelper(pr, "refs/heads/") + return err } log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err) return fmt.Errorf("Push: %s:%s %s:%s %v", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err) From eb324a9402878a13029116bafa8ccce527796522 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 23 Jun 2021 23:08:47 +0200 Subject: [PATCH 07/33] [API] Add repoGetTag (#16166) * GetTag -> GetAnnotatedTag * API: Add repoGetTag * fix swagger docs * support "/" as tag name char Co-authored-by: techknowlogick --- integrations/api_repo_tags_test.go | 16 ++++- routers/api/v1/api.go | 5 +- routers/api/v1/repo/tag.go | 103 ++++++++++++++++++++--------- templates/swagger/v1_json.tmpl | 45 ++++++++++++- 4 files changed, 132 insertions(+), 37 deletions(-) diff --git a/integrations/api_repo_tags_test.go b/integrations/api_repo_tags_test.go index 1bd9fa6168..0bf54d3a95 100644 --- a/integrations/api_repo_tags_test.go +++ b/integrations/api_repo_tags_test.go @@ -39,7 +39,7 @@ func TestAPIRepoTags(t *testing.T) { assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL) assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL) - newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "awesome-tag", "", "nice!\nand some text") + newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "gitea/22", "", "nice!\nand some text") resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &tags) assert.Len(t, tags, 2) @@ -51,6 +51,20 @@ func TestAPIRepoTags(t *testing.T) { assert.EqualValues(t, newTag.Commit.SHA, tag.Commit.SHA) } } + + // get created tag + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token) + resp = session.MakeRequest(t, req, http.StatusOK) + var tag *api.Tag + DecodeJSON(t, resp, &tag) + assert.EqualValues(t, newTag, tag) + + // delete tag + delReq := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token) + resp = session.MakeRequest(t, delReq, http.StatusNoContent) + + // check if it's gone + resp = session.MakeRequest(t, req, http.StatusNotFound) } func createNewTagUsingAPI(t *testing.T, session *TestSession, token string, ownerName, repoName, name, target, msg string) *api.Tag { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 87db804dcb..c6b4ff04de 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -779,8 +779,9 @@ func Routes() *web.Route { }, reqToken(), reqAdmin()) m.Group("/tags", func() { m.Get("", repo.ListTags) + m.Get("/*", repo.GetTag) m.Post("", reqRepoWriter(models.UnitTypeCode), bind(api.CreateTagOption{}), repo.CreateTag) - m.Delete("/{tag}", repo.DeleteTag) + m.Delete("/*", repo.DeleteTag) }, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). @@ -945,7 +946,7 @@ func Routes() *web.Route { m.Get("/refs/*", repo.GetGitRefs) m.Get("/trees/{sha}", context.RepoRefForAPI, repo.GetTree) m.Get("/blobs/{sha}", context.RepoRefForAPI, repo.GetBlob) - m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetTag) + m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag) }, reqRepoReader(models.UnitTypeCode)) m.Group("/contents", func() { m.Get("", repo.GetContentsList) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 51ba43ea89..c95fb63f85 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -64,9 +64,9 @@ func ListTags(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &apiTags) } -// GetTag get the tag of a repository. -func GetTag(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetTag +// GetAnnotatedTag get the tag of a repository. +func GetAnnotatedTag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetAnnotatedTag // --- // summary: Gets the tag object of an annotated tag (not lightweight tags) // produces: @@ -100,21 +100,21 @@ func GetTag(ctx *context.APIContext) { } if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil { - ctx.Error(http.StatusBadRequest, "GetTag", err) + ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) } else { commit, err := tag.Commit() if err != nil { - ctx.Error(http.StatusBadRequest, "GetTag", err) + ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) } ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx.Repo.Repository, tag, commit)) } } -// DeleteTag delete a specific tag of in a repository by name -func DeleteTag(ctx *context.APIContext) { - // swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag +// GetTag get the tag of a repository +func GetTag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/tags/{tag} repository repoGetTag // --- - // summary: Delete a repository's tag by name + // summary: Get the tag of a repository by tag name // produces: // - application/json // parameters: @@ -130,37 +130,22 @@ func DeleteTag(ctx *context.APIContext) { // required: true // - name: tag // in: path - // description: name of tag to delete + // description: name of tag // type: string // required: true // responses: - // "204": - // "$ref": "#/responses/empty" + // "200": + // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" + tagName := ctx.Params("*") - tag, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag")) + tag, err := ctx.Repo.GitRepo.GetTag(tagName) if err != nil { - if models.IsErrReleaseNotExist(err) { - ctx.NotFound() - return - } - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.NotFound(tagName) return } - - if !tag.IsTag { - ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) - return - } - - if err = releaseservice.DeleteReleaseByID(tag.ID, ctx.User, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) - } - - ctx.Status(http.StatusNoContent) + ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag)) } // CreateTag create a new git tag in a repository @@ -187,7 +172,7 @@ func CreateTag(ctx *context.APIContext) { // "$ref": "#/definitions/CreateTagOption" // responses: // "200": - // "$ref": "#/responses/AnnotatedTag" + // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" // "409": @@ -221,3 +206,57 @@ func CreateTag(ctx *context.APIContext) { } ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag)) } + +// DeleteTag delete a specific tag of in a repository by name +func DeleteTag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag + // --- + // summary: Delete a repository's tag by name + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: tag + // in: path + // description: name of tag to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" + tagName := ctx.Params("*") + + tag, err := models.GetRelease(ctx.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + ctx.NotFound() + return + } + ctx.Error(http.StatusInternalServerError, "GetRelease", err) + return + } + + if !tag.IsTag { + ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + return + } + + if err = releaseservice.DeleteReleaseByID(tag.ID, ctx.User, true); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7425150a98..9453b1af32 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3657,7 +3657,7 @@ "repository" ], "summary": "Gets the tag object of an annotated tag (not lightweight tags)", - "operationId": "GetTag", + "operationId": "GetAnnotatedTag", "parameters": [ { "type": "string", @@ -9117,7 +9117,7 @@ ], "responses": { "200": { - "$ref": "#/responses/AnnotatedTag" + "$ref": "#/responses/Tag" }, "404": { "$ref": "#/responses/notFound" @@ -9129,6 +9129,47 @@ } }, "/repos/{owner}/{repo}/tags/{tag}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get the tag of a repository by tag name", + "operationId": "repoGetTag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of tag", + "name": "tag", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Tag" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, "delete": { "produces": [ "application/json" From c9c7afda1a80bda7b61ded222163db796132b78f Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 23 Jun 2021 23:09:51 +0200 Subject: [PATCH 08/33] Add sanitizer rules per renderer (#16110) * Added sanitizer rules per renderer. * Updated documentation. Co-authored-by: techknowlogick --- .../doc/advanced/config-cheat-sheet.en-us.md | 4 + .../doc/advanced/external-renderers.en-us.md | 41 ++++++- modules/markup/csv/csv.go | 10 ++ modules/markup/external/external.go | 7 +- modules/markup/html_test.go | 4 +- modules/markup/markdown/markdown.go | 9 +- modules/markup/orgmode/orgmode.go | 6 + modules/markup/renderer.go | 52 ++++----- modules/markup/sanitizer.go | 88 +++++++++----- modules/setting/markup.go | 107 ++++++++++-------- 10 files changed, 215 insertions(+), 113 deletions(-) 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 a33407d15a..8f1f9ce42d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -907,13 +907,17 @@ Gitea supports customizing the sanitization policy for rendered HTML. The exampl ELEMENT = span ALLOW_ATTR = class REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ +ALLOW_DATA_URI_IMAGES = true ``` - `ELEMENT`: The element this policy applies to. Must be non-empty. - `ALLOW_ATTR`: The attribute this policy allows. Must be non-empty. - `REGEXP`: A regex to match the contents of the attribute against. Must be present but may be empty for unconditional whitelisting of this attribute. + - `ALLOW_DATA_URI_IMAGES`: **false** Allow data uri images (``). Multiple sanitisation rules can be defined by adding unique subsections, e.g. `[markup.sanitizer.TeX-2]`. +To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`. +If the rule is defined above the renderer ini section or the name does not match a renderer it is applied to every renderer. ## Time (`time`) diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index 71fabc529d..c0109b8014 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -64,8 +64,8 @@ IS_INPUT_FILE = false [markup.jupyter] ENABLED = true FILE_EXTENSIONS = .ipynb -RENDER_COMMAND = "jupyter nbconvert --stdout --to html --template basic " -IS_INPUT_FILE = true +RENDER_COMMAND = "jupyter nbconvert --stdin --stdout --to html --template basic" +IS_INPUT_FILE = false [markup.restructuredtext] ENABLED = true @@ -90,15 +90,50 @@ FILE_EXTENSIONS = .md,.markdown RENDER_COMMAND = pandoc -f markdown -t html --katex ``` -You must define `ELEMENT`, `ALLOW_ATTR`, and `REGEXP` in each section. +You must define `ELEMENT` and `ALLOW_ATTR` in each section. To define multiple entries, add a unique alphanumeric suffix (e.g., `[markup.sanitizer.1]` and `[markup.sanitizer.something]`). +To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`, `[markup.sanitizer..rule-1]`. + +**Note**: If the rule is defined above the renderer ini section or the name does not match a renderer it is applied to every renderer. + Once your configuration changes have been made, restart Gitea to have changes take effect. **Note**: Prior to Gitea 1.12 there was a single `markup.sanitiser` section with keys that were redefined for multiple rules, however, there were significant problems with this method of configuration necessitating configuration through multiple sections. +### Example: Office DOCX + +Display Office DOCX files with [`pandoc`](https://pandoc.org/): +```ini +[markup.docx] +ENABLED = true +FILE_EXTENSIONS = .docx +RENDER_COMMAND = "pandoc --from docx --to html --self-contained --template /path/to/basic.html" + +[markup.sanitizer.docx.img] +ALLOW_DATA_URI_IMAGES = true +``` + +The template file has the following content: +``` +$body$ +``` + +### Example: Jupyter Notebook + +Display Jupyter Notebook files with [`nbconvert`](https://github.com/jupyter/nbconvert): +```ini +[markup.jupyter] +ENABLED = true +FILE_EXTENSIONS = .ipynb +RENDER_COMMAND = "jupyter-nbconvert --stdin --stdout --to html --template basic" + +[markup.sanitizer.jupyter.img] +ALLOW_DATA_URI_IMAGES = true +``` + ## Customizing CSS The external renderer is specified in the .ini in the format `[markup.XXXXX]` and the HTML supplied by your external renderer will be wrapped in a `
` with classes `markup` and `XXXXX`. The `markup` class provides out of the box styling (as does `markdown` if `XXXXX` is `markdown`). Otherwise you can use these classes to specifically target the contents of your rendered HTML. diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 6572b0ee1e..8a4df89511 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -10,6 +10,7 @@ import ( "html" "io" "io/ioutil" + "regexp" "strconv" "code.gitea.io/gitea/modules/csv" @@ -38,6 +39,15 @@ func (Renderer) Extensions() []string { return []string{".csv", ".tsv"} } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{ + {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)}, + {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + } +} + func writeField(w io.Writer, element, class, field string) error { if _, err := io.WriteString(w, "<"); err != nil { return err diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 62814c9914..c849f505e7 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -30,7 +30,7 @@ func RegisterRenderers() { // Renderer implements markup.Renderer for external tools type Renderer struct { - setting.MarkupRenderer + *setting.MarkupRenderer } // Name returns the external tool name @@ -48,6 +48,11 @@ func (p *Renderer) Extensions() []string { return p.FileExtensions } +// SanitizerRules implements markup.Renderer +func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return p.MarkupSanitizerRules +} + func envMark(envName string) string { if runtime.GOOS == "windows" { return "%" + envName + "%" diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 4cdd5798c8..8c3d2b5395 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -112,7 +112,7 @@ func TestRender_links(t *testing.T) { defaultCustom := setting.Markdown.CustomURLSchemes setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"} - ReplaceSanitizer() + InitializeSanitizer() CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) test( @@ -192,7 +192,7 @@ func TestRender_links(t *testing.T) { // Restore previous settings setting.Markdown.CustomURLSchemes = defaultCustom - ReplaceSanitizer() + InitializeSanitizer() CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 87fae2a23b..cac2a180fa 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -199,7 +199,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } _ = lw.Close() }() - buf := markup.SanitizeReader(rd) + buf := markup.SanitizeReader(rd, "") _, err := io.Copy(output, buf) return err } @@ -215,7 +215,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error if log.IsDebug() { log.Debug("Panic in markdown: %v\n%s", err, string(log.Stack(2))) } - ret := markup.SanitizeReader(input) + ret := markup.SanitizeReader(input, "") _, err = io.Copy(output, ret) if err != nil { log.Error("SanitizeReader failed: %v", err) @@ -249,6 +249,11 @@ func (Renderer) Extensions() []string { return setting.Markdown.FileExtensions } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{} +} + // Render implements markup.Renderer func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { return render(ctx, input, output) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 851fc97f9a..7e9f1f45c5 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/alecthomas/chroma" @@ -41,6 +42,11 @@ func (Renderer) Extensions() []string { return []string{".org"} } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{} +} + // Render renders orgmode rawbytes to HTML func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { htmlWriter := org.NewHTMLWriter() diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index d60c8ad710..04619caee3 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -81,6 +81,7 @@ type Renderer interface { Name() string // markup format name Extensions() []string NeedPostProcess() bool + SanitizerRules() []setting.MarkupSanitizerRule Render(ctx *RenderContext, input io.Reader, output io.Writer) error } @@ -136,37 +137,32 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr _ = pw.Close() }() - if renderer.NeedPostProcess() { - pr2, pw2 := io.Pipe() - defer func() { - _ = pr2.Close() - _ = pw2.Close() - }() + pr2, pw2 := io.Pipe() + defer func() { + _ = pr2.Close() + _ = pw2.Close() + }() - wg.Add(1) - go func() { - buf := SanitizeReader(pr2) - _, err = io.Copy(output, buf) - _ = pr2.Close() - wg.Done() - }() + wg.Add(1) + go func() { + buf := SanitizeReader(pr2, renderer.Name()) + _, err = io.Copy(output, buf) + _ = pr2.Close() + wg.Done() + }() - wg.Add(1) - go func() { + wg.Add(1) + go func() { + if renderer.NeedPostProcess() { err = PostProcess(ctx, pr, pw2) - _ = pr.Close() - _ = pw2.Close() - wg.Done() - }() - } else { - wg.Add(1) - go func() { - buf := SanitizeReader(pr) - _, err = io.Copy(output, buf) - _ = pr.Close() - wg.Done() - }() - } + } else { + _, err = io.Copy(pw2, pr) + } + _ = pr.Close() + _ = pw2.Close() + wg.Done() + }() + if err1 := renderer.Render(ctx, input, pw); err1 != nil { return err1 } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 5611bd06ad..9342d65de5 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -19,8 +19,9 @@ import ( // Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow // any modification to the underlying policies once it's been created. type Sanitizer struct { - policy *bluemonday.Policy - init sync.Once + defaultPolicy *bluemonday.Policy + rendererPolicies map[string]*bluemonday.Policy + init sync.Once } var sanitizer = &Sanitizer{} @@ -30,47 +31,57 @@ var sanitizer = &Sanitizer{} // entire application lifecycle. func NewSanitizer() { sanitizer.init.Do(func() { - ReplaceSanitizer() + InitializeSanitizer() }) } -// ReplaceSanitizer replaces the current sanitizer to account for changes in settings -func ReplaceSanitizer() { - sanitizer.policy = bluemonday.UGCPolicy() +// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings +func InitializeSanitizer() { + sanitizer.rendererPolicies = map[string]*bluemonday.Policy{} + sanitizer.defaultPolicy = createDefaultPolicy() + + for name, renderer := range renderers { + sanitizerRules := renderer.SanitizerRules() + if len(sanitizerRules) > 0 { + policy := createDefaultPolicy() + addSanitizerRules(policy, sanitizerRules) + sanitizer.rendererPolicies[name] = policy + } + } +} + +func createDefaultPolicy() *bluemonday.Policy { + policy := bluemonday.UGCPolicy() // For Chroma markdown plugin - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre") - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") // Checkboxes - sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") - sanitizer.policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") + policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") + policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") // Custom URL-Schemes if len(setting.Markdown.CustomURLSchemes) > 0 { - sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) } // Allow classes for anchors - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a") // Allow classes for task lists - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") // Allow icons - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") // Allow unlabelled labels - sanitizer.policy.AllowNoAttrs().OnElements("label") + policy.AllowNoAttrs().OnElements("label") // Allow classes for emojis - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") // Allow icons, emojis, chroma syntax and keyword markup on span - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") - - // Allow data tables - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`data-table`)).OnElements("table") - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`line-num`)).OnElements("th", "td") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") // Allow generally safe attributes generalSafeAttrs := []string{"abbr", "accept", "accept-charset", @@ -101,18 +112,29 @@ func ReplaceSanitizer() { "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "wbr", } - sanitizer.policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) + policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) - sanitizer.policy.AllowAttrs("itemscope", "itemtype").OnElements("div") + policy.AllowAttrs("itemscope", "itemtype").OnElements("div") // FIXME: Need to handle longdesc in img but there is no easy way to do it // Custom keyword markup - for _, rule := range setting.ExternalSanitizerRules { - if rule.Regexp != nil { - sanitizer.policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element) - } else { - sanitizer.policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element) + addSanitizerRules(policy, setting.ExternalSanitizerRules) + + return policy +} + +func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) { + for _, rule := range rules { + if rule.AllowDataURIImages { + policy.AllowDataURIImages() + } + if rule.Element != "" { + if rule.Regexp != nil { + policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element) + } else { + policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element) + } } } } @@ -120,11 +142,15 @@ func ReplaceSanitizer() { // Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. func Sanitize(s string) string { NewSanitizer() - return sanitizer.policy.Sanitize(s) + return sanitizer.defaultPolicy.Sanitize(s) } // SanitizeReader sanitizes a Reader -func SanitizeReader(r io.Reader) *bytes.Buffer { +func SanitizeReader(r io.Reader, renderer string) *bytes.Buffer { NewSanitizer() - return sanitizer.policy.SanitizeReader(r) + policy, exist := sanitizer.rendererPolicies[renderer] + if !exist { + policy = sanitizer.defaultPolicy + } + return policy.SanitizeReader(r) } diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 43df4ce442..31ec1dd2eb 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -15,31 +15,34 @@ import ( // ExternalMarkupRenderers represents the external markup renderers var ( - ExternalMarkupRenderers []MarkupRenderer + ExternalMarkupRenderers []*MarkupRenderer ExternalSanitizerRules []MarkupSanitizerRule ) // MarkupRenderer defines the external parser configured in ini type MarkupRenderer struct { - Enabled bool - MarkupName string - Command string - FileExtensions []string - IsInputFile bool - NeedPostProcess bool + Enabled bool + MarkupName string + Command string + FileExtensions []string + IsInputFile bool + NeedPostProcess bool + MarkupSanitizerRules []MarkupSanitizerRule } // MarkupSanitizerRule defines the policy for whitelisting attributes on // certain elements. type MarkupSanitizerRule struct { - Element string - AllowAttr string - Regexp *regexp.Regexp + Element string + AllowAttr string + Regexp *regexp.Regexp + AllowDataURIImages bool } func newMarkup() { - ExternalMarkupRenderers = make([]MarkupRenderer, 0, 10) + ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10) + for _, sec := range Cfg.Section("markup").ChildSections() { name := strings.TrimPrefix(sec.Name(), "markup.") if name == "" { @@ -56,50 +59,62 @@ func newMarkup() { } func newMarkupSanitizer(name string, sec *ini.Section) { - haveElement := sec.HasKey("ELEMENT") - haveAttr := sec.HasKey("ALLOW_ATTR") - haveRegexp := sec.HasKey("REGEXP") + rule, ok := createMarkupSanitizerRule(name, sec) + if ok { + if strings.HasPrefix(name, "sanitizer.") { + names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2) + name = names[0] + } + for _, renderer := range ExternalMarkupRenderers { + if name == renderer.MarkupName { + renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule) + return + } + } + ExternalSanitizerRules = append(ExternalSanitizerRules, rule) + } +} - if !haveElement && !haveAttr && !haveRegexp { - log.Warn("Skipping empty section: markup.%s.", name) - return +func createMarkupSanitizerRule(name string, sec *ini.Section) (MarkupSanitizerRule, bool) { + var rule MarkupSanitizerRule + + ok := false + if sec.HasKey("ALLOW_DATA_URI_IMAGES") { + rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false) + ok = true } - if !haveElement || !haveAttr || !haveRegexp { - log.Error("Missing required keys from markup.%s. Must have all three of ELEMENT, ALLOW_ATTR, and REGEXP defined!", name) - return - } + if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") { + rule.Element = sec.Key("ELEMENT").Value() + rule.AllowAttr = sec.Key("ALLOW_ATTR").Value() - elements := sec.Key("ELEMENT").Value() - allowAttrs := sec.Key("ALLOW_ATTR").Value() - regexpStr := sec.Key("REGEXP").Value() - - if regexpStr == "" { - rule := MarkupSanitizerRule{ - Element: elements, - AllowAttr: allowAttrs, - Regexp: nil, + if rule.Element == "" || rule.AllowAttr == "" { + log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name) + return rule, false } - ExternalSanitizerRules = append(ExternalSanitizerRules, rule) - return + regexpStr := sec.Key("REGEXP").Value() + if regexpStr != "" { + // Validate when parsing the config that this is a valid regular + // expression. Then we can use regexp.MustCompile(...) later. + compiled, err := regexp.Compile(regexpStr) + if err != nil { + log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err) + return rule, false + } + + rule.Regexp = compiled + } + + ok = true } - // Validate when parsing the config that this is a valid regular - // expression. Then we can use regexp.MustCompile(...) later. - compiled, err := regexp.Compile(regexpStr) - if err != nil { - log.Error("In module.%s: REGEXP (%s) at definition %d failed to compile: %v", regexpStr, name, err) - return + if !ok { + log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name) + return rule, false } - rule := MarkupSanitizerRule{ - Element: elements, - AllowAttr: allowAttrs, - Regexp: compiled, - } - - ExternalSanitizerRules = append(ExternalSanitizerRules, rule) + return rule, true } func newMarkupRenderer(name string, sec *ini.Section) { @@ -126,7 +141,7 @@ func newMarkupRenderer(name string, sec *ini.Section) { return } - ExternalMarkupRenderers = append(ExternalMarkupRenderers, MarkupRenderer{ + ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ Enabled: sec.Key("ENABLED").MustBool(false), MarkupName: name, FileExtensions: exts, From b223d361955f8b722f7dd0b358b2e57e6f359edf Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 24 Jun 2021 05:12:38 +0800 Subject: [PATCH 09/33] Rework repository archive (#14723) * Use storage to store archive files * Fix backend lint * Add archiver table on database * Finish archive download * Fix test * Add database migrations * Add status for archiver * Fix lint * Add queue * Add doctor to check and delete old archives * Improve archive queue * Fix tests * improve archive storage * Delete repo archives * Add missing fixture * fix fixture * Fix fixture * Fix test * Fix archiver cleaning * Fix bug * Add docs for repository archive storage * remove repo-archive configuration * Fix test * Fix test * Fix lint Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick --- custom/conf/app.example.ini | 10 + .../doc/advanced/config-cheat-sheet.en-us.md | 17 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 15 + .../user27/repo49.git/refs/heads/test/archive | 1 + models/fixtures/repo_archiver.yml | 1 + models/migrations/migrations.go | 2 + models/migrations/v181.go | 1 + models/migrations/v185.go | 22 + models/models.go | 1 + models/repo.go | 97 ++-- models/repo_archiver.go | 86 ++++ models/unit_tests.go | 2 + modules/context/context.go | 15 + modules/doctor/checkOldArchives.go | 59 +++ .../{commit_archive.go => repo_archive.go} | 31 +- modules/setting/repository.go | 6 + modules/setting/storage.go | 4 + modules/storage/storage.go | 15 +- routers/api/v1/repo/file.go | 3 +- routers/common/repo.go | 26 -- routers/init.go | 4 + routers/web/repo/repo.go | 122 ++++- routers/web/web.go | 3 +- services/archiver/archiver.go | 428 ++++++++---------- services/archiver/archiver_test.go | 157 ++----- 25 files changed, 648 insertions(+), 480 deletions(-) create mode 100644 integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive create mode 100644 models/fixtures/repo_archiver.yml create mode 100644 models/migrations/v185.go create mode 100644 models/repo_archiver.go create mode 100644 modules/doctor/checkOldArchives.go rename modules/git/{commit_archive.go => repo_archive.go} (60%) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 38a27509f7..5adfb0546f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2048,6 +2048,16 @@ PATH = ;; storage type ;STORAGE_TYPE = local +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; settings for repository archives, will override storage setting +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[storage.repo-archive] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; storage type +;STORAGE_TYPE = local + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; lfs storage will override storage 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 8f1f9ce42d..5e976174fb 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -995,6 +995,23 @@ MINIO_USE_SSL = false And used by `[attachment]`, `[lfs]` and etc. as `STORAGE_TYPE`. +## Repository Archive Storage (`storage.repo-archive`) + +Configuration for repository archive storage. It will inherit from default `[storage]` or +`[storage.xxx]` when set `STORAGE_TYPE` to `xxx`. The default of `PATH` +is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`. + +- `STORAGE_TYPE`: **local**: Storage type for repo archive, `local` for local disk or `minio` for s3 compatible object storage service or other name defined with `[storage.xxx]` +- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing. +- `PATH`: **./data/repo-archive**: Where to store archive files, only available when `STORAGE_TYPE` is `local`. +- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio` +- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio` +- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio` +- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio` +- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio` +- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio` +- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio` + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. 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 79cfd94cc7..2303a631d5 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -382,6 +382,21 @@ MINIO_USE_SSL = false 然后你在 `[attachment]`, `[lfs]` 等中可以把这个名字用作 `STORAGE_TYPE` 的值。 +## Repository Archive Storage (`storage.repo-archive`) + +Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx`, 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/repo-archive`,`MINIO_BASE_PATH` 默认为 `repo-archive/`。 + +- `STORAGE_TYPE`: **local**: Repository archive 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。 +- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。 +- `PATH`: 存放 Repository archive 上传的文件的地方,默认是 `data/repo-archive`。 +- `MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。 diff --git a/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive new file mode 100644 index 0000000000..0f13243bfd --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive @@ -0,0 +1 @@ +aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 diff --git a/models/fixtures/repo_archiver.yml b/models/fixtures/repo_archiver.yml new file mode 100644 index 0000000000..ca780a73aa --- /dev/null +++ b/models/fixtures/repo_archiver.yml @@ -0,0 +1 @@ +[] # empty diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 880f55092d..4e17a6a2c8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -319,6 +319,8 @@ var migrations = []Migration{ NewMigration("Create PushMirror table", createPushMirrorTable), // v184 -> v185 NewMigration("Rename Task errors to message", renameTaskErrorsToMessage), + // v185 -> v186 + NewMigration("Add new table repo_archiver", addRepoArchiver), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v181.go b/models/migrations/v181.go index 6ba4edc155..65045593ad 100644 --- a/models/migrations/v181.go +++ b/models/migrations/v181.go @@ -1,3 +1,4 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. diff --git a/models/migrations/v185.go b/models/migrations/v185.go new file mode 100644 index 0000000000..0969948897 --- /dev/null +++ b/models/migrations/v185.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addRepoArchiver(x *xorm.Engine) error { + // RepoArchiver represents all archivers + type RepoArchiver struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Type int `xorm:"unique(s)"` + Status int + CommitID string `xorm:"VARCHAR(40) unique(s)"` + CreatedUnix int64 `xorm:"INDEX NOT NULL created"` + } + return x.Sync2(new(RepoArchiver)) +} diff --git a/models/models.go b/models/models.go index c325fd3811..3266be0f4a 100644 --- a/models/models.go +++ b/models/models.go @@ -136,6 +136,7 @@ func init() { new(RepoTransfer), new(IssueIndex), new(PushMirror), + new(RepoArchiver), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/repo.go b/models/repo.go index dc4e03a28a..2baf6e9bdd 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1587,6 +1587,22 @@ func DeleteRepository(doer *User, uid, repoID int64) error { return err } + // Remove archives + var archives []*RepoArchiver + if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil { + return err + } + + for _, v := range archives { + v.Repo = repo + p, _ := v.RelativePath() + removeStorageWithNotice(sess, storage.RepoArchives, "Delete repo archive file", p) + } + + if _, err := sess.Delete(&RepoArchiver{RepoID: repoID}); err != nil { + return err + } + if repo.NumForks > 0 { if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil { log.Error("reset 'fork_id' and 'is_fork': %v", err) @@ -1768,64 +1784,45 @@ func DeleteRepositoryArchives(ctx context.Context) error { func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error { log.Trace("Doing: ArchiveCleanup") - if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { - return deleteOldRepositoryArchives(ctx, olderThan, idx, bean) - }); err != nil { - log.Trace("Error: ArchiveClean: %v", err) - return err + for { + var archivers []RepoArchiver + err := x.Where("created_unix < ?", time.Now().Add(-olderThan).Unix()). + Asc("created_unix"). + Limit(100). + Find(&archivers) + if err != nil { + log.Trace("Error: ArchiveClean: %v", err) + return err + } + + for _, archiver := range archivers { + if err := deleteOldRepoArchiver(ctx, &archiver); err != nil { + return err + } + } + if len(archivers) < 100 { + break + } } log.Trace("Finished: ArchiveCleanup") return nil } -func deleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration, idx int, bean interface{}) error { - repo := bean.(*Repository) - basePath := filepath.Join(repo.RepoPath(), "archives") +var delRepoArchiver = new(RepoArchiver) - for _, ty := range []string{"zip", "targz"} { - select { - case <-ctx.Done(): - return ErrCancelledf("before deleting old repository archives with filetype %s for %s", ty, repo.FullName()) - default: - } - - path := filepath.Join(basePath, ty) - file, err := os.Open(path) - if err != nil { - if !os.IsNotExist(err) { - log.Warn("Unable to open directory %s: %v", path, err) - return err - } - - // If the directory doesn't exist, that's okay. - continue - } - - files, err := file.Readdir(0) - file.Close() - if err != nil { - log.Warn("Unable to read directory %s: %v", path, err) - return err - } - - minimumOldestTime := time.Now().Add(-olderThan) - for _, info := range files { - if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { - select { - case <-ctx.Done(): - return ErrCancelledf("before deleting old repository archive file %s with filetype %s for %s", info.Name(), ty, repo.FullName()) - default: - } - toDelete := filepath.Join(path, info.Name()) - // This is a best-effort purge, so we do not check error codes to confirm removal. - if err = util.Remove(toDelete); err != nil { - log.Trace("Unable to delete %s, but proceeding: %v", toDelete, err) - } - } - } +func deleteOldRepoArchiver(ctx context.Context, archiver *RepoArchiver) error { + p, err := archiver.RelativePath() + if err != nil { + return err + } + _, err = x.ID(archiver.ID).Delete(delRepoArchiver) + if err != nil { + return err + } + if err := storage.RepoArchives.Delete(p); err != nil { + log.Error("delete repo archive file failed: %v", err) } - return nil } diff --git a/models/repo_archiver.go b/models/repo_archiver.go new file mode 100644 index 0000000000..833a22ee13 --- /dev/null +++ b/models/repo_archiver.go @@ -0,0 +1,86 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/timeutil" +) + +// RepoArchiverStatus represents repo archive status +type RepoArchiverStatus int + +// enumerate all repo archive statuses +const ( + RepoArchiverGenerating = iota // the archiver is generating + RepoArchiverReady // it's ready +) + +// RepoArchiver represents all archivers +type RepoArchiver struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Repo *Repository `xorm:"-"` + Type git.ArchiveType `xorm:"unique(s)"` + Status RepoArchiverStatus + CommitID string `xorm:"VARCHAR(40) unique(s)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` +} + +// LoadRepo loads repository +func (archiver *RepoArchiver) LoadRepo() (*Repository, error) { + if archiver.Repo != nil { + return archiver.Repo, nil + } + + var repo Repository + has, err := x.ID(archiver.RepoID).Get(&repo) + if err != nil { + return nil, err + } + if !has { + return nil, ErrRepoNotExist{ + ID: archiver.RepoID, + } + } + return &repo, nil +} + +// RelativePath returns relative path +func (archiver *RepoArchiver) RelativePath() (string, error) { + repo, err := archiver.LoadRepo() + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/%s/%s.%s", repo.FullName(), archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil +} + +// GetRepoArchiver get an archiver +func GetRepoArchiver(ctx DBContext, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) { + var archiver RepoArchiver + has, err := ctx.e.Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver) + if err != nil { + return nil, err + } + if has { + return &archiver, nil + } + return nil, nil +} + +// AddRepoArchiver adds an archiver +func AddRepoArchiver(ctx DBContext, archiver *RepoArchiver) error { + _, err := ctx.e.Insert(archiver) + return err +} + +// UpdateRepoArchiverStatus updates archiver's status +func UpdateRepoArchiverStatus(ctx DBContext, archiver *RepoArchiver) error { + _, err := ctx.e.ID(archiver.ID).Cols("status").Update(archiver) + return err +} diff --git a/models/unit_tests.go b/models/unit_tests.go index 5a145fa2c0..f8d6819333 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -74,6 +74,8 @@ func MainTest(m *testing.M, pathToGiteaRoot string) { setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars") + setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive") + if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } diff --git a/modules/context/context.go b/modules/context/context.go index 7b3fd2899a..64f8b12084 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -380,6 +380,21 @@ func (ctx *Context) ServeFile(file string, names ...string) { http.ServeFile(ctx.Resp, ctx.Req, file) } +// ServeStream serves file via io stream +func (ctx *Context) ServeStream(rd io.Reader, name string) { + ctx.Resp.Header().Set("Content-Description", "File Transfer") + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name) + ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") + ctx.Resp.Header().Set("Expires", "0") + ctx.Resp.Header().Set("Cache-Control", "must-revalidate") + ctx.Resp.Header().Set("Pragma", "public") + _, err := io.Copy(ctx.Resp, rd) + if err != nil { + ctx.ServerError("Download file failed", err) + } +} + // Error returned an error to web browser func (ctx *Context) Error(status int, contents ...string) { var v = http.StatusText(status) diff --git a/modules/doctor/checkOldArchives.go b/modules/doctor/checkOldArchives.go new file mode 100644 index 0000000000..a4e2ffbd1f --- /dev/null +++ b/modules/doctor/checkOldArchives.go @@ -0,0 +1,59 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package doctor + +import ( + "os" + "path/filepath" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" +) + +func checkOldArchives(logger log.Logger, autofix bool) error { + numRepos := 0 + numReposUpdated := 0 + err := iterateRepositories(func(repo *models.Repository) error { + if repo.IsEmpty { + return nil + } + + p := filepath.Join(repo.RepoPath(), "archives") + isDir, err := util.IsDir(p) + if err != nil { + log.Warn("check if %s is directory failed: %v", p, err) + } + if isDir { + numRepos++ + if autofix { + if err := os.RemoveAll(p); err == nil { + numReposUpdated++ + } else { + log.Warn("remove %s failed: %v", p, err) + } + } + } + return nil + }) + + if autofix { + logger.Info("%d / %d old archives in repository deleted", numReposUpdated, numRepos) + } else { + logger.Info("%d old archives in repository need to be deleted", numRepos) + } + + return err +} + +func init() { + Register(&Check{ + Title: "Check old archives", + Name: "check-old-archives", + IsDefault: false, + Run: checkOldArchives, + Priority: 7, + }) +} diff --git a/modules/git/commit_archive.go b/modules/git/repo_archive.go similarity index 60% rename from modules/git/commit_archive.go rename to modules/git/repo_archive.go index d075ba0911..07003aa6b2 100644 --- a/modules/git/commit_archive.go +++ b/modules/git/repo_archive.go @@ -8,6 +8,7 @@ package git import ( "context" "fmt" + "io" "path/filepath" "strings" ) @@ -33,32 +34,28 @@ func (a ArchiveType) String() string { return "unknown" } -// CreateArchiveOpts represents options for creating an archive -type CreateArchiveOpts struct { - Format ArchiveType - Prefix bool -} - // CreateArchive create archive content to the target path -func (c *Commit) CreateArchive(ctx context.Context, target string, opts CreateArchiveOpts) error { - if opts.Format.String() == "unknown" { - return fmt.Errorf("unknown format: %v", opts.Format) +func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error { + if format.String() == "unknown" { + return fmt.Errorf("unknown format: %v", format) } args := []string{ "archive", } - if opts.Prefix { - args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(c.repo.Path, ".git"))+"/") + if usePrefix { + args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") } args = append(args, - "--format="+opts.Format.String(), - "-o", - target, - c.ID.String(), + "--format="+format.String(), + commitID, ) - _, err := NewCommandContext(ctx, args...).RunInDir(c.repo.Path) - return err + var stderr strings.Builder + err := NewCommandContext(ctx, args...).RunInDirPipeline(repo.Path, target, &stderr) + if err != nil { + return ConcatenateError(err, stderr.String()) + } + return nil } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a7666895e1..c2a6357d94 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -251,6 +251,10 @@ var ( } RepoRootPath string ScriptType = "bash" + + RepoArchive = struct { + Storage + }{} ) func newRepository() { @@ -328,4 +332,6 @@ func newRepository() { if !filepath.IsAbs(Repository.Upload.TempPath) { Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath) } + + RepoArchive.Storage = getStorage("repo-archive", "", nil) } diff --git a/modules/setting/storage.go b/modules/setting/storage.go index 3ab08d8d2a..075152db59 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -43,6 +43,10 @@ func getStorage(name, typ string, targetSec *ini.Section) Storage { sec.Key("MINIO_LOCATION").MustString("us-east-1") sec.Key("MINIO_USE_SSL").MustBool(false) + if targetSec == nil { + targetSec, _ = Cfg.NewSection(name) + } + var storage Storage storage.Section = targetSec storage.Type = typ diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 984f154db4..b3708908f8 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -114,6 +114,9 @@ var ( Avatars ObjectStorage // RepoAvatars represents repository avatars storage RepoAvatars ObjectStorage + + // RepoArchives represents repository archives storage + RepoArchives ObjectStorage ) // Init init the stoarge @@ -130,7 +133,11 @@ func Init() error { return err } - return initLFS() + if err := initLFS(); err != nil { + return err + } + + return initRepoArchives() } // NewStorage takes a storage type and some config and returns an ObjectStorage or an error @@ -169,3 +176,9 @@ func initRepoAvatars() (err error) { RepoAvatars, err = NewStorage(setting.RepoAvatar.Storage.Type, &setting.RepoAvatar.Storage) return } + +func initRepoArchives() (err error) { + log.Info("Initialising Repository Archive storage with type: %s", setting.RepoArchive.Storage.Type) + RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, &setting.RepoArchive.Storage) + return +} diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 39a60df33f..e6427ea4f4 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -18,6 +18,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/routers/web/repo" ) // GetRawFile get a file by path on a repository @@ -126,7 +127,7 @@ func GetArchive(ctx *context.APIContext) { ctx.Repo.GitRepo = gitRepo defer gitRepo.Close() - common.Download(ctx.Context) + repo.Download(ctx.Context) } // GetEditorconfig get editor config of a repository diff --git a/routers/common/repo.go b/routers/common/repo.go index c61b5ec57f..22403da097 100644 --- a/routers/common/repo.go +++ b/routers/common/repo.go @@ -7,7 +7,6 @@ package common import ( "fmt" "io" - "net/http" "path" "path/filepath" "strings" @@ -19,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" - "code.gitea.io/gitea/services/archiver" ) // ServeBlob download a git.Blob @@ -41,30 +39,6 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error { return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc) } -// Download an archive of a repository -func Download(ctx *context.Context) { - uri := ctx.Params("*") - aReq := archiver.DeriveRequestFrom(ctx, uri) - - if aReq == nil { - ctx.Error(http.StatusNotFound) - return - } - - downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName() - complete := aReq.IsComplete() - if !complete { - aReq = archiver.ArchiveRepository(aReq) - complete = aReq.WaitForCompletion(ctx) - } - - if complete { - ctx.ServeFile(aReq.GetArchivePath(), downloadName) - } else { - ctx.Error(http.StatusNotFound) - } -} - // ServeData download file from io.Reader func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error { buf := make([]byte, 1024) diff --git a/routers/init.go b/routers/init.go index 4c28a95395..bbf39a3f50 100644 --- a/routers/init.go +++ b/routers/init.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/private" web_routers "code.gitea.io/gitea/routers/web" + "code.gitea.io/gitea/services/archiver" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" @@ -63,6 +64,9 @@ func NewServices() { mailer.NewContext() _ = cache.NewContext() notification.NewContext() + if err := archiver.Init(); err != nil { + log.Fatal("archiver init failed: %v", err) + } } // GlobalInit is for global configuration reload-able. diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index f149e92a8b..919fd4620d 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -15,8 +15,10 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/web" archiver_service "code.gitea.io/gitea/services/archiver" "code.gitea.io/gitea/services/forms" @@ -364,25 +366,123 @@ func RedirectDownload(ctx *context.Context) { ctx.Error(http.StatusNotFound) } -// InitiateDownload will enqueue an archival request, as needed. It may submit -// a request that's already in-progress, but the archiver service will just -// kind of drop it on the floor if this is the case. -func InitiateDownload(ctx *context.Context) { +// Download an archive of a repository +func Download(ctx *context.Context) { uri := ctx.Params("*") - aReq := archiver_service.DeriveRequestFrom(ctx, uri) - + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + if err != nil { + ctx.ServerError("archiver_service.NewRequest", err) + return + } if aReq == nil { ctx.Error(http.StatusNotFound) return } - complete := aReq.IsComplete() - if !complete { - aReq = archiver_service.ArchiveRepository(aReq) - complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second) + archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("models.GetRepoArchiver", err) + return + } + if archiver != nil && archiver.Status == models.RepoArchiverReady { + download(ctx, aReq.GetArchiveName(), archiver) + return + } + + if err := archiver_service.StartArchive(aReq); err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + + var times int + var t = time.NewTicker(time.Second * 1) + defer t.Stop() + + for { + select { + case <-graceful.GetManager().HammerContext().Done(): + log.Warn("exit archive download because system stop") + return + case <-t.C: + if times > 20 { + ctx.ServerError("wait download timeout", nil) + return + } + times++ + archiver, err = models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + if archiver != nil && archiver.Status == models.RepoArchiverReady { + download(ctx, aReq.GetArchiveName(), archiver) + return + } + } + } +} + +func download(ctx *context.Context, archiveName string, archiver *models.RepoArchiver) { + downloadName := ctx.Repo.Repository.Name + "-" + archiveName + + rPath, err := archiver.RelativePath() + if err != nil { + ctx.ServerError("archiver.RelativePath", err) + return + } + + if setting.RepoArchive.ServeDirect { + //If we have a signed url (S3, object storage), redirect to this directly. + u, err := storage.RepoArchives.URL(rPath, downloadName) + if u != nil && err == nil { + ctx.Redirect(u.String()) + return + } + } + + //If we have matched and access to release or issue + fr, err := storage.RepoArchives.Open(rPath) + if err != nil { + ctx.ServerError("Open", err) + return + } + defer fr.Close() + ctx.ServeStream(fr, downloadName) +} + +// InitiateDownload will enqueue an archival request, as needed. It may submit +// a request that's already in-progress, but the archiver service will just +// kind of drop it on the floor if this is the case. +func InitiateDownload(ctx *context.Context) { + uri := ctx.Params("*") + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + if err != nil { + ctx.ServerError("archiver_service.NewRequest", err) + return + } + if aReq == nil { + ctx.Error(http.StatusNotFound) + return + } + + archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + if archiver == nil || archiver.Status != models.RepoArchiverReady { + if err := archiver_service.StartArchive(aReq); err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + } + + var completed bool + if archiver != nil && archiver.Status == models.RepoArchiverReady { + completed = true } ctx.JSON(http.StatusOK, map[string]interface{}{ - "complete": complete, + "complete": completed, }) } diff --git a/routers/web/web.go b/routers/web/web.go index 2c8a6411a1..883213479c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/misc" - "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/web/admin" "code.gitea.io/gitea/routers/web/dev" "code.gitea.io/gitea/routers/web/events" @@ -888,7 +887,7 @@ func RegisterRoutes(m *web.Route) { }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode)) m.Group("/archive", func() { - m.Get("/*", common.Download) + m.Get("/*", repo.Download) m.Post("/*", repo.InitiateDownload) }, repo.MustBeNotEmpty, reqRepoCodeReader) diff --git a/services/archiver/archiver.go b/services/archiver/archiver.go index dfa6334d95..00c0281306 100644 --- a/services/archiver/archiver.go +++ b/services/archiver/archiver.go @@ -6,22 +6,20 @@ package archiver import ( + "errors" + "fmt" "io" - "io/ioutil" "os" - "path" "regexp" "strings" - "sync" - "time" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/storage" ) // ArchiveRequest defines the parameters of an archive request, which notably @@ -30,223 +28,174 @@ import ( // This is entirely opaque to external entities, though, and mostly used as a // handle elsewhere. type ArchiveRequest struct { - uri string - repo *git.Repository - refName string - ext string - archivePath string - archiveType git.ArchiveType - archiveComplete bool - commit *git.Commit - cchan chan struct{} + RepoID int64 + refName string + Type git.ArchiveType + CommitID string } -var archiveInProgress []*ArchiveRequest -var archiveMutex sync.Mutex - // SHA1 hashes will only go up to 40 characters, but SHA256 hashes will go all // the way to 64. var shaRegex = regexp.MustCompile(`^[0-9a-f]{4,64}$`) -// These facilitate testing, by allowing the unit tests to control (to some extent) -// the goroutine used for processing the queue. -var archiveQueueMutex *sync.Mutex -var archiveQueueStartCond *sync.Cond -var archiveQueueReleaseCond *sync.Cond +// NewRequest creates an archival request, based on the URI. The +// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() +// if it's determined that the request still needs to be satisfied. +func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) { + r := &ArchiveRequest{ + RepoID: repoID, + } -// GetArchivePath returns the path from which we can serve this archive. -func (aReq *ArchiveRequest) GetArchivePath() string { - return aReq.archivePath + var ext string + switch { + case strings.HasSuffix(uri, ".zip"): + ext = ".zip" + r.Type = git.ZIP + case strings.HasSuffix(uri, ".tar.gz"): + ext = ".tar.gz" + r.Type = git.TARGZ + default: + return nil, fmt.Errorf("Unknown format: %s", uri) + } + + r.refName = strings.TrimSuffix(uri, ext) + + var err error + // Get corresponding commit. + if repo.IsBranchExist(r.refName) { + r.CommitID, err = repo.GetBranchCommitID(r.refName) + if err != nil { + return nil, err + } + } else if repo.IsTagExist(r.refName) { + r.CommitID, err = repo.GetTagCommitID(r.refName) + if err != nil { + return nil, err + } + } else if shaRegex.MatchString(r.refName) { + if repo.IsCommitExist(r.refName) { + r.CommitID = r.refName + } else { + return nil, git.ErrNotExist{ + ID: r.refName, + } + } + } else { + return nil, fmt.Errorf("Unknow ref %s type", r.refName) + } + + return r, nil } // GetArchiveName returns the name of the caller, based on the ref used by the // caller to create this request. func (aReq *ArchiveRequest) GetArchiveName() string { - return aReq.refName + aReq.ext + return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() } -// IsComplete returns the completion status of this request. -func (aReq *ArchiveRequest) IsComplete() bool { - return aReq.archiveComplete -} - -// WaitForCompletion will wait for this request to complete, with no timeout. -// It returns whether the archive was actually completed, as the channel could -// have also been closed due to an error. -func (aReq *ArchiveRequest) WaitForCompletion(ctx *context.Context) bool { - select { - case <-aReq.cchan: - case <-ctx.Done(): - } - - return aReq.IsComplete() -} - -// TimedWaitForCompletion will wait for this request to complete, with timeout -// happening after the specified Duration. It returns whether the archive is -// now complete and whether we hit the timeout or not. The latter may not be -// useful if the request is complete or we started to shutdown. -func (aReq *ArchiveRequest) TimedWaitForCompletion(ctx *context.Context, dur time.Duration) (bool, bool) { - timeout := false - select { - case <-time.After(dur): - timeout = true - case <-aReq.cchan: - case <-ctx.Done(): - } - - return aReq.IsComplete(), timeout -} - -// The caller must hold the archiveMutex across calls to getArchiveRequest. -func getArchiveRequest(repo *git.Repository, commit *git.Commit, archiveType git.ArchiveType) *ArchiveRequest { - for _, r := range archiveInProgress { - // Need to be referring to the same repository. - if r.repo.Path == repo.Path && r.commit.ID == commit.ID && r.archiveType == archiveType { - return r - } - } - return nil -} - -// DeriveRequestFrom creates an archival request, based on the URI. The -// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() -// if it's determined that the request still needs to be satisfied. -func DeriveRequestFrom(ctx *context.Context, uri string) *ArchiveRequest { - if ctx.Repo == nil || ctx.Repo.GitRepo == nil { - log.Trace("Repo not initialized") - return nil - } - r := &ArchiveRequest{ - uri: uri, - repo: ctx.Repo.GitRepo, - } - - switch { - case strings.HasSuffix(uri, ".zip"): - r.ext = ".zip" - r.archivePath = path.Join(r.repo.Path, "archives/zip") - r.archiveType = git.ZIP - case strings.HasSuffix(uri, ".tar.gz"): - r.ext = ".tar.gz" - r.archivePath = path.Join(r.repo.Path, "archives/targz") - r.archiveType = git.TARGZ - default: - log.Trace("Unknown format: %s", uri) - return nil - } - - r.refName = strings.TrimSuffix(r.uri, r.ext) - isDir, err := util.IsDir(r.archivePath) +func doArchive(r *ArchiveRequest) (*models.RepoArchiver, error) { + ctx, commiter, err := models.TxDBContext() if err != nil { - ctx.ServerError("Download -> util.IsDir(archivePath)", err) - return nil + return nil, err } - if !isDir { - if err := os.MkdirAll(r.archivePath, os.ModePerm); err != nil { - ctx.ServerError("Download -> os.MkdirAll(archivePath)", err) - return nil - } + defer commiter.Close() + + archiver, err := models.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID) + if err != nil { + return nil, err } - // Get corresponding commit. - if r.repo.IsBranchExist(r.refName) { - r.commit, err = r.repo.GetBranchCommit(r.refName) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return nil - } - } else if r.repo.IsTagExist(r.refName) { - r.commit, err = r.repo.GetTagCommit(r.refName) - if err != nil { - ctx.ServerError("GetTagCommit", err) - return nil - } - } else if shaRegex.MatchString(r.refName) { - r.commit, err = r.repo.GetCommit(r.refName) - if err != nil { - ctx.NotFound("GetCommit", nil) - return nil + if archiver != nil { + // FIXME: If another process are generating it, we think it's not ready and just return + // Or we should wait until the archive generated. + if archiver.Status == models.RepoArchiverGenerating { + return nil, nil } } else { - ctx.NotFound("DeriveRequestFrom", nil) - return nil + archiver = &models.RepoArchiver{ + RepoID: r.RepoID, + Type: r.Type, + CommitID: r.CommitID, + Status: models.RepoArchiverGenerating, + } + if err := models.AddRepoArchiver(ctx, archiver); err != nil { + return nil, err + } } - archiveMutex.Lock() - defer archiveMutex.Unlock() - if rExisting := getArchiveRequest(r.repo, r.commit, r.archiveType); rExisting != nil { - return rExisting - } - - r.archivePath = path.Join(r.archivePath, base.ShortSha(r.commit.ID.String())+r.ext) - r.archiveComplete, err = util.IsFile(r.archivePath) + rPath, err := archiver.RelativePath() if err != nil { - ctx.ServerError("util.IsFile", err) - return nil - } - return r -} - -func doArchive(r *ArchiveRequest) { - var ( - err error - tmpArchive *os.File - destArchive *os.File - ) - - // Close the channel to indicate to potential waiters that this request - // has finished. - defer close(r.cchan) - - // It could have happened that we enqueued two archival requests, due to - // race conditions and difficulties in locking. Do one last check that - // the archive we're referring to doesn't already exist. If it does exist, - // then just mark the request as complete and move on. - isFile, err := util.IsFile(r.archivePath) - if err != nil { - log.Error("Unable to check if %s util.IsFile: %v. Will ignore and recreate.", r.archivePath, err) - } - if isFile { - r.archiveComplete = true - return + return nil, err } - // Create a temporary file to use while the archive is being built. We - // will then copy it into place (r.archivePath) once it's fully - // constructed. - tmpArchive, err = ioutil.TempFile("", "archive") - if err != nil { - log.Error("Unable to create a temporary archive file! Error: %v", err) - return + _, err = storage.RepoArchives.Stat(rPath) + if err == nil { + if archiver.Status == models.RepoArchiverGenerating { + archiver.Status = models.RepoArchiverReady + return archiver, models.UpdateRepoArchiverStatus(ctx, archiver) + } + return archiver, nil } + + if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("unable to stat archive: %v", err) + } + + rd, w := io.Pipe() defer func() { - tmpArchive.Close() - os.Remove(tmpArchive.Name()) + w.Close() + rd.Close() }() - - if err = r.commit.CreateArchive(graceful.GetManager().ShutdownContext(), tmpArchive.Name(), git.CreateArchiveOpts{ - Format: r.archiveType, - Prefix: setting.Repository.PrefixArchiveFiles, - }); err != nil { - log.Error("Download -> CreateArchive "+tmpArchive.Name(), err) - return - } - - // Now we copy it into place - if destArchive, err = os.Create(r.archivePath); err != nil { - log.Error("Unable to open archive " + r.archivePath) - return - } - _, err = io.Copy(destArchive, tmpArchive) - destArchive.Close() + var done = make(chan error) + repo, err := archiver.LoadRepo() if err != nil { - log.Error("Unable to write archive " + r.archivePath) - return + return nil, fmt.Errorf("archiver.LoadRepo failed: %v", err) } - // Block any attempt to finalize creating a new request if we're marking - r.archiveComplete = true + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + go func(done chan error, w *io.PipeWriter, archiver *models.RepoArchiver, gitRepo *git.Repository) { + defer func() { + if r := recover(); r != nil { + done <- fmt.Errorf("%v", r) + } + }() + + err = gitRepo.CreateArchive( + graceful.GetManager().ShutdownContext(), + archiver.Type, + w, + setting.Repository.PrefixArchiveFiles, + archiver.CommitID, + ) + _ = w.CloseWithError(err) + done <- err + }(done, w, archiver, gitRepo) + + // TODO: add lfs data to zip + // TODO: add submodule data to zip + + if _, err := storage.RepoArchives.Save(rPath, rd, -1); err != nil { + return nil, fmt.Errorf("unable to write archive: %v", err) + } + + err = <-done + if err != nil { + return nil, err + } + + if archiver.Status == models.RepoArchiverGenerating { + archiver.Status = models.RepoArchiverReady + if err = models.UpdateRepoArchiverStatus(ctx, archiver); err != nil { + return nil, err + } + } + + return archiver, commiter.Commit() } // ArchiveRepository satisfies the ArchiveRequest being passed in. Processing @@ -255,65 +204,46 @@ func doArchive(r *ArchiveRequest) { // anything. In all cases, the caller should be examining the *ArchiveRequest // being returned for completion, as it may be different than the one they passed // in. -func ArchiveRepository(request *ArchiveRequest) *ArchiveRequest { - // We'll return the request that's already been enqueued if it has been - // enqueued, or we'll immediately enqueue it if it has not been enqueued - // and it is not marked complete. - archiveMutex.Lock() - defer archiveMutex.Unlock() - if rExisting := getArchiveRequest(request.repo, request.commit, request.archiveType); rExisting != nil { - return rExisting - } - if request.archiveComplete { - return request - } +func ArchiveRepository(request *ArchiveRequest) (*models.RepoArchiver, error) { + return doArchive(request) +} - request.cchan = make(chan struct{}) - archiveInProgress = append(archiveInProgress, request) - go func() { - // Wait to start, if we have the Cond for it. This is currently only - // useful for testing, so that the start and release of queued entries - // can be controlled to examine the queue. - if archiveQueueStartCond != nil { - archiveQueueMutex.Lock() - archiveQueueStartCond.Wait() - archiveQueueMutex.Unlock() - } +var archiverQueue queue.UniqueQueue - // Drop the mutex while we process the request. This may take a long - // time, and it's not necessary now that we've added the reequest to - // archiveInProgress. - doArchive(request) - - if archiveQueueReleaseCond != nil { - archiveQueueMutex.Lock() - archiveQueueReleaseCond.Wait() - archiveQueueMutex.Unlock() - } - - // Purge this request from the list. To do so, we'll just take the - // index at which we ended up at and swap the final element into that - // position, then chop off the now-redundant final element. The slice - // may have change in between these two segments and we may have moved, - // so we search for it here. We could perhaps avoid this search - // entirely if len(archiveInProgress) == 1, but we should verify - // correctness. - archiveMutex.Lock() - defer archiveMutex.Unlock() - - idx := -1 - for _idx, req := range archiveInProgress { - if req == request { - idx = _idx - break +// Init initlize archive +func Init() error { + handler := func(data ...queue.Data) { + for _, datum := range data { + archiveReq, ok := datum.(*ArchiveRequest) + if !ok { + log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum) + continue + } + log.Trace("ArchiverData Process: %#v", archiveReq) + if _, err := doArchive(archiveReq); err != nil { + log.Error("Archive %v faild: %v", datum, err) } } - if idx == -1 { - log.Error("ArchiveRepository: Failed to find request for removal.") - return - } - archiveInProgress = append(archiveInProgress[:idx], archiveInProgress[idx+1:]...) - }() + } - return request + archiverQueue = queue.CreateUniqueQueue("repo-archive", handler, new(ArchiveRequest)) + if archiverQueue == nil { + return errors.New("unable to create codes indexer queue") + } + + go graceful.GetManager().RunWithShutdownFns(archiverQueue.Run) + + return nil +} + +// StartArchive push the archive request to the queue +func StartArchive(request *ArchiveRequest) error { + has, err := archiverQueue.Has(request) + if err != nil { + return err + } + if has { + return nil + } + return archiverQueue.Push(request) } diff --git a/services/archiver/archiver_test.go b/services/archiver/archiver_test.go index 6dcd942bf5..3f3f369987 100644 --- a/services/archiver/archiver_test.go +++ b/services/archiver/archiver_test.go @@ -6,108 +6,75 @@ package archiver import ( "path/filepath" - "sync" "testing" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) -var queueMutex sync.Mutex - func TestMain(m *testing.M) { models.MainTest(m, filepath.Join("..", "..")) } func waitForCount(t *testing.T, num int) { - var numQueued int - // Wait for up to 10 seconds for the queue to be impacted. - timeout := time.Now().Add(10 * time.Second) - for { - numQueued = len(archiveInProgress) - if numQueued == num || time.Now().After(timeout) { - break - } - } - - assert.Len(t, archiveInProgress, num) -} - -func releaseOneEntry(t *testing.T, inFlight []*ArchiveRequest) { - var nowQueued, numQueued int - - numQueued = len(archiveInProgress) - - // Release one, then wait up to 10 seconds for it to complete. - queueMutex.Lock() - archiveQueueReleaseCond.Signal() - queueMutex.Unlock() - timeout := time.Now().Add(10 * time.Second) - for { - nowQueued = len(archiveInProgress) - if nowQueued != numQueued || time.Now().After(timeout) { - break - } - } - - // Make sure we didn't just timeout. - assert.NotEqual(t, numQueued, nowQueued) - - // Also make sure that we released only one. - assert.Equal(t, numQueued-1, nowQueued) } func TestArchive_Basic(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) - archiveQueueMutex = &queueMutex - archiveQueueStartCond = sync.NewCond(&queueMutex) - archiveQueueReleaseCond = sync.NewCond(&queueMutex) - defer func() { - archiveQueueMutex = nil - archiveQueueStartCond = nil - archiveQueueReleaseCond = nil - }() - ctx := test.MockContext(t, "user27/repo49") firstCommit, secondCommit := "51f84af23134", "aacbdfe9e1c4" - bogusReq := DeriveRequestFrom(ctx, firstCommit+".zip") - assert.Nil(t, bogusReq) - test.LoadRepo(t, ctx, 49) - bogusReq = DeriveRequestFrom(ctx, firstCommit+".zip") - assert.Nil(t, bogusReq) - test.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() + bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName()) + // Check a series of bogus requests. // Step 1, valid commit with a bad extension. - bogusReq = DeriveRequestFrom(ctx, firstCommit+".dilbert") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert") + assert.Error(t, err) assert.Nil(t, bogusReq) // Step 2, missing commit. - bogusReq = DeriveRequestFrom(ctx, "dbffff.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") + assert.Error(t, err) assert.Nil(t, bogusReq) // Step 3, doesn't look like branch/tag/commit. - bogusReq = DeriveRequestFrom(ctx, "db.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") + assert.Error(t, err) assert.Nil(t, bogusReq) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName()) + + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName()) + // Now two valid requests, firstCommit with valid extensions. - zipReq := DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) assert.NotNil(t, zipReq) - tgzReq := DeriveRequestFrom(ctx, firstCommit+".tar.gz") + tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") + assert.NoError(t, err) assert.NotNil(t, tgzReq) - secondReq := DeriveRequestFrom(ctx, secondCommit+".zip") + secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip") + assert.NoError(t, err) assert.NotNil(t, secondReq) inFlight := make([]*ArchiveRequest, 3) @@ -128,41 +95,9 @@ func TestArchive_Basic(t *testing.T) { // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) - assert.Len(t, archiveInProgress, 3) - // Release them all, they'll then stall at the archiveQueueReleaseCond while - // we examine the queue state. - queueMutex.Lock() - archiveQueueStartCond.Broadcast() - queueMutex.Unlock() - - // Iterate through all of the in-flight requests and wait for their - // completion. - for _, req := range inFlight { - req.WaitForCompletion(ctx) - } - - for _, req := range inFlight { - assert.True(t, req.IsComplete()) - exist, err := util.IsExist(req.GetArchivePath()) - assert.NoError(t, err) - assert.True(t, exist) - } - - arbitraryReq := inFlight[0] - // Reopen the channel so we don't double-close, mark it incomplete. We're - // going to run it back through the archiver, and it should get marked - // complete again. - arbitraryReq.cchan = make(chan struct{}) - arbitraryReq.archiveComplete = false - doArchive(arbitraryReq) - assert.True(t, arbitraryReq.IsComplete()) - - // Queues should not have drained yet, because we haven't released them. - // Do so now. - assert.Len(t, archiveInProgress, 3) - - zipReq2 := DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) // This zipReq should match what's sitting in the queue, as we haven't // let it release yet. From the consumer's point of view, this looks like // a long-running archive task. @@ -173,46 +108,22 @@ func TestArchive_Basic(t *testing.T) { // predecessor has cleared out of the queue. ArchiveRepository(zipReq2) - // Make sure the queue hasn't grown any. - assert.Len(t, archiveInProgress, 3) - - // Make sure the queue drains properly - releaseOneEntry(t, inFlight) - assert.Len(t, archiveInProgress, 2) - releaseOneEntry(t, inFlight) - assert.Len(t, archiveInProgress, 1) - releaseOneEntry(t, inFlight) - assert.Empty(t, archiveInProgress) - // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout // cases. - var completed, timedout bool - timedReq := DeriveRequestFrom(ctx, secondCommit+".tar.gz") + timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") + assert.NoError(t, err) assert.NotNil(t, timedReq) ArchiveRepository(timedReq) - // Guaranteed to timeout; we haven't signalled the request to start.. - completed, timedout = timedReq.TimedWaitForCompletion(ctx, 2*time.Second) - assert.False(t, completed) - assert.True(t, timedout) - - queueMutex.Lock() - archiveQueueStartCond.Broadcast() - queueMutex.Unlock() - - // Shouldn't timeout, we've now signalled it and it's a small request. - completed, timedout = timedReq.TimedWaitForCompletion(ctx, 15*time.Second) - assert.True(t, completed) - assert.False(t, timedout) - - zipReq2 = DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) // Now, we're guaranteed to have released the original zipReq from the queue. // Ensure that we don't get handed back the released entry somehow, but they // should remain functionally equivalent in all fields. The exception here // is zipReq.cchan, which will be non-nil because it's a completed request. // It's fine to go ahead and set it to nil now. - zipReq.cchan = nil + assert.Equal(t, zipReq, zipReq2) assert.False(t, zipReq == zipReq2) From 5f2ef17fdb7523398f1b84d8a1cad0f24b72667e Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 23 Jun 2021 22:41:39 +0100 Subject: [PATCH 10/33] Don't WARN log UserNotExist errors on ExternalUserLogin failure (#16238) Instead log these at debug - with warn logging for other errors. Fix #16235 Signed-off-by: Andrew Thornton --- models/login_source.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/models/login_source.go b/models/login_source.go index 098b48a8cd..359b562b65 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -856,7 +856,11 @@ func UserSignIn(username, password string) (*User, error) { return authUser, nil } - log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err) + if IsErrUserNotExist(err) { + log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err) + } else { + log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err) + } } return nil, ErrUserNotExist{user.ID, user.Name, 0} From d13a0e621b9a87a50392450c96e7f293cb1e550c Mon Sep 17 00:00:00 2001 From: zeripath Date: Thu, 24 Jun 2021 00:02:23 +0100 Subject: [PATCH 11/33] Do not show No match found for tribute (#16231) Tribute.js will show an untranslated no match found if no emoji or mentions. Further the mentions should really require a preceding space. This PR fixes both of these. Signed-off-by: Andrew Thornton --- web_src/js/features/tribute.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js index 851ff74e57..6fdb2f5df0 100644 --- a/web_src/js/features/tribute.js +++ b/web_src/js/features/tribute.js @@ -32,7 +32,7 @@ function makeCollections({mentions, emoji}) { if (emoji) { collections.push({ values: window.config.tributeValues, - noMatchTemplate: () => null, + requireLeadingSpace: true, menuItemTemplate: (item) => { return `
@@ -69,7 +69,7 @@ export default async function attachTribute(elementOrNodeList, {mentions, emoji} emoji: emoji || emojiNodes.length > 0, }); - const tribute = new Tribute({collection: collections}); + const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); for (const node of uniqueNodes) { tribute.attach(node); } From 71c5a8f7f8b127c559115a6bbbfe6372ecf6dd10 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 24 Jun 2021 00:11:37 +0000 Subject: [PATCH 12/33] [skip ci] Updated translations via Crowdin --- options/locale/locale_bg-BG.ini | 11 +++++++++-- options/locale/locale_cs-CZ.ini | 11 +++++++++-- options/locale/locale_de-DE.ini | 15 +++++++++++++-- options/locale/locale_es-ES.ini | 11 +++++++++-- options/locale/locale_fa-IR.ini | 11 +++++++++-- options/locale/locale_fi-FI.ini | 11 +++++++++-- options/locale/locale_fr-FR.ini | 11 +++++++++-- options/locale/locale_hu-HU.ini | 11 +++++++++-- options/locale/locale_id-ID.ini | 11 +++++++++-- options/locale/locale_it-IT.ini | 11 +++++++++-- options/locale/locale_ja-JP.ini | 33 +++++++++++++++++++++++++++++++-- options/locale/locale_ko-KR.ini | 11 +++++++++-- options/locale/locale_lv-LV.ini | 11 +++++++++-- options/locale/locale_ml-IN.ini | 11 +++++++++-- options/locale/locale_nl-NL.ini | 11 +++++++++-- options/locale/locale_pl-PL.ini | 11 +++++++++-- options/locale/locale_pt-BR.ini | 11 +++++++++-- options/locale/locale_pt-PT.ini | 11 +++++++++-- options/locale/locale_ru-RU.ini | 11 +++++++++-- options/locale/locale_sr-SP.ini | 7 +++++++ options/locale/locale_sv-SE.ini | 11 +++++++++-- options/locale/locale_tr-TR.ini | 11 +++++++++-- options/locale/locale_uk-UA.ini | 11 +++++++++-- options/locale/locale_zh-CN.ini | 11 +++++++++-- options/locale/locale_zh-HK.ini | 9 ++++++++- options/locale/locale_zh-TW.ini | 11 +++++++++-- 26 files changed, 257 insertions(+), 49 deletions(-) diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index ebfc59d166..834c88e177 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -259,12 +259,19 @@ authorization_failed=Оторизацията беше неуспешна sspi_auth_failed=SSPI удостоверяването беше неуспешно [mail] + activate_account=Моля активирайте Вашия профил + activate_email=Провери адрес на ел. поща -reset_password=Възстановете акаунта си -register_success=Успешна регистрация + register_notify=Добре дошли в Gitea +reset_password=Възстановете акаунта си + +register_success=Успешна регистрация + + + diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 576cf0615e..8fbdfd4a55 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -314,12 +314,19 @@ password_pwned=Heslo, které jste zvolili, je na %s auf folgenden Link, um dein Konto zu aktivieren: + register_notify=Willkommen bei Gitea +register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! +register_notify.text_3=Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen. + +reset_password=Stelle dein Konto wieder her +reset_password.title=%s, du hast um Wiederherstellung deines Kontos gebeten + +register_success=Registrierung erfolgreich + + release.new.subject=Release %s in %s erschienen diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 2ece1992e6..6a62ff7726 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -316,12 +316,19 @@ password_pwned=La contraseña que eligió está en una 存在しないか、閲覧が許可されていません。 +never=無し [error] occurred=エラーが発生しました @@ -314,12 +316,19 @@ password_pwned=あなたが選択したパスワードは、過去の情報漏 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした [mail] + activate_account=あなたのアカウントをアクティベートしてください。 + activate_email=メール アドレスを確認します -reset_password=アカウントを回復 -register_success=登録が完了しました + register_notify=Giteaへようこそ +reset_password=アカウントを回復 + +register_success=登録が完了しました + + + release.new.subject=%[2]s の %[1]s がリリースされました repo.transfer.subject_to=%s が "%s" を %s に移転しようとしています @@ -724,6 +733,7 @@ mirror_prune_desc=不要になった古いリモートトラッキング参照 mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 自動的な同期を無効にする場合は0。 mirror_interval_invalid=ミラー間隔が不正です。 mirror_address=クローンするURL +mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。 mirror_address_url_invalid=入力したURLは無効です。 URLの構成要素はすべて正しくエスケープする必要があります。 mirror_address_protocol_invalid=入力したURLは無効です。 ミラーできるのは、http(s):// または git:// の場所からだけです。 mirror_lfs=Large File Storage (LFS) @@ -786,6 +796,7 @@ form.reach_limit_of_creation_n=すでにあなたが作成できるリポジト form.name_reserved=リポジトリ名 '%s' は予約されています。 form.name_pattern_not_allowed='%s' の形式はリポジトリ名に使用できません。 +need_auth=認証 migrate_options=移行オプション migrate_service=移行するサービス migrate_options_mirror_helper=このリポジトリをミラーにする @@ -819,11 +830,19 @@ migrated_from_fake=%[1]sから移行 migrate.migrate=%s からの移行 migrate.migrating=%s から移行しています ... migrate.migrating_failed=%s からの移行が失敗しました。 +migrate.migrating_failed.error=エラー: %s migrate.github.description=Github.com または Github Enterprise からデータを移行します。 migrate.git.description=Gitサービスからgitデータを移行またはミラーを作成します migrate.gitlab.description=GitLab.com またはセルフホストのgitlabサーバーからデータを移行します。 migrate.gitea.description=Gitea.comまたはセルフホストのGiteaサーバーからデータを移行します。 migrate.gogs.description=notabug.org や、他のセルフホストのGogsサーバーからデータを移行します。 +migrate.migrating_git=Gitデータ移行中 +migrate.migrating_topics=トピック移行中 +migrate.migrating_milestones=マイルストーン移行中 +migrate.migrating_labels=ラベル移行中 +migrate.migrating_releases=リリース移行中 +migrate.migrating_issues=課題移行中 +migrate.migrating_pulls=プルリクエスト移行中 mirror_from=ミラー元 forked_from=フォーク元 @@ -1546,6 +1565,15 @@ settings.hooks=Webhook settings.githooks=Gitフック settings.basic_settings=基本設定 settings.mirror_settings=ミラー設定 +settings.mirror_settings.docs=他のリポジトリへの自動的なプッシュ/プルを行うよう、プロジェクトを設定します。 ブランチ、タグ、コミットが自動的に同期されます。 リポジトリをミラーするには? +settings.mirror_settings.mirrored_repository=同期するリポジトリ +settings.mirror_settings.direction=方向 +settings.mirror_settings.direction.pull=プル +settings.mirror_settings.direction.push=プッシュ +settings.mirror_settings.last_update=最終更新 +settings.mirror_settings.push_mirror.none=プッシュミラーは設定されていません +settings.mirror_settings.push_mirror.remote_url=リモートGitリポジトリのURL +settings.mirror_settings.push_mirror.add=プッシュミラーを追加 settings.sync_mirror=今すぐ同期 settings.mirror_sync_in_progress=ミラー同期を実行しています。 しばらくあとでまた確認してください。 settings.email_notifications.enable=メール通知有効 @@ -1611,6 +1639,7 @@ settings.transfer_form_title=確認のためリポジトリ名を入力: settings.transfer_in_progress=現在進行中の転送があります。このリポジトリを別のユーザーに転送したい場合はキャンセルしてください。 settings.transfer_notices_1=- 個人ユーザーに移転すると、あなたはリポジトリへのアクセス権を失います。 settings.transfer_notices_2=- あなたが所有(または共同で所有)している組織に移転すると、リポジトリへのアクセス権は維持されます。 +settings.transfer_notices_3=- プライベートリポジトリを個人ユーザーに移転した場合は、最低限そのユーザーが読み取り権限を持つよう設定されます (必要に応じて権限が変更されます)。 settings.transfer_owner=新しいオーナー settings.transfer_perform=転送を実行 settings.transfer_started=このリポジトリは転送のためにマークされており、「%s」からの確認を待っています diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index acf7efb478..f4a05a7e49 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -268,12 +268,19 @@ authorization_failed=인증 실패 sspi_auth_failed=SSPI 인증 실패 [mail] + activate_account=계정을 활성화하세요 + activate_email=이메일 주소 확인 -reset_password=계정 복구 -register_success=등록 완료 + register_notify=Gitea에 오신것을 환영합니다! +reset_password=계정 복구 + +register_success=등록 완료 + + + diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index f4db35671a..2fad43b543 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -314,12 +314,19 @@ password_pwned=Ievadītā parole ir Date: Fri, 25 Jun 2021 02:37:07 +0800 Subject: [PATCH 14/33] Replace ARCCache with TwoQueueCache to avoid patent issue (#16240) Co-authored-by: Mura Li Co-authored-by: techknowlogick --- modules/highlight/highlight.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index e22e9d5b32..568035fbb7 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -33,7 +33,7 @@ var ( once sync.Once - cache *lru.ARCCache + cache *lru.TwoQueueCache ) // NewContext loads custom highlight map from local config @@ -45,7 +45,7 @@ func NewContext() { } // The size 512 is simply a conservative rule of thumb - c, err := lru.NewARC(512) + c, err := lru.New2Q(512) if err != nil { panic(fmt.Sprintf("failed to initialize LRU cache for highlighter: %s", err)) } From 6c3433151fdb84a9dc1214442573da2d7cc76e3e Mon Sep 17 00:00:00 2001 From: sebastian-sauer Date: Fri, 25 Jun 2021 00:05:51 +0200 Subject: [PATCH 15/33] API: Allow COMMENT reviews to not specify a body (#16229) * Allow COMMENT reviews to not specify a body when using web ui there is no need to specify a body. so we don't need to specify a body if adding a COMMENT-review via our api. * Ensure comments or Body is provided and add some integration tests for reviewtype COMMENT. Signed-off-by: Sebastian Sauer --- integrations/api_pull_review_test.go | 54 ++++++++++++++++++++++++++++ routers/api/v1/repo/pull_review.go | 22 ++++++++---- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go index ebe8539a82..bcc0cbffcb 100644 --- a/integrations/api_pull_review_test.go +++ b/integrations/api_pull_review_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" + jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" ) @@ -139,6 +140,59 @@ func TestAPIPullReview(t *testing.T) { req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token) resp = session.MakeRequest(t, req, http.StatusNoContent) + // test CreatePullReview Comment without body but with comments + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + // Body: "", + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{{ + Path: "README.md", + Body: "first new line", + OldLineNum: 0, + NewLineNum: 1, + }, { + Path: "README.md", + Body: "first old line", + OldLineNum: 1, + NewLineNum: 0, + }, + }, + }) + var commentReview api.PullReview + + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &commentReview) + assert.EqualValues(t, "COMMENT", commentReview.State) + assert.EqualValues(t, 2, commentReview.CodeCommentsCount) + assert.EqualValues(t, "", commentReview.Body) + assert.EqualValues(t, false, commentReview.Dismissed) + + // test CreatePullReview Comment with body but without comments + commentBody := "This is a body of the comment." + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + Body: commentBody, + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{}, + }) + + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &commentReview) + assert.EqualValues(t, "COMMENT", commentReview.State) + assert.EqualValues(t, 0, commentReview.CodeCommentsCount) + assert.EqualValues(t, commentBody, commentReview.Body) + assert.EqualValues(t, false, commentReview.Dismissed) + + // test CreatePullReview Comment without body and no comments + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + Body: "", + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{}, + }) + resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity) + errMap := make(map[string]interface{}) + json := jsoniter.ConfigCompatibleWithStandardLibrary + json.Unmarshal(resp.Body.Bytes(), &errMap) + assert.EqualValues(t, "review event COMMENT requires a body or a comment", errMap["message"].(string)) + // test get review requests // to make it simple, use same api with get review pullIssue12 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 35414e0a80..323904f45c 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -307,7 +307,7 @@ func CreatePullReview(ctx *context.APIContext) { } // determine review type - reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) + reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0) if isWrong { return } @@ -429,7 +429,7 @@ func SubmitPullReview(ctx *context.APIContext) { } // determine review type - reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) + reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0) if isWrong { return } @@ -463,12 +463,15 @@ func SubmitPullReview(ctx *context.APIContext) { } // preparePullReviewType return ReviewType and false or nil and true if an error happen -func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string) (models.ReviewType, bool) { +func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) { if err := pr.LoadIssue(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err) return -1, true } + needsBody := true + hasBody := len(strings.TrimSpace(body)) > 0 + var reviewType models.ReviewType switch event { case api.ReviewStateApproved: @@ -478,6 +481,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even return -1, true } reviewType = models.ReviewTypeApprove + needsBody = false case api.ReviewStateRequestChanges: // can not reject your own PR @@ -489,13 +493,19 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even case api.ReviewStateComment: reviewType = models.ReviewTypeComment + needsBody = false + // if there is no body we need to ensure that there are comments + if !hasBody && !hasComments { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event)) + return -1, true + } default: reviewType = models.ReviewTypePending } - // reject reviews with empty body if not approve type - if reviewType != models.ReviewTypeApprove && len(strings.TrimSpace(body)) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s need body", event)) + // reject reviews with empty body if a body is required for this call + if needsBody && !hasBody { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event)) return -1, true } From 837e8b30a76293fd96d2aae872e30637668ddb3f Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 25 Jun 2021 00:11:36 +0000 Subject: [PATCH 16/33] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 37 ++++++++++++++++++++++ options/locale/locale_es-ES.ini | 56 +++++++++++++++++++++++++++++++++ options/locale/locale_fr-FR.ini | 44 ++++++++++++++++++++++++++ options/locale/locale_hu-HU.ini | 49 +++++++++++++++++++++++++++++ options/locale/locale_id-ID.ini | 2 ++ options/locale/locale_it-IT.ini | 2 ++ options/locale/locale_ko-KR.ini | 4 +++ options/locale/locale_nl-NL.ini | 13 ++++++++ options/locale/locale_zh-TW.ini | 7 +++-- 9 files changed, 211 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 4572aefb0d..e8f2d1d587 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -316,30 +316,64 @@ password_pwned=Das von dir gewählte Passwort ist auf einer %s, activate_account=Bitte aktiviere dein Konto +activate_account.title=%s, bitte aktiviere dein Konto +activate_account.test_1=Hallo %[1]s, danke für deine Registrierung bei %[2]! +activate_account.test_2=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: activate_email=Bestätige deine E-Mail-Adresse +activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: register_notify=Willkommen bei Gitea +register_notify.title=%[1]s, willkommen bei %[2]s register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! +register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden. register_notify.text_3=Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen. reset_password=Stelle dein Konto wieder her reset_password.title=%s, du hast um Wiederherstellung deines Kontos gebeten +reset_password.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto wiederherzustellen: register_success=Registrierung erfolgreich +issue_assigned.pull=@%[1]s hat dich im Repository %[3]s dem Pull Request %[2]s zugewiesen. +issue_assigned.issue=@%[1]s hat dich im Repository %[3]s dem Issue %[2]s zugewiesen. +issue.x_mentioned_you=@%s hat dich erwähnt: +issue.action.force_push=%[1]s hat %[3]s mit %[4]s auf %[2]s überschrieben. +issue.action.push_1=@%[1]s hat einen Commit auf %[2]s gepusht +issue.action.push_n=@%[1]s hat %[3]d Commits auf %[2]s gepusht +issue.action.close=@%[1]s hat #%[2]d geschlossen. +issue.action.reopen=@%[1]s hat #%[2]d wieder geöffnet. +issue.action.merge=@%[1]s hat #%[2]d in %[3]s gemergt. +issue.action.approve=@%[1]s hat diesen Pull-Request approved. +issue.action.reject=@%[1]s hat Änderungen auf diesem Pull-Request angefordert. +issue.action.review=@%[1]s hat diesen Pull-Request kommentiert. +issue.action.review_dismissed=@%[1]s hat das letzte Review von %[2]s für diesen Pull Request verworfen. +issue.action.ready_for_review=@%[1]s hat diesen Pull Request zum Review freigegeben. +issue.action.new=@%[1]s hat #%[2]d geöffnet. +issue.in_tree_path=In %s: release.new.subject=Release %s in %s erschienen +release.new.text=@%[1]s hat %[2]s in %[3]s released +release.title=Titel: %s +release.note=Anmerkung: +release.downloads=Downloads: +release.download.zip=Quellcode (ZIP Datei) +release.download.targz=Quellcode (TAR.GZ Datei) repo.transfer.subject_to=%s möchte "%s" an %s übertragen repo.transfer.subject_to_you=%s möchte dir "%s" übertragen repo.transfer.to_you=dir +repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere es einfach. repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt +repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt: [modal] yes=Ja @@ -841,6 +875,7 @@ migrate.gitlab.description=Migriere Daten von GitLab.com oder einem selbst gehos migrate.gitea.description=Migriere Daten von Gitea.com oder einem selbst gehostetem Gitea Server. migrate.gogs.description=Migriere Daten von notabug.org oder einem anderen, selbst gehosteten Gogs Server. migrate.migrating_git=Git Daten werden migriert +migrate.migrating_topics=Themen werden migriert migrate.migrating_milestones=Meilensteine werden migriert migrate.migrating_labels=Labels werden migriert migrate.migrating_releases=Releases werden migriert @@ -1568,6 +1603,8 @@ settings.hooks=Webhooks settings.githooks=Git-Hooks settings.basic_settings=Grundeinstellungen settings.mirror_settings=Mirror-Einstellungen +settings.mirror_settings.docs=Richte dein Projekt so ein, dass Änderungen automatisch in ein anderes Repository gepusht, oder aus einem anderen Repository gepullt werden. Branches, tags und commits werden dann automatisch synchronisiert. Wie kann ich ein Repository spiegeln? (Englisch) +settings.mirror_settings.mirrored_repository=Gespiegeltes Repository settings.mirror_settings.direction=Richtung settings.mirror_settings.direction.pull=Pull settings.mirror_settings.direction.push=Push diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 6a62ff7726..017b83a9b3 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -316,26 +316,64 @@ password_pwned=La contraseña que eligió está en una establezca su contraseña primero. reset_password=Recupere su cuenta +reset_password.title=%s, has solicitado recuperar tu cuenta +reset_password.text=Haga clic en el siguiente enlace para recuperar su cuenta dentro de %s: register_success=Registro completado +issue_assigned.pull=@%[1]s le asignó al pull request %[2]s en el repositorio %[3]s. +issue_assigned.issue=@%[1]s le asignó a la incidencia %[2]s en el repositorio %[3]s. +issue.x_mentioned_you=@%s te mencionó: +issue.action.force_push=%[1]s empujó a la fuerza el %[2]s de %[3]s a %[4]s. +issue.action.push_1=@%[1]s hizo 1 commit al %[2]s +issue.action.push_n=@%[1]s push %[3]d commits a %[2]s +issue.action.close=@%[1]s cerró #%[2]d. +issue.action.reopen=@%[1]s reabrió #%[2]d. +issue.action.merge=@%[1]s fusionó #%[2]d en %[3]s. +issue.action.approve=@%[1]s aprobó este pull request. +issue.action.reject=@%[1]s solicitó cambios en este pull request. +issue.action.review=@%[1]s comentó en este pull request. +issue.action.review_dismissed=@%[1]s descartó la última revisión de %[2]s para este pull request. +issue.action.ready_for_review=@%[1]s marcó este pull request listo para ser revisado. +issue.action.new=@%[1]s creó #%[2]d. +issue.in_tree_path=En %s: release.new.subject=%s en %s publicado +release.new.text=@%[1]s lanzó %[2]s en %[3]s +release.title=Título: %s +release.note=Nota: +release.downloads=Descargas: +release.download.zip=Código fuente (ZIP) +release.download.targz=Código fuente (TAR.GZ) repo.transfer.subject_to=%s desea transferir "%s" a %s repo.transfer.subject_to_you=%s desea transferir "%s" a usted repo.transfer.to_you=usted +repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórelo. repo.collaborator.added.subject=%s le añadió en %s +repo.collaborator.added.text=Has sido añadido como colaborador del repositorio: [modal] yes=Sí @@ -733,6 +771,7 @@ mirror_prune_desc=Eliminar referencias de seguimiento de remotes obsoletas mirror_interval=Intervalo de réplica (Las unidades de tiempo válidas son 'h', 'm', 's'). Pone 0 para deshabilitar la sincronización automática. mirror_interval_invalid=El intervalo de réplica no es válido. mirror_address=Clonar desde URL +mirror_address_desc=Ponga cualquier credencial requerida en la sección de Autorización. mirror_address_url_invalid=La url proporcionada no es válida. Debe escapar correctamente de todos los componentes de la url. mirror_address_protocol_invalid=La url proporcionada no es válida. Sólo las ubicaciones http(s):// o git:// pueden ser replicadas desde. mirror_lfs=Almacenamiento de archivos grande (LFS) @@ -741,6 +780,8 @@ mirror_lfs_endpoint=Punto final de LFS mirror_lfs_endpoint_desc=Sync intentará usar la url del clon para determinar el servidor LFS. También puede especificar un punto final personalizado si los datos LFS del repositorio se almacenan en otro lugar. mirror_last_synced=Sincronizado por última vez mirror_password_placeholder=(Sin cambios) +mirror_password_blank_placeholder=(Indefinido) +mirror_password_help=Cambie el nombre de usario para eliminar una contraseña almacenada. watchers=Seguidores stargazers=Fans forks=Forks @@ -827,11 +868,19 @@ migrated_from_fake=Migrado desde %[1]s migrate.migrate=Migrar desde %s migrate.migrating=Migrando desde %s... migrate.migrating_failed=La migración desde %s ha fallado. +migrate.migrating_failed.error=Error: %s migrate.github.description=Migrar datos de Github.com o Github Enterprise. migrate.git.description=Migrar o replicar de datos de git desde los servicios de Git migrate.gitlab.description=Migrar datos de GitLab.com o servidor gitlab autoalojado. migrate.gitea.description=Migrando datos de Gitea.com o servidor Gitea autoalojado. migrate.gogs.description=Migrando datos de notabug.org u otro servidor de Gogs autoalojado. +migrate.migrating_git=Migrando datos de Git +migrate.migrating_topics=Migrando Temas +migrate.migrating_milestones=Migrando Hitos +migrate.migrating_labels=Migrando etiquetas +migrate.migrating_releases=Migrando Lanzamientos +migrate.migrating_issues=Migrando Incidencías +migrate.migrating_pulls=Migrando Pull Requests mirror_from=réplica de forked_from=forkeado de @@ -1325,7 +1374,10 @@ pulls.manually_merged_as=El Pull Request se ha fusionado manualmente como Comience el título con %s para prevenir que el pull request se fusione accidentalmente.` +pulls.cannot_merge_work_in_progress=Este pull request está marcado como un trabajo en curso. pulls.still_in_progress=¿Aún en curso? +pulls.add_prefix=Añadir prefijo %s +pulls.remove_prefix=Eliminar prefijo %s pulls.data_broken=Este pull request está rota debido a que falta información del fork. pulls.files_conflicted=Este pull request tiene cambios en conflicto con la rama de destino. pulls.is_checking=La comprobación de conflicto de fusión está en progreso. Inténtalo de nuevo en unos momentos. @@ -1552,10 +1604,14 @@ settings.githooks=Git Hooks settings.basic_settings=Configuración Básica settings.mirror_settings=Configuración de réplica settings.mirror_settings.docs=Configure su proyecto para insertar y/o extraer automáticamente los cambios hacia/desde otro repositorio. Las ramas, etiquetas y commits se sincronizarán automáticamente. ¿Cómo replico los repositorios? +settings.mirror_settings.mirrored_repository=Repositorio Replicado settings.mirror_settings.direction=Dirección settings.mirror_settings.direction.pull=Pull +settings.mirror_settings.direction.push=Push settings.mirror_settings.last_update=Última actualización +settings.mirror_settings.push_mirror.none=No hay Réplicas de Push configurados settings.mirror_settings.push_mirror.remote_url=URL del repositorio remoto de Git +settings.mirror_settings.push_mirror.add=Añadir Réplica de Push settings.sync_mirror=Sincronizar ahora settings.mirror_sync_in_progress=La sincronización del repositorio replicado está en curso. Vuelva a intentarlo más tarde. settings.email_notifications.enable=Habilitar las notificaciones por correo electrónico diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 42d7ada018..493c950581 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -91,8 +91,10 @@ loading=Chargement… step1=Étape 1: step2=Étape 2: +error=Erreur error404=La page que vous essayez d'atteindre n'existe pas ou vous n'êtes pas autorisé à la voir. +never=Jamais [error] occurred=Une erreur est survenue @@ -316,6 +318,7 @@ password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned [mail] activate_account=Veuillez activer votre compte +activate_account.title=%s, veuillez activer votre compte activate_email=Veuillez vérifier votre adresse e-mail @@ -326,14 +329,24 @@ reset_password=Récupérer votre compte register_success=Inscription réussie +issue.x_mentioned_you=@%s vous a mentionné: +issue.action.approve=@%[1]s a approuvé cette demande d'ajout. +issue.action.reject=@%[1]s a demandé des modifications sur cette demande d'ajout. +issue.action.review=@%[1]s a commenté sur cette demande d'ajout. +issue.action.review_dismissed=@%[1]s a rejeté la dernière révision de %[2]s pour cette demande d'ajout. +issue.action.ready_for_review=@%[1]s a marqué cette demande d'ajout prête à être revue. +issue.in_tree_path=Dans %s: release.new.subject=%s publiée dans %s +release.title=Titre: %s +release.downloads=Téléchargements : repo.transfer.subject_to=%s aimerait transférer "%s" à %s repo.transfer.subject_to_you=%s aimerait vous transférer "%s" repo.transfer.to_you=vous repo.collaborator.added.subject=%s vous a ajouté à %s +repo.collaborator.added.text=Vous avez été ajouté en tant que collaborateur du dépôt : [modal] yes=Oui @@ -729,6 +742,7 @@ mirror_prune_desc=Supprimer les références externes obsolètes mirror_interval=Intervalle de synchronisation ('h', 'm', et 's' sont des unités valides), 0 pour désactiver. mirror_interval_invalid=L'intervalle de synchronisation est invalide. mirror_address=Cloner depuis une URL +mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation. mirror_address_url_invalid=L'url fournie est invalide. Vous devez échapper tous les composants de l'url correctement. mirror_address_protocol_invalid=L'url fournie est invalide. Seuls les protocoles http(s):// ou git:// peuvent être la source du miroir. mirror_lfs=Stockage de fichiers volumineux (LFS) @@ -736,6 +750,9 @@ mirror_lfs_desc=Activer la mise en miroir des données LFS. mirror_lfs_endpoint=Point d'accès LFS mirror_lfs_endpoint_desc=La synchronisation tentera d'utiliser l'url de clonage pour déterminer le serveur LFS. Vous pouvez également spécifier un point d'accès personnalisé si les données LFS du dépôt sont stockées ailleurs. mirror_last_synced=Dernière synchronisation +mirror_password_placeholder=(Aucune modification) +mirror_password_blank_placeholder=(Non défini) +mirror_password_help=Modifiez le nom d'utilisateur pour effacer un mot de passe enregistré. watchers=Observateurs stargazers=Fans forks=Bifurcations @@ -788,6 +805,7 @@ form.reach_limit_of_creation_n=Vous avez déjà atteint la limite de %d dépôts form.name_reserved=Le dépôt "%s" a un nom réservé. form.name_pattern_not_allowed="%s" n'est pas autorisé dans un nom de dépôt. +need_auth=Autorisation migrate_options=Options de migration migrate_service=Service de migration migrate_options_mirror_helper=Ce dépôt sera un miroir @@ -821,11 +839,19 @@ migrated_from_fake=Migré de %[1]s migrate.migrate=Migrer depuis %s migrate.migrating=Migration de %s ... migrate.migrating_failed=La migration de %s a échoué. +migrate.migrating_failed.error=Erreur: %s migrate.github.description=Migration de données depuis Github.com ou Github Enterprise. migrate.git.description=Migration ou Miroir des données git depuis des services Git migrate.gitlab.description=Migration des données depuis GitLab.com ou d'un serveur gitlab hébergé. migrate.gitea.description=Migration des données depuis Gitea.com ou un serveur Gitea hébergé soi-même. migrate.gogs.description=Migration de données depuis notabug.org ou un autre serveur Gogs auto-hébergé. +migrate.migrating_git=Migration des données Git +migrate.migrating_topics=Migration des sujets +migrate.migrating_milestones=Migration des jalons +migrate.migrating_labels=Migration des étiquettes +migrate.migrating_releases=Migration des versions +migrate.migrating_issues=Migration des tickets +migrate.migrating_pulls=Migration des demandes d'ajout mirror_from=miroir de forked_from=bifurqué depuis @@ -858,6 +884,7 @@ branch=Branche tree=Aborescence clear_ref=`Effacer la référence actuelle` filter_branch_and_tag=Filtrer une branche ou un tag +find_tag=Rechercher un tag branches=Branches tags=Tags issues=Tickets @@ -1288,6 +1315,8 @@ issues.review.resolved_by=marquer cette conversation comme résolue issues.assignee.error=Tous les assignés n'ont pas été ajoutés en raison d'une erreur inattendue. issues.reference_issue.body=Corps +compare.compare_base=base +compare.compare_head=comparer pulls.desc=Activer les demandes de fusion et la revue de code. pulls.new=Nouvelle demande d'ajout @@ -1316,6 +1345,7 @@ pulls.manually_merged_as=La demande d'ajout a été fusionnée manuellement en t pulls.is_closed=La demande de fusion a été fermée. pulls.has_merged=La pull request a été fusionnée. pulls.title_wip_desc=`Préfixer le titre par %s pour empêcher cette demande d'ajout d'être fusionnée par erreur.` +pulls.remove_prefix=Enlever le préfixe %s pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée. pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants. @@ -1541,6 +1571,9 @@ settings.hooks=Déclencheurs Web settings.githooks=Déclencheurs Git settings.basic_settings=Paramètres de base settings.mirror_settings=Réglages Miroir +settings.mirror_settings.direction=Direction +settings.mirror_settings.last_update=Dernière mise à jour +settings.mirror_settings.push_mirror.remote_url=URL du dépôt distant Git settings.sync_mirror=Synchroniser maintenant settings.mirror_sync_in_progress=La synchronisation est en cours. Revenez dans une minute. settings.email_notifications.enable=Activer les notifications par e-mail @@ -1549,6 +1582,7 @@ settings.email_notifications.disable=Désactiver les notifications par e-mail settings.email_notifications.submit=Définir la préférence e-mail settings.site=Site Web settings.update_settings=Valider +settings.branches.update_default_branch=Changer la Branche par Défaut settings.advanced_settings=Paramètres avancés settings.wiki_desc=Activer le wiki du dépôt settings.use_internal_wiki=Utiliser le wiki interne @@ -1911,6 +1945,7 @@ diff.image.overlay=Superposition releases.desc=Suivi des versions et des téléchargements. release.releases=Versions release.detail=Détails de la version +release.tags=Tags release.new_release=Nouvelle version release.draft=Brouillon release.prerelease=Pré-publication @@ -2103,8 +2138,12 @@ dashboard.operation_switch=Basculer dashboard.operation_run=Exécuter dashboard.clean_unbind_oauth=Effacer les connexions OAuth associées dashboard.clean_unbind_oauth_success=Toutes les connexions OAuth associées ont été supprimées. +dashboard.cron.finished=Tâche planifiée : %[1]s a terminé +dashboard.delete_repo_archives=Supprimer toutes les archives des dépôts (ZIP, TAR.GZ, etc..) +dashboard.delete_repo_archives.started=Tâche de suppression de toutes les archives de dépôts démarrée. dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git sont manquants dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés +dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts dashboard.check_repo_stats=Voir les statistiques de tous les dépôts dashboard.archive_cleanup=Supprimer les archives des vieux dépôts dashboard.git_gc_repos=Collecter les déchets des dépôts @@ -2141,6 +2180,7 @@ dashboard.total_gc_time=Pause GC dashboard.total_gc_pause=Pause GC dashboard.last_gc_pause=Dernière Pause GC dashboard.gc_times=Nombres de GC +dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données users.user_manage_panel=Gestion du compte utilisateur users.new_account=Créer un compte @@ -2177,6 +2217,7 @@ users.delete_account=Supprimer cet utilisateur users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur. users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable. users.deletion_success=Le compte a été supprimé. +users.reset_2fa=Réinitialiser l'authentification à deux facteurs emails.email_manage_panel=Gestion des courriels des utilisateurs emails.primary=Principale @@ -2198,6 +2239,7 @@ orgs.members=Membres orgs.new_orga=Nouvelle organisation repos.repo_manage_panel=Gestion des dépôts +repos.unadopted=Dépôts non adoptés repos.owner=Propriétaire repos.name=Nom repos.private=Privé @@ -2383,6 +2425,7 @@ config.mailer_use_sendmail=Utiliser Sendmail config.mailer_sendmail_path=Chemin d’accès à Sendmail config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail config.mailer_sendmail_timeout=Délai d’attente de Sendmail +config.test_email_placeholder=E-mail (ex: test@example.com) config.send_test_mail=Envoyer un e-mail de test config.test_mail_failed=Impossible d'envoyer un e-mail de test à '%s' : %v config.test_mail_sent=Un e-mail de test à été envoyé à '%s'. @@ -2510,6 +2553,7 @@ notices.delete_selected=Supprimé les éléments sélectionnés notices.delete_all=Supprimer toutes les notifications notices.type=Type notices.type_1=Dépôt +notices.type_2=Tâche notices.desc=Description notices.op=Opération notices.delete_success=Les informations systèmes ont été supprimées. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 5671a998d1..953d5ab8ac 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -19,6 +19,9 @@ create_new=Létrehozás… user_profile_and_more=Profil és beállítások... signed_in_as=Bejelentkezve mint enable_javascript=Ez az oldal jobban működik JavaScript-tel. +toc=Tartalomjegyzék +licenses=Licencek +return_to_gitea=Vissza a Gitea-hoz username=Felhasználónév email=E-mail cím @@ -50,6 +53,8 @@ new_migrate=Új migráció new_mirror=Új tükör new_fork=Új másolat new_org=Új szervezet +new_project=Új projekt +new_project_board=Új projekt tábla manage_org=Szervezetek kezelése admin_panel=Rendszergazdai felület account_settings=Fiók beállítások @@ -70,6 +75,7 @@ issues=Hibajegyek milestones=Mérföldkövek cancel=Mégse +save=Mentés add=Hozzáadás add_all=Összes hozzáadása remove=Eltávolítás @@ -79,6 +85,8 @@ write=Írás preview=Előnézet loading=Betöltés… +step1=1. lépés: +step2=2. lépés: error404=Az elérni kívánt oldal vagy nem létezik, vagy nincs jogosultsága a megtekintéséhez. @@ -91,6 +99,7 @@ report_message=Ha biztos benne, hogy ez egy Gitea hiba, keressen a problémára app_desc=Fájdalommentes, saját gépre telepíthető Git szolgáltatás install=Könnyen telepíthető platform=Keresztplatformos +platform_desc=A Gitea minden platformon fut, ahol a Go fordíthat: Windows, macOS, Linux, ARM, stb. Válassza azt, amelyet szereti! lightweight=Könnyűsúlyú license=Nyílt forráskódú @@ -202,7 +211,12 @@ my_mirrors=Tükreim view_home=Nézet %s search_repos=Tároló keresés… +show_archived=Archivált +show_private=Privát +show_both_private_public=Publikus és privát mutatása +show_only_private=Csak privát mutatása +show_only_public=Csak publikus mutatása issues.in_your_repos=A tárolóidban @@ -225,6 +239,7 @@ register_helper_msg=Van már felhasználói fiókja? Jelentkezzen be! social_register_helper_msg=Van már felhasználói fiókja? Csatlakoztassa most! disable_register_prompt=Regisztráció le van tiltva. Kérjük, lépjen kapcsolatba az oldal adminisztrátorával. disable_register_mail=Ki van kapcsolva a visszaigazoló e-mail küldése a regisztrációnál. +remember_me=Eszköz megjegyzése forgot_password_title=Elfelejtett jelszó forgot_password=Elfelejtette a jelszavát? sign_up_now=Szeretne bejelentkezni? Regisztráljon most. @@ -374,6 +389,7 @@ repositories=Tárolók activity=Nyilvános tevékenységek followers=Követők starred=Csillagozott tárolók +projects=Projektek following=Követve follow=Követés unfollow=Követés törlése @@ -417,6 +433,7 @@ continue=Folytatás cancel=Mégsem language=Nyelv ui=Téma +privacy=Adatvédelem lookup_avatar_by_mail=Avatar mutatása email cím alapján federated_avatar_lookup=Összevont profilkép keresés @@ -480,6 +497,7 @@ subkeys=Alkulcsok key_id=Kulcs ID key_name=Kulcs neve key_content=Tartalom +principal_content=Tartalom add_key_success=A SSH kulcsod sikeresen hozzáadva: '%s' add_gpg_key_success=A GPG kulcsod sikeresen hozzáadva: '%s' delete_key=Eltávolítás @@ -511,6 +529,7 @@ new_token_desc=A tokent használó alkalmazásoknak teljes hozzáférése van a token_name=Token neve generate_token=Token generálása generate_token_success=Új token létrehozva. Másold le most, mivel többször nem fog megjelenni. +generate_token_name_duplicate=A %s nevet már használja egy alkalmazás. Válassz kérlek más nevet. delete_token=Törlés access_token_deletion=Hozzáférési Token Törlése access_token_deletion_desc=Egy token törlésével visszavonja a hozzáférést a fiókjához az ezt használó alkalmazásoktól. Folytatja? @@ -548,6 +567,7 @@ twofa_is_enrolled=A fiókja jelenleg használ kétlépcsős hit twofa_not_enrolled=A fiókja jelenleg nem használ kétlépcsős hitelesítést. twofa_disable=Kétlépcsős hitelesítés letiltása twofa_scratch_token_regenerate=Kaparós kód újragenerálása +twofa_enroll=Kétlépcsős hitelesítés használata twofa_disable_note=A kétlépcsős azonosítás szükség esetén letiltható. twofa_disable_desc=A kétlépcsős hitelesítés letiltása a fiókot kevésbé biztonságossá teszi. Folytatható? twofa_disabled=Kétlépcsős hitelesítés letiltva. @@ -623,9 +643,17 @@ reactions_more=és további %d language_other=Egyéb +desc.private=Privát +desc.public=Nyilvános +desc.private_template=Privát sablon +desc.public_template=Sablon +desc.internal=Belső +desc.archived=Archivált template.items=Sablon elemek template.git_content=Git tartalom (alapértelmezett branch) +template.git_hooks=Git Hook-ok +template.webhooks=Webhook-ok template.topics=Témák template.avatar=Avatar template.issue_labels=Hibajegy címkék @@ -686,11 +714,14 @@ tags=Címkék issues=Hibajegyek pulls=Egyesítési kérések labels=Címkék +org_labels_desc_manage=kezelés milestones=Mérföldkövek commits=Commit-ok commit=Commit +release=Kiadás releases=Kiadások +tag=Címke file_raw=Nyers file_history=Előzmények file_view_raw=Nyers fájl megtekintése @@ -701,6 +732,7 @@ audio_not_supported_in_browser=A böngésző nem támogatja a HTML5 audio tag-et stored_lfs=Git LFS-el eltárolva symbolic_link=Szimbolikus hivatkozás commit_graph=Commit gráf +commit_graph.hide_pr_refs=Pull request-ek elrejtése normal_view=Normál nézet line=sor lines=sor @@ -763,12 +795,17 @@ ext_issues.desc=Külső hibakövető csatlakoztatás. issues.desc=Hibajelentések, feladatok és mérföldkövek elrendezése. +issues.filter_milestones=Mérföldkövek szűrése +issues.filter_labels=Címkék szűrése issues.new=Új hibajegy issues.new.title_empty=A cím nem lehet üres issues.new.labels=Címkék +issues.new.add_labels_title=Címke alkalmazása issues.new.no_label=Nincs címke issues.new.clear_labels=Címkék kiürítése +issues.new.no_items=Nincsenek elemek issues.new.milestone=Mérföldkő +issues.new.add_milestone_title=Mérföldkő beállítása issues.new.no_milestone=Nincs mérföldkő issues.new.clear_milestone=Mérföldkő eltávolítása issues.new.open_milestone=Nyitott mérföldkövek @@ -776,6 +813,8 @@ issues.new.closed_milestone=Lezárt mérföldkövek issues.new.assignees=Megbízottak issues.new.clear_assignees=Megbízottak eltávolítása issues.new.no_assignees=Nincsenek megbízottak +issues.new.no_reviewers=Nincs véleményező +issues.new.add_reviewer_title=Véleményezés kérése issues.no_ref=Nincsen ág/címke megadva issues.create=Hibajegy létrehozása issues.new_label=Új címke @@ -851,6 +890,7 @@ issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból Jelentkezz be hogy csatlakozz a beszélgetéshez. issues.edit=Szerkesztés issues.cancel=Mégsem @@ -1160,6 +1200,8 @@ settings.add_team_duplicate=A csapat már rendelkezik a tárolóval settings.add_team_success=A csapatnak most van hozzáférése a tárolóhoz. settings.remove_team_success=A csapat hozzáférése a tárolóhoz törölve lett. settings.add_webhook=Webhook hozzáadása +settings.webhook_deletion=Webhook eltávolítása +settings.webhook_deletion_success=A webhook el lett távolítva. settings.webhook.test_delivery=Küldés Kipróbálása settings.webhook.request=Kérés settings.webhook.response=Válasz @@ -1260,6 +1302,7 @@ diff.show_unified_view=Egyesített nézet diff.stats_desc=%d fájl változott, egészen pontosan %d új sor hozzáadva és %d régi sor törölve diff.bin=BINáris diff.view_file=Fájl megtekintése +diff.file_byte_size=Méret diff.file_suppressed=A különbségek nem kerülnek megjelenítésre, mivel a fájl túl nagy diff.too_many_files=Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott diff.comment.placeholder=Hozzászólás létrehozása @@ -1283,6 +1326,7 @@ release.cancel=Mégse release.publish=Kiadás közzététele release.save_draft=Piszkozat mentése release.deletion_success=A kiadás törölve. +release.deletion_tag_success=A cimke törölve lett. release.tag_name_invalid=Ez a címkenév érvénytelen. release.downloads=Letöltések release.download_count=Letöltések: %s @@ -1657,6 +1701,7 @@ config.session_life_time=Munkamenet Élettartama config.https_only=Csak HTTPS config.cookie_life_time=Süti Élettartam +config.picture_config=Kép és Avatár Konfiguráció config.picture_service=Kép Szolgáltatás config.disable_gravatar=Gravatar Kikapcsolása config.enable_federated_avatar=Összevont profilkép lekérés engedélyezése @@ -1677,12 +1722,14 @@ config.log_config=Naplózási Beállítások config.log_mode=Naplózási Módja config.disabled_logger=Letiltva config.access_log_template=Sablon +config.xorm_log_sql=SQL naplózása monitor.cron=Ütemezett Feladatok monitor.name=Név monitor.schedule=Ütemezés monitor.next=Legközelebb monitor.previous=Legutóbb +monitor.execute_times=Végrehajtások monitor.process=Futó Folyamatok monitor.desc=Leírás monitor.start=Kezdés Időpontja @@ -1706,6 +1753,7 @@ notices.delete_selected=Kiválasztottak Törlése notices.delete_all=Minden Értesítés Törlése notices.type=Típus notices.type_1=Tároló +notices.type_2=Feladat notices.desc=Leírás notices.op=Op. notices.delete_success=A rendszer-értesítések törölve lettek. @@ -1724,6 +1772,7 @@ merge_pull_request=`végrehajtott egy egyesítési kérést: %s delete_tag=címke %[2]s törölve innen: %[3]s delete_branch=ág %[2]s törölve innen: %[3]s +compare_branch=Összehasonlítás compare_commits=%d commit összehasonlítása compare_commits_general=Commitok összehasonlítása diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 86fb628e5f..e7d2113452 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -19,6 +19,7 @@ create_new=Buat… user_profile_and_more=Profil dan Pengaturan… signed_in_as=Masuk sebagai enable_javascript=Situs web ini bekerja lebih baik dengan JavaScript. +toc=Daftar Isi username=Nama Pengguna email=Alamat Email @@ -70,6 +71,7 @@ issues=Masalah milestones=Tonggak cancel=Batal +save=Simpan add=Tambah add_all=Tambah Semua remove=Buang diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 43f70d8305..a59531b2b6 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -836,6 +836,7 @@ commits=Commit commit=Commit release=Rilascio releases=Rilasci +tag=Etichetta file_raw=Originale file_history=Cronologia file_view_source=Visualizza sorgente @@ -957,6 +958,7 @@ projects.board.edit_title=Nuovo Nome Della Scheda projects.board.new_title=Nuovo Nome Della Scheda projects.board.new_submit=Invia projects.board.new=Nuova Scheda +projects.board.set_default=Imposta come predefinito projects.board.delete=Elimina Scheda projects.board.deletion_desc=L'eliminazione di una scheda di progetto sposta tutti i problemi correlati a 'Uncategorized'. Continuare? projects.open=Apri diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index f4a05a7e49..e0b195d5e8 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -50,6 +50,8 @@ new_migrate=새 마이그레이션 new_mirror=새로운 미러 new_fork=새 저장소 포크 new_org=새로운 조직 +new_project=새 프로젝트 +new_project_board=새 프로젝트 보드 manage_org=조직 관리 admin_panel=사이트 관리 account_settings=계정 설정 @@ -70,6 +72,7 @@ issues=이슈들 milestones=마일스톤 cancel=취소 +save=저장 add=추가 add_all=모두 추가 remove=삭제 @@ -83,6 +86,7 @@ loading=불러오는 중... [error] +occurred=오류가 발생했습니다 [startpage] app_desc=편리한 설치형 Git 서비스 diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 91492b0aab..52436a6bc1 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -15,6 +15,7 @@ page=Pagina template=Sjabloon language=Taal notifications=Meldingen +active_stopwatch=Actieve Tijd Tracker create_new=Maken… user_profile_and_more=Profiel en instellingen… signed_in_as=Aangemeld als @@ -75,6 +76,7 @@ pull_requests=Pull requests issues=Kwesties milestones=Mijlpalen +ok=OK cancel=Annuleren save=Opslaan add=Toevoegen @@ -86,6 +88,8 @@ write=Schrijf preview=Voorbeeld loading=Laden… +step1=Stap 1: +step2=Stap 2: error404=De pagina die u probeert te bereiken bestaat niet of u bent niet gemachtigd om het te bekijken. @@ -202,6 +206,7 @@ default_enable_timetracking=Tijdregistratie standaard inschakelen default_enable_timetracking_popup=Tijdsregistratie voor nieuwe repositories standaard inschakelen. no_reply_address=Verborgen e-maildomein no_reply_address_helper=Domeinnaam voor gebruikers met een verborgen e-mailadres. Bijvoorbeeld zal de gebruikersnaam 'joe' in Git worden geregistreerd als 'joe@noreply.example.org' als het verborgen email domein is ingesteld op 'noreply.example.org'. +password_algorithm=Wachtwoord Hash Algoritme [home] uname_holder=Gebruikersnaam of e-mailadres @@ -215,6 +220,7 @@ my_mirrors=Mijn kopieën view_home=Bekijk %s search_repos=Zoek een repository… filter=Andere filters +filter_by_team_repositories=Filter op team repositories show_archived=Gearchiveerd show_both_archived_unarchived=Toont zowel gearchiveerd als niet-gearchiveerd @@ -316,8 +322,12 @@ register_success=Registratie succesvol +release.new.subject=%s in %s vrijgegeven +repo.transfer.subject_to=%s zou "%s" willen overdragen aan %s +repo.transfer.subject_to_you=%s wil "%s" aan jou overdragen +repo.collaborator.added.subject=%s heeft jou toegevoegd aan %s [modal] yes=Ja @@ -364,6 +374,7 @@ password_not_match=De wachtwoorden komen niet overeen. lang_select_error=Selecteer een taal uit de lijst. username_been_taken=Deze naam is al in gebruik. +username_change_not_local_user=Niet-lokale gebruikers mogen hun gebruikersnaam niet wijzigen. repo_name_been_taken=De repository-naam wordt al gebruikt. repository_files_already_exist=Er bestaan al bestanden voor deze repository. Neem contact op met de systeembeheerder. repository_files_already_exist.adopt=Bestanden bestaan al voor deze repository en kunnen alleen worden geadopteerd. @@ -659,6 +670,7 @@ email_notifications.submit=E-mailvoorkeur instellen [repo] owner=Eigenaar +owner_helper=Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories. repo_name=Naam van repository repo_name_helper=Goede repository-namen zijn kort, makkelijk te onthouden en uniek. repo_size=Repositorygrootte @@ -679,6 +691,7 @@ use_template=Gebruik dit sjabloon generate_repo=Repository genereren generate_from=Genereer van repo_desc=Omschrijving +repo_desc_helper=Voer korte beschrijving in (optioneel) repo_lang=Taal repo_gitignore_helper=Selecteer .gitignore templates. issue_labels=Issuelabels diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 902b9b2512..da0f2192a7 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -101,6 +101,7 @@ occurred=發生錯誤 report_message=如果你確定這是一個 Gitea 的 bug,請去 GitHub 搜尋相關的問題,如果有需要你也可以開一個新的問題 [startpage] +app_desc=一套極易架設的 Git 服務 install=安裝容易 install_desc=簡單地執行您平台的二進位檔,或是使用 Docker,你也可以從套件管理員安裝。 platform=跨平台 @@ -262,10 +263,10 @@ forgot_password_title=忘記密碼 forgot_password=忘記密碼? sign_up_now=還沒有帳戶?馬上註冊。 sign_up_successful=帳戶已成功建立。 -confirmation_mail_sent_prompt=一封新的確認信已發送至 %s。請檢查您的收件匣,並在 %s 內完成註冊作業。 +confirmation_mail_sent_prompt=新的確認信已發送至 %s。請在 %s內檢查您的收件匣並完成註冊作業。 must_change_password=更新您的密碼 allow_password_change=要求使用者更改密碼 (推薦) -reset_password_mail_sent_prompt=一封確認信已發送至 %s。請檢查您的收件匣,並在 %s 內完成帳戶救援作業。 +reset_password_mail_sent_prompt=確認信已發送至 %s。請在 %s內檢查您的收件匣並完成帳戶救援作業。 active_your_account=啟用您的帳戶 account_activated=帳戶已啟用 prohibit_login=禁止登入 @@ -525,7 +526,7 @@ add_new_email=新增電子信箱 add_new_openid=新增 OpenID URI add_email=新增電子信箱 add_openid=新增 OpenID URI -add_email_confirmation_sent=一封新的確認郵件已發送至 '%s',請檢查您的收件匣並在 %s 內確認您的電郵地址。 +add_email_confirmation_sent=確認信已發送至「%s」,請在 %s內檢查您的收件匣並確認您的電子信箱。 add_email_success=已加入新的電子信箱。 email_preference_set_success=已套用郵件偏好設定 add_openid_success=該 OpenID 已添加。 From 7a0ed9a0469b24768c9041e137bfcd2d28f05319 Mon Sep 17 00:00:00 2001 From: Martin Strob Date: Fri, 25 Jun 2021 14:38:41 +0200 Subject: [PATCH 17/33] fix IIS reverse proxy doc (#16246) --- docs/content/doc/usage/reverse-proxies.en-us.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/content/doc/usage/reverse-proxies.en-us.md b/docs/content/doc/usage/reverse-proxies.en-us.md index e2fdb1d2b7..b339048d68 100644 --- a/docs/content/doc/usage/reverse-proxies.en-us.md +++ b/docs/content/doc/usage/reverse-proxies.en-us.md @@ -221,6 +221,9 @@ If you wish to run Gitea with IIS. You will need to setup IIS with URL Rewrite a ```xml + + + From 44b8b07631666e3ae691149bdba31ca0f51569f5 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 25 Jun 2021 16:28:55 +0200 Subject: [PATCH 18/33] Add tag protection (#15629) * Added tag protection in hook. * Prevent UI tag creation if protected. * Added settings page. * Added tests. * Added suggestions. * Moved tests. * Use individual errors. * Removed unneeded methods. * Switched delete selector. * Changed method names. * No reason to be unique. * Allow editing of protected tags. * Removed unique key from migration. * Added docs page. * Changed date. * Respond with 404 to not found tags. * Replaced glob with regex pattern. * Added support for glob and regex pattern. * Updated documentation. * Changed white* to allow*. * Fixed edit button link. * Added cancel button. Co-authored-by: zeripath Co-authored-by: Lunny Xiao --- cmd/hook.go | 8 +- .../doc/advanced/protected-tags.en-us.md | 57 +++ integrations/mirror_pull_test.go | 2 + integrations/repo_tag_test.go | 74 ++++ models/error.go | 15 + models/migrations/migrations.go | 2 + models/migrations/v186.go | 26 ++ models/models.go | 1 + models/protected_tag.go | 131 +++++++ models/protected_tag_test.go | 162 ++++++++ models/repo.go | 1 + modules/validation/binding.go | 59 ++- modules/validation/binding_test.go | 7 +- modules/validation/regex_pattern_test.go | 60 +++ modules/web/middleware/binding.go | 2 + options/locale/locale_en-US.ini | 18 +- routers/private/hook.go | 367 ++++++++++-------- routers/web/repo/release.go | 16 + routers/web/repo/setting.go | 1 + routers/web/repo/tag.go | 182 +++++++++ routers/web/web.go | 9 + services/forms/repo_tag_form.go | 27 ++ services/release/release.go | 27 +- services/release/release_test.go | 26 ++ templates/repo/settings/nav.tmpl | 1 + templates/repo/settings/navbar.tmpl | 3 + templates/repo/settings/tags.tmpl | 132 +++++++ 27 files changed, 1227 insertions(+), 189 deletions(-) create mode 100644 docs/content/doc/advanced/protected-tags.en-us.md create mode 100644 integrations/repo_tag_test.go create mode 100644 models/migrations/v186.go create mode 100644 models/protected_tag.go create mode 100644 models/protected_tag_test.go create mode 100644 modules/validation/regex_pattern_test.go create mode 100644 routers/web/repo/tag.go create mode 100644 services/forms/repo_tag_form.go create mode 100644 templates/repo/settings/tags.tmpl diff --git a/cmd/hook.go b/cmd/hook.go index 312c9a14fc..2fbbfb4d21 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -221,8 +221,8 @@ Gitea or set your environment appropriately.`, "") total++ lastline++ - // If the ref is a branch, check if it's protected - if strings.HasPrefix(refFullName, git.BranchPrefix) { + // If the ref is a branch or tag, check if it's protected + if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { oldCommitIDs[count] = oldCommitID newCommitIDs[count] = newCommitID refFullNames[count] = refFullName @@ -230,7 +230,7 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, "*") if count >= hookBatchSize { - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs @@ -261,7 +261,7 @@ Gitea or set your environment appropriately.`, "") hookOptions.NewCommitIDs = newCommitIDs[:count] hookOptions.RefFullNames = refFullNames[:count] - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) statusCode, msg := private.HookPreReceive(username, reponame, hookOptions) switch statusCode { diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md new file mode 100644 index 0000000000..36e6e16975 --- /dev/null +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -0,0 +1,57 @@ +--- +date: "2021-05-14T00:00:00-00:00" +title: "Protected tags" +slug: "protected-tags" +weight: 45 +toc: false +draft: false +menu: + sidebar: + parent: "advanced" + name: "Protected tags" + weight: 45 + identifier: "protected-tags" +--- + +# Protected tags + +Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use an appropriate pattern to control multiple tags at once. + +**Table of Contents** + +{{< toc >}} + +## Setting up protected tags + +To protect a tag, you need to follow these steps: + +1. Go to the repository’s **Settings** > **Tags** page. +1. Type a pattern to match a name. You can use a single name, a [glob pattern](https://pkg.go.dev/github.com/gobwas/glob#Compile) or a regular expression. +1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. +1. Select **Save** to save the configuration. + +## Pattern protected tags + +The pattern uses [glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) or regular expressions to match a tag name. For regular expressions you need to enclose the pattern in slashes. + +Examples: + +| Type | Pattern Protected Tag | Possible Matching Tags | +| ----- | ------------------------ | --------------------------------------- | +| Glob | `v*` | `v`, `v-1`, `version2` | +| Glob | `v[0-9]` | `v0`, `v1` up to `v9` | +| Glob | `*-release` | `2.1-release`, `final-release` | +| Glob | `gitea` | only `gitea` | +| Glob | `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Glob | `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Glob | `*` | matches all possible tag names | +| Regex | `/\Av/` | `v`, `v-1`, `version2` | +| Regex | `/\Av[0-9]\z/` | `v0`, `v1` up to `v9` | +| Regex | `/\Av\d+\.\d+\.\d+\z/` | `v1.0.17`, `v2.1.0` | +| Regex | `/\Av\d+(\.\d+){0,2}\z/` | `v1`, `v2.1`, `v1.2.34` | +| Regex | `/-release\z/` | `2.1-release`, `final-release` | +| Regex | `/gitea/` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Regex | `/\Agitea\z/` | only `gitea` | +| Regex | `/^gitea$/` | only `gitea` | +| Regex | `/\A(v\|rel)-/` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Regex | `/.+/` | matches all possible tag names | diff --git a/integrations/mirror_pull_test.go b/integrations/mirror_pull_test.go index 0e4da74fcf..3908f35557 100644 --- a/integrations/mirror_pull_test.go +++ b/integrations/mirror_pull_test.go @@ -59,7 +59,9 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.2", Target: "master", Title: "v0.2 is released", diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go new file mode 100644 index 0000000000..eb3f2b47fb --- /dev/null +++ b/integrations/repo_tag_test.go @@ -0,0 +1,74 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "io/ioutil" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/release" + + "github.com/stretchr/testify/assert" +) + +func TestCreateNewTagProtected(t *testing.T) { + defer prepareTestEnv(t)() + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) + + t.Run("API", func(t *testing.T) { + defer PrintCurrentTest(t)() + + err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag") + assert.NoError(t, err) + + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-*", + }) + assert.NoError(t, err) + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-1.1", + AllowlistUserIDs: []int64{repo.OwnerID}, + }) + assert.NoError(t, err) + + err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag") + assert.Error(t, err) + assert.True(t, models.IsErrProtectedTagName(err)) + + err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag") + assert.NoError(t, err) + }) + + t.Run("Git", func(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + username := "user2" + httpContext := NewAPITestContext(t, username, "repo1") + + dstPath, err := ioutil.TempDir("", httpContext.Reponame) + assert.NoError(t, err) + defer util.RemoveAll(dstPath) + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) + + doGitClone(dstPath, u)(t) + + _, err = git.NewCommand("tag", "v-2").RunInDir(dstPath) + assert.NoError(t, err) + + _, err = git.NewCommand("push", "--tags").RunInDir(dstPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Tag v-2 is protected") + }) + }) +} diff --git a/models/error.go b/models/error.go index 501bf86869..513effdb02 100644 --- a/models/error.go +++ b/models/error.go @@ -985,6 +985,21 @@ func (err ErrInvalidTagName) Error() string { return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName) } +// ErrProtectedTagName represents a "ProtectedTagName" kind of error. +type ErrProtectedTagName struct { + TagName string +} + +// IsErrProtectedTagName checks if an error is a ErrProtectedTagName. +func IsErrProtectedTagName(err error) bool { + _, ok := err.(ErrProtectedTagName) + return ok +} + +func (err ErrProtectedTagName) Error() string { + return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName) +} + // ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error. type ErrRepoFileAlreadyExists struct { Path string diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4e17a6a2c8..978ba6368f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -321,6 +321,8 @@ var migrations = []Migration{ NewMigration("Rename Task errors to message", renameTaskErrorsToMessage), // v185 -> v186 NewMigration("Add new table repo_archiver", addRepoArchiver), + // v186 -> v187 + NewMigration("Create protected tag table", createProtectedTagTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v186.go b/models/migrations/v186.go new file mode 100644 index 0000000000..eb6ec7118c --- /dev/null +++ b/models/migrations/v186.go @@ -0,0 +1,26 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createProtectedTagTable(x *xorm.Engine) error { + type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + NamePattern string + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync2(new(ProtectedTag)) +} diff --git a/models/models.go b/models/models.go index 3266be0f4a..610933d327 100644 --- a/models/models.go +++ b/models/models.go @@ -137,6 +137,7 @@ func init() { new(IssueIndex), new(PushMirror), new(RepoArchiver), + new(ProtectedTag), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/protected_tag.go b/models/protected_tag.go new file mode 100644 index 0000000000..88f20dd29a --- /dev/null +++ b/models/protected_tag.go @@ -0,0 +1,131 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "regexp" + "strings" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/gobwas/glob" +) + +// ProtectedTag struct +type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + NamePattern string + RegexPattern *regexp.Regexp `xorm:"-"` + GlobPattern glob.Glob `xorm:"-"` + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// InsertProtectedTag inserts a protected tag to database +func InsertProtectedTag(pt *ProtectedTag) error { + _, err := x.Insert(pt) + return err +} + +// UpdateProtectedTag updates the protected tag +func UpdateProtectedTag(pt *ProtectedTag) error { + _, err := x.ID(pt.ID).AllCols().Update(pt) + return err +} + +// DeleteProtectedTag deletes a protected tag by ID +func DeleteProtectedTag(pt *ProtectedTag) error { + _, err := x.ID(pt.ID).Delete(&ProtectedTag{}) + return err +} + +// EnsureCompiledPattern ensures the glob pattern is compiled +func (pt *ProtectedTag) EnsureCompiledPattern() error { + if pt.RegexPattern != nil || pt.GlobPattern != nil { + return nil + } + + var err error + if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") { + pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1 : len(pt.NamePattern)-1]) + } else { + pt.GlobPattern, err = glob.Compile(pt.NamePattern) + } + return err +} + +// IsUserAllowed returns true if the user is allowed to modify the tag +func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) { + if base.Int64sContains(pt.AllowlistUserIDs, userID) { + return true, nil + } + + if len(pt.AllowlistTeamIDs) == 0 { + return false, nil + } + + in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs) + if err != nil { + return false, err + } + return in, nil +} + +// GetProtectedTags gets all protected tags of the repository +func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { + tags := make([]*ProtectedTag, 0) + return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) +} + +// GetProtectedTagByID gets the protected tag with the specific id +func GetProtectedTagByID(id int64) (*ProtectedTag, error) { + tag := new(ProtectedTag) + has, err := x.ID(id).Get(tag) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return tag, nil +} + +// IsUserAllowedToControlTag checks if a user can control the specific tag. +// It returns true if the tag name is not protected or the user is allowed to control it. +func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) { + isAllowed := true + for _, tag := range tags { + err := tag.EnsureCompiledPattern() + if err != nil { + return false, err + } + + if !tag.matchString(tagName) { + continue + } + + isAllowed, err = tag.IsUserAllowed(userID) + if err != nil { + return false, err + } + if isAllowed { + break + } + } + + return isAllowed, nil +} + +func (pt *ProtectedTag) matchString(name string) bool { + if pt.RegexPattern != nil { + return pt.RegexPattern.MatchString(name) + } + return pt.GlobPattern.Match(name) +} diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go new file mode 100644 index 0000000000..3dc895c69f --- /dev/null +++ b/models/protected_tag_test.go @@ -0,0 +1,162 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsUserAllowed(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + pt := &ProtectedTag{} + allowed, err := pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.False(t, allowed) + + pt = &ProtectedTag{ + AllowlistUserIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.True(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.False(t, allowed) + + pt = &ProtectedTag{ + AllowlistTeamIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.False(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.True(t, allowed) + + pt = &ProtectedTag{ + AllowlistUserIDs: []int64{1}, + AllowlistTeamIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.True(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.True(t, allowed) +} + +func TestIsUserAllowedToControlTag(t *testing.T) { + cases := []struct { + name string + userid int64 + allowed bool + }{ + { + name: "test", + userid: 1, + allowed: true, + }, + { + name: "test", + userid: 3, + allowed: true, + }, + { + name: "gitea", + userid: 1, + allowed: true, + }, + { + name: "gitea", + userid: 3, + allowed: false, + }, + { + name: "test-gitea", + userid: 1, + allowed: true, + }, + { + name: "test-gitea", + userid: 3, + allowed: false, + }, + { + name: "gitea-test", + userid: 1, + allowed: true, + }, + { + name: "gitea-test", + userid: 3, + allowed: true, + }, + { + name: "v-1", + userid: 1, + allowed: false, + }, + { + name: "v-1", + userid: 2, + allowed: true, + }, + { + name: "release", + userid: 1, + allowed: false, + }, + } + + t.Run("Glob", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `*gitea`, + AllowlistUserIDs: []int64{1}, + }, + { + NamePattern: `v-*`, + AllowlistUserIDs: []int64{2}, + }, + { + NamePattern: "release", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) + + t.Run("Regex", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `/gitea\z/`, + AllowlistUserIDs: []int64{1}, + }, + { + NamePattern: `/\Av-/`, + AllowlistUserIDs: []int64{2}, + }, + { + NamePattern: "/release/", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) +} diff --git a/models/repo.go b/models/repo.go index 2baf6e9bdd..4ce3d4839b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1498,6 +1498,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { &Mirror{RepoID: repoID}, &Notification{RepoID: repoID}, &ProtectedBranch{RepoID: repoID}, + &ProtectedTag{RepoID: repoID}, &PullRequest{BaseRepoID: repoID}, &PushMirror{RepoID: repoID}, &Release{RepoID: repoID}, diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 5cfd994d2d..4cef48daf3 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -19,6 +19,9 @@ const ( // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" + + // ErrRegexPattern is returned when a regex pattern is invalid + ErrRegexPattern = "RegexPattern" ) var ( @@ -53,6 +56,8 @@ func AddBindingRules() { addGitRefNameBindingRule() addValidURLBindingRule() addGlobPatternRule() + addRegexPatternRule() + addGlobOrRegexPatternRule() } func addGitRefNameBindingRule() { @@ -102,17 +107,55 @@ func addGlobPatternRule() { IsMatch: func(rule string) bool { return rule == "GlobPattern" }, + IsValid: globPatternValidator, + }) +} + +func globPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + + if len(str) != 0 { + if _, err := glob.Compile(str); err != nil { + errs.Add([]string{name}, ErrGlobPattern, err.Error()) + return false, errs + } + } + + return true, errs +} + +func addRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "RegexPattern" + }, + IsValid: regexPatternValidator, + }) +} + +func regexPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + + if _, err := regexp.Compile(str); err != nil { + errs.Add([]string{name}, ErrRegexPattern, err.Error()) + return false, errs + } + + return true, errs +} + +func addGlobOrRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "GlobOrRegexPattern" + }, IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { - str := fmt.Sprintf("%v", val) + str := strings.TrimSpace(fmt.Sprintf("%v", val)) - if len(str) != 0 { - if _, err := glob.Compile(str); err != nil { - errs.Add([]string{name}, ErrGlobPattern, err.Error()) - return false, errs - } + if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + return regexPatternValidator(errs, name, str[1:len(str)-1]) } - - return true, errs + return globPatternValidator(errs, name, val) }, }) } diff --git a/modules/validation/binding_test.go b/modules/validation/binding_test.go index e0daba89e5..d3b4e686ae 100644 --- a/modules/validation/binding_test.go +++ b/modules/validation/binding_test.go @@ -26,9 +26,10 @@ type ( } TestForm struct { - BranchName string `form:"BranchName" binding:"GitRefName"` - URL string `form:"ValidUrl" binding:"ValidUrl"` - GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + BranchName string `form:"BranchName" binding:"GitRefName"` + URL string `form:"ValidUrl" binding:"ValidUrl"` + GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + RegexPattern string `form:"RegexPattern" binding:"RegexPattern"` } ) diff --git a/modules/validation/regex_pattern_test.go b/modules/validation/regex_pattern_test.go new file mode 100644 index 0000000000..afe1bcf425 --- /dev/null +++ b/modules/validation/regex_pattern_test.go @@ -0,0 +1,60 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package validation + +import ( + "regexp" + "testing" + + "gitea.com/go-chi/binding" +) + +func getRegexPatternErrorString(pattern string) string { + if _, err := regexp.Compile(pattern); err != nil { + return err.Error() + } + return "" +} + +var regexValidationTestCases = []validationTestCase{ + { + description: "Empty regex pattern", + data: TestForm{ + RegexPattern: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Valid regex", + data: TestForm{ + RegexPattern: `(\d{1,3})+`, + }, + expectedErrors: binding.Errors{}, + }, + + { + description: "Invalid regex", + data: TestForm{ + RegexPattern: "[a-", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"RegexPattern"}, + Classification: ErrRegexPattern, + Message: getRegexPatternErrorString("[a-"), + }, + }, + }, +} + +func Test_RegexPatternValidation(t *testing.T) { + AddBindingRules() + + for _, testCase := range regexValidationTestCases { + t.Run(testCase.description, func(t *testing.T) { + performValidationTest(t, testCase) + }) + } +} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index cd418c9792..cbdb29b812 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -135,6 +135,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) + case validation.ErrRegexPattern: + data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) default: data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 59ee6e48ea..a809f49eeb 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -83,6 +83,7 @@ add = Add add_all = Add All remove = Remove remove_all = Remove All +edit = Edit write = Write preview = Preview @@ -415,6 +416,7 @@ email_error = ` is not a valid email address.` url_error = ` is not a valid URL.` include_error = ` must contain substring '%s'.` glob_pattern_error = ` glob pattern is invalid: %s.` +regex_pattern_error = ` regex pattern is invalid: %s.` unknown_error = Unknown error: captcha_incorrect = The CAPTCHA code is incorrect. password_not_match = The passwords do not match. @@ -1802,7 +1804,7 @@ settings.event_pull_request_review_desc = Pull request approved, rejected, or re settings.event_pull_request_sync = Pull Request Synchronized settings.event_pull_request_sync_desc = Pull request synchronized. settings.branch_filter = Branch filter -settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master, {master,release*}. +settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master, {master,release*}. settings.active = Active settings.active_helper = Information about triggered events will be sent to this webhook URL. settings.add_hook_success = The webhook has been added. @@ -1872,7 +1874,7 @@ settings.dismiss_stale_approvals_desc = When new commits that change the content settings.require_signed_commits = Require Signed Commits settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable. settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'): -settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt. +settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt. settings.add_protected_branch = Enable protection settings.delete_protected_branch = Disable protection settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. @@ -1891,6 +1893,16 @@ settings.choose_branch = Choose a branch… settings.no_protected_branch = There are no protected branches. settings.edit_protected_branch = Edit settings.protected_branch_required_approvals_min = Required approvals cannot be negative. +settings.tags = Tags +settings.tags.protection = Tag Protection +settings.tags.protection.pattern = Tag Pattern +settings.tags.protection.allowed = Allowed +settings.tags.protection.allowed.users = Allowed users +settings.tags.protection.allowed.teams = Allowed teams +settings.tags.protection.allowed.noone = No One +settings.tags.protection.create = Protect Tag +settings.tags.protection.none = There are no protected tags. +settings.tags.protection.pattern.description = You can use a single name or a glob pattern or regular expression to match multiple tags. Read more in the protected tags guide. settings.bot_token = Bot Token settings.chat_id = Chat ID settings.matrix.homeserver_url = Homeserver URL @@ -1904,6 +1916,7 @@ settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. settings.archive.branchsettings_unavailable = Branch settings are not available if the repo is archived. +settings.archive.tagsettings_unavailable = Tag settings are not available if the repo is archived. settings.unarchive.button = Un-Archive Repo settings.unarchive.header = Un-Archive This Repo settings.unarchive.text = Un-Archiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull-requests. @@ -2018,6 +2031,7 @@ release.deletion_tag_desc = Will delete this tag from repository. Repository con release.deletion_tag_success = The tag has been deleted. release.tag_name_already_exist = A release with this tag name already exists. release.tag_name_invalid = The tag name is not valid. +release.tag_name_protected = The tag name is protected. release.tag_already_exist = This tag name already exists. release.downloads = Downloads release.download_count = Downloads: %s diff --git a/routers/private/hook.go b/routers/private/hook.go index 17ea4f2437..9f5579b6ae 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -155,125 +155,202 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { private.GitQuarantinePath+"="+opts.GitQuarantinePath) } + protectedTags, err := repo.GetProtectedTags() + if err != nil { + log.Error("Unable to get protected tags for %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + // Iterate across the provided old commit IDs for i := range opts.OldCommitIDs { oldCommitID := opts.OldCommitIDs[i] newCommitID := opts.NewCommitIDs[i] refFullName := opts.RefFullNames[i] - branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) - if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), - }) - return - } + if strings.HasPrefix(refFullName, git.BranchPrefix) { + branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) + if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + }) + return + } - protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) - if err != nil { - log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: err.Error(), - }) - return - } - - // Allow pushes to non-protected branches - if protectBranch == nil || !protectBranch.IsProtected() { - continue - } - - // This ref is a protected branch. - // - // First of all we need to enforce absolutely: - // - // 1. Detect and prevent deletion of the branch - if newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from deletion", branchName), - }) - return - } - - // 2. Disallow force pushes to protected branches - if git.EmptySHA != oldCommitID { - output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) if err != nil { - log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) + log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Fail to detect force push: %v", err), + Err: err.Error(), }) return - } else if len(output) > 0 { - log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from force push", branchName), - }) - return - } - } - // 3. Enforce require signed commits - if protectBranch.RequireSignedCommits { - err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) - if err != nil { - if !isErrUnverifiedCommit(err) { - log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + // Allow pushes to non-protected branches + if protectBranch == nil || !protectBranch.IsProtected() { + continue + } + + // This ref is a protected branch. + // + // First of all we need to enforce absolutely: + // + // 1. Detect and prevent deletion of the branch + if newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from deletion", branchName), + }) + return + } + + // 2. Disallow force pushes to protected branches + if git.EmptySHA != oldCommitID { + output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + if err != nil { + log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + Err: fmt.Sprintf("Fail to detect force push: %v", err), + }) + return + } else if len(output) > 0 { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + + } + } + + // 3. Enforce require signed commits + if protectBranch.RequireSignedCommits { + err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) + if err != nil { + if !isErrUnverifiedCommit(err) { + log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + unverifiedCommit := err.(*errUnverifiedCommit).sha + log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), }) return } - unverifiedCommit := err.(*errUnverifiedCommit).sha - log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), - }) - return } - } - // Now there are several tests which can be overridden: - // - // 4. Check protected file patterns - this is overridable from the UI - changedProtectedfiles := false - protectedFilePath := "" + // Now there are several tests which can be overridden: + // + // 4. Check protected file patterns - this is overridable from the UI + changedProtectedfiles := false + protectedFilePath := "" - globs := protectBranch.GetProtectedFilePatterns() - if len(globs) > 0 { - _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) - if err != nil { - if !models.IsErrFilePathProtected(err) { - log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + globs := protectBranch.GetProtectedFilePatterns() + if len(globs) > 0 { + _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) + if err != nil { + if !models.IsErrFilePathProtected(err) { + log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + + changedProtectedfiles = true + protectedFilePath = err.(models.ErrFilePathProtected).Path + } + } + + // 5. Check if the doer is allowed to push + canPush := false + if opts.IsDeployKey { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + } else { + canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) + } + + // 6. If we're not allowed to push directly + if !canPush { + // Is this is a merge from the UI/API? + if opts.PullRequestID == 0 { + // 6a. If we're not merging from the UI/API then there are two ways we got here: + // + // We are changing a protected file and we're not allowed to do that + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Or we're simply not able to push to this protected branch + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } + // 6b. Merge (from UI or API) + + // Get the PR, user and permissions for the user in the repository + pr, err := models.GetPullRequestByID(opts.PullRequestID) + if err != nil { + log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err) ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err), + }) + return + } + user, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("Unable to get User id %d Error: %v", opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), + }) + return + } + perm, err := models.GetUserRepoPermission(repo, user) + if err != nil { + log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), }) return } - changedProtectedfiles = true - protectedFilePath = err.(models.ErrFilePathProtected).Path - } - } + // Now check if the user is allowed to merge PRs for this repository + allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) + if err != nil { + log.Error("Error calculating if allowed to merge: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err), + }) + return + } - // 5. Check if the doer is allowed to push - canPush := false - if opts.IsDeployKey { - canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) - } else { - canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) - } + if !allowedMerge { + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } - // 6. If we're not allowed to push directly - if !canPush { - // Is this is a merge from the UI/API? - if opts.PullRequestID == 0 { - // 6a. If we're not merging from the UI/API then there are two ways we got here: - // - // We are changing a protected file and we're not allowed to do that + // If we're an admin for the repository we can ignore status checks, reviews and override protected files + if perm.IsAdmin() { + continue + } + + // Now if we're not an admin - we can't overwrite protected files so fail now if changedProtectedfiles { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) ctx.JSON(http.StatusForbidden, private.Response{ @@ -282,88 +359,44 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { return } - // Or we're simply not able to push to this protected branch - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } - // 6b. Merge (from UI or API) - - // Get the PR, user and permissions for the user in the repository - pr, err := models.GetPullRequestByID(opts.PullRequestID) - if err != nil { - log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err), - }) - return - } - user, err := models.GetUserByID(opts.UserID) - if err != nil { - log.Error("Unable to get User id %d Error: %v", opts.UserID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), - }) - return - } - perm, err := models.GetUserRepoPermission(repo, user) - if err != nil { - log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), - }) - return - } - - // Now check if the user is allowed to merge PRs for this repository - allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) - if err != nil { - log.Error("Error calculating if allowed to merge: %v", err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err), - }) - return - } - - if !allowedMerge { - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } - - // If we're an admin for the repository we can ignore status checks, reviews and override protected files - if perm.IsAdmin() { - continue - } - - // Now if we're not an admin - we can't overwrite protected files so fail now - if changedProtectedfiles { - log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), - }) - return - } - - // Check all status checks and reviews are ok - if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { - if models.IsErrNotAllowedToMerge(err) { - log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) - ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()), + // Check all status checks and reviews are ok + if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { + if models.IsErrNotAllowedToMerge(err) { + log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()), + }) + return + } + log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err), }) return } - log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) + } + } else if strings.HasPrefix(refFullName, git.TagPrefix) { + tagName := strings.TrimPrefix(refFullName, git.TagPrefix) + + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID) + if err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err), + Err: err.Error(), }) return } + if !isAllowed { + log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Tag %s is protected", tagName), + }) + return + } + } else { + log.Error("Unexpected ref: %s", refFullName) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unexpected ref: %s", refFullName), + }) } } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 3b700e8016..0665496d44 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -322,6 +322,18 @@ func NewReleasePost(ctx *context.Context) { return } + if models.IsErrInvalidTagName(err) { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + return + } + + if models.IsErrProtectedTagName(err) { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + return + } + ctx.ServerError("releaseservice.CreateNewTag", err) return } @@ -333,7 +345,9 @@ func NewReleasePost(ctx *context.Context) { rel = &models.Release{ RepoID: ctx.Repo.Repository.ID, + Repo: ctx.Repo.Repository, PublisherID: ctx.User.ID, + Publisher: ctx.User, Title: form.Title, TagName: form.TagName, Target: form.Target, @@ -350,6 +364,8 @@ func NewReleasePost(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) case models.IsErrInvalidTagName(err): ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + case models.IsErrProtectedTagName(err): + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) default: ctx.ServerError("CreateRelease", err) } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index c48b19b63c..5e8c2c5276 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -40,6 +40,7 @@ const ( tplSettingsOptions base.TplName = "repo/settings/options" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" + tplTags base.TplName = "repo/settings/tags" tplGithooks base.TplName = "repo/settings/githooks" tplGithookEdit base.TplName = "repo/settings/githook_edit" tplDeployKeys base.TplName = "repo/settings/deploy_keys" diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go new file mode 100644 index 0000000000..7928591371 --- /dev/null +++ b/routers/web/repo/tag.go @@ -0,0 +1,182 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "fmt" + "net/http" + "strings" + + "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/web" + "code.gitea.io/gitea/services/forms" +) + +// Tags render the page to protect tags +func Tags(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.HTML(http.StatusOK, tplTags) +} + +// NewProtectedTagPost handles creation of a protect tag +func NewProtectedTagPost(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } + + repo := ctx.Repo.Repository + form := web.GetForm(ctx).(*forms.ProtectTagForm) + + pt := &models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: strings.TrimSpace(form.NamePattern), + } + + if strings.TrimSpace(form.AllowlistUsers) != "" { + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) + } + if strings.TrimSpace(form.AllowlistTeams) != "" { + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) + } + + if err := models.InsertProtectedTag(pt); err != nil { + ctx.ServerError("InsertProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) +} + +// EditProtectedTag render the page to edit a protect tag +func EditProtectedTag(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.Data["PageIsEditProtectedTag"] = true + + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + ctx.Data["name_pattern"] = pt.NamePattern + ctx.Data["allowlist_users"] = strings.Join(base.Int64sToStrings(pt.AllowlistUserIDs), ",") + ctx.Data["allowlist_teams"] = strings.Join(base.Int64sToStrings(pt.AllowlistTeamIDs), ",") + + ctx.HTML(http.StatusOK, tplTags) +} + +// EditProtectedTagPost handles creation of a protect tag +func EditProtectedTagPost(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.Data["PageIsEditProtectedTag"] = true + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } + + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + form := web.GetForm(ctx).(*forms.ProtectTagForm) + + pt.NamePattern = strings.TrimSpace(form.NamePattern) + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) + + if err := models.UpdateProtectedTag(pt); err != nil { + ctx.ServerError("UpdateProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags") +} + +// DeleteProtectedTagPost handles deletion of a protected tag +func DeleteProtectedTagPost(ctx *context.Context) { + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + if err := models.DeleteProtectedTag(pt); err != nil { + ctx.ServerError("DeleteProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags") +} + +func setTagsContext(ctx *context.Context) error { + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsTags"] = true + + protectedTags, err := ctx.Repo.Repository.GetProtectedTags() + if err != nil { + ctx.ServerError("GetProtectedTags", err) + return err + } + ctx.Data["ProtectedTags"] = protectedTags + + users, err := ctx.Repo.Repository.GetReaders() + if err != nil { + ctx.ServerError("Repo.Repository.GetReaders", err) + return err + } + ctx.Data["Users"] = users + + if ctx.Repo.Owner.IsOrganization() { + teams, err := ctx.Repo.Owner.TeamsWithAccessToRepo(ctx.Repo.Repository.ID, models.AccessModeRead) + if err != nil { + ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) + return err + } + ctx.Data["Teams"] = teams + } + + return nil +} + +func selectProtectedTagByContext(ctx *context.Context) *models.ProtectedTag { + id := ctx.QueryInt64("id") + if id == 0 { + id = ctx.ParamsInt64(":id") + } + + tag, err := models.GetProtectedTagByID(id) + if err != nil { + ctx.ServerError("GetProtectedTagByID", err) + return nil + } + + if tag != nil && tag.RepoID == ctx.Repo.Repository.ID { + return tag + } + + ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository)) + + return nil +} diff --git a/routers/web/web.go b/routers/web/web.go index 883213479c..627c88aab1 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -594,12 +594,21 @@ func RegisterRoutes(m *web.Route) { m.Post("/delete", repo.DeleteTeam) }) }) + m.Group("/branches", func() { m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) m.Combo("/*").Get(repo.SettingsProtectedBranch). Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) }, repo.MustBeNotEmpty) + m.Group("/tags", func() { + m.Get("", repo.Tags) + m.Post("", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost) + m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost) + m.Get("/{id}", repo.EditProtectedTag) + m.Post("/{id}", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost) + }) + m.Group("/hooks/git", func() { m.Get("", repo.GitHooks) m.Combo("/{name}").Get(repo.GitHooksEdit). diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go new file mode 100644 index 0000000000..337e7fe1ea --- /dev/null +++ b/services/forms/repo_tag_form.go @@ -0,0 +1,27 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/middleware" + + "gitea.com/go-chi/binding" +) + +// ProtectTagForm form for changing protected tag settings +type ProtectTagForm struct { + NamePattern string `binding:"Required;GlobOrRegexPattern"` + AllowlistUsers string + AllowlistTeams string +} + +// Validate validates the fields +func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/services/release/release.go b/services/release/release.go index 9d201edf6d..6f5aa02c85 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -23,6 +23,25 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, // Only actual create when publish. if !rel.IsDraft { if !gitRepo.IsTagExist(rel.TagName) { + if err := rel.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return false, err + } + + protectedTags, err := rel.Repo.GetProtectedTags() + if err != nil { + return false, fmt.Errorf("GetProtectedTags: %v", err) + } + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) + if err != nil { + return false, err + } + if !isAllowed { + return false, models.ErrProtectedTagName{ + TagName: rel.TagName, + } + } + commit, err := gitRepo.GetCommit(rel.Target) if err != nil { return false, fmt.Errorf("GetCommit: %v", err) @@ -49,11 +68,7 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, } created = true rel.LowerTagName = strings.ToLower(rel.TagName) - // Prepare Notify - if err := rel.LoadAttributes(); err != nil { - log.Error("LoadAttributes: %v", err) - return false, err - } + notification.NotifyPushCommits( rel.Publisher, rel.Repo, &repository.PushUpdateOptions{ @@ -137,7 +152,9 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m rel := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: doer.ID, + Publisher: doer, TagName: tagName, Target: commit, IsDraft: false, diff --git a/services/release/release_test.go b/services/release/release_test.go index 085be55cb4..9f665fabab 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -33,7 +33,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1", Target: "master", Title: "v0.1 is released", @@ -45,7 +47,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.1", Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d", Title: "v0.1.1 is released", @@ -57,7 +61,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.2", Target: "65f1bf2", Title: "v0.1.2 is released", @@ -69,7 +75,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.3", Target: "65f1bf2", Title: "v0.1.3 is released", @@ -81,7 +89,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.4", Target: "65f1bf2", Title: "v0.1.4 is released", @@ -99,7 +109,9 @@ func TestRelease_Create(t *testing.T) { var release = models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.5", Target: "65f1bf2", Title: "v0.1.5 is released", @@ -125,7 +137,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.1", Target: "master", Title: "v1.1.1 is released", @@ -147,7 +161,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed draft assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.2.1", Target: "65f1bf2", Title: "v1.2.1 is draft", @@ -169,7 +185,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed pre-release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.3.1", Target: "65f1bf2", Title: "v1.3.1 is pre-released", @@ -192,7 +210,9 @@ func TestRelease_Update(t *testing.T) { // Test create release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.2", Target: "master", Title: "v1.1.2 is released", @@ -258,7 +278,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed release release := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.1.1", Target: "master", Title: "v2.1.1 is released", @@ -280,7 +302,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed draft release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.2.1", Target: "65f1bf2", Title: "v2.2.1 is draft", @@ -301,7 +325,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed pre-release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.3.1", Target: "65f1bf2", Title: "v2.3.1 is pre-released", diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl index 4b89ece349..31672cb5ea 100644 --- a/templates/repo/settings/nav.tmpl +++ b/templates/repo/settings/nav.tmpl @@ -5,6 +5,7 @@
  • {{.i18n.Tr "repo.settings.options"}}
  • {{.i18n.Tr "repo.settings.collaboration"}}
  • {{.i18n.Tr "repo.settings.branches"}}
  • +
  • {{.i18n.Tr "repo.settings.tags"}}
  • {{if not DisableWebhooks}}
  • {{.i18n.Tr "repo.settings.hooks"}}
  • {{end}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 501c3c4630..d8cdf21871 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -11,6 +11,9 @@ {{.i18n.Tr "repo.settings.branches"}} {{end}} + + {{.i18n.Tr "repo.settings.tags"}} + {{if not DisableWebhooks}} {{.i18n.Tr "repo.settings.hooks"}} diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl new file mode 100644 index 0000000000..a2c887b1f8 --- /dev/null +++ b/templates/repo/settings/tags.tmpl @@ -0,0 +1,132 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} + {{if .Repository.IsArchived}} +
    + {{.i18n.Tr "repo.settings.archive.tagsettings_unavailable"}} +
    + {{else}} +

    + {{.i18n.Tr "repo.settings.tags.protection"}} +

    + +
    +
    + + +
    + + + + + + + + {{range .ProtectedTags}} + + + + + + {{else}} + + {{end}} + +
    {{.i18n.Tr "repo.settings.tags.protection.pattern"}}{{.i18n.Tr "repo.settings.tags.protection.allowed"}}
    {{.NamePattern}}
    + {{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}} + {{$userIDs := .AllowlistUserIDs}} + {{range $.Users}} + {{if contain $userIDs .ID }} + {{avatar . 26}} {{.GetDisplayName}} + {{end}} + {{end}} + {{if $.Owner.IsOrganization}} + {{$teamIDs := .AllowlistTeamIDs}} + {{range $.Teams}} + {{if contain $teamIDs .ID }} + {{.Name}} + {{end}} + {{end}} + {{end}} + {{else}} + {{$.i18n.Tr "repo.settings.tags.protection.allowed.noone"}} + {{end}} + + {{$.i18n.Tr "edit"}} +
    + {{$.CsrfTokenHtml}} + + +
    +
    {{.i18n.Tr "repo.settings.tags.protection.none"}}
    +
    +
    +
    + {{end}} +
    +
    +{{template "base/footer" .}} From 3ef23d5411732b4b714d6fc9739bc5dac75aadd4 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 25 Jun 2021 18:54:08 +0200 Subject: [PATCH 19/33] Use gitea logging module for git module (#16243) remove log() func from gogs times and switch to proper logging Signed-off-by: Andrew Thornton Co-authored-by: Andrew Thornton --- modules/git/batch_reader.go | 6 ++++-- modules/git/blob_nogogit.go | 6 ++++-- modules/git/command.go | 10 +++++----- modules/git/commit_info_nogogit.go | 4 +++- modules/git/diff.go | 3 ++- modules/git/git.go | 16 ---------------- modules/git/git_test.go | 4 ++++ modules/git/hook.go | 6 ++++-- modules/git/last_commit_cache.go | 4 +++- modules/git/last_commit_cache_gogit.go | 6 ++++-- modules/git/last_commit_cache_nogogit.go | 6 ++++-- modules/git/parse_nogogit.go | 4 +++- modules/git/repo_base_nogogit.go | 6 ++++-- modules/git/repo_branch_nogogit.go | 6 ++++-- modules/git/repo_commit_nogogit.go | 4 +++- modules/git/repo_language_stats_nogogit.go | 9 +++++---- modules/git/repo_tag.go | 4 +++- routers/init.go | 12 +----------- services/gitdiff/gitdiff_test.go | 2 -- 19 files changed, 60 insertions(+), 58 deletions(-) diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 678b184708..bdf82bde89 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -12,6 +12,8 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/log" + "github.com/djherbis/buffer" "github.com/djherbis/nio/v3" ) @@ -99,7 +101,7 @@ func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err er } idx := strings.IndexByte(typ, ' ') if idx < 0 { - log("missing space typ: %s", typ) + log.Debug("missing space typ: %s", typ) err = ErrNotExist{ID: string(sha)} return } @@ -230,7 +232,7 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) err = &ErrNotExist{} return diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index cdaeb636a9..5b42920ebe 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "io/ioutil" "math" + + "code.gitea.io/gitea/modules/log" ) // Blob represents a Git object. @@ -69,12 +71,12 @@ func (b *Blob) Size() int64 { defer cancel() _, err := wr.Write([]byte(b.ID.String() + "\n")) if err != nil { - log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } _, _, b.size, err = ReadBatchLine(rd) if err != nil { - log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } diff --git a/modules/git/command.go b/modules/git/command.go index ef78464d5f..2e375fd4f9 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" ) @@ -114,9 +115,9 @@ func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time. } if len(dir) == 0 { - log(c.String()) + log.Debug("%s", c) } else { - log("%s: %v", dir, c) + log.Debug("%s: %v", dir, c) } ctx, cancel := context.WithTimeout(c.parentContext, timeout) @@ -197,9 +198,8 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil { return nil, ConcatenateError(err, stderr.String()) } - - if stdout.Len() > 0 { - log("stdout:\n%s", stdout.Bytes()[:1024]) + if stdout.Len() > 0 && log.IsTrace() { + log.Trace("Stdout:\n %s", stdout.Bytes()[:1024]) } return stdout.Bytes(), nil } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 2283510d96..060ecba261 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "path" "sort" + + "code.gitea.io/gitea/modules/log" ) // GetCommitsInfo gets information of all commits that are corresponding to these entries @@ -78,7 +80,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath commitsInfo[i].SubModuleFile = subModuleFile } } else { - log("missing commit for %s", entry.Name()) + log.Debug("missing commit for %s", entry.Name()) } } diff --git a/modules/git/diff.go b/modules/git/diff.go index 5da53568e5..20f25c1bee 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -15,6 +15,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" ) @@ -113,7 +114,7 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu righHunk, _ = strconv.Atoi(rightRange[1]) } } else { - log("Parse line number failed: %v", diffhunk) + log.Debug("Parse line number failed: %v", diffhunk) rightLine = leftLine righHunk = leftHunk } diff --git a/modules/git/git.go b/modules/git/git.go index 6b15138a5c..ce1b15c953 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -19,9 +19,6 @@ import ( ) var ( - // Debug enables verbose logging on everything. - // This should be false in case Gogs starts in SSH mode. - Debug = false // Prefix the log prefix Prefix = "[git-module] " // GitVersionRequired is the minimum Git version required @@ -41,19 +38,6 @@ var ( goVersionLessThan115 = true ) -func log(format string, args ...interface{}) { - if !Debug { - return - } - - fmt.Print(Prefix) - if len(args) == 0 { - fmt.Println(format) - } else { - fmt.Printf(format+"\n", args...) - } -} - // LocalVersion returns current Git version from shell. func LocalVersion() (*version.Version, error) { if err := LoadGitVersion(); err != nil { diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 27951d639b..c62a55badc 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -9,6 +9,8 @@ import ( "fmt" "os" "testing" + + "code.gitea.io/gitea/modules/log" ) func fatalTestError(fmtStr string, args ...interface{}) { @@ -17,6 +19,8 @@ func fatalTestError(fmtStr string, args ...interface{}) { } func TestMain(m *testing.M) { + _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) + if err := Init(context.Background()); err != nil { fatalTestError("Init failed: %v", err) } diff --git a/modules/git/hook.go b/modules/git/hook.go index c23fbf8aa1..7007d23be2 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -1,4 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2021 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -126,11 +128,11 @@ const ( // SetUpdateHook writes given content to update hook of the repository. func SetUpdateHook(repoPath, content string) (err error) { - log("Setting update hook: %s", repoPath) + log.Debug("Setting update hook: %s", repoPath) hookPath := path.Join(repoPath, HookPathUpdate) isExist, err := util.IsExist(hookPath) if err != nil { - log("Unable to check if %s exists. Error: %v", hookPath, err) + log.Debug("Unable to check if %s exists. Error: %v", hookPath, err) return err } if isExist { diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 37a59e1fa8..e2d296641f 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -7,6 +7,8 @@ package git import ( "crypto/sha256" "fmt" + + "code.gitea.io/gitea/modules/log" ) // Cache represents a caching interface @@ -24,6 +26,6 @@ func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string { // Put put the last commit id with commit and entry path func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { - log("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) + log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl()) } diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go index 16fb1c988c..b8e0db46a9 100644 --- a/modules/git/last_commit_cache_gogit.go +++ b/modules/git/last_commit_cache_gogit.go @@ -10,6 +10,8 @@ import ( "context" "path" + "code.gitea.io/gitea/modules/log" + "github.com/go-git/go-git/v5/plumbing/object" cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph" ) @@ -41,9 +43,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) { v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) if vs, ok := v.(string); ok { - log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) if commit, ok := c.commitCache[vs]; ok { - log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) return commit, nil } id, err := c.repo.ConvertToSHA1(vs) diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go index 84c8ee132c..ff9f9ff1cf 100644 --- a/modules/git/last_commit_cache_nogogit.go +++ b/modules/git/last_commit_cache_nogogit.go @@ -10,6 +10,8 @@ import ( "bufio" "context" "path" + + "code.gitea.io/gitea/modules/log" ) // LastCommitCache represents a cache to store last commit @@ -39,9 +41,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, func (c *LastCommitCache) Get(ref, entryPath string, wr WriteCloserError, rd *bufio.Reader) (interface{}, error) { v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) if vs, ok := v.(string); ok { - log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) if commit, ok := c.commitCache[vs]; ok { - log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) return commit, nil } id, err := c.repo.ConvertToSHA1(vs) diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index b45b31f239..667111ec4a 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -13,6 +13,8 @@ import ( "io" "strconv" "strings" + + "code.gitea.io/gitea/modules/log" ) // ParseTreeEntries parses the output of a `git ls-tree -l` command. @@ -120,7 +122,7 @@ loop: case "40000": entry.entryMode = EntryModeTree default: - log("Unknown mode: %v", string(mode)) + log.Debug("Unknown mode: %v", string(mode)) return nil, fmt.Errorf("unknown mode: %v", string(mode)) } diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index c7d6019d77..1675967d18 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -12,6 +12,8 @@ import ( "context" "errors" "path/filepath" + + "code.gitea.io/gitea/modules/log" ) // Repository represents a Git repository. @@ -54,7 +56,7 @@ func OpenRepository(repoPath string) (*Repository, error) { // CatFileBatch obtains a CatFileBatch for this repository func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func()) { if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 { - log("Opening temporary cat file batch for: %s", repo.Path) + log.Debug("Opening temporary cat file batch for: %s", repo.Path) return CatFileBatch(repo.Path) } return repo.batchWriter, repo.batchReader, func() {} @@ -63,7 +65,7 @@ func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func()) // CatFileBatchCheck obtains a CatFileBatchCheck for this repository func (repo *Repository) CatFileBatchCheck() (WriteCloserError, *bufio.Reader, func()) { if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 { - log("Opening temporary cat file batch-check: %s", repo.Path) + log.Debug("Opening temporary cat file batch-check: %s", repo.Path) return CatFileBatchCheck(repo.Path) } return repo.checkWriter, repo.checkReader, func() {} diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index dd34e48899..7d10b8ba0f 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -12,6 +12,8 @@ import ( "bytes" "io" "strings" + + "code.gitea.io/gitea/modules/log" ) // IsObjectExist returns true if given reference exists in the repository. @@ -24,7 +26,7 @@ func (repo *Repository) IsObjectExist(name string) bool { defer cancel() _, err := wr.Write([]byte(name + "\n")) if err != nil { - log("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error writing to CatFileBatchCheck %v", err) return false } sha, _, _, err := ReadBatchLine(rd) @@ -41,7 +43,7 @@ func (repo *Repository) IsReferenceExist(name string) bool { defer cancel() _, err := wr.Write([]byte(name + "\n")) if err != nil { - log("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error writing to CatFileBatchCheck %v", err) return false } _, _, _, err = ReadBatchLine(rd) diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index d00c1bfc67..afd5166f1d 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "io/ioutil" "strings" + + "code.gitea.io/gitea/modules/log" ) // ResolveReference resolves a name to a reference @@ -110,7 +112,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co return commit, nil default: - log("Unknown typ: %s", typ) + log.Debug("Unknown typ: %s", typ) _, err = rd.Discard(int(size) + 1) if err != nil { return nil, err diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 46b084cf01..1684f21d16 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -13,6 +13,7 @@ import ( "math" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/log" "github.com/go-enry/go-enry/v2" ) @@ -34,19 +35,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } shaBytes, typ, size, err := ReadBatchLine(batchReader) if typ != "commit" { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, ErrNotExist{commitID, ""} } sha, err := NewIDFromString(string(shaBytes)) if err != nil { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, ErrNotExist{commitID, ""} } commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) if err != nil { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err } if _, err = batchReader.Discard(1); err != nil { @@ -79,7 +80,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } _, _, size, err := ReadBatchLine(batchReader) if err != nil { - log("Error reading blob: %s Err: %v", f.ID.String(), err) + log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err) return nil, err } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 59ab702096..d91c3ca979 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -8,6 +8,8 @@ package git import ( "fmt" "strings" + + "code.gitea.io/gitea/modules/log" ) // TagPrefix tags prefix path on the repository @@ -33,7 +35,7 @@ func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error func (repo *Repository) getTag(tagID SHA1, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { - log("Hit cache: %s", tagID) + log.Debug("Hit cache: %s", tagID) tagClone := *t.(*Tag) tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil diff --git a/routers/init.go b/routers/init.go index bbf39a3f50..e52e547517 100644 --- a/routers/init.go +++ b/routers/init.go @@ -42,16 +42,6 @@ import ( "code.gitea.io/gitea/services/webhook" ) -func checkRunMode() { - switch setting.RunMode { - case "dev", "test": - git.Debug = true - default: - git.Debug = false - } - log.Info("Run Mode: %s", strings.Title(setting.RunMode)) -} - // NewServices init new services func NewServices() { setting.NewServices() @@ -84,7 +74,7 @@ func GlobalInit(ctx context.Context) { log.Trace("AppWorkPath: %s", setting.AppWorkPath) log.Trace("Custom path: %s", setting.CustomPath) log.Trace("Log path: %s", setting.LogRootPath) - checkRunMode() + log.Info("Run Mode: %s", strings.Title(setting.RunMode)) // Setup i18n translation.InitLocales() diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index f8c25a3912..2386552efe 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -13,7 +13,6 @@ import ( "testing" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/setting" jsoniter "github.com/json-iterator/go" @@ -514,7 +513,6 @@ func TestDiffLine_GetCommentSide(t *testing.T) { } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { - git.Debug = true for _, behavior := range []string{"-w", "--ignore-space-at-eol", "-b", ""} { diffs, err := GetDiffRangeWithWhitespaceBehavior("./testdata/academic-module", "559c156f8e0178b71cb44355428f24001b08fc68", "bd7063cc7c04689c4d082183d32a604ed27a24f9", setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles, behavior) From f573e93ed429d1d1dd0a7be8a3b0042f0817cc00 Mon Sep 17 00:00:00 2001 From: siddweiker Date: Fri, 25 Jun 2021 12:59:25 -0400 Subject: [PATCH 20/33] Fix heatmap activity (#15252) * Group heatmap actions by 15 minute intervals Signed-off-by: Sidd Weiker * Add multi-contribution test for user heatmap Signed-off-by: Sidd Weiker * Add timezone aware summation for activity heatmap Signed-off-by: Sidd Weiker * Fix api user heatmap test Signed-off-by: Sidd Weiker * Update variable declaration style Co-authored-by: Lunny Xiao Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick --- integrations/api_user_heatmap_test.go | 2 +- models/fixtures/action.yml | 24 +++++++++++++++++++++++ models/user_heatmap.go | 11 ++++------- models/user_heatmap_test.go | 28 +++++++++++++++++++-------- web_src/js/features/heatmap.js | 11 +++++++++-- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go index 105d39e9ae..a0f0552a17 100644 --- a/integrations/api_user_heatmap_test.go +++ b/integrations/api_user_heatmap_test.go @@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) { var heatmap []*models.UserHeatmapData DecodeJSON(t, resp, &heatmap) var dummyheatmap []*models.UserHeatmapData - dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603152000, Contributions: 1}) + dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603227600, Contributions: 1}) assert.Equal(t, dummyheatmap, heatmap) } diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index 14cfd90423..e3f3d2a971 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -32,3 +32,27 @@ repo_id: 22 is_private: true created_unix: 1603267920 + +- id: 5 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 6 + is_private: true + created_unix: 1603010100 + +- id: 6 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 7 + is_private: true + created_unix: 1603011300 + +- id: 7 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 8 + is_private: false + created_unix: 1603011540 # grouped with id:7 diff --git a/models/user_heatmap.go b/models/user_heatmap.go index 0e2767212e..306bd1819b 100644 --- a/models/user_heatmap.go +++ b/models/user_heatmap.go @@ -32,17 +32,14 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, return hdata, nil } - var groupBy string + // Group by 15 minute intervals which will allow the client to accurately shift the timestamp to their timezone. + // The interval is based on the fact that there are timezones such as UTC +5:30 and UTC +12:45. + groupBy := "created_unix / 900 * 900" groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias switch { - case setting.Database.UseSQLite3: - groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))" case setting.Database.UseMySQL: - groupBy = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(created_unix)))" - case setting.Database.UsePostgreSQL: - groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))" + groupBy = "created_unix DIV 900 * 900" case setting.Database.UseMSSQL: - groupBy = "datediff(SECOND, '19700101', dateadd(DAY, 0, datediff(day, 0, dateadd(s, created_unix, '19700101'))))" groupByName = groupBy } diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go index 31e78a19cc..b2aaea6499 100644 --- a/models/user_heatmap_test.go +++ b/models/user_heatmap_test.go @@ -19,12 +19,20 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { CountResult int JSONResult string }{ - {2, 2, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // self looks at action in private repo - {2, 1, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // admin looks at action in private repo - {2, 3, 0, `[]`}, // other user looks at action in private repo - {2, 0, 0, `[]`}, // nobody looks at action in private repo - {16, 15, 1, `[{"timestamp":1603238400,"contributions":1}]`}, // collaborator looks at action in private repo - {3, 3, 0, `[]`}, // no action action not performed by target user + // self looks at action in private repo + {2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // admin looks at action in private repo + {2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // other user looks at action in private repo + {2, 3, 0, `[]`}, + // nobody looks at action in private repo + {2, 0, 0, `[]`}, + // collaborator looks at action in private repo + {16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`}, + // no action action not performed by target user + {3, 3, 0, `[]`}, + // multiple actions performed with two grouped together + {10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`}, } // Prepare assert.NoError(t, PrepareTestDatabase()) @@ -51,9 +59,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { // Get the heatmap and compare heatmap, err := GetUserHeatmapDataByUser(user, doer) + var contributions int + for _, hm := range heatmap { + contributions += int(hm.Contributions) + } assert.NoError(t, err) - assert.Len(t, heatmap, len(actions), "invalid action count: did the test data became too old?") - assert.Len(t, heatmap, tc.CountResult, fmt.Sprintf("testcase %d", i)) + assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") + assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i)) // Test JSON rendering json := jsoniter.ConfigCompatibleWithStandardLibrary diff --git a/web_src/js/features/heatmap.js b/web_src/js/features/heatmap.js index d1cb43dde0..07ecaee461 100644 --- a/web_src/js/features/heatmap.js +++ b/web_src/js/features/heatmap.js @@ -7,8 +7,15 @@ export default async function initHeatmap() { if (!el) return; try { - const values = JSON.parse(el.dataset.heatmapData).map(({contributions, timestamp}) => { - return {date: new Date(timestamp * 1000), count: contributions}; + const heatmap = {}; + JSON.parse(el.dataset.heatmapData).forEach(({contributions, timestamp}) => { + // Convert to user timezone and sum contributions by date + const dateStr = new Date(timestamp * 1000).toDateString(); + heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions; + }); + + const values = Object.keys(heatmap).map((v) => { + return {date: new Date(v), count: heatmap[v]}; }); const View = Vue.extend({ From 31acd3c0c2de788fa995d5660b617f51d5b94078 Mon Sep 17 00:00:00 2001 From: Steven <61625851+justusbunsi@users.noreply.github.com> Date: Fri, 25 Jun 2021 19:00:09 +0200 Subject: [PATCH 21/33] Prevent webhook action buttons from shifting (#16087) On long webhook urls the action buttons (edit, delete) have been shifted by the url text. Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick --- templates/repo/settings/webhook/base_list.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index 916272a97a..e77e747742 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -41,7 +41,7 @@ {{.Description | Str2html}}
    {{range .Webhooks}} -
    +
    {{if eq .LastStatus 1}} {{svg "octicon-check"}} {{else if eq .LastStatus 2}} @@ -49,8 +49,8 @@ {{else}} {{svg "octicon-dot-fill"}} {{end}} - {{.URL}} -
    + {{.URL}} + From 06f483d0c4d11f32faae60a2a6c77b164f7e88e6 Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Fri, 25 Jun 2021 19:01:43 +0200 Subject: [PATCH 22/33] Append to existing trailers in generated squash commit message (#15980) * Remove superfluous newline before Co-authored-by trailers * Append to existing PR description trailer section If the existing PR description message already contains a trailer section (e.g. Signed-off-by: ), append to it instead of creating a new trailer section. * Reuse compiled regexp * Simplify regex and deal with trailing \n in PR description * Add tests for CommitMessageTrailersPattern - add support for Key:Value (no space after colon) - add support for whitespace "folding" * Update services/pull/pull_test.go Co-authored-by: Norwin Co-authored-by: zeripath Co-authored-by: Norwin Co-authored-by: Lunny Xiao Co-authored-by: techknowlogick --- services/pull/pull.go | 17 ++++++++--------- services/pull/pull_test.go | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/services/pull/pull.go b/services/pull/pull.go index 7b5dd6a964..db216ddbf4 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "fmt" + "regexp" "strings" "time" @@ -528,6 +529,8 @@ func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error { return nil } +var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) + // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) func GetSquashMergeCommitMessages(pr *models.PullRequest) string { if err := pr.LoadIssue(); err != nil { @@ -583,10 +586,13 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { stringBuilder := strings.Builder{} if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { - stringBuilder.WriteString(pr.Issue.Content) + message := strings.TrimSpace(pr.Issue.Content) + stringBuilder.WriteString(message) if stringBuilder.Len() > 0 { stringBuilder.WriteRune('\n') - stringBuilder.WriteRune('\n') + if !commitMessageTrailersPattern.MatchString(message) { + stringBuilder.WriteRune('\n') + } } } @@ -657,13 +663,6 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { } } - if len(authors) > 0 { - if _, err := stringBuilder.WriteRune('\n'); err != nil { - log.Error("Unable to write to string builder Error: %v", err) - return "" - } - } - for _, author := range authors { if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil { log.Error("Unable to write to string builder Error: %v", err) diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 64920e3550..81627ebb77 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -5,4 +5,27 @@ package pull +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + // TODO TestPullRequest_PushToBaseRepo + +func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { + // Not a valid trailer section + assert.False(t, commitMessageTrailersPattern.MatchString("")) + assert.False(t, commitMessageTrailersPattern.MatchString("No trailer.")) + assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nNot a trailer due to following text.")) + assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob ")) + // Valid trailer section + assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nOther-Trailer: Value")) + assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob \nOther-Trailer: Value")) + assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob \n")) + assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) +} From 1a1ce9b7216ab80e94987270da8fc2def57237c0 Mon Sep 17 00:00:00 2001 From: zeripath Date: Fri, 25 Jun 2021 19:14:49 +0100 Subject: [PATCH 23/33] Fuzzer finds an NPE due to incorrect URLPrefix (#16249) The Fuzzer is running on a non-repo urlprefix which is incorrect for RenderRaw Signed-off-by: Andrew Thornton Co-authored-by: 6543 <6543@obermui.de> --- tools/fuzz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fuzz.go b/tools/fuzz.go index 4b5b72d1d0..b48ae0add9 100644 --- a/tools/fuzz.go +++ b/tools/fuzz.go @@ -23,7 +23,7 @@ import ( var ( renderContext = markup.RenderContext{ - URLPrefix: "https://example.com", + URLPrefix: "https://example.com/go-gitea/gitea", Metas: map[string]string{ "user": "go-gitea", "repo": "gitea", From 9b33d18899b7e825e4754969ffcc9d7b541d2d28 Mon Sep 17 00:00:00 2001 From: ayb Date: Sat, 26 Jun 2021 00:38:27 +0200 Subject: [PATCH 24/33] Added support for gopher URLs. (#14749) * Added support for gopher URLs. * Add setting and make this user settable instead Signed-off-by: Andrew Thornton Co-authored-by: Andrew Thornton --- custom/conf/app.example.ini | 2 ++ .../doc/advanced/config-cheat-sheet.en-us.md | 1 + modules/setting/service.go | 12 ++++++++++++ modules/validation/binding.go | 19 +++++++++++++++++++ modules/validation/helpers.go | 19 +++++++++++++++++++ services/forms/user_form.go | 2 +- 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5adfb0546f..fa6a9e3fac 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -705,6 +705,8 @@ PATH = ;; ;; Minimum amount of time a user must exist before comments are kept when the user is deleted. ;USER_DELETE_WITH_COMMENTS_MAX_TIME = 0 +;; Valid site url schemes for user profiles +;VALID_SITE_URL_SCHEMES=http,https ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 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 5e976174fb..aa9eb7e0ca 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -519,6 +519,7 @@ relation to port exhaustion. - `NO_REPLY_ADDRESS`: **noreply.DOMAIN** Value for the domain part of the user's email address in the git log if user has set KeepEmailPrivate to true. DOMAIN resolves to the value in server.DOMAIN. The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. - `USER_DELETE_WITH_COMMENTS_MAX_TIME`: **0** Minimum amount of time a user must exist before comments are kept when the user is deleted. +- `VALID_SITE_URL_SCHEMES`: **http, https**: Valid site url schemes for user profiles ### Service - Expore (`service.explore`) diff --git a/modules/setting/service.go b/modules/setting/service.go index 41e834e8e6..bd70c7e6eb 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -6,6 +6,7 @@ package setting import ( "regexp" + "strings" "time" "code.gitea.io/gitea/modules/log" @@ -55,6 +56,7 @@ var Service struct { AutoWatchOnChanges bool DefaultOrgMemberVisible bool UserDeleteWithCommentsMaxTime time.Duration + ValidSiteURLSchemes []string // OpenID settings EnableOpenIDSignIn bool @@ -120,6 +122,16 @@ func newService() { Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) + sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https") + Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",") + schemes := make([]string, len(Service.ValidSiteURLSchemes)) + for _, scheme := range Service.ValidSiteURLSchemes { + scheme = strings.ToLower(strings.TrimSpace(scheme)) + if scheme != "" { + schemes = append(schemes, scheme) + } + } + Service.ValidSiteURLSchemes = schemes if err := Cfg.Section("service.explore").MapTo(&Service.Explore); err != nil { log.Fatal("Failed to map service.explore settings: %v", err) diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 4cef48daf3..5d5c64611f 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -55,6 +55,7 @@ func CheckGitRefAdditionalRulesValid(name string) bool { func AddBindingRules() { addGitRefNameBindingRule() addValidURLBindingRule() + addValidSiteURLBindingRule() addGlobPatternRule() addRegexPatternRule() addGlobOrRegexPatternRule() @@ -102,6 +103,24 @@ func addValidURLBindingRule() { }) } +func addValidSiteURLBindingRule() { + // URL validation rule + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "ValidSiteUrl") + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + if len(str) != 0 && !IsValidSiteURL(str) { + errs.Add([]string{name}, binding.ERR_URL, "Url") + return false, errs + } + + return true, errs + }, + }) +} + func addGlobPatternRule() { binding.AddRule(&binding.Rule{ IsMatch: func(rule string) bool { diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index c22e667a2e..343261aac5 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -52,6 +52,25 @@ func IsValidURL(uri string) bool { return true } +// IsValidSiteURL checks if URL is valid +func IsValidSiteURL(uri string) bool { + u, err := url.ParseRequestURI(uri) + if err != nil { + return false + } + + if !validPort(portOnly(u.Host)) { + return false + } + + for _, scheme := range setting.Service.ValidSiteURLSchemes { + if scheme == u.Scheme { + return true + } + } + return false +} + // IsAPIURL checks if URL is current Gitea instance API URL func IsAPIURL(uri string) bool { return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api")) diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 2c065dc511..903a625da0 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -226,7 +226,7 @@ type UpdateProfileForm struct { Name string `binding:"AlphaDashDot;MaxSize(40)"` FullName string `binding:"MaxSize(100)"` KeepEmailPrivate bool - Website string `binding:"ValidUrl;MaxSize(255)"` + Website string `binding:"ValidSiteUrl;MaxSize(255)"` Location string `binding:"MaxSize(50)"` Language string Description string `binding:"MaxSize(255)"` From 62a4879e84c7474dd72ab0eb4c54923f2690510c Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 26 Jun 2021 00:11:33 +0100 Subject: [PATCH 25/33] Improve efficiency in FindRenderizableReferenceNumeric and getReferences (#16251) * Fuzzer finds an NPE due to incorrect URLPrefix The Fuzzer is running on a non-repo urlprefix which is incorrect for RenderRaw * Make FindRenderizableReferenceNumeric and getReferences more efficient Signed-off-by: Andrew Thornton Co-authored-by: techknowlogick --- modules/references/references.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/references/references.go b/modules/references/references.go index 106e66b47b..ef859abcc7 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -5,6 +5,7 @@ package references import ( + "bytes" "net/url" "regexp" "strconv" @@ -14,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/mdstripper" "code.gitea.io/gitea/modules/setting" + + "github.com/yuin/goldmark/util" ) var ( @@ -321,7 +324,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende return false, nil } } - r := getCrossReference([]byte(content), match[2], match[3], false, prOnly) + r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly) if r == nil { return false, nil } @@ -465,18 +468,17 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference } func getCrossReference(content []byte, start, end int, fromLink bool, prOnly bool) *rawReference { - refid := string(content[start:end]) - sep := strings.IndexAny(refid, "#!") + sep := bytes.IndexAny(content[start:end], "#!") if sep < 0 { return nil } - isPull := refid[sep] == '!' + isPull := content[start+sep] == '!' if prOnly && !isPull { return nil } - repo := refid[:sep] - issue := refid[sep+1:] - index, err := strconv.ParseInt(issue, 10, 64) + repo := string(content[start : start+sep]) + issue := string(content[start+sep+1 : end]) + index, err := strconv.ParseInt(string(issue), 10, 64) if err != nil { return nil } From 622f1e764c6230023cc1944ad727cd2ad1544b68 Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Fri, 25 Jun 2021 23:16:36 -0500 Subject: [PATCH 26/33] Add better errors for disabled account recovery (#15117) Signed-off-by: jolheiser Co-authored-by: Andrew Thornton Co-authored-by: 6543 <6543@obermui.de> --- options/locale/locale_en-US.ini | 4 ++-- routers/web/user/auth.go | 1 + templates/user/auth/forgot_passwd.tmpl | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a809f49eeb..4a79ffa7eb 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -303,7 +303,8 @@ openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new openid_register_title = Create new account openid_register_desc = The chosen OpenID URI is unknown. Associate it with a new account here. openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.openid.org.cn or gnusocial.net/carry. -disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. +disable_forgot_password_mail = Account recovery is disabled because no email is set up. Please contact your site administrator. +disable_forgot_password_mail_admin = Account recovery is only available when email is set up. Please set up email to enable account recovery. email_domain_blacklisted = You cannot register with your email address. authorize_application = Authorize Application authorize_redirect_notice = You will be redirected to %s if you authorize this application. @@ -312,7 +313,6 @@ authorize_application_description = If you grant the access, it will be able to authorize_title = Authorize "%s" to access your account? authorization_failed = Authorization failed authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize. -disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. sspi_auth_failed = SSPI authentication failed password_pwned = The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password. password_pwned_err = Could not complete request to HaveIBeenPwned diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go index 827b7cdef0..6b4beff0e0 100644 --- a/routers/web/user/auth.go +++ b/routers/web/user/auth.go @@ -1478,6 +1478,7 @@ func ForgotPasswd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title") if setting.MailService == nil { + log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin")) ctx.Data["IsResetDisable"] = true ctx.HTML(http.StatusOK, tplForgotPassword) return diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl index 241deeed4a..2ff7acb97d 100644 --- a/templates/user/auth/forgot_passwd.tmpl +++ b/templates/user/auth/forgot_passwd.tmpl @@ -22,7 +22,13 @@
    {{else if .IsResetDisable}} -

    {{.i18n.Tr "auth.disable_forgot_password_mail"}}

    +

    + {{if $.IsAdmin}} + {{.i18n.Tr "auth.disable_forgot_password_mail_admin"}} + {{else}} + {{.i18n.Tr "auth.disable_forgot_password_mail"}} + {{end}} +

    {{else if .ResendLimited}}

    {{.i18n.Tr "auth.resent_limit_prompt"}}

    {{end}} From e673e42f7efafb184ffbe84f6998087713d8e373 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 26 Jun 2021 11:13:51 +0200 Subject: [PATCH 27/33] Fixed issues not updated by commits (#16254) `UpdateIssuesCommit` may get called with fewer commits because of `FeedMaxCommitNum` and therefore may miss some commands. --- services/repository/push.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/repository/push.go b/services/repository/push.go index f031073b2e..dcb3bc779f 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -193,16 +193,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } commits = repo_module.ListToPushCommits(l) + + if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil { + log.Error("updateIssuesCommit: %v", err) + } + if len(commits.Commits) > setting.UI.FeedMaxCommitNum { commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] } commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) notification.NotifyPushCommits(pusher, repo, opts, commits) - if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil { - log.Error("updateIssuesCommit: %v", err) - } - if err = models.RemoveDeletedBranch(repo.ID, branch); err != nil { log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err) } From e3c626834b34fae7728ee7869ed73ee4d1b26a26 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 26 Jun 2021 19:28:55 +0800 Subject: [PATCH 28/33] Let package git depend on setting but not opposite (#15241) * Let package git depend on setting but not opposite * private some package variables --- contrib/pr/checkout.go | 3 +- integrations/git_test.go | 6 +-- integrations/integration_test.go | 3 +- integrations/lfs_getobject_test.go | 13 ++++--- integrations/migration-test/migration_test.go | 3 +- models/migrations/migrations_test.go | 3 +- modules/git/command.go | 6 +-- modules/git/git.go | 33 ++++++++++++++++ modules/git/lfs.go | 37 ++++++++++++++++++ modules/git/repo_commit.go | 14 +++---- modules/setting/git.go | 38 ++----------------- modules/setting/lfs.go | 22 ----------- routers/api/v1/repo/commits.go | 4 +- routers/init.go | 4 +- routers/web/repo/branch.go | 7 ++-- routers/web/repo/commit.go | 6 +-- routers/web/repo/wiki.go | 3 +- 17 files changed, 113 insertions(+), 92 deletions(-) create mode 100644 modules/git/lfs.go diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 9ce84f762c..902c9ea623 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -26,6 +26,7 @@ import ( "time" "code.gitea.io/gitea/models" + gitea_git "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" @@ -79,7 +80,7 @@ func runPR() { setting.RunUser = curUser.Username log.Printf("[PR] Loading fixtures data ...\n") - setting.CheckLFSVersion() + gitea_git.CheckLFSVersion() //models.LoadConfigs() /* setting.Database.Type = "sqlite3" diff --git a/integrations/git_test.go b/integrations/git_test.go index 13a60076a7..a9848eaa4c 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -143,7 +143,7 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { t.Run("LFS", func(t *testing.T) { defer PrintCurrentTest(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -213,7 +213,7 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - setting.CheckLFSVersion() + git.CheckLFSVersion() if setting.LFS.StartServer { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) resp := session.MakeRequest(t, req, http.StatusOK) @@ -255,7 +255,7 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - setting.CheckLFSVersion() + git.CheckLFSVersion() if setting.LFS.StartServer { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) diff --git a/integrations/integration_test.go b/integrations/integration_test.go index d755977d1a..8a008ac621 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" @@ -162,7 +163,7 @@ func initIntegrationTest() { setting.SetCustomPathAndConf("", "", "") setting.NewContext() util.RemoveAll(models.LocalCopyPath()) - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() if err := storage.Init(); err != nil { fmt.Printf("Init storage failed: %v", err) diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go index 337a93567a..c99500f469 100644 --- a/integrations/lfs_getobject_test.go +++ b/integrations/lfs_getobject_test.go @@ -13,6 +13,7 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web" @@ -81,7 +82,7 @@ func checkResponseTestContentEncoding(t *testing.T, content *[]byte, resp *httpt func TestGetLFSSmall(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -94,7 +95,7 @@ func TestGetLFSSmall(t *testing.T) { func TestGetLFSLarge(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -110,7 +111,7 @@ func TestGetLFSLarge(t *testing.T) { func TestGetLFSGzip(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -131,7 +132,7 @@ func TestGetLFSGzip(t *testing.T) { func TestGetLFSZip(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -154,7 +155,7 @@ func TestGetLFSZip(t *testing.T) { func TestGetLFSRangeNo(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -167,7 +168,7 @@ func TestGetLFSRangeNo(t *testing.T) { func TestGetLFSRange(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 852c0b737c..209ff5a058 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -61,7 +62,7 @@ func initMigrationTest(t *testing.T) func() { assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() setting.NewLogServices(true) return deferFn diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go index 641d972b8b..26066580d8 100644 --- a/models/migrations/migrations_test.go +++ b/models/migrations/migrations_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -55,7 +56,7 @@ func TestMain(m *testing.M) { setting.SetCustomPathAndConf("", "", "") setting.NewContext() - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() setting.NewLogServices(true) diff --git a/modules/git/command.go b/modules/git/command.go index 2e375fd4f9..127e95ecfb 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -23,8 +23,8 @@ var ( // GlobalCommandArgs global command args for external package setting GlobalCommandArgs []string - // DefaultCommandExecutionTimeout default command execution timeout duration - DefaultCommandExecutionTimeout = 360 * time.Second + // defaultCommandExecutionTimeout default command execution timeout duration + defaultCommandExecutionTimeout = 360 * time.Second ) // DefaultLocale is the default LC_ALL to run git commands in. @@ -111,7 +111,7 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura // it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. Between cmd.Start and cmd.Wait the passed in function is run. func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader, fn func(context.Context, context.CancelFunc) error) error { if timeout == -1 { - timeout = DefaultCommandExecutionTimeout + timeout = defaultCommandExecutionTimeout } if len(dir) == 0 { diff --git a/modules/git/git.go b/modules/git/git.go index ce1b15c953..ef6ec0c2bf 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -14,6 +14,7 @@ import ( "time" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" "github.com/hashicorp/go-version" ) @@ -106,10 +107,42 @@ func SetExecutablePath(path string) error { return nil } +// VersionInfo returns git version information +func VersionInfo() string { + var format = "Git Version: %s" + var args = []interface{}{gitVersion.Original()} + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + format += ", Wire Protocol %s Enabled" + args = append(args, "Version 2") // for focus color + } + + return fmt.Sprintf(format, args...) +} + // Init initializes git module func Init(ctx context.Context) error { DefaultContext = ctx + defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second + + if err := SetExecutablePath(setting.Git.Path); err != nil { + return err + } + + // force cleanup args + GlobalCommandArgs = []string{} + + if CheckGitVersionAtLeast("2.9") == nil { + // Explicitly disable credential helper, otherwise Git credentials might leak + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "credential.helper=") + } + + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "protocol.version=2") + } + // Save current git version on init to gitVersion otherwise it would require an RWMutex if err := LoadGitVersion(); err != nil { return err diff --git a/modules/git/lfs.go b/modules/git/lfs.go new file mode 100644 index 0000000000..79049c9824 --- /dev/null +++ b/modules/git/lfs.go @@ -0,0 +1,37 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "sync" + + logger "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +var once sync.Once + +// CheckLFSVersion will check lfs version, if not satisfied, then disable it. +func CheckLFSVersion() { + if setting.LFS.StartServer { + //Disable LFS client hooks if installed for the current OS user + //Needs at least git v2.1.2 + + err := LoadGitVersion() + if err != nil { + logger.Fatal("Error retrieving git version: %v", err) + } + + if CheckGitVersionAtLeast("2.1.2") != nil { + setting.LFS.StartServer = false + logger.Error("LFS server support needs at least Git v2.1.2") + } else { + once.Do(func() { + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "filter.lfs.required=", + "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") + }) + } + } +} diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 664a7445dd..815aa141e5 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -12,6 +12,8 @@ import ( "io/ioutil" "strconv" "strings" + + "code.gitea.io/gitea/modules/setting" ) // GetBranchCommitID returns last commit ID string of given branch. @@ -85,12 +87,6 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { return commits.Front().Value.(*Commit), nil } -// CommitsRangeSize the default commits range size -var CommitsRangeSize = 50 - -// BranchesRangeSize the default branches range size -var BranchesRangeSize = 20 - func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) (*list.List, error) { stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*pageSize), "--max-count="+strconv.Itoa(pageSize), prettyLogFormat).RunInDirBytes(repo.Path) @@ -206,7 +202,7 @@ func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { // CommitsByFileAndRange return the commits according revison file and the page func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (*list.List, error) { - skip := (page - 1) * CommitsRangeSize + skip := (page - 1) * setting.Git.CommitsRangeSize stdoutReader, stdoutWriter := io.Pipe() defer func() { @@ -216,7 +212,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( go func() { stderr := strings.Builder{} err := NewCommand("log", revision, "--follow", - "--max-count="+strconv.Itoa(CommitsRangeSize*page), + "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize*page), prettyLogFormat, "--", file). RunInDirPipeline(repo.Path, stdoutWriter, &stderr) if err != nil { @@ -247,7 +243,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( // CommitsByFileAndRangeNoFollow return the commits according revison file and the page func (repo *Repository) CommitsByFileAndRangeNoFollow(revision, file string, page int) (*list.List, error) { stdout, err := NewCommand("log", revision, "--skip="+strconv.Itoa((page-1)*50), - "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path) + "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path) if err != nil { return nil, err } diff --git a/modules/setting/git.go b/modules/setting/git.go index 308d94894b..7383996cb9 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -7,7 +7,6 @@ package setting import ( "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" ) @@ -19,8 +18,8 @@ var ( MaxGitDiffLines int MaxGitDiffLineCharacters int MaxGitDiffFiles int - CommitsRangeSize int - BranchesRangeSize int + CommitsRangeSize int // CommitsRangeSize the default commits range size + BranchesRangeSize int // BranchesRangeSize the default branches range size VerbosePush bool VerbosePushDelay time.Duration GCArgs []string `ini:"GC_ARGS" delim:" "` @@ -54,7 +53,7 @@ var ( Pull int GC int `ini:"GC"` }{ - Default: int(git.DefaultCommandExecutionTimeout / time.Second), + Default: 360, Migrate: 600, Mirror: 300, Clone: 300, @@ -68,35 +67,4 @@ func newGit() { if err := Cfg.Section("git").MapTo(&Git); err != nil { log.Fatal("Failed to map Git settings: %v", err) } - if err := git.SetExecutablePath(Git.Path); err != nil { - log.Fatal("Failed to initialize Git settings: %v", err) - } - git.DefaultCommandExecutionTimeout = time.Duration(Git.Timeout.Default) * time.Second - - version, err := git.LocalVersion() - if err != nil { - log.Fatal("Error retrieving git version: %v", err) - } - - // force cleanup args - git.GlobalCommandArgs = []string{} - - if git.CheckGitVersionAtLeast("2.9") == nil { - // Explicitly disable credential helper, otherwise Git credentials might leak - git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "credential.helper=") - } - - var format = "Git Version: %s" - var args = []interface{}{version.Original()} - // Since git wire protocol has been released from git v2.18 - if Git.EnableAutoGitWireProtocol && git.CheckGitVersionAtLeast("2.18") == nil { - git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "protocol.version=2") - format += ", Wire Protocol %s Enabled" - args = append(args, "Version 2") // for focus color - } - - git.CommitsRangeSize = Git.CommitsRangeSize - git.BranchesRangeSize = Git.BranchesRangeSize - - log.Info(format, args...) } diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 8b9224b86a..a4bbd3c3ff 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -9,7 +9,6 @@ import ( "time" "code.gitea.io/gitea/modules/generate" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" ini "gopkg.in/ini.v1" @@ -67,24 +66,3 @@ func newLFSService() { } } } - -// CheckLFSVersion will check lfs version, if not satisfied, then disable it. -func CheckLFSVersion() { - if LFS.StartServer { - //Disable LFS client hooks if installed for the current OS user - //Needs at least git v2.1.2 - - err := git.LoadGitVersion() - if err != nil { - log.Fatal("Error retrieving git version: %v", err) - } - - if git.CheckGitVersionAtLeast("2.1.2") != nil { - LFS.StartServer = false - log.Error("LFS server support needs at least Git v2.1.2") - } else { - git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=", - "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") - } - } -} diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index a16cca0f4e..9a0fd1d0b6 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -143,8 +143,8 @@ func GetAllCommits(ctx *context.APIContext) { listOptions.Page = 1 } - if listOptions.PageSize > git.CommitsRangeSize { - listOptions.PageSize = git.CommitsRangeSize + if listOptions.PageSize > setting.Git.CommitsRangeSize { + listOptions.PageSize = setting.Git.CommitsRangeSize } sha := ctx.Query("sha") diff --git a/routers/init.go b/routers/init.go index e52e547517..cc9f703214 100644 --- a/routers/init.go +++ b/routers/init.go @@ -69,7 +69,9 @@ func GlobalInit(ctx context.Context) { if err := git.Init(ctx); err != nil { log.Fatal("Git module init failed: %v", err) } - setting.CheckLFSVersion() + log.Info(git.VersionInfo()) + + git.CheckLFSVersion() log.Trace("AppPath: %s", setting.AppPath) log.Trace("AppWorkPath: %s", setting.AppWorkPath) log.Trace("Custom path: %s", setting.CustomPath) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 4625b1a272..da72940144 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/repofiles" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" @@ -62,8 +63,8 @@ func Branches(ctx *context.Context) { } limit := ctx.QueryInt("limit") - if limit <= 0 || limit > git.BranchesRangeSize { - limit = git.BranchesRangeSize + if limit <= 0 || limit > setting.Git.BranchesRangeSize { + limit = setting.Git.BranchesRangeSize } skip := (page - 1) * limit @@ -73,7 +74,7 @@ func Branches(ctx *context.Context) { return } ctx.Data["Branches"] = branches - pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5) + pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 3e6148bcbb..45ef22f498 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -63,7 +63,7 @@ func Commits(ctx *context.Context) { pageSize := ctx.QueryInt("limit") if pageSize <= 0 { - pageSize = git.CommitsRangeSize + pageSize = setting.Git.CommitsRangeSize } // Both `git log branchName` and `git log commitId` work. @@ -82,7 +82,7 @@ func Commits(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount ctx.Data["Branch"] = ctx.Repo.BranchName - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -250,7 +250,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount ctx.Data["Branch"] = branchName - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index cceb8451e5..5271fe9b4a 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -316,7 +317,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Commits"] = commitsHistory - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager From 19ac575d572af655ab691f829d0b4de38a1f10be Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 26 Jun 2021 13:47:56 +0100 Subject: [PATCH 29/33] Limit stdout tracelog to actual stdout (#16258) Related #16243 Signed-off-by: Andrew Thornton --- modules/git/command.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/git/command.go b/modules/git/command.go index 127e95ecfb..d83c42fdc2 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -199,7 +199,11 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st return nil, ConcatenateError(err, stderr.String()) } if stdout.Len() > 0 && log.IsTrace() { - log.Trace("Stdout:\n %s", stdout.Bytes()[:1024]) + tracelen := stdout.Len() + if tracelen > 1024 { + tracelen = 1024 + } + log.Trace("Stdout:\n %s", stdout.Bytes()[:tracelen]) } return stdout.Bytes(), nil } From 22a0636544237bcffb46b36b593a501e77ae02cc Mon Sep 17 00:00:00 2001 From: Sergey Dryabzhinsky Date: Sat, 26 Jun 2021 22:53:14 +0300 Subject: [PATCH 30/33] Add Visible modes function from Organisation to Users too (#16069) You can limit or hide organisations. This pull make it also posible for users - new strings to translte - add checkbox to user profile form - add checkbox to admin user.edit form - filter explore page user search - filter api admin and public user searches - allow admins view "hidden" users - add app option DEFAULT_USER_VISIBILITY - rewrite many files to use Visibility field - check for teams intersection - fix context output - right fake 404 if not visible Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Andrew Thornton --- custom/conf/app.example.ini | 12 +- .../doc/advanced/config-cheat-sheet.en-us.md | 1 + integrations/api_user_search_test.go | 31 ++++++ models/fixtures/user.yml | 18 ++- models/org.go | 16 +-- models/org_test.go | 18 +-- models/repo.go | 3 +- models/repo_permission.go | 6 +- models/user.go | 105 +++++++++++++++--- modules/convert/user.go | 4 + modules/convert/user_test.go | 8 ++ modules/setting/service.go | 4 + modules/structs/admin_user.go | 2 + modules/structs/user.go | 2 + options/locale/locale_en-US.ini | 8 ++ routers/api/v1/admin/user.go | 15 ++- routers/api/v1/org/org.go | 4 +- routers/api/v1/repo/repo.go | 4 +- routers/api/v1/user/helper.go | 2 +- routers/api/v1/user/user.go | 7 ++ routers/web/admin/orgs.go | 3 +- routers/web/admin/users.go | 10 +- routers/web/admin/users_test.go | 81 ++++++++++++++ routers/web/org/home.go | 4 +- routers/web/user/profile.go | 16 ++- routers/web/user/setting/profile.go | 1 + services/forms/admin.go | 3 + services/forms/user_form.go | 2 + templates/admin/user/edit.tmpl | 27 +++++ templates/admin/user/new.tmpl | 19 ++++ templates/swagger/v1_json.tmpl | 13 +++ templates/user/settings/profile.tmpl | 59 ++++++++-- 32 files changed, 440 insertions(+), 68 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index fa6a9e3fac..e7fe9206ed 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -651,9 +651,15 @@ PATH = ;DEFAULT_ALLOW_CREATE_ORGANIZATION = true ;; ;; Either "public", "limited" or "private", default is "public" -;; Limited is for signed user only -;; Private is only for member of the organization -;; Public is for everyone +;; Limited is for users visible only to signed users +;; Private is for users visible only to members of their organizations +;; Public is for users visible for everyone +;DEFAULT_USER_VISIBILITY = public +;; +;; Either "public", "limited" or "private", default is "public" +;; Limited is for organizations visible only to signed users +;; Private is for organizations visible only to members of the organization +;; Public is for organizations visible to everyone ;DEFAULT_ORG_VISIBILITY = public ;; ;; Default value for DefaultOrgMemberVisible 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 aa9eb7e0ca..21359dcab1 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -512,6 +512,7 @@ relation to port exhaustion. - `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created - `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it +- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private". - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". - `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. - `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea. diff --git a/integrations/api_user_search_test.go b/integrations/api_user_search_test.go index c5295fbba5..f7349827e5 100644 --- a/integrations/api_user_search_test.go +++ b/integrations/api_user_search_test.go @@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) { } } } + +func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) { + defer prepareTestEnv(t)() + adminUsername := "user1" + session := loginUser(t, adminUsername) + token := getTokenForLoggedInUser(t, session) + query := "user31" + req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query) + req.SetBasicAuth(token, "x-oauth-basic") + resp := session.MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.NotEmpty(t, results.Data) + for _, user := range results.Data { + assert.Contains(t, user.UserName, query) + assert.NotEmpty(t, user.Email) + assert.EqualValues(t, "private", user.Visibility) + } +} + +func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) { + defer prepareTestEnv(t)() + query := "user31" + req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query) + resp := MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.Empty(t, results.Data) +} diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index d903a7942f..850ee4041d 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -508,7 +508,6 @@ num_repos: 0 is_active: true - - id: 30 lower_name: user30 @@ -525,3 +524,20 @@ avatar_email: user30@example.com num_repos: 2 is_active: true + +- + id: 31 + lower_name: user31 + name: user31 + full_name: "user31" + email: user31@example.com + passwd_hash_algo: argon2 + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password + type: 0 # individual + salt: ZogKvWdyEx + is_admin: false + visibility: 2 + avatar: avatar31 + avatar_email: user31@example.com + num_repos: 0 + is_active: true diff --git a/models/org.go b/models/org.go index 7f9e3cce5b..073b26c2f8 100644 --- a/models/org.go +++ b/models/org.go @@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) { Find(&orgs) } -// HasOrgVisible tells if the given user can see the given org -func HasOrgVisible(org, user *User) bool { - return hasOrgVisible(x, org, user) +// HasOrgOrUserVisible tells if the given user can see the given org or user +func HasOrgOrUserVisible(org, user *User) bool { + return hasOrgOrUserVisible(x, org, user) } -func hasOrgVisible(e Engine, org, user *User) bool { +func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool { // Not SignedUser if user == nil { - return org.Visibility == structs.VisibleTypePublic + return orgOrUser.Visibility == structs.VisibleTypePublic } - if user.IsAdmin { + if user.IsAdmin || orgOrUser.ID == user.ID { return true } - if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.hasMemberWithUserID(e, user.ID) { + if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !orgOrUser.hasMemberWithUserID(e, user.ID) { return false } return true @@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool { } for _, org := range orgs { - if HasOrgVisible(org, user) { + if HasOrgOrUserVisible(org, user) { return true } } diff --git a/models/org_test.go b/models/org_test.go index bed7a6eb86..e494e502dd 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.True(t, test3) // logged out user @@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.False(t, test3) // logged out user @@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.False(t, test2) // user not a part of org assert.False(t, test3) // logged out user diff --git a/models/repo.go b/models/repo.go index 4ce3d4839b..92d8427fab 100644 --- a/models/repo.go +++ b/models/repo.go @@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User, var users []*User - if repo.IsPrivate || - (repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) { + if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { // This a private repository: // Anyone who can read the repository is a requestable reviewer if err := e. diff --git a/models/repo_permission.go b/models/repo_permission.go index 138613b2e9..4f043a58cc 100644 --- a/models/repo_permission.go +++ b/models/repo_permission.go @@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss return } - // Prevent strangers from checking out public repo of private orginization - // Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself - if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator { + // Prevent strangers from checking out public repo of private orginization/users + // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator { perm.AccessMode = AccessModeNone return } diff --git a/models/user.go b/models/user.go index 5998341422..221c840a7f 100644 --- a/models/user.go +++ b/models/user.go @@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool { return len(u.Passwd) != 0 } +// IsVisibleToUser check if viewer is able to see user profile +func (u *User) IsVisibleToUser(viewer *User) bool { + return u.isVisibleToUser(x, viewer) +} + +func (u *User) isVisibleToUser(e Engine, viewer *User) bool { + if viewer != nil && viewer.IsAdmin { + return true + } + + switch u.Visibility { + case structs.VisibleTypePublic: + return true + case structs.VisibleTypeLimited: + if viewer == nil || viewer.IsRestricted { + return false + } + return true + case structs.VisibleTypePrivate: + if viewer == nil || viewer.IsRestricted { + return false + } + + // If they follow - they see each over + follower := IsFollowing(u.ID, viewer.ID) + if follower { + return true + } + + // Now we need to check if they in some organization together + count, err := x.Table("team_user"). + Where( + builder.And( + builder.Eq{"uid": viewer.ID}, + builder.Or( + builder.Eq{"org_id": u.ID}, + builder.In("org_id", + builder.Select("org_id"). + From("team_user", "t2"). + Where(builder.Eq{"uid": u.ID}))))). + Count(new(TeamUser)) + if err != nil { + return false + } + + if count < 0 { + // No common organization + return false + } + + // they are in an organization together + return true + } + return false +} + // IsOrganization returns true if user is actually a organization. func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization @@ -796,8 +852,13 @@ func IsUsableUsername(name string) error { return isUsableName(reservedUsernames, reservedUserPatterns, name) } +// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation +type CreateUserOverwriteOptions struct { + Visibility structs.VisibleType +} + // CreateUser creates record of a new user. -func CreateUser(u *User) (err error) { +func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) { return ErrEmailAlreadyUsed{u.Email} } - u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate - u.LowerName = strings.ToLower(u.Name) u.AvatarEmail = u.Email if u.Rands, err = GetUserSalt(); err != nil { @@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) { if err = u.SetPassword(u.Passwd); err != nil { return err } + + // set system defaults + u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate + u.Visibility = setting.Service.DefaultUserVisibilityMode u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme + // overwrite defaults if set + if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { + u.Visibility = overwriteDefault[0].Visibility + } if _, err = sess.Insert(u); err != nil { return err @@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { cond = cond.And(keywordCond) } + // If visibility filtered if len(opts.Visible) > 0 { cond = cond.And(builder.In("visibility", opts.Visible)) - } else { - cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) } if opts.Actor != nil { @@ -1543,16 +1609,27 @@ func (opts *SearchUserOptions) toConds() builder.Cond { exprCond = builder.Expr("org_user.org_id = \"user\".id") } - var accessCond builder.Cond - if !opts.Actor.IsRestricted { - accessCond = builder.Or( - builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), - builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) - } else { - // restricted users only see orgs they are a member of - accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) + // If Admin - they see all users! + if !opts.Actor.IsAdmin { + // Force visiblity for privacy + var accessCond builder.Cond + if !opts.Actor.IsRestricted { + accessCond = builder.Or( + builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), + builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) + } else { + // restricted users only see orgs they are a member of + accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) + } + // Don't forget about self + accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) + cond = cond.And(accessCond) } - cond = cond.And(accessCond) + + } else { + // Force visiblity for privacy + // Not logged in - only public users + cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) } if opts.UID > 0 { diff --git a/modules/convert/user.go b/modules/convert/user.go index 894be3bd44..164ffb71fd 100644 --- a/modules/convert/user.go +++ b/modules/convert/user.go @@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User { Following: user.NumFollowing, StarredRepos: user.NumStars, } + + result.Visibility = user.Visibility.String() + // hide primary email if API caller is anonymous or user keep email private if signed && (!user.KeepEmailPrivate || authed) { result.Email = user.Email } + // only site admin will get these information and possibly user himself if authed { result.IsAdmin = user.IsAdmin diff --git a/modules/convert/user_test.go b/modules/convert/user_test.go index 7837910ffe..679c4f9894 100644 --- a/modules/convert/user_test.go +++ b/modules/convert/user_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) { apiUser = toUser(user1, false, false) assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility) + + user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User) + + apiUser = toUser(user31, true, true) + assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility) } diff --git a/modules/setting/service.go b/modules/setting/service.go index bd70c7e6eb..3f689212f3 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -15,6 +15,8 @@ import ( // Service settings var Service struct { + DefaultUserVisibility string + DefaultUserVisibilityMode structs.VisibleType DefaultOrgVisibility string DefaultOrgVisibilityMode structs.VisibleType ActiveCodeLives int @@ -118,6 +120,8 @@ func newService() { Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) + Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) + Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index 5da4e9608b..facf16a395 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -19,6 +19,7 @@ type CreateUserOption struct { Password string `json:"password" binding:"Required;MaxSize(255)"` MustChangePassword *bool `json:"must_change_password"` SendNotify bool `json:"send_notify"` + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } // EditUserOption edit user options @@ -43,4 +44,5 @@ type EditUserOption struct { ProhibitLogin *bool `json:"prohibit_login"` AllowCreateOrganization *bool `json:"allow_create_organization"` Restricted *bool `json:"restricted"` + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/user.go b/modules/structs/user.go index 0d8b0300c3..a3c8f0c32a 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -43,6 +43,8 @@ type User struct { Website string `json:"website"` // the user's description Description string `json:"description"` + // User visibility level option: public, limited, private + Visibility string `json:"visibility"` // user counts Followers int `json:"followers_count"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4a79ffa7eb..e0ece8f9f0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -724,6 +724,14 @@ email_notifications.onmention = Only Email on Mention email_notifications.disable = Disable Email Notifications email_notifications.submit = Set Email Preference +visibility = User visibility +visibility.public = Public +visibility.public_tooltip = Visible to all users +visibility.limited = Limited +visibility.limited_tooltip = Visible to logged in users only +visibility.private = Private +visibility.private_tooltip = Visible only to organization members + [repo] new_repo_helper = A repository contains all project files, including revision history. Already have it elsewhere? Migrate repository. owner = Owner diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 4bbe7f77ba..6bc9b849b1 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -66,6 +66,7 @@ func CreateUser(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateUserOption) + u := &models.User{ Name: form.Username, FullName: form.FullName, @@ -97,7 +98,15 @@ func CreateUser(ctx *context.APIContext) { ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) return } - if err := models.CreateUser(u); err != nil { + + var overwriteDefault *models.CreateUserOverwriteOptions + if form.Visibility != "" { + overwriteDefault = &models.CreateUserOverwriteOptions{ + Visibility: api.VisibilityModes[form.Visibility], + } + } + + if err := models.CreateUser(u, overwriteDefault); err != nil { if models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err) || models.IsErrNameReserved(err) || @@ -209,6 +218,9 @@ func EditUser(ctx *context.APIContext) { if form.Active != nil { u.IsActive = *form.Active } + if len(form.Visibility) != 0 { + u.Visibility = api.VisibilityModes[form.Visibility] + } if form.Admin != nil { u.IsAdmin = *form.Admin } @@ -395,6 +407,7 @@ func GetAllUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{ + Actor: ctx.User, Type: models.UserTypeIndividual, OrderBy: models.SearchOrderByAlphabetically, ListOptions: listOptions, diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index f4a634f4d5..5c16594f89 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -225,8 +225,8 @@ func Get(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Organization" - if !models.HasOrgVisible(ctx.Org.Organization, ctx.User) { - ctx.NotFound("HasOrgVisible", nil) + if !models.HasOrgOrUserVisible(ctx.Org.Organization, ctx.User) { + ctx.NotFound("HasOrgOrUserVisible", nil) return } ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization)) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7a3160fa99..35d3490510 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) { return } - if !models.HasOrgVisible(org, ctx.User) { - ctx.NotFound("HasOrgVisible", nil) + if !models.HasOrgOrUserVisible(org, ctx.User) { + ctx.NotFound("HasOrgOrUserVisible", nil) return } diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index fcdac257ed..a3500e0ee6 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { user, err := models.GetUserByName(username) if err != nil { if models.IsErrUserNotExist(err) { - if redirectUserID, err := models.LookupUserRedirect(username); err == nil { + if redirectUserID, err2 := models.LookupUserRedirect(username); err2 == nil { context.RedirectToUser(ctx.Context, username, redirectUserID) } else { ctx.NotFound("GetUserByName", err) diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 4adae532fd..ac543d597d 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) opts := &models.SearchUserOptions{ + Actor: ctx.User, Keyword: strings.Trim(ctx.Query("q"), " "), UID: ctx.QueryInt64("uid"), Type: models.UserTypeIndividual, @@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) { // "$ref": "#/responses/notFound" u := GetUserByParams(ctx) + if ctx.Written() { return } + if !u.IsVisibleToUser(ctx.User) { + // fake ErrUserNotExist error message to not leak information about existence + ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")}) + return + } ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User)) } diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index 618f945704..a2b3ed1bcc 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -25,7 +25,8 @@ func Organizations(ctx *context.Context) { ctx.Data["PageIsAdminOrganizations"] = true explore.RenderUserSearch(ctx, &models.SearchUserOptions{ - Type: models.UserTypeOrganization, + Actor: ctx.User, + Type: models.UserTypeOrganization, ListOptions: models.ListOptions{ PageSize: setting.UI.Admin.OrgPagingNum, }, diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 1b65795865..dc2a97e526 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -37,7 +37,8 @@ func Users(ctx *context.Context) { ctx.Data["PageIsAdminUsers"] = true explore.RenderUserSearch(ctx, &models.SearchUserOptions{ - Type: models.UserTypeIndividual, + Actor: ctx.User, + Type: models.UserTypeIndividual, ListOptions: models.ListOptions{ PageSize: setting.UI.Admin.UserPagingNum, }, @@ -50,6 +51,7 @@ func NewUser(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users.new_account") ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminUsers"] = true + ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode ctx.Data["login_type"] = "0-0" @@ -70,6 +72,7 @@ func NewUserPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users.new_account") ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminUsers"] = true + ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode sources, err := models.LoginSources() if err != nil { @@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) { } u.MustChangePassword = form.MustChangePassword } - if err := models.CreateUser(u); err != nil { + + if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil { switch { case models.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true @@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) { u.AllowImportLocal = form.AllowImportLocal u.AllowCreateOrganization = form.AllowCreateOrganization + u.Visibility = form.Visibility + // skip self Prohibit Login if ctx.User.ID == u.ID { u.ProhibitLogin = false diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go index b19dcb886b..17c5a309b4 100644 --- a/routers/web/admin/users_test.go +++ b/routers/web/admin/users_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" @@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { assert.NotEmpty(t, ctx.Flash.ErrorMsg) } + +func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) { + + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "admin/users/new") + + u := models.AssertExistsAndLoadBean(t, &models.User{ + IsAdmin: true, + ID: 2, + }).(*models.User) + + ctx.User = u + + username := "gitea" + email := "gitea@gitea.io" + + form := forms.AdminCreateUserForm{ + LoginType: "local", + LoginName: "local", + UserName: username, + Email: email, + Password: "abc123ABC!=$", + SendNotify: false, + MustChangePassword: false, + } + + web.SetForm(ctx, &form) + NewUserPost(ctx) + + assert.NotEmpty(t, ctx.Flash.SuccessMsg) + + u, err := models.GetUserByName(username) + + assert.NoError(t, err) + assert.Equal(t, username, u.Name) + assert.Equal(t, email, u.Email) + // As default user visibility + assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility) +} + +func TestNewUserPost_VisibilityPrivate(t *testing.T) { + + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "admin/users/new") + + u := models.AssertExistsAndLoadBean(t, &models.User{ + IsAdmin: true, + ID: 2, + }).(*models.User) + + ctx.User = u + + username := "gitea" + email := "gitea@gitea.io" + + form := forms.AdminCreateUserForm{ + LoginType: "local", + LoginName: "local", + UserName: username, + Email: email, + Password: "abc123ABC!=$", + SendNotify: false, + MustChangePassword: false, + Visibility: api.VisibleTypePrivate, + } + + web.SetForm(ctx, &form) + NewUserPost(ctx) + + assert.NotEmpty(t, ctx.Flash.SuccessMsg) + + u, err := models.GetUserByName(username) + + assert.NoError(t, err) + assert.Equal(t, username, u.Name) + assert.Equal(t, email, u.Email) + // As default user visibility + assert.True(t, u.Visibility.IsPrivate()) +} diff --git a/routers/web/org/home.go b/routers/web/org/home.go index ad14f18454..aad0a2a90b 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -30,8 +30,8 @@ func Home(ctx *context.Context) { org := ctx.Org.Organization - if !models.HasOrgVisible(org, ctx.User) { - ctx.NotFound("HasOrgVisible", nil) + if !models.HasOrgOrUserVisible(org, ctx.User) { + ctx.NotFound("HasOrgOrUserVisible", nil) return } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 72d0066645..631ca21135 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -75,6 +75,17 @@ func Profile(ctx *context.Context) { return } + if ctxUser.IsOrganization() { + org.Home(ctx) + return + } + + // check view permissions + if !ctxUser.IsVisibleToUser(ctx.User) { + ctx.NotFound("user", fmt.Errorf(uname)) + return + } + // Show SSH keys. if isShowKeys { ShowSSHKeys(ctx, ctxUser.ID) @@ -87,11 +98,6 @@ func Profile(ctx *context.Context) { return } - if ctxUser.IsOrganization() { - org.Home(ctx) - return - } - // Show OpenID URIs openIDs, err := models.GetUserOpenIDs(ctxUser.ID) if err != nil { diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 20042caca4..463c4ec203 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) { } ctx.User.Description = form.Description ctx.User.KeepActivityPrivate = form.KeepActivityPrivate + ctx.User.Visibility = form.Visibility if err := models.UpdateUserSetting(ctx.User); err != nil { if _, ok := err.(models.ErrEmailAlreadyUsed); ok { ctx.Flash.Error(ctx.Tr("form.email_been_used")) diff --git a/services/forms/admin.go b/services/forms/admin.go index 2e6bbaf172..5abef0550e 100644 --- a/services/forms/admin.go +++ b/services/forms/admin.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -22,6 +23,7 @@ type AdminCreateUserForm struct { Password string `binding:"MaxSize(255)"` SendNotify bool MustChangePassword bool + Visibility structs.VisibleType } // Validate validates form fields @@ -49,6 +51,7 @@ type AdminEditUserForm struct { AllowCreateOrganization bool ProhibitLogin bool Reset2FA bool `form:"reset_2fa"` + Visibility structs.VisibleType } // Validate validates form fields diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 903a625da0..439ddfc7c6 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -230,6 +231,7 @@ type UpdateProfileForm struct { Location string `binding:"MaxSize(50)"` Language string Description string `binding:"MaxSize(255)"` + Visibility structs.VisibleType KeepActivityPrivate bool } diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index af01489c0a..dba24d9837 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -28,6 +28,33 @@
    + +
    + + +
    +
    diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 885045dd02..2e39172535 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -24,6 +24,25 @@
    + +
    + + +
    +
    diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 9453b1af32..7f7907b3b0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13334,6 +13334,10 @@ "username": { "type": "string", "x-go-name": "Username" + }, + "visibility": { + "type": "string", + "x-go-name": "Visibility" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -14143,6 +14147,10 @@ "format": "int64", "x-go-name": "SourceID" }, + "visibility": { + "type": "string", + "x-go-name": "Visibility" + }, "website": { "type": "string", "x-go-name": "Website" @@ -16637,6 +16645,11 @@ "format": "int64", "x-go-name": "StarredRepos" }, + "visibility": { + "description": "User visibility level option: public, limited, private", + "type": "string", + "x-go-name": "Visibility" + }, "website": { "description": "the user's website", "type": "string", diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 9f07226632..4b860049d8 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -47,27 +47,62 @@
    -
    - -