* Add require signed commit for protected branch * Fix fmt * Make editor show if they will be signed * bugfix * Add basic merge check and better information for CRUD * linting comment * Add descriptors to merge signing * Slight refactor * Slight improvement to appearances * Handle Merge API * manage CRUD API * Move error to error.go * Remove fix to delete.go * prep for merge * need to tolerate \r\n in message * check protected branch before trying to load it * Apply suggestions from code review Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * fix commit-reader Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>tags/v1.10.5
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` | RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` | ||||
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` | BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` | ||||
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` | DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` | ||||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` | |||||
CreatedUnix timeutil.TimeStamp `xorm:"created"` | CreatedUnix timeutil.TimeStamp `xorm:"created"` | ||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"` | UpdatedUnix timeutil.TimeStamp `xorm:"updated"` |
return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) | return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) | ||||
} | } | ||||
// ErrWontSign explains the first reason why a commit would not be signed | |||||
// There may be other reasons - this is just the first reason found | |||||
type ErrWontSign struct { | |||||
Reason signingMode | |||||
} | |||||
func (e *ErrWontSign) Error() string { | |||||
return fmt.Sprintf("wont sign: %s", e.Reason) | |||||
} | |||||
// IsErrWontSign checks if an error is a ErrWontSign | |||||
func IsErrWontSign(err error) bool { | |||||
_, ok := err.(*ErrWontSign) | |||||
return ok | |||||
} | |||||
// __________ .__ | // __________ .__ | ||||
// \______ \____________ ____ ____ | |__ | // \______ \____________ ____ ____ | |__ | ||||
// | | _/\_ __ \__ \ / \_/ ___\| | \ | // | | _/\_ __ \__ \ / \_/ ___\| | \ |
NewMigration("Add owner_name on table repository", addOwnerNameOnRepository), | NewMigration("Add owner_name on table repository", addOwnerNameOnRepository), | ||||
// v121 -> v122 | // v121 -> v122 | ||||
NewMigration("add is_restricted column for users table", addIsRestricted), | NewMigration("add is_restricted column for users table", addIsRestricted), | ||||
// v122 -> v123 | |||||
NewMigration("Add Require Signed Commits to ProtectedBranch", addRequireSignedCommits), | |||||
} | } | ||||
// Migrate database to current version | // Migrate database to current version |
// 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 addRequireSignedCommits(x *xorm.Engine) error { | |||||
type ProtectedBranch struct { | |||||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` | |||||
} | |||||
return x.Sync2(new(ProtectedBranch)) | |||||
} |
} | } | ||||
func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) { | func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) { | ||||
if pr.BaseRepo == nil { | |||||
if pr.BaseRepoID == 0 { | |||||
return nil | |||||
} | |||||
pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) | |||||
if err != nil { | |||||
return | |||||
if pr.ProtectedBranch == nil { | |||||
if pr.BaseRepo == nil { | |||||
if pr.BaseRepoID == 0 { | |||||
return nil | |||||
} | |||||
pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) | |||||
if err != nil { | |||||
return | |||||
} | |||||
} | } | ||||
pr.ProtectedBranch, err = getProtectedBranchBy(e, pr.BaseRepo.ID, pr.BaseBranch) | |||||
} | } | ||||
pr.ProtectedBranch, err = getProtectedBranchBy(e, pr.BaseRepo.ID, pr.BaseBranch) | |||||
return | return | ||||
} | } | ||||
) | ) | ||||
// SignMerge determines if we should sign a PR merge commit to the base repository | // 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) { | |||||
func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string, error) { | |||||
if err := pr.GetBaseRepo(); err != nil { | if err := pr.GetBaseRepo(); err != nil { | ||||
log.Error("Unable to get Base Repo for pull request") | log.Error("Unable to get Base Repo for pull request") | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
repo := pr.BaseRepo | repo := pr.BaseRepo | ||||
signingKey := signingKey(repo.RepoPath()) | signingKey := signingKey(repo.RepoPath()) | ||||
if signingKey == "" { | if signingKey == "" { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{noKey} | |||||
} | } | ||||
rules := signingModeFromStrings(setting.Repository.Signing.Merges) | rules := signingModeFromStrings(setting.Repository.Signing.Merges) | ||||
for _, rule := range rules { | for _, rule := range rules { | ||||
switch rule { | switch rule { | ||||
case never: | case never: | ||||
return false, "" | |||||
return false, "", &ErrWontSign{never} | |||||
case always: | case always: | ||||
break | break | ||||
case pubkey: | case pubkey: | ||||
keys, err := ListGPGKeys(u.ID) | keys, err := ListGPGKeys(u.ID) | ||||
if err != nil || len(keys) == 0 { | |||||
return false, "" | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if len(keys) == 0 { | |||||
return false, "", &ErrWontSign{pubkey} | |||||
} | } | ||||
case twofa: | case twofa: | ||||
twofa, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil || twofa == nil { | |||||
return false, "" | |||||
twofaModel, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if twofaModel == nil { | |||||
return false, "", &ErrWontSign{twofa} | |||||
} | } | ||||
case approved: | case approved: | ||||
protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch) | protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch) | ||||
if err != nil || protectedBranch == nil { | |||||
return false, "" | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if protectedBranch == nil { | |||||
return false, "", &ErrWontSign{approved} | |||||
} | } | ||||
if protectedBranch.GetGrantedApprovalsCount(pr) < 1 { | if protectedBranch.GetGrantedApprovalsCount(pr) < 1 { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{approved} | |||||
} | } | ||||
case baseSigned: | case baseSigned: | ||||
if gitRepo == nil { | if gitRepo == nil { | ||||
gitRepo, err = git.OpenRepository(tmpBasePath) | gitRepo, err = git.OpenRepository(tmpBasePath) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
} | } | ||||
commit, err := gitRepo.GetCommit(baseCommit) | commit, err := gitRepo.GetCommit(baseCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{baseSigned} | |||||
} | } | ||||
case headSigned: | case headSigned: | ||||
if gitRepo == nil { | if gitRepo == nil { | ||||
gitRepo, err = git.OpenRepository(tmpBasePath) | gitRepo, err = git.OpenRepository(tmpBasePath) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
} | } | ||||
commit, err := gitRepo.GetCommit(headCommit) | commit, err := gitRepo.GetCommit(headCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{headSigned} | |||||
} | } | ||||
case commitsSigned: | case commitsSigned: | ||||
if gitRepo == nil { | if gitRepo == nil { | ||||
gitRepo, err = git.OpenRepository(tmpBasePath) | gitRepo, err = git.OpenRepository(tmpBasePath) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
} | } | ||||
commit, err := gitRepo.GetCommit(headCommit) | commit, err := gitRepo.GetCommit(headCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{commitsSigned} | |||||
} | } | ||||
// need to work out merge-base | // need to work out merge-base | ||||
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) | mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) | commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
for e := commitList.Front(); e != nil; e = e.Next() { | for e := commitList.Front(); e != nil; e = e.Next() { | ||||
commit = e.Value.(*git.Commit) | commit = e.Value.(*git.Commit) | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{commitsSigned} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return true, signingKey | |||||
return true, signingKey, nil | |||||
} | } |
headSigned signingMode = "headsigned" | headSigned signingMode = "headsigned" | ||||
commitsSigned signingMode = "commitssigned" | commitsSigned signingMode = "commitssigned" | ||||
approved signingMode = "approved" | approved signingMode = "approved" | ||||
noKey signingMode = "nokey" | |||||
) | ) | ||||
func signingModeFromStrings(modeStrings []string) []signingMode { | func signingModeFromStrings(modeStrings []string) []signingMode { | ||||
} | } | ||||
// SignInitialCommit determines if we should sign the initial commit to this repository | // SignInitialCommit determines if we should sign the initial commit to this repository | ||||
func SignInitialCommit(repoPath string, u *User) (bool, string) { | |||||
func SignInitialCommit(repoPath string, u *User) (bool, string, error) { | |||||
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) | rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) | ||||
signingKey := signingKey(repoPath) | signingKey := signingKey(repoPath) | ||||
if signingKey == "" { | if signingKey == "" { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{noKey} | |||||
} | } | ||||
for _, rule := range rules { | for _, rule := range rules { | ||||
switch rule { | switch rule { | ||||
case never: | case never: | ||||
return false, "" | |||||
return false, "", &ErrWontSign{never} | |||||
case always: | case always: | ||||
break | break | ||||
case pubkey: | case pubkey: | ||||
keys, err := ListGPGKeys(u.ID) | keys, err := ListGPGKeys(u.ID) | ||||
if err != nil || len(keys) == 0 { | |||||
return false, "" | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if len(keys) == 0 { | |||||
return false, "", &ErrWontSign{pubkey} | |||||
} | } | ||||
case twofa: | case twofa: | ||||
twofa, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil || twofa == nil { | |||||
return false, "" | |||||
twofaModel, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if twofaModel == nil { | |||||
return false, "", &ErrWontSign{twofa} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return true, signingKey | |||||
return true, signingKey, nil | |||||
} | } | ||||
// SignWikiCommit determines if we should sign the commits to this repository wiki | // SignWikiCommit determines if we should sign the commits to this repository wiki | ||||
func (repo *Repository) SignWikiCommit(u *User) (bool, string) { | |||||
func (repo *Repository) SignWikiCommit(u *User) (bool, string, error) { | |||||
rules := signingModeFromStrings(setting.Repository.Signing.Wiki) | rules := signingModeFromStrings(setting.Repository.Signing.Wiki) | ||||
signingKey := signingKey(repo.WikiPath()) | signingKey := signingKey(repo.WikiPath()) | ||||
if signingKey == "" { | if signingKey == "" { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{noKey} | |||||
} | } | ||||
for _, rule := range rules { | for _, rule := range rules { | ||||
switch rule { | switch rule { | ||||
case never: | case never: | ||||
return false, "" | |||||
return false, "", &ErrWontSign{never} | |||||
case always: | case always: | ||||
break | break | ||||
case pubkey: | case pubkey: | ||||
keys, err := ListGPGKeys(u.ID) | keys, err := ListGPGKeys(u.ID) | ||||
if err != nil || len(keys) == 0 { | |||||
return false, "" | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if len(keys) == 0 { | |||||
return false, "", &ErrWontSign{pubkey} | |||||
} | } | ||||
case twofa: | case twofa: | ||||
twofa, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil || twofa == nil { | |||||
return false, "" | |||||
twofaModel, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if twofaModel == nil { | |||||
return false, "", &ErrWontSign{twofa} | |||||
} | } | ||||
case parentSigned: | case parentSigned: | ||||
gitRepo, err := git.OpenRepository(repo.WikiPath()) | gitRepo, err := git.OpenRepository(repo.WikiPath()) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
commit, err := gitRepo.GetCommit("HEAD") | commit, err := gitRepo.GetCommit("HEAD") | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
if commit.Signature == nil { | if commit.Signature == nil { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{parentSigned} | |||||
} | } | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{parentSigned} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return true, signingKey | |||||
return true, signingKey, nil | |||||
} | } | ||||
// SignCRUDAction determines if we should sign a CRUD commit to this repository | // SignCRUDAction determines if we should sign a CRUD commit to this repository | ||||
func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string) { | |||||
func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string, error) { | |||||
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) | rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) | ||||
signingKey := signingKey(repo.RepoPath()) | signingKey := signingKey(repo.RepoPath()) | ||||
if signingKey == "" { | if signingKey == "" { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{noKey} | |||||
} | } | ||||
for _, rule := range rules { | for _, rule := range rules { | ||||
switch rule { | switch rule { | ||||
case never: | case never: | ||||
return false, "" | |||||
return false, "", &ErrWontSign{never} | |||||
case always: | case always: | ||||
break | break | ||||
case pubkey: | case pubkey: | ||||
keys, err := ListGPGKeys(u.ID) | keys, err := ListGPGKeys(u.ID) | ||||
if err != nil || len(keys) == 0 { | |||||
return false, "" | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if len(keys) == 0 { | |||||
return false, "", &ErrWontSign{pubkey} | |||||
} | } | ||||
case twofa: | case twofa: | ||||
twofa, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil || twofa == nil { | |||||
return false, "" | |||||
twofaModel, err := GetTwoFactorByUID(u.ID) | |||||
if err != nil { | |||||
return false, "", err | |||||
} | |||||
if twofaModel == nil { | |||||
return false, "", &ErrWontSign{twofa} | |||||
} | } | ||||
case parentSigned: | case parentSigned: | ||||
gitRepo, err := git.OpenRepository(tmpBasePath) | gitRepo, err := git.OpenRepository(tmpBasePath) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
commit, err := gitRepo.GetCommit(parentCommit) | commit, err := gitRepo.GetCommit(parentCommit) | ||||
if err != nil { | if err != nil { | ||||
return false, "" | |||||
return false, "", err | |||||
} | } | ||||
if commit.Signature == nil { | if commit.Signature == nil { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{parentSigned} | |||||
} | } | ||||
verification := ParseCommitWithSignature(commit) | verification := ParseCommitWithSignature(commit) | ||||
if !verification.Verified { | if !verification.Verified { | ||||
return false, "" | |||||
return false, "", &ErrWontSign{parentSigned} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return true, signingKey | |||||
return true, signingKey, nil | |||||
} | } |
ApprovalsWhitelistTeams string | ApprovalsWhitelistTeams string | ||||
BlockOnRejectedReviews bool | BlockOnRejectedReviews bool | ||||
DismissStaleApprovals bool | DismissStaleApprovals bool | ||||
RequireSignedCommits bool | |||||
} | } | ||||
// Validate validates the fields | // Validate validates the fields |
} | } | ||||
} | } | ||||
// CanCommitToBranchResults represents the results of CanCommitToBranch | |||||
type CanCommitToBranchResults struct { | |||||
CanCommitToBranch bool | |||||
EditorEnabled bool | |||||
UserCanPush bool | |||||
RequireSigned bool | |||||
WillSign bool | |||||
SigningKey string | |||||
WontSignReason string | |||||
} | |||||
// CanCommitToBranch returns true if repository is editable and user has proper access level | // CanCommitToBranch returns true if repository is editable and user has proper access level | ||||
// and branch is not protected for push | // and branch is not protected for push | ||||
func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) { | |||||
protectedBranch, err := r.Repository.IsProtectedBranchForPush(r.BranchName, doer) | |||||
func (r *Repository) CanCommitToBranch(doer *models.User) (CanCommitToBranchResults, error) { | |||||
protectedBranch, err := models.GetProtectedBranchBy(r.Repository.ID, r.BranchName) | |||||
if err != nil { | if err != nil { | ||||
return false, err | |||||
return CanCommitToBranchResults{}, err | |||||
} | } | ||||
return r.CanEnableEditor() && !protectedBranch, nil | |||||
userCanPush := true | |||||
requireSigned := false | |||||
if protectedBranch != nil { | |||||
userCanPush = protectedBranch.CanUserPush(doer.ID) | |||||
requireSigned = protectedBranch.RequireSignedCommits | |||||
} | |||||
sign, keyID, err := r.Repository.SignCRUDAction(doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) | |||||
canCommit := r.CanEnableEditor() && userCanPush | |||||
if requireSigned { | |||||
canCommit = canCommit && sign | |||||
} | |||||
wontSignReason := "" | |||||
if err != nil { | |||||
if models.IsErrWontSign(err) { | |||||
wontSignReason = string(err.(*models.ErrWontSign).Reason) | |||||
err = nil | |||||
} else { | |||||
wontSignReason = "error" | |||||
} | |||||
} | |||||
return CanCommitToBranchResults{ | |||||
CanCommitToBranch: canCommit, | |||||
EditorEnabled: r.CanEnableEditor(), | |||||
UserCanPush: userCanPush, | |||||
RequireSigned: requireSigned, | |||||
WillSign: sign, | |||||
SigningKey: keyID, | |||||
WontSignReason: wontSignReason, | |||||
}, err | |||||
} | } | ||||
// CanUseTimetracker returns whether or not a user can use the timetracker. | // CanUseTimetracker returns whether or not a user can use the timetracker. |
// RunInDirTimeoutEnvFullPipelineFunc executes the command in given directory with given timeout, | // RunInDirTimeoutEnvFullPipelineFunc executes the command in given directory with given timeout, | ||||
// 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. | // 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 { | |||||
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 { | if timeout == -1 { | ||||
timeout = DefaultCommandExecutionTimeout | timeout = DefaultCommandExecutionTimeout | ||||
defer process.GetManager().Remove(pid) | defer process.GetManager().Remove(pid) | ||||
if fn != nil { | if fn != nil { | ||||
fn(ctx, cancel) | |||||
err := fn(ctx, cancel) | |||||
if err != nil { | |||||
cancel() | |||||
return err | |||||
} | |||||
} | } | ||||
if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded { | if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded { |
CommitMessage string | CommitMessage string | ||||
Signature *CommitGPGSignature | Signature *CommitGPGSignature | ||||
parents []SHA1 // SHA1 strings | |||||
Parents []SHA1 // SHA1 strings | |||||
submoduleCache *ObjectCache | submoduleCache *ObjectCache | ||||
} | } | ||||
Committer: &c.Committer, | Committer: &c.Committer, | ||||
Author: &c.Author, | Author: &c.Author, | ||||
Signature: convertPGPSignature(c), | Signature: convertPGPSignature(c), | ||||
parents: c.ParentHashes, | |||||
Parents: c.ParentHashes, | |||||
} | } | ||||
} | } | ||||
// ParentID returns oid of n-th parent (0-based index). | // ParentID returns oid of n-th parent (0-based index). | ||||
// It returns nil if no such parent exists. | // It returns nil if no such parent exists. | ||||
func (c *Commit) ParentID(n int) (SHA1, error) { | func (c *Commit) ParentID(n int) (SHA1, error) { | ||||
if n >= len(c.parents) { | |||||
if n >= len(c.Parents) { | |||||
return SHA1{}, ErrNotExist{"", ""} | return SHA1{}, ErrNotExist{"", ""} | ||||
} | } | ||||
return c.parents[n], nil | |||||
return c.Parents[n], nil | |||||
} | } | ||||
// Parent returns n-th parent (0-based index) of the commit. | // Parent returns n-th parent (0-based index) of the commit. | ||||
// ParentCount returns number of parents of the commit. | // ParentCount returns number of parents of the commit. | ||||
// 0 if this is the root commit, otherwise 1,2, etc. | // 0 if this is the root commit, otherwise 1,2, etc. | ||||
func (c *Commit) ParentCount() int { | func (c *Commit) ParentCount() int { | ||||
return len(c.parents) | |||||
return len(c.Parents) | |||||
} | } | ||||
func isImageFile(data []byte) (string, bool) { | func isImageFile(data []byte) (string, bool) { |
// 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 git | |||||
import ( | |||||
"bufio" | |||||
"bytes" | |||||
"io" | |||||
"strings" | |||||
"gopkg.in/src-d/go-git.v4/plumbing" | |||||
) | |||||
// CommitFromReader will generate a Commit from a provided reader | |||||
// We will need this to interpret commits from cat-file | |||||
func CommitFromReader(gitRepo *Repository, sha plumbing.Hash, reader io.Reader) (*Commit, error) { | |||||
commit := &Commit{ | |||||
ID: sha, | |||||
} | |||||
payloadSB := new(strings.Builder) | |||||
signatureSB := new(strings.Builder) | |||||
messageSB := new(strings.Builder) | |||||
message := false | |||||
pgpsig := false | |||||
scanner := bufio.NewScanner(reader) | |||||
// Split by '\n' but include the '\n' | |||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | |||||
if atEOF && len(data) == 0 { | |||||
return 0, nil, nil | |||||
} | |||||
if i := bytes.IndexByte(data, '\n'); i >= 0 { | |||||
// We have a full newline-terminated line. | |||||
return i + 1, data[0 : i+1], nil | |||||
} | |||||
// If we're at EOF, we have a final, non-terminated line. Return it. | |||||
if atEOF { | |||||
return len(data), data, nil | |||||
} | |||||
// Request more data. | |||||
return 0, nil, nil | |||||
}) | |||||
for scanner.Scan() { | |||||
line := scanner.Bytes() | |||||
if pgpsig { | |||||
if len(line) > 0 && line[0] == ' ' { | |||||
_, _ = signatureSB.Write(line[1:]) | |||||
continue | |||||
} else { | |||||
pgpsig = false | |||||
} | |||||
} | |||||
if !message { | |||||
// This is probably not correct but is copied from go-gits interpretation... | |||||
trimmed := bytes.TrimSpace(line) | |||||
if len(trimmed) == 0 { | |||||
message = true | |||||
_, _ = payloadSB.Write(line) | |||||
continue | |||||
} | |||||
split := bytes.SplitN(trimmed, []byte{' '}, 2) | |||||
var data []byte | |||||
if len(split) > 1 { | |||||
data = split[1] | |||||
} | |||||
switch string(split[0]) { | |||||
case "tree": | |||||
commit.Tree = *NewTree(gitRepo, plumbing.NewHash(string(data))) | |||||
_, _ = payloadSB.Write(line) | |||||
case "parent": | |||||
commit.Parents = append(commit.Parents, plumbing.NewHash(string(data))) | |||||
_, _ = payloadSB.Write(line) | |||||
case "author": | |||||
commit.Author = &Signature{} | |||||
commit.Author.Decode(data) | |||||
_, _ = payloadSB.Write(line) | |||||
case "committer": | |||||
commit.Committer = &Signature{} | |||||
commit.Committer.Decode(data) | |||||
_, _ = payloadSB.Write(line) | |||||
case "gpgsig": | |||||
_, _ = signatureSB.Write(data) | |||||
_ = signatureSB.WriteByte('\n') | |||||
pgpsig = true | |||||
} | |||||
} else { | |||||
_, _ = messageSB.Write(line) | |||||
} | |||||
} | |||||
commit.CommitMessage = messageSB.String() | |||||
_, _ = payloadSB.WriteString(commit.CommitMessage) | |||||
commit.Signature = &CommitGPGSignature{ | |||||
Signature: signatureSB.String(), | |||||
Payload: payloadSB.String(), | |||||
} | |||||
if len(commit.Signature.Signature) == 0 { | |||||
commit.Signature = nil | |||||
} | |||||
return commit, scanner.Err() | |||||
} |
BranchName: opts.NewBranch, | BranchName: opts.NewBranch, | ||||
} | } | ||||
} | } | ||||
} else if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected { | |||||
return nil, models.ErrUserCannotCommit{ | |||||
UserName: doer.LowerName, | |||||
} else { | |||||
protectedBranch, err := repo.GetBranchProtection(opts.OldBranch) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if protectedBranch != nil && !protectedBranch.CanUserPush(doer.ID) { | |||||
return nil, models.ErrUserCannotCommit{ | |||||
UserName: doer.LowerName, | |||||
} | |||||
} | |||||
if protectedBranch != nil && protectedBranch.RequireSignedCommits { | |||||
_, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) | |||||
if err != nil { | |||||
if !models.IsErrWontSign(err) { | |||||
return nil, err | |||||
} | |||||
return nil, models.ErrUserCannotCommit{ | |||||
UserName: doer.LowerName, | |||||
} | |||||
} | |||||
} | } | ||||
} | } | ||||
// Determine if we should sign | // Determine if we should sign | ||||
if version.Compare(binVersion, "1.7.9", ">=") { | if version.Compare(binVersion, "1.7.9", ">=") { | ||||
sign, keyID := t.repo.SignCRUDAction(author, t.basePath, "HEAD") | |||||
sign, keyID, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") | |||||
if sign { | if sign { | ||||
args = append(args, "-S"+keyID) | args = append(args, "-S"+keyID) | ||||
} else if version.Compare(binVersion, "2.0.0", ">=") { | } else if version.Compare(binVersion, "2.0.0", ">=") { | ||||
var finalErr error | var finalErr error | ||||
if err := git.NewCommand("diff-index", "--cached", "-p", "HEAD"). | if err := git.NewCommand("diff-index", "--cached", "-p", "HEAD"). | ||||
RunInDirTimeoutEnvFullPipelineFunc(nil, 30*time.Second, t.basePath, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) { | |||||
RunInDirTimeoutEnvFullPipelineFunc(nil, 30*time.Second, t.basePath, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) error { | |||||
_ = stdoutWriter.Close() | _ = stdoutWriter.Close() | ||||
diff, finalErr = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader) | diff, finalErr = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader) | ||||
if finalErr != nil { | if finalErr != nil { | ||||
cancel() | cancel() | ||||
} | } | ||||
_ = stdoutReader.Close() | _ = stdoutReader.Close() | ||||
return finalErr | |||||
}); err != nil { | }); err != nil { | ||||
if finalErr != nil { | if finalErr != nil { | ||||
log.Error("Unable to ParsePatch in temporary repo %s (%s). Error: %v", t.repo.FullName(), t.basePath, finalErr) | log.Error("Unable to ParsePatch in temporary repo %s (%s). Error: %v", t.repo.FullName(), t.basePath, finalErr) |
if err != nil && !git.IsErrBranchNotExist(err) { | if err != nil && !git.IsErrBranchNotExist(err) { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
} else if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected { | |||||
return nil, models.ErrUserCannotCommit{UserName: doer.LowerName} | |||||
} else { | |||||
protectedBranch, err := repo.GetBranchProtection(opts.OldBranch) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if protectedBranch != nil && !protectedBranch.CanUserPush(doer.ID) { | |||||
return nil, models.ErrUserCannotCommit{ | |||||
UserName: doer.LowerName, | |||||
} | |||||
} | |||||
if protectedBranch != nil && protectedBranch.RequireSignedCommits { | |||||
_, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) | |||||
if err != nil { | |||||
if !models.IsErrWontSign(err) { | |||||
return nil, err | |||||
} | |||||
return nil, models.ErrUserCannotCommit{ | |||||
UserName: doer.LowerName, | |||||
} | |||||
} | |||||
} | |||||
} | } | ||||
// If FromTreePath is not set, set it to the opts.TreePath | // If FromTreePath is not set, set it to the opts.TreePath |
} | } | ||||
if version.Compare(binVersion, "1.7.9", ">=") { | if version.Compare(binVersion, "1.7.9", ">=") { | ||||
sign, keyID := models.SignInitialCommit(tmpPath, u) | |||||
sign, keyID, _ := models.SignInitialCommit(tmpPath, u) | |||||
if sign { | if sign { | ||||
args = append(args, "-S"+keyID) | args = append(args, "-S"+keyID) | ||||
} else if version.Compare(binVersion, "2.0.0", ">=") { | } else if version.Compare(binVersion, "2.0.0", ">=") { |
editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. | editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. | ||||
editor.or = or | editor.or = or | ||||
editor.cancel_lower = Cancel | editor.cancel_lower = Cancel | ||||
editor.commit_signed_changes = Commit Signed Changes | |||||
editor.commit_changes = Commit Changes | editor.commit_changes = Commit Changes | ||||
editor.add_tmpl = Add '<filename>' | editor.add_tmpl = Add '<filename>' | ||||
editor.add = Add '%s' | editor.add = Add '%s' | ||||
editor.upload_file_is_locked = File '%s' is locked by %s. | editor.upload_file_is_locked = File '%s' is locked by %s. | ||||
editor.upload_files_to_dir = Upload files to '%s' | editor.upload_files_to_dir = Upload files to '%s' | ||||
editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s'. | editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s'. | ||||
editor.no_commit_to_branch = Unable to commit directly to branch because: | |||||
editor.user_no_push_to_branch = User cannot push to branch | |||||
editor.require_signed_commit = Branch requires a signed commit | |||||
commits.desc = Browse source code change history. | commits.desc = Browse source code change history. | ||||
commits.commits = Commits | commits.commits = Commits | ||||
pulls.rebase_merge_pull_request = Rebase and Merge | pulls.rebase_merge_pull_request = Rebase and Merge | ||||
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff) | pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff) | ||||
pulls.squash_merge_pull_request = Squash and Merge | pulls.squash_merge_pull_request = Squash and Merge | ||||
pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed | |||||
pulls.invalid_merge_option = You cannot use this merge option for this pull request. | pulls.invalid_merge_option = You cannot use this merge option for this pull request. | ||||
pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<br>%[2]s<br>Hint: Try a different strategy | pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<br>%[2]s<br>Hint: Try a different strategy | ||||
pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s<br>%[2]s<br>%[3]s<br>Hint:Try a different strategy | pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s<br>%[2]s<br>%[3]s<br>Hint:Try a different strategy | ||||
milestones.filter_sort.most_issues = Most issues | milestones.filter_sort.most_issues = Most issues | ||||
milestones.filter_sort.least_issues = Least issues | milestones.filter_sort.least_issues = Least issues | ||||
signing.will_sign = This commit will be signed with key '%s' | |||||
signing.wont_sign.error = There was an error whilst checking if the commit could be signed | |||||
signing.wont_sign.nokey = There is no key available to sign this commit | |||||
signing.wont_sign.never = Commits are never signed | |||||
signing.wont_sign.always = Commits are always signed | |||||
signing.wont_sign.pubkey = The commit will not be signed because you do not have a public key associated with your account | |||||
signing.wont_sign.twofa = You must have two factor authentication enabled to have commits signed | |||||
signing.wont_sign.parentsigned = The commit will not be signed as the parent commit is not signed | |||||
signing.wont_sign.basesigned = The merge will not be signed as the base commit is not signed | |||||
signing.wont_sign.headsigned = The merge will not be signed as the head commit is not signed | |||||
signing.wont_sign.commitssigned = The merge will not be signed as all the associated commits are not signed | |||||
signing.wont_sign.approved = The merge will not be signed as the PR is not approved | |||||
ext_wiki = Ext. Wiki | ext_wiki = Ext. Wiki | ||||
ext_wiki.desc = Link to an external wiki. | ext_wiki.desc = Link to an external wiki. | ||||
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews: | settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews: | ||||
settings.dismiss_stale_approvals = Dismiss stale approvals | settings.dismiss_stale_approvals = Dismiss stale approvals | ||||
settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed. | settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed. | ||||
settings.require_signed_commits = Require Signed Commits | |||||
settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable | |||||
settings.add_protected_branch = Enable protection | settings.add_protected_branch = Enable protection | ||||
settings.delete_protected_branch = Disable protection | settings.delete_protected_branch = Disable protection | ||||
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. | settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. |
} | } | ||||
} | } | ||||
if _, err := pull_service.IsSignedIfRequired(pr, ctx.User); err != nil { | |||||
if !models.IsErrWontSign(err) { | |||||
ctx.Error(http.StatusInternalServerError, "IsSignedIfRequired", err) | |||||
return | |||||
} | |||||
ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err) | |||||
return | |||||
} | |||||
if len(form.Do) == 0 { | if len(form.Do) == 0 { | ||||
form.Do = string(models.MergeStyleMerge) | form.Do = string(models.MergeStyleMerge) | ||||
} | } |
package private | package private | ||||
import ( | import ( | ||||
"bufio" | |||||
"context" | |||||
"fmt" | "fmt" | ||||
"io" | |||||
"net/http" | "net/http" | ||||
"os" | "os" | ||||
"strings" | "strings" | ||||
"code.gitea.io/gitea/modules/repofiles" | "code.gitea.io/gitea/modules/repofiles" | ||||
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
pull_service "code.gitea.io/gitea/services/pull" | pull_service "code.gitea.io/gitea/services/pull" | ||||
"gopkg.in/src-d/go-git.v4/plumbing" | |||||
"gitea.com/macaron/macaron" | "gitea.com/macaron/macaron" | ||||
) | ) | ||||
func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error { | |||||
stdoutReader, stdoutWriter, err := os.Pipe() | |||||
if err != nil { | |||||
log.Error("Unable to create os.Pipe for %s", repo.Path) | |||||
return err | |||||
} | |||||
defer func() { | |||||
_ = stdoutReader.Close() | |||||
_ = stdoutWriter.Close() | |||||
}() | |||||
err = git.NewCommand("rev-list", oldCommitID+"..."+newCommitID). | |||||
RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path, | |||||
stdoutWriter, nil, nil, | |||||
func(ctx context.Context, cancel context.CancelFunc) error { | |||||
_ = stdoutWriter.Close() | |||||
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env) | |||||
if err != nil { | |||||
log.Error("%v", err) | |||||
cancel() | |||||
} | |||||
_ = stdoutReader.Close() | |||||
return err | |||||
}) | |||||
if err != nil && !isErrUnverifiedCommit(err) { | |||||
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) | |||||
} | |||||
return err | |||||
} | |||||
func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error { | |||||
scanner := bufio.NewScanner(input) | |||||
for scanner.Scan() { | |||||
line := scanner.Text() | |||||
err := readAndVerifyCommit(line, repo, env) | |||||
if err != nil { | |||||
log.Error("%v", err) | |||||
return err | |||||
} | |||||
} | |||||
return scanner.Err() | |||||
} | |||||
func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error { | |||||
stdoutReader, stdoutWriter, err := os.Pipe() | |||||
if err != nil { | |||||
log.Error("Unable to create pipe for %s: %v", repo.Path, err) | |||||
return err | |||||
} | |||||
defer func() { | |||||
_ = stdoutReader.Close() | |||||
_ = stdoutWriter.Close() | |||||
}() | |||||
hash := plumbing.NewHash(sha) | |||||
return git.NewCommand("cat-file", "commit", sha). | |||||
RunInDirTimeoutEnvFullPipelineFunc(env, -1, repo.Path, | |||||
stdoutWriter, nil, nil, | |||||
func(ctx context.Context, cancel context.CancelFunc) error { | |||||
_ = stdoutWriter.Close() | |||||
commit, err := git.CommitFromReader(repo, hash, stdoutReader) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
log.Info("have commit %s", commit.ID.String()) | |||||
verification := models.ParseCommitWithSignature(commit) | |||||
if !verification.Verified { | |||||
log.Info("unverified commit %s", commit.ID.String()) | |||||
cancel() | |||||
return &errUnverifiedCommit{ | |||||
commit.ID.String(), | |||||
} | |||||
} | |||||
return nil | |||||
}) | |||||
} | |||||
type errUnverifiedCommit struct { | |||||
sha string | |||||
} | |||||
func (e *errUnverifiedCommit) Error() string { | |||||
return fmt.Sprintf("Unverified commit: %s", e.sha) | |||||
} | |||||
func isErrUnverifiedCommit(err error) bool { | |||||
_, ok := err.(*errUnverifiedCommit) | |||||
return ok | |||||
} | |||||
// HookPreReceive checks whether a individual commit is acceptable | // HookPreReceive checks whether a individual commit is acceptable | ||||
func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | ||||
ownerName := ctx.Params(":owner") | ownerName := ctx.Params(":owner") | ||||
return | return | ||||
} | } | ||||
repo.OwnerName = ownerName | repo.OwnerName = ownerName | ||||
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(), | |||||
}) | |||||
return | |||||
} | |||||
defer gitRepo.Close() | |||||
// Generate git environment for checking commits | |||||
env := os.Environ() | |||||
if opts.GitAlternativeObjectDirectories != "" { | |||||
env = append(env, | |||||
private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories) | |||||
} | |||||
if opts.GitObjectDirectory != "" { | |||||
env = append(env, | |||||
private.GitObjectDirectory+"="+opts.GitObjectDirectory) | |||||
} | |||||
if opts.GitQuarantinePath != "" { | |||||
env = append(env, | |||||
private.GitQuarantinePath+"="+opts.GitQuarantinePath) | |||||
} | |||||
for i := range opts.OldCommitIDs { | for i := range opts.OldCommitIDs { | ||||
oldCommitID := opts.OldCommitIDs[i] | oldCommitID := opts.OldCommitIDs[i] | ||||
return | return | ||||
} | } | ||||
if protectBranch != nil && protectBranch.IsProtected() { | if protectBranch != nil && protectBranch.IsProtected() { | ||||
// check and deletion | |||||
// detect and prevent deletion | |||||
if newCommitID == git.EmptySHA { | if newCommitID == git.EmptySHA { | ||||
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) | log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) | ||||
ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
// detect force push | // detect force push | ||||
if git.EmptySHA != oldCommitID { | if git.EmptySHA != oldCommitID { | ||||
env := os.Environ() | |||||
if opts.GitAlternativeObjectDirectories != "" { | |||||
env = append(env, | |||||
private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories) | |||||
} | |||||
if opts.GitObjectDirectory != "" { | |||||
env = append(env, | |||||
private.GitObjectDirectory+"="+opts.GitObjectDirectory) | |||||
} | |||||
if opts.GitQuarantinePath != "" { | |||||
env = append(env, | |||||
private.GitQuarantinePath+"="+opts.GitQuarantinePath) | |||||
} | |||||
output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) | output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) | ||||
if err != nil { | 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 detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) | ||||
} | } | ||||
} | } | ||||
// 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, map[string]interface{}{ | |||||
"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), | |||||
}) | |||||
return | |||||
} | |||||
} | |||||
canPush := false | canPush := false | ||||
if opts.IsDeployKey { | if opts.IsDeployKey { | ||||
canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) | canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) |
) | ) | ||||
func renderCommitRights(ctx *context.Context) bool { | func renderCommitRights(ctx *context.Context) bool { | ||||
canCommit, err := ctx.Repo.CanCommitToBranch(ctx.User) | |||||
canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx.User) | |||||
if err != nil { | if err != nil { | ||||
log.Error("CanCommitToBranch: %v", err) | log.Error("CanCommitToBranch: %v", err) | ||||
} | } | ||||
ctx.Data["CanCommitToBranch"] = canCommit | |||||
return canCommit | |||||
ctx.Data["CanCommitToBranch"] = canCommitToBranch | |||||
return canCommitToBranch.CanCommitToBranch | |||||
} | } | ||||
// getParentTreeFields returns list of parent tree names and corresponding tree paths | // getParentTreeFields returns list of parent tree names and corresponding tree paths |
ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull) | ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull) | ||||
ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull) | ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull) | ||||
ctx.Data["GrantedApprovals"] = cnt | ctx.Data["GrantedApprovals"] = cnt | ||||
ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits | |||||
} | |||||
ctx.Data["WillSign"] = false | |||||
if ctx.User != nil { | |||||
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 { | |||||
if models.IsErrWontSign(err) { | |||||
ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason | |||||
} else { | |||||
ctx.Data["WontSignReason"] = "error" | |||||
log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) | |||||
} | |||||
} | |||||
} | } | ||||
ctx.Data["IsPullBranchDeletable"] = canDelete && | ctx.Data["IsPullBranchDeletable"] = canDelete && | ||||
pull.HeadRepo != nil && | pull.HeadRepo != nil && |
} | } | ||||
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews | protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews | ||||
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals | protectBranch.DismissStaleApprovals = f.DismissStaleApprovals | ||||
protectBranch.RequireSignedCommits = f.RequireSignedCommits | |||||
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ | err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ | ||||
UserIDs: whitelistUsers, | UserIDs: whitelistUsers, |
// Determine if we should sign | // Determine if we should sign | ||||
signArg := "" | signArg := "" | ||||
if version.Compare(binVersion, "1.7.9", ">=") { | if version.Compare(binVersion, "1.7.9", ">=") { | ||||
sign, keyID := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) | |||||
sign, keyID, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) | |||||
if sign { | if sign { | ||||
signArg = "-S" + keyID | signArg = "-S" + keyID | ||||
} else if version.Compare(binVersion, "2.0.0", ">=") { | } else if version.Compare(binVersion, "2.0.0", ">=") { | ||||
return out.String(), nil | return out.String(), nil | ||||
} | } | ||||
// IsSignedIfRequired check if merge will be signed if required | |||||
func IsSignedIfRequired(pr *models.PullRequest, doer *models.User) (bool, error) { | |||||
if err := pr.LoadProtectedBranch(); err != nil { | |||||
return false, err | |||||
} | |||||
if pr.ProtectedBranch == nil || !pr.ProtectedBranch.RequireSignedCommits { | |||||
return true, nil | |||||
} | |||||
sign, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) | |||||
return sign, err | |||||
} | |||||
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections | // IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections | ||||
func IsUserAllowedToMerge(pr *models.PullRequest, p models.Permission, user *models.User) (bool, error) { | func IsUserAllowedToMerge(pr *models.PullRequest, p models.Permission, user *models.User) (bool, error) { | ||||
if p.IsAdmin() { | if p.IsAdmin() { |
RunInDirTimeoutEnvFullPipelineFunc( | RunInDirTimeoutEnvFullPipelineFunc( | ||||
nil, -1, tmpBasePath, | nil, -1, tmpBasePath, | ||||
nil, stderrWriter, nil, | nil, stderrWriter, nil, | ||||
func(ctx context.Context, cancel context.CancelFunc) { | |||||
func(ctx context.Context, cancel context.CancelFunc) error { | |||||
_ = stderrWriter.Close() | _ = stderrWriter.Close() | ||||
const prefix = "error: patch failed:" | const prefix = "error: patch failed:" | ||||
const errorPrefix = "error: " | const errorPrefix = "error: " | ||||
} | } | ||||
} | } | ||||
_ = stderrReader.Close() | _ = stderrReader.Close() | ||||
return nil | |||||
}) | }) | ||||
if err != nil { | if err != nil { |
Message: message, | Message: message, | ||||
} | } | ||||
sign, signingKey := repo.SignWikiCommit(doer) | |||||
sign, signingKey, _ := repo.SignWikiCommit(doer) | |||||
if sign { | if sign { | ||||
commitTreeOpts.KeyID = signingKey | commitTreeOpts.KeyID = signingKey | ||||
} else { | } else { | ||||
Parents: []string{"HEAD"}, | Parents: []string{"HEAD"}, | ||||
} | } | ||||
sign, signingKey := repo.SignWikiCommit(doer) | |||||
sign, signingKey, _ := repo.SignWikiCommit(doer) | |||||
if sign { | if sign { | ||||
commitTreeOpts.KeyID = signingKey | commitTreeOpts.KeyID = signingKey | ||||
} else { | } else { |
<div class="commit-form-wrapper"> | <div class="commit-form-wrapper"> | ||||
<img width="48" height="48" class="ui image commit-avatar" src="{{.SignedUser.RelAvatarLink}}"> | <img width="48" height="48" class="ui image commit-avatar" src="{{.SignedUser.RelAvatarLink}}"> | ||||
<div class="commit-form"> | <div class="commit-form"> | ||||
<h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3> | |||||
<h3>{{- if .CanCommitToBranch.WillSign}} | |||||
<i title="{{.i18n.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}" class="lock green icon"></i>{{.i18n.Tr "repo.editor.commit_signed_changes"}} | |||||
{{- else}} | |||||
<i title="{{.i18n.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}" class="unlock grey icon"></i>{{.i18n.Tr "repo.editor.commit_changes"}} | |||||
{{- end}}</h3> | |||||
<div class="field"> | <div class="field"> | ||||
<input name="commit_summary" placeholder="{{if .PageIsDelete}}{{.i18n.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{.i18n.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{.i18n.Tr "repo.editor.add_tmpl"}}{{else}}{{.i18n.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus> | <input name="commit_summary" placeholder="{{if .PageIsDelete}}{{.i18n.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{.i18n.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{.i18n.Tr "repo.editor.add_tmpl"}}{{else}}{{.i18n.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="quick-pull-choice js-quick-pull-choice"> | <div class="quick-pull-choice js-quick-pull-choice"> | ||||
<div class="field"> | <div class="field"> | ||||
<div class="ui radio checkbox {{if not .CanCommitToBranch}}disabled{{end}}"> | |||||
<div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}"> | |||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{.i18n.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> | <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{.i18n.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> | ||||
<label> | <label> | ||||
<i class="octicon octicon-git-commit" height="16" width="14"></i> | <i class="octicon octicon-git-commit" height="16" width="14"></i> | ||||
{{.i18n.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape) | Safe}} | {{.i18n.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape) | Safe}} | ||||
{{if not .CanCommitToBranch.CanCommitToBranch}} | |||||
<div class="ui visible small warning message"> | |||||
{{.i18n.Tr "repo.editor.no_commit_to_branch"}} | |||||
<ul> | |||||
{{if not .CanCommitToBranch.UserCanPush}}<li>{{.i18n.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} | |||||
{{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{.i18n.Tr "repo.editor.require_signed_commit"}}</li>{{end}} | |||||
</ul> | |||||
</div> | |||||
{{end}} | |||||
</label> | </label> | ||||
</div> | </div> | ||||
</div> | </div> |
{{else if .IsBlockedByApprovals}}red | {{else if .IsBlockedByApprovals}}red | ||||
{{else if .IsBlockedByRejection}}red | {{else if .IsBlockedByRejection}}red | ||||
{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}red | {{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}red | ||||
{{else if and .RequireSigned (not .WillSign)}}}red | |||||
{{else if .Issue.PullRequest.IsChecking}}yellow | {{else if .Issue.PullRequest.IsChecking}}yellow | ||||
{{else if .Issue.PullRequest.CanAutoMerge}}green | {{else if .Issue.PullRequest.CanAutoMerge}}green | ||||
{{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a> | {{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a> | ||||
</div> | </div> | ||||
{{else if .IsPullRequestBroken}} | {{else if .IsPullRequestBroken}} | ||||
<div class="item text red"> | <div class="item text red"> | ||||
<span class="octicon octicon-x"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.data_broken"}} | {{$.i18n.Tr "repo.pulls.data_broken"}} | ||||
</div> | </div> | ||||
{{else if .IsPullWorkInProgress}} | {{else if .IsPullWorkInProgress}} | ||||
<div class="item text grey"> | <div class="item text grey"> | ||||
<span class="octicon octicon-x"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}} | {{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}} | ||||
</div> | </div> | ||||
{{else if .Issue.PullRequest.IsChecking}} | {{else if .Issue.PullRequest.IsChecking}} | ||||
<div class="item text yellow"> | <div class="item text yellow"> | ||||
<span class="octicon octicon-sync"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-sync"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.is_checking"}} | {{$.i18n.Tr "repo.pulls.is_checking"}} | ||||
</div> | </div> | ||||
{{else if .Issue.PullRequest.CanAutoMerge}} | {{else if .Issue.PullRequest.CanAutoMerge}} | ||||
{{if .IsBlockedByApprovals}} | {{if .IsBlockedByApprovals}} | ||||
<div class="item text red"> | <div class="item text red"> | ||||
<span class="octicon octicon-x"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}} | {{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}} | ||||
</div> | </div> | ||||
{{else if .IsBlockedByRejection}} | {{else if .IsBlockedByRejection}} | ||||
<div class="item text red"> | <div class="item text red"> | ||||
<span class="octicon octicon-x"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}} | {{$.i18n.Tr "repo.pulls.blocked_by_rejection"}} | ||||
</div> | </div> | ||||
{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}} | {{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}} | ||||
<div class="item text red"> | <div class="item text red"> | ||||
<span class="octicon octicon-x"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.required_status_check_failed"}} | {{$.i18n.Tr "repo.pulls.required_status_check_failed"}} | ||||
</div> | </div> | ||||
{{else if and .RequireSigned (not .WillSign)}} | |||||
<div class="item text red"> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-x"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.require_signed_wont_sign"}} | |||||
</div> | |||||
<div class="item text yellow"> | |||||
<i class="icon unlock grey"></i> | |||||
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} | |||||
</div> | |||||
{{end}} | {{end}} | ||||
{{$notAllOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}} | |||||
{{if or $.IsRepoAdmin (not $notAllOk)}} | |||||
{{$notAllOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .RequireSigned (not .WillSign)) (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}} | |||||
{{if and (or $.IsRepoAdmin (not $notAllOk)) (or (not .RequireSigned) .WillSign)}} | |||||
{{if $notAllOk}} | {{if $notAllOk}} | ||||
<div class="item text yellow"> | <div class="item text yellow"> | ||||
<span class="octicon octicon-primitive-dot"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-primitive-dot"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.required_status_check_administrator"}} | {{$.i18n.Tr "repo.pulls.required_status_check_administrator"}} | ||||
</div> | </div> | ||||
{{else}} | {{else}} | ||||
<div class="item text green"> | <div class="item text green"> | ||||
<span class="octicon octicon-check"></span> | |||||
<i class="icon icon-octicon"><span class="octicon octicon-check"></span></i> | |||||
{{$.i18n.Tr "repo.pulls.can_auto_merge_desc"}} | {{$.i18n.Tr "repo.pulls.can_auto_merge_desc"}} | ||||
</div> | </div> | ||||
{{end}} | {{end}} | ||||
{{if .WillSign}} | |||||
<div class="item text green"> | |||||
<i class="icon lock green"></i> | |||||
{{$.i18n.Tr "repo.signing.will_sign" .SigningKey}} | |||||
</div> | |||||
{{else}} | |||||
<div class="item text yellow"> | |||||
<i class="icon unlock grey"></i> | |||||
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} | |||||
</div> | |||||
{{end}} | |||||
{{if .AllowMerge}} | {{if .AllowMerge}} | ||||
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} | {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} | ||||
{{$approvers := .Issue.PullRequest.GetApprovers}} | {{$approvers := .Issue.PullRequest.GetApprovers}} | ||||
<span class="octicon octicon-x"></span> | <span class="octicon octicon-x"></span> | ||||
{{$.i18n.Tr "repo.pulls.required_status_check_failed"}} | {{$.i18n.Tr "repo.pulls.required_status_check_failed"}} | ||||
</div> | </div> | ||||
{{else if and .RequireSigned (not .WillSign)}} | |||||
<div class="item text red"> | |||||
<span class="octicon octicon-x"></span> | |||||
{{$.i18n.Tr "repo.pulls.require_signed_wont_sign"}} | |||||
</div> | |||||
{{else}} | {{else}} | ||||
<div class="item text red"> | <div class="item text red"> | ||||
<span class="octicon octicon-x"></span> | <span class="octicon octicon-x"></span> |
<label for="block_on_rejected_reviews">{{.i18n.Tr "repo.settings.block_rejected_reviews"}}</label> | <label for="block_on_rejected_reviews">{{.i18n.Tr "repo.settings.block_rejected_reviews"}}</label> | ||||
<p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p> | <p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p> | ||||
</div> | </div> | ||||
</div> | |||||
</div> | |||||
<div class="field"> | <div class="field"> | ||||
<div class="ui checkbox"> | <div class="ui checkbox"> | ||||
<input name="dismiss_stale_approvals" type="checkbox" {{if .Branch.DismissStaleApprovals}}checked{{end}}> | <input name="dismiss_stale_approvals" type="checkbox" {{if .Branch.DismissStaleApprovals}}checked{{end}}> | ||||
<p class="help">{{.i18n.Tr "repo.settings.dismiss_stale_approvals_desc"}}</p> | <p class="help">{{.i18n.Tr "repo.settings.dismiss_stale_approvals_desc"}}</p> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="field"> | |||||
<div class="ui checkbox"> | |||||
<input name="require_signed_commits" type="checkbox" {{if .Branch.RequireSignedCommits}}checked{{end}}> | |||||
<label for="require_signed_commits">{{.i18n.Tr "repo.settings.require_signed_commits"}}</label> | |||||
<p class="help">{{.i18n.Tr "repo.settings.require_signed_commits_desc"}}</p> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
margin-left: 10px; | margin-left: 10px; | ||||
margin-top: 10px; | margin-top: 10px; | ||||
} | } | ||||
.icon-octicon { | |||||
padding-left: 2px; | |||||
} | |||||
} | } | ||||
.review-item { | .review-item { |