diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index af3418f70..ffc4e4067 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -124,6 +124,8 @@ SIGNING_KEY = default ; by setting the SIGNING_KEY ID to the correct ID.) SIGNING_NAME = SIGNING_EMAIL = +; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter +DEFAULT_TRUST_MODEL=collaborator ; Determines when gitea should sign the initial commit when creating a repository ; Either: ; - never 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 7f969add2..1e48ee259 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -101,6 +101,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `twofa`: Only sign if the user is logged in with twofa - `always`: Always sign - Options other than `never` and `always` can be combined as a comma separated list. +- `DEFAULT_TRUST_MODEL`: **collaborator**: \[collaborator, committer, collaboratorcommitter\]: The default trust model used for verifying commits. + - `collaborator`: Trust signatures signed by keys of collaborators. + - `committer`: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the commmitter). + - `collaboratorcommitter`: Trust signatures signed by keys of collaborators which match the commiter. - `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki. - `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions. - Options as above, with the addition of: diff --git a/models/gpg_key.go b/models/gpg_key.go index 662eac939..b944fdcbf 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -831,7 +831,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l newCommits = list.New() e = oldCommits.Front() ) - memberMap := map[int64]bool{} + keyMap := map[string]bool{} for e != nil { c := e.Value.(UserCommit) @@ -840,7 +840,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l Verification: ParseCommitWithSignature(c.Commit), } - _ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap) + _ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap) newCommits.PushBack(signCommit) e = e.Next() @@ -849,31 +849,70 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l } // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository -func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) { - if verification.Verified { - verification.TrustStatus = "trusted" - if verification.SigningUser.ID != 0 { - var isMember bool - if memberMap != nil { - var has bool - isMember, has = (*memberMap)[verification.SigningUser.ID] - if !has { - isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) - (*memberMap)[verification.SigningUser.ID] = isMember - } - } else { - isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) - } +func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) { + if !verification.Verified { + return + } - if !isMember { - verification.TrustStatus = "untrusted" - if verification.CommittingUser.ID != verification.SigningUser.ID { - // The committing user and the signing user are not the same and are not the default key - // This should be marked as questionable unless the signing user is a collaborator/team member etc. - verification.TrustStatus = "unmatched" - } - } + // There are several trust models in Gitea + trustModel := repository.GetTrustModel() + + // In the Committer trust model a signature is trusted if it matches the committer + // - it doesn't matter if they're a collaborator, the owner, Gitea or Github + // NB: This model is commit verification only + if trustModel == CommitterTrustModel { + // default to "unmatched" + verification.TrustStatus = "unmatched" + + // We can only verify against users in our database but the default key will match + // against by email if it is not in the db. + if (verification.SigningUser.ID != 0 && + verification.CommittingUser.ID == verification.SigningUser.ID) || + (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && + verification.SigningUser.Email == verification.CommittingUser.Email) { + verification.TrustStatus = "trusted" } + return } + + // Now we drop to the more nuanced trust models... + verification.TrustStatus = "trusted" + + if verification.SigningUser.ID == 0 { + // This commit is signed by the default key - but this key is not assigned to a user in the DB. + + // However in the CollaboratorCommitterTrustModel we cannot mark this as trusted + // unless the default key matches the email of a non-user. + if trustModel == CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || + verification.SigningUser.Email != verification.CommittingUser.Email) { + verification.TrustStatus = "untrusted" + } + return + } + + var isMember bool + if keyMap != nil { + var has bool + isMember, has = (*keyMap)[verification.SigningKey.KeyID] + if !has { + isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) + (*keyMap)[verification.SigningKey.KeyID] = isMember + } + } else { + isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) + } + + if !isMember { + verification.TrustStatus = "untrusted" + if verification.CommittingUser.ID != verification.SigningUser.ID { + // The committing user and the signing user are not the same + // This should be marked as questionable unless the signing user is a collaborator/team member etc. + verification.TrustStatus = "unmatched" + } + } else if trustModel == CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { + // The committing user and the signing user are not the same and our trustmodel states that they must match + verification.TrustStatus = "unmatched" + } + return } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 5317cc574..ea1bf5964 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -237,6 +237,8 @@ var migrations = []Migration{ NewMigration("add primary key to repo_topic", addPrimaryKeyToRepoTopic), // v151 -> v152 NewMigration("set default password algorithm to Argon2", setDefaultPasswordToArgon2), + // v152 -> v153 + NewMigration("add TrustModel field to Repository", addTrustModelToRepository), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v152.go b/models/migrations/v152.go new file mode 100644 index 000000000..f71f71e22 --- /dev/null +++ b/models/migrations/v152.go @@ -0,0 +1,14 @@ +// Copyright 2020 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 addTrustModelToRepository(x *xorm.Engine) error { + type Repository struct { + TrustModel int + } + return x.Sync2(new(Repository)) +} diff --git a/models/notification.go b/models/notification.go index 80d837078..1f51c99c4 100644 --- a/models/notification.go +++ b/models/notification.go @@ -354,6 +354,7 @@ func (n *Notification) APIFormat() *api.NotificationThread { if n.Issue != nil { result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL() + result.Subject.State = n.Issue.State() comment, err := n.Issue.GetLastComment() if err == nil && comment != nil { result.Subject.LatestCommentURL = comment.APIURL() @@ -364,6 +365,7 @@ func (n *Notification) APIFormat() *api.NotificationThread { if n.Issue != nil { result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL() + result.Subject.State = n.Issue.State() comment, err := n.Issue.GetLastComment() if err == nil && comment != nil { result.Subject.LatestCommentURL = comment.APIURL() diff --git a/models/pull_sign.go b/models/pull_sign.go index ab61232d6..10a6522eb 100644 --- a/models/pull_sign.go +++ b/models/pull_sign.go @@ -11,16 +11,16 @@ import ( ) // SignMerge determines if we should sign a PR merge commit to the base repository -func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string, error) { +func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) { if err := pr.LoadBaseRepo(); err != nil { log.Error("Unable to get Base Repo for pull request") - return false, "", err + return false, "", nil, err } repo := pr.BaseRepo - signingKey := signingKey(repo.RepoPath()) + signingKey, signer := SigningKey(repo.RepoPath()) if signingKey == "" { - return false, "", &ErrWontSign{noKey} + return false, "", nil, &ErrWontSign{noKey} } rules := signingModeFromStrings(setting.Repository.Signing.Merges) @@ -31,101 +31,101 @@ Loop: for _, rule := range rules { switch rule { case never: - return false, "", &ErrWontSign{never} + return false, "", nil, &ErrWontSign{never} case always: break Loop case pubkey: keys, err := ListGPGKeys(u.ID, ListOptions{}) if err != nil { - return false, "", err + return false, "", nil, err } if len(keys) == 0 { - return false, "", &ErrWontSign{pubkey} + return false, "", nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := GetTwoFactorByUID(u.ID) if err != nil && !IsErrTwoFactorNotEnrolled(err) { - return false, "", err + return false, "", nil, err } if twofaModel == nil { - return false, "", &ErrWontSign{twofa} + return false, "", nil, &ErrWontSign{twofa} } case approved: protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch) if err != nil { - return false, "", err + return false, "", nil, err } if protectedBranch == nil { - return false, "", &ErrWontSign{approved} + return false, "", nil, &ErrWontSign{approved} } if protectedBranch.GetGrantedApprovalsCount(pr) < 1 { - return false, "", &ErrWontSign{approved} + return false, "", nil, &ErrWontSign{approved} } case baseSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(tmpBasePath) if err != nil { - return false, "", err + return false, "", nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { - return false, "", err + return false, "", nil, err } verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{baseSigned} + return false, "", nil, &ErrWontSign{baseSigned} } case headSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(tmpBasePath) if err != nil { - return false, "", err + return false, "", nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", err + return false, "", nil, err } verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{headSigned} + return false, "", nil, &ErrWontSign{headSigned} } case commitsSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(tmpBasePath) if err != nil { - return false, "", err + return false, "", nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", err + return false, "", nil, err } verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{commitsSigned} + return false, "", nil, &ErrWontSign{commitsSigned} } // need to work out merge-base mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) if err != nil { - return false, "", err + return false, "", nil, err } commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) if err != nil { - return false, "", err + return false, "", nil, err } for e := commitList.Front(); e != nil; e = e.Next() { commit = e.Value.(*git.Commit) verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{commitsSigned} + return false, "", nil, &ErrWontSign{commitsSigned} } } } } - return true, signingKey, nil + return true, signingKey, signer, nil } diff --git a/models/repo.go b/models/repo.go index 2bbc74f43..25fe3f63d 100644 --- a/models/repo.go +++ b/models/repo.go @@ -143,6 +143,47 @@ const ( RepositoryBeingMigrated // repository is migrating ) +// TrustModelType defines the types of trust model for this repository +type TrustModelType int + +// kinds of TrustModel +const ( + DefaultTrustModel TrustModelType = iota // default trust model + CommitterTrustModel + CollaboratorTrustModel + CollaboratorCommitterTrustModel +) + +// String converts a TrustModelType to a string +func (t TrustModelType) String() string { + switch t { + case DefaultTrustModel: + return "default" + case CommitterTrustModel: + return "committer" + case CollaboratorTrustModel: + return "collaborator" + case CollaboratorCommitterTrustModel: + return "collaboratorcommitter" + } + return "default" +} + +// ToTrustModel converts a string to a TrustModelType +func ToTrustModel(model string) TrustModelType { + switch strings.ToLower(strings.TrimSpace(model)) { + case "default": + return DefaultTrustModel + case "collaborator": + return CollaboratorTrustModel + case "committer": + return CommitterTrustModel + case "collaboratorcommitter": + return CollaboratorCommitterTrustModel + } + return DefaultTrustModel +} + // Repository represents a git repository. type Repository struct { ID int64 `xorm:"pk autoincr"` @@ -198,6 +239,8 @@ type Repository struct { CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` Topics []string `xorm:"TEXT JSON"` + TrustModel TrustModelType + // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols Avatar string `xorm:"VARCHAR(64)"` @@ -1038,6 +1081,7 @@ type CreateRepoOptions struct { IsMirror bool AutoInit bool Status RepositoryStatus + TrustModel TrustModelType } // GetRepoInitFile returns repository init files @@ -2383,6 +2427,18 @@ func UpdateRepositoryCols(repo *Repository, cols ...string) error { return updateRepositoryCols(x, repo, cols...) } +// GetTrustModel will get the TrustModel for the repo or the default trust model +func (repo *Repository) GetTrustModel() TrustModelType { + trustModel := repo.TrustModel + if trustModel == DefaultTrustModel { + trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel) + if trustModel == DefaultTrustModel { + return CollaboratorTrustModel + } + } + return trustModel +} + // DoctorUserStarNum recalculate Stars number for all user func DoctorUserStarNum() (err error) { const batchSize = 100 diff --git a/models/repo_sign.go b/models/repo_sign.go index c9dd5ea4d..be9309ed4 100644 --- a/models/repo_sign.go +++ b/models/repo_sign.go @@ -31,7 +31,7 @@ const ( func signingModeFromStrings(modeStrings []string) []signingMode { returnable := make([]signingMode, 0, len(modeStrings)) for _, mode := range modeStrings { - signMode := signingMode(strings.ToLower(mode)) + signMode := signingMode(strings.ToLower(strings.TrimSpace(mode))) switch signMode { case never: return []signingMode{never} @@ -59,9 +59,10 @@ func signingModeFromStrings(modeStrings []string) []signingMode { return returnable } -func signingKey(repoPath string) string { +// SigningKey returns the KeyID and git Signature for the repo +func SigningKey(repoPath string) (string, *git.Signature) { if setting.Repository.Signing.SigningKey == "none" { - return "" + return "", nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { @@ -69,19 +70,27 @@ func signingKey(repoPath string) string { value, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath) sign, valid := git.ParseBool(strings.TrimSpace(value)) if !sign || !valid { - return "" + return "", nil } signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath) - return strings.TrimSpace(signingKey) + signingName, _ := git.NewCommand("config", "--get", "user.name").RunInDir(repoPath) + signingEmail, _ := git.NewCommand("config", "--get", "user.email").RunInDir(repoPath) + return strings.TrimSpace(signingKey), &git.Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } } - return setting.Repository.Signing.SigningKey + return setting.Repository.Signing.SigningKey, &git.Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } } // PublicSigningKey gets the public signing key within a provided repository directory func PublicSigningKey(repoPath string) (string, error) { - signingKey := signingKey(repoPath) + signingKey, _ := SigningKey(repoPath) if signingKey == "" { return "", nil } @@ -96,143 +105,143 @@ func PublicSigningKey(repoPath string) (string, error) { } // SignInitialCommit determines if we should sign the initial commit to this repository -func SignInitialCommit(repoPath string, u *User) (bool, string, error) { +func SignInitialCommit(repoPath string, u *User) (bool, string, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) - signingKey := signingKey(repoPath) + signingKey, sig := SigningKey(repoPath) if signingKey == "" { - return false, "", &ErrWontSign{noKey} + return false, "", nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", &ErrWontSign{never} + return false, "", nil, &ErrWontSign{never} case always: break Loop case pubkey: keys, err := ListGPGKeys(u.ID, ListOptions{}) if err != nil { - return false, "", err + return false, "", nil, err } if len(keys) == 0 { - return false, "", &ErrWontSign{pubkey} + return false, "", nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := GetTwoFactorByUID(u.ID) if err != nil && !IsErrTwoFactorNotEnrolled(err) { - return false, "", err + return false, "", nil, err } if twofaModel == nil { - return false, "", &ErrWontSign{twofa} + return false, "", nil, &ErrWontSign{twofa} } } } - return true, signingKey, nil + return true, signingKey, sig, nil } // SignWikiCommit determines if we should sign the commits to this repository wiki -func (repo *Repository) SignWikiCommit(u *User) (bool, string, error) { +func (repo *Repository) SignWikiCommit(u *User) (bool, string, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.Wiki) - signingKey := signingKey(repo.WikiPath()) + signingKey, sig := SigningKey(repo.WikiPath()) if signingKey == "" { - return false, "", &ErrWontSign{noKey} + return false, "", nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", &ErrWontSign{never} + return false, "", nil, &ErrWontSign{never} case always: break Loop case pubkey: keys, err := ListGPGKeys(u.ID, ListOptions{}) if err != nil { - return false, "", err + return false, "", nil, err } if len(keys) == 0 { - return false, "", &ErrWontSign{pubkey} + return false, "", nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := GetTwoFactorByUID(u.ID) if err != nil && !IsErrTwoFactorNotEnrolled(err) { - return false, "", err + return false, "", nil, err } if twofaModel == nil { - return false, "", &ErrWontSign{twofa} + return false, "", nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(repo.WikiPath()) if err != nil { - return false, "", err + return false, "", nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { - return false, "", err + return false, "", nil, err } if commit.Signature == nil { - return false, "", &ErrWontSign{parentSigned} + return false, "", nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{parentSigned} + return false, "", nil, &ErrWontSign{parentSigned} } } } - return true, signingKey, nil + return true, signingKey, sig, nil } // SignCRUDAction determines if we should sign a CRUD commit to this repository -func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string, error) { +func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) - signingKey := signingKey(repo.RepoPath()) + signingKey, sig := SigningKey(repo.RepoPath()) if signingKey == "" { - return false, "", &ErrWontSign{noKey} + return false, "", nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", &ErrWontSign{never} + return false, "", nil, &ErrWontSign{never} case always: break Loop case pubkey: keys, err := ListGPGKeys(u.ID, ListOptions{}) if err != nil { - return false, "", err + return false, "", nil, err } if len(keys) == 0 { - return false, "", &ErrWontSign{pubkey} + return false, "", nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := GetTwoFactorByUID(u.ID) if err != nil && !IsErrTwoFactorNotEnrolled(err) { - return false, "", err + return false, "", nil, err } if twofaModel == nil { - return false, "", &ErrWontSign{twofa} + return false, "", nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(tmpBasePath) if err != nil { - return false, "", err + return false, "", nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { - return false, "", err + return false, "", nil, err } if commit.Signature == nil { - return false, "", &ErrWontSign{parentSigned} + return false, "", nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(commit) if !verification.Verified { - return false, "", &ErrWontSign{parentSigned} + return false, "", nil, &ErrWontSign{parentSigned} } } } - return true, signingKey, nil + return true, signingKey, sig, nil } diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 3ad57085b..f1130f372 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -45,6 +45,7 @@ type CreateRepoForm struct { Webhooks bool Avatar bool Labels bool + TrustModel string } // Validate validates the fields @@ -142,6 +143,9 @@ type RepoSettingForm struct { EnableIssueDependencies bool IsArchived bool + // Signing Settings + TrustModel string + // Admin settings EnableHealthCheck bool EnableCloseIssuesViaCommitInAnyBranch bool diff --git a/modules/context/repo.go b/modules/context/repo.go index 2c7736146..cb2e60d26 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -114,7 +114,7 @@ func (r *Repository) CanCommitToBranch(doer *models.User) (CanCommitToBranchResu requireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, err := r.Repository.SignCRUDAction(doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) + sign, keyID, _, err := r.Repository.SignCRUDAction(doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) canCommit := r.CanEnableEditor() && userCanPush if requireSigned { diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index a662aaab4..57896318a 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -62,7 +62,7 @@ type CommitTreeOpts struct { } // CommitTree creates a commit from a given tree id for the user with provided message -func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { +func (repo *Repository) CommitTree(author *Signature, committer *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { err := LoadGitVersion() if err != nil { return SHA1{}, err @@ -72,11 +72,11 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp // Because this may call hooks we should pass in the environment env := append(os.Environ(), - "GIT_AUTHOR_NAME="+sig.Name, - "GIT_AUTHOR_EMAIL="+sig.Email, + "GIT_AUTHOR_NAME="+author.Name, + "GIT_AUTHOR_EMAIL="+author.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_NAME="+committer.Name, + "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) cmd := NewCommand("commit-tree", tree.ID.String()) diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go index 2dbd8ffd4..8432a1eec 100644 --- a/modules/migrations/gitea_test.go +++ b/modules/migrations/gitea_test.go @@ -28,7 +28,7 @@ func TestGiteaUploadRepo(t *testing.T) { user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) var ( - downloader = NewGithubDownloaderV3(context.Background(), "", "", "", "go-xorm", "builder") + downloader = NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", "", "go-xorm", "builder") repoName = "builder-" + time.Now().Format("2006-01-02-15-04-05") uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName) ) diff --git a/modules/migrations/github.go b/modules/migrations/github.go index d31db136e..c053596c1 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -47,13 +47,14 @@ func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOp return nil, err } + baseURL := u.Scheme + "://" + u.Host fields := strings.Split(u.Path, "/") oldOwner := fields[1] oldName := strings.TrimSuffix(fields[2], ".git") log.Trace("Create github downloader: %s/%s", oldOwner, oldName) - return NewGithubDownloaderV3(ctx, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil + return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil } // GitServiceType returns the type of git service @@ -74,7 +75,7 @@ type GithubDownloaderV3 struct { } // NewGithubDownloaderV3 creates a github Downloader via github v3 API -func NewGithubDownloaderV3(ctx context.Context, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { +func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { var downloader = GithubDownloaderV3{ userName: userName, password: password, @@ -98,6 +99,9 @@ func NewGithubDownloaderV3(ctx context.Context, userName, password, token, repoO client = oauth2.NewClient(downloader.ctx, ts) } downloader.client = github.NewClient(client) + if baseURL != "https://github.com" { + downloader.client, _ = github.NewEnterpriseClient(baseURL, baseURL, client) + } return &downloader } diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 955050107..3a1affbc2 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -65,7 +65,7 @@ func assertLabelEqual(t *testing.T, name, color, description string, label *base func TestGitHubDownloadRepo(t *testing.T) { GithubLimitRateRemaining = 3 //Wait at 3 remaining since we could have 3 CI in // - downloader := NewGithubDownloaderV3(context.Background(), "", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo") + downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo") err := downloader.RefreshRate() assert.NoError(t, err) diff --git a/modules/repofiles/delete.go b/modules/repofiles/delete.go index 2ffc75e7c..8343776c4 100644 --- a/modules/repofiles/delete.go +++ b/modules/repofiles/delete.go @@ -67,7 +67,7 @@ func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepo } } if protectedBranch.RequireSignedCommits { - _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) + _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) if err != nil { if !models.IsErrWontSign(err) { return nil, err diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index ec671a932..e0d6c9fcb 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -204,8 +204,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models "GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_EMAIL="+authorSig.Email, "GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339), - "GIT_COMMITTER_NAME="+committerSig.Name, - "GIT_COMMITTER_EMAIL="+committerSig.Email, "GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339), ) @@ -217,14 +215,32 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models // Determine if we should sign if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") + sign, keyID, signer, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") if sign { args = append(args, "-S"+keyID) + if t.repo.GetTrustModel() == models.CommitterTrustModel || t.repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { + // Add trailers + _, _ = messageBytes.WriteString("\n") + _, _ = messageBytes.WriteString("Co-Authored-By: ") + _, _ = messageBytes.WriteString(committerSig.String()) + _, _ = messageBytes.WriteString("\n") + _, _ = messageBytes.WriteString("Co-Committed-By: ") + _, _ = messageBytes.WriteString(committerSig.String()) + _, _ = messageBytes.WriteString("\n") + } + committerSig = signer + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } + env = append(env, + "GIT_COMMITTER_NAME="+committerSig.Name, + "GIT_COMMITTER_EMAIL="+committerSig.Email, + ) + stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) if err := git.NewCommand(args...).RunInDirTimeoutEnvFullPipeline(env, -1, t.basePath, stdout, stderr, messageBytes); err != nil { diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index dcb87ec92..f7fa5f028 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -161,7 +161,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up } } if protectedBranch.RequireSignedCommits { - _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) + _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) if err != nil { if !models.IsErrWontSign(err) { return nil, err diff --git a/modules/repository/create.go b/modules/repository/create.go index abbec05a3..c180b9b94 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -41,6 +41,7 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, Status: opts.Status, IsEmpty: !opts.AutoInit, + TrustModel: opts.TrustModel, } err = models.WithTx(func(ctx models.DBContext) error { diff --git a/modules/repository/generate.go b/modules/repository/generate.go index c5fb0af38..1314464a6 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -243,6 +243,7 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template IsEmpty: !opts.GitContent || templateRepo.IsEmpty, IsFsckEnabled: templateRepo.IsFsckEnabled, TemplateID: templateRepo.ID, + TrustModel: templateRepo.TrustModel, } if err = models.CreateRepository(ctx, doer, owner, generateRepo); err != nil { diff --git a/modules/repository/init.go b/modules/repository/init.go index c2038e18d..d066544a8 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -109,10 +109,10 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def "GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) + committerName := sig.Name + committerEmail := sig.Email if stdout, err := git.NewCommand("add", "--all"). SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)). @@ -132,14 +132,25 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def } if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := models.SignInitialCommit(tmpPath, u) + sign, keyID, signer, _ := models.SignInitialCommit(tmpPath, u) if sign { args = append(args, "-S"+keyID) + + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + // need to set the committer to the KeyID owner + committerName = signer.Name + committerEmail = signer.Email + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } + env = append(env, + "GIT_COMMITTER_NAME="+committerName, + "GIT_COMMITTER_EMAIL="+committerEmail, + ) + if stdout, err := git.NewCommand(args...). SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). RunInDirWithEnv(tmpPath, env); err != nil { diff --git a/modules/setting/repository.go b/modules/setting/repository.go index eb1501d7b..67dd80535 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -83,13 +83,14 @@ var ( } `ini:"repository.issue"` Signing struct { - SigningKey string - SigningName string - SigningEmail string - InitialCommit []string - CRUDActions []string `ini:"CRUD_ACTIONS"` - Merges []string - Wiki []string + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + DefaultTrustModel string } `ini:"repository.signing"` }{ DetectedCharsetsOrder: []string{ @@ -209,21 +210,23 @@ var ( // Signing settings Signing: struct { - SigningKey string - SigningName string - SigningEmail string - InitialCommit []string - CRUDActions []string `ini:"CRUD_ACTIONS"` - Merges []string - Wiki []string + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + DefaultTrustModel string }{ - SigningKey: "default", - SigningName: "", - SigningEmail: "", - InitialCommit: []string{"always"}, - CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, - Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, - Wiki: []string{"never"}, + SigningKey: "default", + SigningName: "", + SigningEmail: "", + InitialCommit: []string{"always"}, + CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, + Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, + Wiki: []string{"never"}, + DefaultTrustModel: "collaborator", }, } RepoRootPath string @@ -268,6 +271,13 @@ func newRepository() { log.Fatal("Failed to map Repository.PullRequest settings: %v", err) } + // Handle default trustmodel settings + Repository.Signing.DefaultTrustModel = strings.ToLower(strings.TrimSpace(Repository.Signing.DefaultTrustModel)) + if Repository.Signing.DefaultTrustModel == "default" { + Repository.Signing.DefaultTrustModel = "collaborator" + } + + // Handle preferred charset orders preferred := make([]string, 0, len(Repository.DetectedCharsetsOrder)) for _, charset := range Repository.DetectedCharsetsOrder { canonicalCharset := strings.ToLower(strings.TrimSpace(charset)) diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index b6c9774a9..8daa6de16 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -21,10 +21,11 @@ type NotificationThread struct { // NotificationSubject contains the notification subject (Issue/Pull/Commit) type NotificationSubject struct { - Title string `json:"title"` - URL string `json:"url"` - LatestCommentURL string `json:"latest_comment_url"` - Type string `json:"type" binding:"In(Issue,Pull,Commit)"` + Title string `json:"title"` + URL string `json:"url"` + LatestCommentURL string `json:"latest_comment_url"` + Type string `json:"type" binding:"In(Issue,Pull,Commit)"` + State StateType `json:"state"` } // NotificationCount number of unread notifications diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c57702b28..c86b19dfd 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -117,6 +117,9 @@ type CreateRepoOption struct { Readme string `json:"readme"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` + // TrustModel of the repository + // enum: default,collaborator,committer,collaboratorcommitter + TrustModel string `json:"trust_model"` } // EditRepoOption options when editing a repository's properties diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index dc7d22fe1..c8ff189aa 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -939,6 +939,8 @@ issues.new.no_assignees=Niemand zuständig issues.new.no_reviewers=Keine Reviewer issues.new.add_reviewer_title=Überprüfung anfordern issues.choose.get_started=Los geht's +issues.choose.blank=Standard +issues.choose.blank_about=Erstelle einen Issue aus dem Standardtemplate. issues.no_ref=Keine Branch/Tag angegeben issues.create=Issue erstellen issues.new_label=Neues Label @@ -1461,6 +1463,19 @@ settings.transfer_desc=Übertrage dieses Repository auf einen anderen Benutzer o settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. settings.transfer_form_title=Gib den Repository-Namen zur Bestätigung ein: +settings.signing_settings=Signaturüberprüfungseinstellungen +settings.trust_model=Signaturvertrauensmodell +settings.trust_model.default=Standardvertrauensmodell +settings.trust_model.default.desc=Verwende das Standardvertrauensmodell für diese Installation. +settings.trust_model.collaborator=Mitarbeiter +settings.trust_model.collaborator.long=Mitarbeiter: Vertraue Signaturen von Mitarbeitern +settings.trust_model.collaborator.desc=Gültige Signaturen von Mitarbeitern dieses Projekts werden als "vertrauenswürdig" markiert - ( egal ob sie mit dem Committer übereinstimmen oder nicht). Andernfalls werden gültige Signaturen als "nicht vertrauenswürdig" markiert, unabhängig ob die Signatur mit dem Committer übereinstimmt oder nicht. +settings.trust_model.committer=Committer +settings.trust_model.committer.long=Committer: Vertraue Signaturen, die zu Committern passen (Dies stimmt mit GitHub überein und zwingt signierte Commits von Gitea dazu, Gitea als Committer zu haben) +settings.trust_model.committer.desc=Gültige Signaturen werden nur dann als "vertrauenswürdig" markiert, wenn sie mit dem Committer übereinstimmen, andernfalls werden sie als "nicht übereinstimmend" markiert. Dies zwingt Gitea dazu, der Committer bei signierten Commits zu sein, wobei der eigentliche Committer als Co-Authored-By: und Co-Committed-By: Trailer im Commit markiert ist. Der Standard-Gitea-Schlüssel muss mit einem Benutzer in der Datenbank übereinstimmen. +settings.trust_model.collaboratorcommitter=Mitarbeiter+Committer +settings.trust_model.collaboratorcommitter.long=Mitarbeiter+Committer: Signaturen der Mitarbeiter vertrauen die mit dem Committer übereinstimmen +settings.trust_model.collaboratorcommitter.desc=Gültige Signaturen von Mitarbeitern dieses Projekts werden als "vertrauenswürdig" markiert, wenn sie mit dem Committer übereinstimmen. Andernfalls werden gültige Signaturen als "nicht vertrauenswürdig" markiert, wenn die Signatur mit dem Committer übereinstimmt als "nicht übereinstimmend". Dies zwingt Gitea als Committer bei signierten Commits mit dem tatsächlichen Committer als Co-Authored-By: und Co-Committed-By: Trailer im Commit. Der Standard-Gitea-Schlüssel muss mit einem Benutzer in der Datenbank übereinstimmen settings.wiki_delete=Wiki-Daten löschen settings.wiki_delete_desc=Das Löschen von Wiki-Daten kann nicht rückgängig gemacht werden. Bitte sei vorsichtig. settings.wiki_delete_notices_1=– Dies löscht und deaktiviert das Wiki für %s. @@ -2103,7 +2118,9 @@ auths.admin_filter=Admin-Filter auths.restricted_filter=Eingeschränkte Filter auths.restricted_filter_helper=Leer lassen, um keine Benutzer als eingeschränkt festzulegen. Verwende einen Stern ('*'), um alle Benutzer, die nicht dem Admin-Filter entsprechen, als eingeschränkt zu setzen. auths.verify_group_membership=Gruppenmitgliedschaft in LDAP überprüfen +auths.group_search_base=Gruppensuche Basisdomainname auths.valid_groups_filter=Gültiger Gruppenfilter +auths.group_attribute_list_users=Gruppenattribut, welches die die Benutzerliste enthält auths.user_attribute_in_group=Benutzerattribut in der Gruppenliste auths.ms_ad_sa=MS-AD-Suchattribute auths.smtp_auth=SMTP-Authentifizierungstyp diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 376a63125..ea76d4cb6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1464,6 +1464,19 @@ settings.transfer_desc = Transfer this repository to a user or to an organizatio settings.transfer_notices_1 = - You will lose access to the repository if you transfer it to an individual user. settings.transfer_notices_2 = - You will keep access to the repository if you transfer it to an organization that you (co-)own. settings.transfer_form_title = Enter the repository name as confirmation: +settings.signing_settings = Signing Verification Settings +settings.trust_model = Signature Trust Model +settings.trust_model.default = Default Trust Model +settings.trust_model.default.desc= Use the default repository trust model for this installation. +settings.trust_model.collaborator = Collaborator +settings.trust_model.collaborator.long = Collaborator: Trust signatures by collaborators +settings.trust_model.collaborator.desc = Valid signatures by collaborators of this repository will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not. +settings.trust_model.committer = Committer +settings.trust_model.committer.long = Committer: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer) +settings.trust_model.committer.desc = Valid signatures will only be marked "trusted" if they match the committer, otherwise they will be marked "unmatched". This will force Gitea to be the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database. +settings.trust_model.collaboratorcommitter = Collaborator+Committer +settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer +settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database, settings.wiki_delete = Delete Wiki Data settings.wiki_delete_desc = Deleting repository wiki data is permanent and cannot be undone. settings.wiki_delete_notices_1 = - This will permanently delete and disable the repository wiki for %s. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index db6aca023..f939fed0c 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -10,7 +10,7 @@ link_account=Bağlantı hesabı register=Üye Ol website=Web sitesi version=Sürüm -powered_by=%s tarafından desteklenmektedir +powered_by=%s tarafından desteklenen page=Sayfa template=Şablon language=Dil @@ -771,7 +771,7 @@ branch=Dal tree=Ağaç clear_ref='Geçerli referansı temizle' filter_branch_and_tag=Dal veya biçim imini filtrele -branches=Dallar +branches=Dal tags=Biçim İmleri issues=Konular pulls=Değişiklik İstekleri @@ -858,7 +858,7 @@ editor.user_no_push_to_branch=Kullanıcı dala gönderemez editor.require_signed_commit=Dal imzalı bir işleme gerektirir commits.desc=Kaynak kodu değişiklik geçmişine göz atın. -commits.commits=İşlemeler +commits.commits=İşleme commits.no_commits=Ortak bir işleme yok. '%s' ve '%s' tamamen farklı geçmişlere sahip. commits.search=İşlemeleri ara… commits.search.tooltip=Anahtar kelimeleri "yazar:", "yorumcu:", "sonra:" veya "önce:", örneğin; "eski haline yazan: Alice önce: 2019-04-01" önekleyebilirsiniz. @@ -955,18 +955,18 @@ issues.label_templates.fail_to_load_file=Etiket şablon dosyası yüklemesi baş issues.add_label_at=
%s
%s etiketini ekledi issues.remove_label_at=
%s
%s etiketini kaldırdı issues.add_milestone_at=`%[2]s %[1]s kilometre taşına ekledi` -issues.add_project_at=`bunu %s %s projesine ekledi` +issues.add_project_at=`bunu %s projesine %s ekledi` issues.change_milestone_at=`%s kilometre taşını %s iken %s olarak değiştirdi` issues.change_project_at=`%s %s olan projeyi %s olarak değiştirdi issues.remove_milestone_at=`%[2]s %[1]s kilometre taşından kaldırdı` -issues.remove_project_at=`bunu %s %s projesinden kaldırdı` +issues.remove_project_at=`bunu %s projesinden %s kaldırdı` issues.deleted_milestone=`(silindi)` issues.deleted_project=`(silindi)` issues.self_assign_at=`%s kendini atadı` issues.add_assignee_at=`%[2]s %[1]s tarafından atandı` issues.remove_assignee_at=`ataması %[2]s %[1]s tarafından kaldırıldı` issues.remove_self_assignment=`atamalarını kaldırdı %s` -issues.change_title_at=`%s başlığı %s iken %s olarak değiştirdi` +issues.change_title_at=`başlığı %s iken %s olarak %s değiştirdi` issues.delete_branch_at=`%s dalı silindi %s` issues.open_tab=%d açık issues.close_tab=%d kapanmış @@ -1189,7 +1189,7 @@ pulls.title_desc=%[2]s içindeki %[1]d işlemeyi 被盜密碼清單中,該清單因公共資料外洩而暴露。請試試其它密碼。 password_pwned_err=無法完成對 HaveIBeenPwned 的請求。 [mail] @@ -379,7 +381,7 @@ target_branch_not_exist=目標分支不存在 change_avatar=更改大頭貼... join_on=加入於 repositories=儲存庫列表 -activity=公開活動 +activity=公開動態 followers=追蹤者 starred=已加星號 projects=專案 @@ -388,6 +390,7 @@ follow=追蹤 unfollow=取消追蹤 heatmap.loading=正在載入熱點圖... user_bio=個人簡介 +disabled_public_activity=這個使用者已對外隱藏動態 form.name_reserved=帳號「%s」是被保留的。 form.name_pattern_not_allowed=帳號不可包含字元「%s」。 @@ -428,8 +431,8 @@ cancel=取消操作 language=語言 ui=佈景主題 privacy=隱私 -keep_activity_private=在個人資料頁面隱藏最近活動 -keep_activity_private_popup=讓最近的活動只有你和管理員看得到 +keep_activity_private=在個人資料頁面隱藏動態 +keep_activity_private_popup=讓動態只有你和管理員看得到 lookup_avatar_by_mail=以電子信箱查詢大頭貼 federated_avatar_lookup=Federated Avatar 查詢 @@ -490,8 +493,11 @@ ssh_helper=需要協助嗎?建議可看看 GitHub 的文件 gpg_helper=需要協助嗎?建議可看看 GitHub 的 about GPG 文件。 add_new_key=增加 SSH 金鑰 add_new_gpg_key=新增 GPG 金鑰 +key_content_ssh_placeholder=以 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521' 開頭 +key_content_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 開頭 ssh_key_been_used=此 SSH 金鑰已添加到伺服器。 ssh_key_name_used=早已有相同名稱的 SSH 金鑰存在於你的帳戶。 +gpg_key_id_used=已存在具有相同 ID 的 GPG 金鑰。 gpg_no_key_email_found=此 GPG 金鑰不適用於您的任何電子信箱。 subkeys=次金鑰 key_id=金鑰 ID @@ -509,7 +515,7 @@ add_on=增加於 valid_until=有效期至 valid_forever=永遠有效 last_used=上次使用在 -no_activity=沒有最近活動 +no_activity=沒有近期動態 can_read_info=讀取 can_write_info=寫入 key_state_desc=該金鑰在 7 天內被使用過 @@ -638,6 +644,8 @@ generate_from=產生自 repo_desc=儲存庫描述 repo_lang=儲存庫語言 repo_gitignore_helper=選擇 .gitignore 範本 +issue_labels=問題標籤 +issue_labels_helper=選擇一個問題標籤集 license=授權條款 license_helper=請選擇授權條款檔案 readme=讀我 @@ -668,6 +676,7 @@ template.git_hooks=Git Hook template.webhooks=Webhook template.topics=主題 template.avatar=大頭貼 +template.issue_labels=問題標籤 archive.title=此存儲庫已封存。您可以查看檔案及 Clone 此存儲庫,但不能推送、建立問題及發出合併請求。 @@ -753,6 +762,8 @@ video_not_supported_in_browser=您的瀏覽器不支援使用 HTML5 播放影片 audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 'audio' 標籤 stored_lfs=儲存到到 Git LFS commit_graph=提交線圖 +commit_graph.monochrome=單色 +commit_graph.color=彩色 blame=Blame normal_view=標準檢視 line=行 @@ -791,6 +802,7 @@ editor.add_subdir=加入目錄 editor.unable_to_upload_files=上傳檔案失敗到 '%s', 錯誤訊息: %v editor.upload_file_is_locked=檔案「%s」已被 %s 鎖定 editor.upload_files_to_dir=上傳檔案到 '%s' +editor.cannot_commit_to_protected_branch=無法提交到受保護的分支「%s」。 commits.commits=次程式碼提交 commits.search=搜尋提交歷史... @@ -805,6 +817,7 @@ commits.signed_by=簽署人 commits.gpg_key_id=GPG 金鑰 ID ext_issues=外部問題 +ext_issues.desc=連結到外部問題追蹤器。 projects=專案 projects.desc=在專案看板中管理問題和 pull。 @@ -982,6 +995,7 @@ issues.tracker=時間追蹤 issues.start_tracking_short=開始 issues.start_tracking=開始時間追蹤 issues.start_tracking_history=`開始工作 %s` +issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器 issues.tracking_already_started=`您已經開始時間追蹤這個 問題!` issues.stop_tracking=停止 issues.stop_tracking_history=`結束工作 %s` @@ -1086,12 +1100,13 @@ wiki.file_revision=頁面修訂記錄 wiki.wiki_page_revisions=Wiki 頁面修訂記錄 wiki.back_to_wiki=回到 Wiki 頁面 wiki.delete_page_button=刪除頁面 +wiki.delete_page_notice_1=刪除 Wiki 頁面「%s」將不可還原。是否繼續? wiki.page_already_exists=相同名稱的 Wiki 頁面已經存在。 wiki.reserved_page=Wiki 頁面名稱 "%s" 是被保留的。 wiki.pages=所有頁面 wiki.last_updated=最後更新於 %s -activity=活動 +activity=動態 activity.period.filter_label=期間: activity.period.daily=1 天 activity.period.halfweekly=3 天 @@ -1130,6 +1145,7 @@ activity.title.releases_1=%d 版本發佈 activity.title.releases_n=%d 版本發佈 activity.title.releases_published_by=%s 由 %s 發佈 activity.published_release_label=已發佈 +activity.no_git_activity=期間內沒有任何提交動態 activity.git_stats_and_deletions=和 search=搜尋 @@ -1145,7 +1161,7 @@ settings.collaboration.write=可寫權限 settings.collaboration.read=可讀權限 settings.collaboration.owner=擁有者 settings.collaboration.undefined=未定義 -settings.hooks=管理 Webhooks +settings.hooks=Webhook settings.githooks=管理 Git Hooks settings.basic_settings=基本設定 settings.mirror_settings=鏡像設定 @@ -1164,31 +1180,44 @@ settings.use_external_wiki=使用外部 Wiki settings.external_wiki_url=外部 Wiki 連結 settings.external_wiki_url_error=外部 Wiki 網址不是有效的網址。 settings.external_wiki_url_desc=點擊問題標籤時,使用者會被導向到外部 Wiki URL。 -settings.external_tracker_url=外部 Issue 追蹤網址 -settings.tracker_url_format=外部問題管理系統的 URL 格式 +settings.issues_desc=啟用儲存庫問題追蹤器 +settings.use_internal_issue_tracker=使用內建問題追蹤器 +settings.use_external_issue_tracker=使用外部問題追蹤器 +settings.external_tracker_url=外部問題追蹤器 URL +settings.external_tracker_url_error=該外部問題追蹤器 URL 無效。 +settings.external_tracker_url_desc=點擊問題頁籤時,使用者會被導向至外部問題追蹤器 URL。 +settings.tracker_url_format=外部問題追蹤器的 URL 格式 +settings.tracker_url_format_error=該外部問題追蹤器 URL 格式無效。 +settings.tracker_issue_style=外部問題追蹤器的編號格式 settings.tracker_issue_style.numeric=數字 settings.tracker_issue_style.alphanumeric=字母及數字 +settings.tracker_url_format_desc=使用占位符 {user}, {repo}{index} 代表帳號、儲存庫名稱和問題編號。 settings.enable_timetracker=啟用時間追蹤 +settings.allow_only_contributors_to_track_time=只讓貢獻者追蹤時間 settings.projects_desc=啟用儲存庫專案 settings.admin_settings=管理員設定 settings.danger_zone=危險操作區 settings.new_owner_has_same_repo=新的儲存庫擁有者已經存在同名儲存庫! settings.convert=轉換為普通儲存庫 -settings.convert_desc=您可以將此鏡像轉成普通儲存庫。此動作不可恢復。 +settings.convert_desc=您可以將此鏡像轉成普通儲存庫。此動作不可還原。 +settings.convert_notices_1=此操作會將此鏡像轉換成普通儲存庫且不可還原。 settings.convert_confirm=轉換儲存庫 settings.convert_succeed=鏡像儲存庫已成功轉換為一般儲存庫。 settings.convert_fork=轉換成普通儲存庫 settings.convert_fork_desc=您可以將此 fork 轉換成普通儲存庫。此動作不可還原。 -settings.convert_fork_notices_1=此操作會將此 fork 轉換成普通儲存庫。此動作不可還原。 +settings.convert_fork_notices_1=此操作會將此 fork 轉換成普通儲存庫且不可還原。 settings.convert_fork_confirm=轉換儲存庫 settings.convert_fork_succeed=此 fork 已轉換成普通儲存庫。 settings.transfer=轉移儲存庫所有權 +settings.transfer_desc=將此儲存庫轉移給其它使用者或受您管理的組織。 settings.transfer_form_title=輸入儲存庫名稱以確認: settings.wiki_delete=刪除 Wiki 資料 +settings.wiki_delete_desc=刪除儲存庫 Wiki 資料是永久的且不可還原。 settings.confirm_wiki_delete=刪除 Wiki 資料 settings.wiki_deletion_success=已刪除儲存庫的 Wiki 資料。 settings.delete=刪除本儲存庫 -settings.delete_notices_1=- 此操作 不可以 被回滾。 +settings.delete_desc=刪除儲存庫是永久的且不可還原。 +settings.delete_notices_1=- 此操作不可還原。 settings.delete_notices_fork_1=- 在此儲存庫刪除後,它的 fork 將會變成獨立儲存庫。 settings.deletion_success=這個儲存庫已被刪除。 settings.update_settings_success=已更新儲存庫的設定。 @@ -1206,6 +1235,7 @@ settings.teams=團隊 settings.add_team=增加團隊 settings.search_team=搜尋團隊... settings.add_webhook=建立 Webhook +settings.hooks_desc=當觸發某些 Gitea 事件時,Webhook 會自動發出 HTTP POST 請求到指定的伺服器。在Webhook 指南閱讀更多內容。 settings.webhook_deletion=刪除 Webhook settings.webhook_deletion_success=Webhook 已刪除。 settings.webhook.test_delivery=測試推送 @@ -1255,8 +1285,9 @@ settings.slack_token=Token settings.slack_domain=域名 settings.slack_channel=頻道 settings.add_msteams_hook_desc=將 Microsoft Teams 整合到你的儲存庫。 -settings.deploy_keys=管理部署金鑰 +settings.deploy_keys=部署金鑰 settings.add_deploy_key=新增部署金鑰 +settings.no_deploy_keys=沒有任何部屬金鑰。 settings.title=標題 settings.deploy_key_content=金鑰文本 settings.deploy_key_deletion=刪除部署金鑰 @@ -1265,14 +1296,29 @@ settings.protected_branch=分支保護 settings.protected_branch_can_push=允許推送? settings.protected_branch_can_push_yes=你可以推送 settings.protected_branch_can_push_no=你不能推送 +settings.branch_protection=%s 的分支保護 settings.protect_this_branch=啟用分支保護 +settings.protect_this_branch_desc=防止刪除分支,並限制 Git 推送與合併到分支。 +settings.protect_disable_push=停用推送 +settings.protect_disable_push_desc=不允許推送到此分支。 +settings.protect_enable_push=啟用推送 +settings.protect_enable_push_desc=任何擁有寫入權限的使用者將可推送至該分支(但不可使用force push)。 +settings.protect_whitelist_committers=使用白名單限制推送 +settings.protect_whitelist_committers_desc=僅允許白名單內的使用者或團隊推送至該分支(但不可使用force push)。 +settings.protect_whitelist_users=允許推送的使用者: settings.protect_whitelist_search_users=搜尋使用者... +settings.protect_whitelist_teams=允許推送的團隊: settings.protect_whitelist_search_teams=搜尋團隊... +settings.protect_merge_whitelist_committers=啟動合併白清單 +settings.protect_merge_whitelist_users=允許合併的使用者: +settings.protect_merge_whitelist_teams=允許合併的團隊: settings.add_protected_branch=啟用保護 settings.delete_protected_branch=停用保護 settings.update_protect_branch_success='%s' 的分支保護已被更新 settings.remove_protected_branch_success='%s' 的分支保護已被停用 settings.protected_branch_deletion=停用分支保護 +settings.protected_branch_deletion_desc=停用分支保護將允許有寫入權限的使用者推送至該分支,是否繼續? +settings.default_branch_desc=請選擇一個用來提交程式碼和合併請求的預設分支。 settings.choose_branch=選擇一個分支... settings.no_protected_branch=沒有受保護的分支。 settings.edit_protected_branch=編輯 @@ -1281,6 +1327,7 @@ settings.matrix.access_token=Access Token settings.matrix.message_type=訊息類型 settings.archive.button=封存儲存庫 settings.archive.header=封存本儲存庫 +settings.archive.text=封存此儲存庫會將它完全進入唯讀狀態。它將自資訊主頁隱藏、無法進行推送且不能建立問題及合併請求。 settings.archive.success=此儲存庫已被封存 settings.unarchive.button=解除封存儲存庫 settings.unarchive.header=解除封存本儲存庫 @@ -1298,8 +1345,12 @@ diff.browse_source=瀏覽代碼 diff.parent=父節點 diff.commit=當前提交 diff.data_not_available=沒有內容比較可以使用 +diff.options_button=Diff 選項 +diff.show_diff_stats=顯示統計資料 +diff.download_patch=下載 Patch 檔 +diff.download_diff=下載 Diff 檔 diff.show_split_view=分割檢視 -diff.show_unified_view=統一視圖 +diff.show_unified_view=統一檢視 diff.stats_desc=共有 %d 個檔案被更改,包括 %d 行新增%d 行删除 diff.bin=二進制 diff.view_file=查看文件 @@ -1339,7 +1390,7 @@ branch.already_exists=分支名稱 ”%s“ 已經存在 branch.delete_head=刪除 branch.delete=刪除分支 '%s' branch.delete_html=刪除分支 -branch.delete_desc=刪除分支是永久的。 此動作無法復原,繼續? +branch.delete_desc=刪除分支是永久的。 此動作不可還原,是否繼續? branch.deletion_success=分支 '%s' 已被刪除。 branch.deletion_failed=刪除分支 '%s' 失敗。 branch.create_branch=建立分支 %s @@ -1350,6 +1401,8 @@ branch.deleted_by=刪除人: %s branch.restore_failed=還原分支 %s 失敗 branch.protected_deletion_failed=分支 '%s' 已被保護,不能刪除。 branch.download=下載分支 '%s' +branch.included_desc=此分支是預設分支的一部分 +branch.included=包含 topic.manage_topics=管理主題 topic.done=完成 @@ -1400,6 +1453,7 @@ settings.change_orgname_prompt=注意:修改組織名稱將會同時修改對 settings.update_avatar_success=已更新組織的大頭貼。 settings.delete=刪除組織 settings.delete_account=刪除這個組織 +settings.delete_prompt=該組織將被永久刪除。此動作不可還原! settings.confirm_delete_account=確認刪除組織 settings.delete_org_title=刪除組織 settings.hooks_desc=新增 webhooks 將觸發在這個組織下 全部的儲存庫 。 @@ -1480,7 +1534,7 @@ total=總計:%d dashboard.statistic=摘要 dashboard.operations=維護操作 dashboard.system_status=系統狀態 -dashboard.statistic_info=Gitea 資料庫統計:%d 位使用者,%d 個組織,%d 個公鑰,%d 個儲存庫,%d 個儲存庫關注,%d 個星號,%d 次行為,%d 條權限記錄,%d 個問題,%d 次評論,%d 個社群帳戶,%d 個用戶關注,%d 個鏡像,%d 個版本發佈,%d 個登錄源,%d 個 Webhook ,%d 個里程碑,%d 個標籤,%d 個 Hook 任務,%d 個團隊,%d 個更新任務,%d 個附件。 +dashboard.statistic_info=Gitea 資料庫統計:%d 位使用者,%d 個組織,%d 個公鑰,%d 個儲存庫,%d 個儲存庫關注,%d 個星號,%d 次行為,%d 條權限記錄,%d 個問題,%d 次評論,%d 個社群帳戶,%d 個用戶關注,%d 個鏡像,%d 個版本發佈,%d 個認證來源,%d 個 Webhook ,%d 個里程碑,%d 個標籤,%d 個 Hook 任務,%d 個團隊,%d 個更新任務,%d 個附件。 dashboard.operation_name=操作名稱 dashboard.operation_switch=開關 dashboard.operation_run=執行 @@ -1539,8 +1593,9 @@ users.never_login=從未登入 users.send_register_notify=寄送使用者註冊通知 users.new_success=已建立新帳戶 '%s'。 users.edit=編輯 -users.auth_source=認證源 +users.auth_source=認證來源 users.local=本地 +users.auth_login_name=認證登入名稱 users.password_helper=密碼留空則不修改。 users.update_profile_success=已更新使用者帳戶。 users.edit_account=編輯使用者帳戶 @@ -1585,11 +1640,14 @@ repos.forks=Fork 數 repos.issues=問題數 repos.size=大小 +hooks.desc=當觸發某些 Gitea 事件時,Webhook 會自動發出 HTTP POST 請求到指定的伺服器。這裡所定義的 Webhook 是預設的,並且會複製到所有新儲存庫。在Webhook 指南閱讀更多內容。 hooks.add_webhook=新增預設 Webhook hooks.update_webhook=更新預設 Webhook +systemhooks.desc=當觸發某些 Gitea 事件時,Webhook 會自動發出 HTTP POST 請求到指定的伺服器。由於這裡所定義的 Webhook 會影響此系統上的所有儲存庫,因此請評估這會對效能造成多少影響。在Webhook 指南閱讀更多內容。 systemhooks.add_webhook=新增系統 Webhook +auths.auth_manage_panel=認證來源管理 auths.new=新增認證來源 auths.name=認證名稱 auths.type=認證類型 @@ -1601,7 +1659,7 @@ auths.auth_name=認證名稱 auths.security_protocol=安全協定 auths.domain=域名 auths.host=主機地址 -auths.port=主機端口 +auths.port=連接埠 auths.bind_dn=Bind DN auths.bind_password=Bind 密碼 auths.bind_password_helper=警告:此密碼以明文存儲。 請儘可能使用唯讀帳戶。 @@ -1620,7 +1678,7 @@ auths.restricted_filter_helper=留白則不限制任何使用者。使用米字 auths.ms_ad_sa=MS AD 搜尋屬性 auths.smtp_auth=SMTP 驗證類型 auths.smtphost=SMTP 主機地址 -auths.smtpport=SMTP 主機端口 +auths.smtpport=SMTP 連接埠 auths.allowed_domains=域名白名單 auths.enable_tls=啟用 TLS 加密 auths.skip_tls_verify=忽略 TLS 驗證 @@ -1647,12 +1705,17 @@ auths.tip.github=在 https://github.com/settings/applications/new 註冊一個 auths.tip.gitlab=在 https://gitlab.com/profile/applications 註冊一個新的應用程式 auths.tip.openid_connect=使用 OpenID 連接探索 URL (/.well-known/openid-configuration) 來指定節點 auths.edit=修改認證來源 -auths.activated=該授權來源已啟用 +auths.activated=該認證來源已啟用 auths.new_success=已增加認證'%s'。 -auths.update_success=認證來源已更新。 -auths.update=更新驗證來源 -auths.delete=刪除驗證來源 +auths.update_success=已更新認證來源。 +auths.update=更新認證來源 +auths.delete=刪除認證來源 auths.delete_auth_title=刪除認證來源 +auths.delete_auth_desc=刪除認證來源將會拒絕使用它登入的使用者。是否繼續? +auths.still_in_used=此認證來源正在使用中。請先轉換或刪除使用此授權來源的使用者。 +auths.deletion_success=已刪除認證來源。 +auths.login_source_exist=認證來源「%s」已經存在。 +auths.login_source_of_type_exist=已經有相同類型的認證來源。 config.server_config=伺服器組態 config.app_name=網站標題 @@ -1711,6 +1774,7 @@ config.default_keep_email_private=預設隱藏電子郵件地址 config.default_allow_create_organization=預設允許新增組織 config.enable_timetracking=啟用時間追蹤 config.default_enable_timetracking=預設啟用時間追蹤 +config.default_allow_only_contributors_to_track_time=只讓貢獻者追蹤時間 config.no_reply_address=隱藏電子郵件域名 config.default_visibility_organization=新組織的預設瀏覽權限 config.default_enable_dependencies=預設啟用問題相依 diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 0b295f389..3c3391bfe 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -10,11 +10,10 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" ) // SigningKey returns the public key of the default signing key if it exists -func SigningKey(ctx *context.Context) { +func SigningKey(ctx *context.APIContext) { // swagger:operation GET /signing-key.gpg miscellaneous getSigningKey // --- // summary: Get default signing-key.gpg @@ -55,12 +54,11 @@ func SigningKey(ctx *context.Context) { content, err := models.PublicSigningKey(path) if err != nil { - ctx.ServerError("gpg export", err) + ctx.Error(http.StatusInternalServerError, "gpg export", err) return } _, err = ctx.Write([]byte(content)) if err != nil { - log.Error("Error writing key content %v", err) - ctx.Error(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %v", err)) } } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index b4a8a5ac3..9a8a4fa29 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -201,7 +201,7 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) { label.Description = *form.Description } if err := models.UpdateLabel(label); err != nil { - ctx.ServerError("UpdateLabel", err) + ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } ctx.JSON(http.StatusOK, convert.ToLabel(label)) diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 7577700ab..987f14107 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -85,7 +85,7 @@ func ListUserOrgs(ctx *context.APIContext) { if ctx.Written() { return } - listUserOrgs(ctx, u, ctx.User.IsAdmin) + listUserOrgs(ctx, u, ctx.User != nil && (ctx.User.IsAdmin || ctx.User.ID == u.ID)) } // GetAll return list of all public organizations diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 220bb0fd7..8c877285a 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -63,7 +63,7 @@ func GetSingleCommit(ctx *context.APIContext) { func getCommit(ctx *context.APIContext, identifier string) { gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath()) if err != nil { - ctx.ServerError("OpenRepository", err) + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } defer gitRepo.Close() @@ -75,7 +75,7 @@ func getCommit(ctx *context.APIContext, identifier string) { json, err := convert.ToCommit(ctx.Repo.Repository, commit, nil) if err != nil { - ctx.ServerError("toCommit", err) + ctx.Error(http.StatusInternalServerError, "toCommit", err) return } ctx.JSON(http.StatusOK, json) @@ -129,7 +129,7 @@ func GetAllCommits(ctx *context.APIContext) { gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath()) if err != nil { - ctx.ServerError("OpenRepository", err) + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } defer gitRepo.Close() @@ -150,20 +150,20 @@ func GetAllCommits(ctx *context.APIContext) { // no sha supplied - use default branch head, err := gitRepo.GetHEADBranch() if err != nil { - ctx.ServerError("GetHEADBranch", err) + ctx.Error(http.StatusInternalServerError, "GetHEADBranch", err) return } baseCommit, err = gitRepo.GetBranchCommit(head.Name) if err != nil { - ctx.ServerError("GetCommit", err) + ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } } else { // get commit specified by sha baseCommit, err = gitRepo.GetCommit(sha) if err != nil { - ctx.ServerError("GetCommit", err) + ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } } @@ -171,7 +171,7 @@ func GetAllCommits(ctx *context.APIContext) { // Total commit count commitsCountTotal, err := baseCommit.CommitsCount() if err != nil { - ctx.ServerError("GetCommitsCount", err) + ctx.Error(http.StatusInternalServerError, "GetCommitsCount", err) return } @@ -180,7 +180,7 @@ func GetAllCommits(ctx *context.APIContext) { // Query commits commits, err := baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize) if err != nil { - ctx.ServerError("CommitsByRange", err) + ctx.Error(http.StatusInternalServerError, "CommitsByRange", err) return } @@ -195,7 +195,7 @@ func GetAllCommits(ctx *context.APIContext) { // Create json struct apiCommits[i], err = convert.ToCommit(ctx.Repo.Repository, commit, userCache) if err != nil { - ctx.ServerError("toCommit", err) + ctx.Error(http.StatusInternalServerError, "toCommit", err) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 0dd961d7f..e6f5f5e63 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -109,7 +109,7 @@ func CreateFork(ctx *context.APIContext, form api.CreateForkOption) { } isMember, err := org.IsOrgMember(ctx.User.ID) if err != nil { - ctx.ServerError("IsOrgMember", err) + ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) return } else if !isMember { ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index a293801c4..fef6ebf07 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -222,7 +222,7 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) { label.Description = *form.Description } if err := models.UpdateLabel(label); err != nil { - ctx.ServerError("UpdateLabel", err) + ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } ctx.JSON(http.StatusOK, convert.ToLabel(label)) diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 5a250dbd8..db86d0868 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -215,7 +215,7 @@ func EditMilestone(ctx *context.APIContext, form api.EditMilestoneOption) { } if err := models.UpdateMilestone(milestone, oldIsClosed); err != nil { - ctx.ServerError("UpdateMilestone", err) + ctx.Error(http.StatusInternalServerError, "UpdateMilestone", err) return } ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone)) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 126815839..afbe7ddab 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -725,7 +725,7 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) { } if err = pr.LoadHeadRepo(); err != nil { - ctx.ServerError("LoadHeadRepo", err) + ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) return } @@ -885,7 +885,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) if models.IsErrUserNotExist(err) { ctx.NotFound("GetUserByName") } else { - ctx.ServerError("GetUserByName", err) + ctx.Error(http.StatusInternalServerError, "GetUserByName", err) } return nil, nil, nil, nil, "", "" } @@ -929,7 +929,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User) if err != nil { headGitRepo.Close() - ctx.ServerError("GetUserRepoPermission", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(models.UnitTypeCode) { @@ -948,7 +948,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) permHead, err := models.GetUserRepoPermission(headRepo, ctx.User) if err != nil { headGitRepo.Close() - ctx.ServerError("GetUserRepoPermission", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } if !permHead.CanRead(models.UnitTypeCode) { diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 3f2cb011d..86c084acd 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -318,14 +318,14 @@ func CreatePullReview(ctx *context.APIContext, opts api.CreatePullReviewOptions) if opts.CommitID == "" { gitRepo, err := git.OpenRepository(pr.Issue.Repo.RepoPath()) if err != nil { - ctx.ServerError("git.OpenRepository", err) + ctx.Error(http.StatusInternalServerError, "git.OpenRepository", err) return } defer gitRepo.Close() headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - ctx.ServerError("GetRefCommitID", err) + ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err) return } @@ -350,7 +350,7 @@ func CreatePullReview(ctx *context.APIContext, opts api.CreatePullReviewOptions) 0, // no reply opts.CommitID, ); err != nil { - ctx.ServerError("CreateCodeComment", err) + ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err) return } } diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 752b5c76e..e7cc1e867 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -151,7 +151,7 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) { rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName) if err != nil { if !models.IsErrReleaseNotExist(err) { - ctx.ServerError("GetRelease", err) + ctx.Error(http.StatusInternalServerError, "GetRelease", err) return } // If target is not provided use default branch @@ -195,7 +195,7 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) { rel.Publisher = ctx.User if err = releaseservice.UpdateReleaseOrCreatReleaseFromTag(ctx.User, ctx.Repo.GitRepo, rel, nil, true); err != nil { - ctx.ServerError("UpdateReleaseOrCreatReleaseFromTag", err) + ctx.Error(http.StatusInternalServerError, "UpdateReleaseOrCreatReleaseFromTag", err) return } } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 603187c16..b8a24e253 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -244,6 +244,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR IsPrivate: opt.Private, AutoInit: opt.AutoInit, DefaultBranch: opt.DefaultBranch, + TrustModel: models.ToTrustModel(opt.TrustModel), }) if err != nil { if models.IsErrRepoAlreadyExist(err) { @@ -372,7 +373,7 @@ func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) { if !ctx.User.IsAdmin { canCreate, err := org.CanCreateOrgRepo(ctx.User.ID) if err != nil { - ctx.ServerError("CanCreateOrgRepo", err) + ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err) return } else if !canCreate { ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 7c4f2cea9..be46ddbeb 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1259,7 +1259,7 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["WillSign"] = false if ctx.User != nil { - sign, key, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) + sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) ctx.Data["WillSign"] = sign ctx.Data["SigningKey"] = key if err != nil { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index d12640dd6..4a088ff9c 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -238,6 +238,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { IsPrivate: form.Private || setting.Repository.ForcePrivate, DefaultBranch: form.DefaultBranch, AutoInit: form.AutoInit, + TrustModel: models.ToTrustModel(form.TrustModel), }) if err == nil { log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 8d07bf09a..d2c20fb03 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -51,6 +51,11 @@ func Settings(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate + + signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath()) + ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningSettings"] = setting.Repository.Signing + ctx.HTML(200, tplSettingsOptions) } @@ -318,6 +323,26 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "signing": + changed := false + + trustModel := models.ToTrustModel(form.TrustModel) + if trustModel != repo.TrustModel { + repo.TrustModel = trustModel + changed = true + } + + if changed { + if err := models.UpdateRepository(repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + } + log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "admin": if !ctx.User.IsAdmin { ctx.Error(403) diff --git a/services/pull/merge.go b/services/pull/merge.go index b430a9080..7a1964932 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -209,18 +209,23 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge outbuf.Reset() errbuf.Reset() + sig := doer.NewGitSig() + committer := sig + // Determine if we should sign signArg := "" if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) + sign, keyID, signer, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) if sign { signArg = "-S" + keyID + if pr.BaseRepo.GetTrustModel() == models.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { signArg = "--no-gpg-sign" } } - sig := doer.NewGitSig() commitTimeStr := time.Now().Format(time.RFC3339) // Because this may call hooks we should pass in the environment @@ -228,8 +233,8 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge "GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_NAME="+committer.Name, + "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) @@ -346,6 +351,10 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } else { + if committer != sig { + // add trailer + message += fmt.Sprintf("\nCo-Authored-By: %s\nCo-Committed-By: %s\n", sig.String(), sig.String()) + } if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) @@ -526,7 +535,7 @@ func IsSignedIfRequired(pr *models.PullRequest, doer *models.User) (bool, error) return true, nil } - sign, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) + sign, _, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) return sign, err } diff --git a/services/repository/push.go b/services/repository/push.go index 05871dea5..91a9d23b7 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/cache" @@ -291,6 +292,10 @@ func commitRepoAction(repo *models.Repository, gitRepo *git.Repository, optsList } } } + // Update the is empty and default_branch columns + if err := models.UpdateRepositoryCols(repo, "default_branch", "is_empty"); err != nil { + return fmt.Errorf("UpdateRepositoryCols: %v", err) + } } opType := models.ActionCommitRepo @@ -359,9 +364,9 @@ func commitRepoAction(repo *models.Repository, gitRepo *git.Repository, optsList } } - // Change repository empty status and update last updated time. - if err := models.UpdateRepository(repo, false); err != nil { - return fmt.Errorf("UpdateRepository: %v", err) + // Change repository last updated time. + if err := models.UpdateRepositoryUpdatedTime(repo.ID, time.Now()); err != nil { + return fmt.Errorf("UpdateRepositoryUpdatedTime: %v", err) } if err := models.NotifyWatchers(actions...); err != nil { diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 3616823c5..fab02bae0 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -185,16 +185,22 @@ func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, new Message: message, } - sign, signingKey, _ := repo.SignWikiCommit(doer) + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := repo.SignWikiCommit(doer) if sign { commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else { commitTreeOpts.NoGPGSign = true } if hasMasterBranch { commitTreeOpts.Parents = []string{"HEAD"} } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) + + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { log.Error("%v", err) return err @@ -302,14 +308,19 @@ func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) Parents: []string{"HEAD"}, } - sign, signingKey, _ := repo.SignWikiCommit(doer) + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := repo.SignWikiCommit(doer) if sign { commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else { commitTreeOpts.NoGPGSign = true } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { return err } diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index c4b25c73d..d5c540724 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -167,6 +167,19 @@ +
+ +

diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 8a490ac64..a8e050c58 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -340,6 +340,52 @@ +

+ {{.i18n.Tr "repo.settings.signing_settings"}} +

+
+
+ {{.CsrfTokenHtml}} + +
+ +
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.default.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.collaborator.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.committer.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.collaboratorcommitter.desc"}}

+
+
+
+ +
+
+ +
+
+
+ {{if .IsAdmin}}

{{.i18n.Tr "repo.settings.admin_settings"}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4b78b40dd..6792f7444 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11937,6 +11937,17 @@ "description": "Readme of the repository to create", "type": "string", "x-go-name": "Readme" + }, + "trust_model": { + "description": "TrustModel of the repository", + "type": "string", + "enum": [ + "default", + "collaborator", + "committer", + "collaboratorcommitter" + ], + "x-go-name": "TrustModel" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -13824,6 +13835,9 @@ "type": "string", "x-go-name": "LatestCommentURL" }, + "state": { + "$ref": "#/definitions/StateType" + }, "title": { "type": "string", "x-go-name": "Title" diff --git a/web_src/js/features/migration.js b/web_src/js/features/migration.js index 7a406616f..09ab49b3e 100644 --- a/web_src/js/features/migration.js +++ b/web_src/js/features/migration.js @@ -2,7 +2,8 @@ const $service = $('#service_type'); const $user = $('#auth_username'); const $pass = $('#auth_password'); const $token = $('#auth_token'); -const $items = $('#migrate_items').find('.field'); +const $mirror = $('#mirror'); +const $items = $('#migrate_items').find('input[type=checkbox]'); export default function initMigration() { checkAuth(); @@ -10,6 +11,7 @@ export default function initMigration() { $user.on('keyup', () => {checkItems(false)}); $pass.on('keyup', () => {checkItems(false)}); $token.on('keyup', () => {checkItems(true)}); + $mirror.on('change', () => {checkItems(true)}); const $cloneAddr = $('#clone_addr'); $cloneAddr.on('change', () => { @@ -34,8 +36,13 @@ function checkItems(tokenAuth) { enableItems = $user.val() !== '' || $pass.val() !== ''; } if (enableItems && $service.val() > 1) { - $items.removeClass('disabled'); + if ($mirror.is(':checked')) { + $items.not('[name="wiki"]').attr('disabled', true); + $items.filter('[name="wiki"]').attr('disabled', false); + return; + } + $items.attr('disabled', false); } else { - $items.addClass('disabled'); + $items.attr('disabled', true); } } diff --git a/web_src/js/index.js b/web_src/js/index.js index e4b5d213e..e97bade93 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -1484,7 +1484,26 @@ function setCommentSimpleMDE($editArea) { spellChecker: false, toolbar: ['bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|', - 'code', 'quote', '|', + 'code', 'quote', '|', { + name: 'checkbox-empty', + action(e) { + const cm = e.codemirror; + cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`); + cm.focus(); + }, + className: 'fa fa-square-o', + title: 'Add Checkbox (empty)', + }, + { + name: 'checkbox-checked', + action(e) { + const cm = e.codemirror; + cm.replaceSelection(`\n- [x] ${cm.getSelection()}`); + cm.focus(); + }, + className: 'fa fa-check-square-o', + title: 'Add Checkbox (checked)', + }, '|', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', 'horizontal-rule', '|', 'clean-block', '|',