Add require signed commit for protected branch (#9708)
* 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>
This commit is contained in:
		
							parent
							
								
									6b1fa12359
								
							
						
					
					
						commit
						66ee9b87f9
					
				
					 29 changed files with 618 additions and 122 deletions
				
			
		|  | @ -46,6 +46,7 @@ type ProtectedBranch struct { | ||||||
| 	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"` | ||||||
|  |  | ||||||
|  | @ -916,6 +916,22 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string { | ||||||
| 	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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // __________                             .__
 | // __________                             .__
 | ||||||
| // \______   \____________    ____   ____ |  |__
 | // \______   \____________    ____   ____ |  |__
 | ||||||
| //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
 | //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
 | ||||||
|  |  | ||||||
|  | @ -298,6 +298,8 @@ var migrations = []Migration{ | ||||||
| 	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
 | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								models/migrations/v122.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/migrations/v122.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | // 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)) | ||||||
|  | } | ||||||
|  | @ -152,16 +152,18 @@ func (pr *PullRequest) LoadProtectedBranch() (err error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) { | func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) { | ||||||
| 	if pr.BaseRepo == nil { | 	if pr.ProtectedBranch == nil { | ||||||
| 		if pr.BaseRepoID == 0 { | 		if pr.BaseRepo == nil { | ||||||
| 			return nil | 			if pr.BaseRepoID == 0 { | ||||||
| 		} | 				return nil | ||||||
| 		pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) | 			} | ||||||
| 		if err != nil { | 			pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) | ||||||
| 			return | 			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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,16 +11,16 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // 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) | ||||||
| 
 | 
 | ||||||
|  | @ -30,92 +30,101 @@ func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit st | ||||||
| 	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 { | 			if err != nil { | ||||||
| 				return false, "" | 				return false, "", err | ||||||
|  | 			} | ||||||
|  | 			if len(keys) == 0 { | ||||||
|  | 				return false, "", &ErrWontSign{pubkey} | ||||||
| 			} | 			} | ||||||
| 		case twofa: | 		case twofa: | ||||||
| 			twofa, err := GetTwoFactorByUID(u.ID) | 			twofaModel, err := GetTwoFactorByUID(u.ID) | ||||||
| 			if err != nil || twofa == nil { | 			if err != nil { | ||||||
| 				return false, "" | 				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 { | 			if err != nil { | ||||||
| 				return false, "" | 				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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ const ( | ||||||
| 	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 { | ||||||
|  | @ -95,122 +96,140 @@ func PublicSigningKey(repoPath string) (string, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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 { | 			if err != nil { | ||||||
| 				return false, "" | 				return false, "", err | ||||||
|  | 			} | ||||||
|  | 			if len(keys) == 0 { | ||||||
|  | 				return false, "", &ErrWontSign{pubkey} | ||||||
| 			} | 			} | ||||||
| 		case twofa: | 		case twofa: | ||||||
| 			twofa, err := GetTwoFactorByUID(u.ID) | 			twofaModel, err := GetTwoFactorByUID(u.ID) | ||||||
| 			if err != nil || twofa == nil { | 			if err != nil { | ||||||
| 				return false, "" | 				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 { | 			if err != nil { | ||||||
| 				return false, "" | 				return false, "", err | ||||||
|  | 			} | ||||||
|  | 			if len(keys) == 0 { | ||||||
|  | 				return false, "", &ErrWontSign{pubkey} | ||||||
| 			} | 			} | ||||||
| 		case twofa: | 		case twofa: | ||||||
| 			twofa, err := GetTwoFactorByUID(u.ID) | 			twofaModel, err := GetTwoFactorByUID(u.ID) | ||||||
| 			if err != nil || twofa == nil { | 			if err != nil { | ||||||
| 				return false, "" | 				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 { | 			if err != nil { | ||||||
| 				return false, "" | 				return false, "", err | ||||||
|  | 			} | ||||||
|  | 			if len(keys) == 0 { | ||||||
|  | 				return false, "", &ErrWontSign{pubkey} | ||||||
| 			} | 			} | ||||||
| 		case twofa: | 		case twofa: | ||||||
| 			twofa, err := GetTwoFactorByUID(u.ID) | 			twofaModel, err := GetTwoFactorByUID(u.ID) | ||||||
| 			if err != nil || twofa == nil { | 			if err != nil { | ||||||
| 				return false, "" | 				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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -173,6 +173,7 @@ type ProtectBranchForm struct { | ||||||
| 	ApprovalsWhitelistTeams  string | 	ApprovalsWhitelistTeams  string | ||||||
| 	BlockOnRejectedReviews   bool | 	BlockOnRejectedReviews   bool | ||||||
| 	DismissStaleApprovals    bool | 	DismissStaleApprovals    bool | ||||||
|  | 	RequireSignedCommits     bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Validate validates the fields
 | // Validate validates the fields
 | ||||||
|  |  | ||||||
|  | @ -74,14 +74,57 @@ func RepoMustNotBeArchived() macaron.Handler { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // 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) { | func (r *Repository) CanCommitToBranch(doer *models.User) (CanCommitToBranchResults, error) { | ||||||
| 	protectedBranch, err := r.Repository.IsProtectedBranchForPush(r.BranchName, doer) | 	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.
 | ||||||
|  |  | ||||||
|  | @ -97,7 +97,7 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura | ||||||
| 
 | 
 | ||||||
| // 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 | ||||||
|  | @ -135,7 +135,11 @@ func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time. | ||||||
| 	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 { | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ type Commit struct { | ||||||
| 	CommitMessage string | 	CommitMessage string | ||||||
| 	Signature     *CommitGPGSignature | 	Signature     *CommitGPGSignature | ||||||
| 
 | 
 | ||||||
| 	parents        []SHA1 // SHA1 strings
 | 	Parents        []SHA1 // SHA1 strings
 | ||||||
| 	submoduleCache *ObjectCache | 	submoduleCache *ObjectCache | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -94,7 +94,7 @@ func convertCommit(c *object.Commit) *Commit { | ||||||
| 		Committer:     &c.Committer, | 		Committer:     &c.Committer, | ||||||
| 		Author:        &c.Author, | 		Author:        &c.Author, | ||||||
| 		Signature:     convertPGPSignature(c), | 		Signature:     convertPGPSignature(c), | ||||||
| 		parents:       c.ParentHashes, | 		Parents:       c.ParentHashes, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -111,10 +111,10 @@ func (c *Commit) Summary() string { | ||||||
| // 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.
 | ||||||
|  | @ -133,7 +133,7 @@ func (c *Commit) Parent(n int) (*Commit, error) { | ||||||
| // 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) { | ||||||
|  |  | ||||||
							
								
								
									
										108
									
								
								modules/git/commit_reader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								modules/git/commit_reader.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,108 @@ | ||||||
|  | // 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() | ||||||
|  | } | ||||||
|  | @ -55,9 +55,26 @@ func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepo | ||||||
| 				BranchName: opts.NewBranch, | 				BranchName: opts.NewBranch, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected { | 	} else { | ||||||
| 		return nil, models.ErrUserCannotCommit{ | 		protectedBranch, err := repo.GetBranchProtection(opts.OldBranch) | ||||||
| 			UserName: doer.LowerName, | 		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, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -219,7 +219,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models | ||||||
| 
 | 
 | ||||||
| 	// 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", ">=") { | ||||||
|  | @ -268,7 +268,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { | ||||||
| 	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 { | ||||||
|  | @ -276,6 +276,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { | ||||||
| 				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) | ||||||
|  |  | ||||||
|  | @ -151,8 +151,27 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up | ||||||
| 		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 { | 	} else { | ||||||
| 		return nil, models.ErrUserCannotCommit{UserName: doer.LowerName} | 		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
 | ||||||
|  |  | ||||||
|  | @ -130,7 +130,7 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User) (er | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	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", ">=") { | ||||||
|  |  | ||||||
|  | @ -748,6 +748,7 @@ editor.name_your_file = Name your file… | ||||||
| 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' | ||||||
|  | @ -780,6 +781,9 @@ editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v | ||||||
| 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 | ||||||
|  | @ -1068,6 +1072,7 @@ pulls.merge_pull_request = Merge Pull Request | ||||||
| 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 | ||||||
|  | @ -1109,6 +1114,19 @@ milestones.filter_sort.most_complete = Most complete | ||||||
| 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. | ||||||
| 
 | 
 | ||||||
|  | @ -1416,6 +1434,8 @@ settings.protect_approvals_whitelist_users = Whitelisted reviewers: | ||||||
| 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. | ||||||
|  |  | ||||||
|  | @ -639,6 +639,15 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -6,7 +6,10 @@ | ||||||
| package private | package private | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -18,10 +21,101 @@ import ( | ||||||
| 	"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") | ||||||
|  | @ -35,6 +129,30 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | ||||||
| 		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] | ||||||
|  | @ -51,7 +169,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | ||||||
| 			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{}{ | ||||||
|  | @ -62,20 +180,6 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | ||||||
| 
 | 
 | ||||||
| 			// 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) | ||||||
|  | @ -92,6 +196,27 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { | ||||||
| 
 | 
 | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			// 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) | ||||||
|  |  | ||||||
|  | @ -36,12 +36,13 @@ const ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 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 | 	ctx.Data["CanCommitToBranch"] = canCommitToBranch | ||||||
| 	return canCommit | 
 | ||||||
|  | 	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
 | ||||||
|  |  | ||||||
|  | @ -971,6 +971,21 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 			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 && | ||||||
|  |  | ||||||
|  | @ -246,6 +246,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm) | ||||||
| 		} | 		} | ||||||
| 		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, | ||||||
|  |  | ||||||
|  | @ -158,7 +158,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor | ||||||
| 	// 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", ">=") { | ||||||
|  | @ -470,6 +470,21 @@ func getDiffTree(repoPath, baseBranch, headBranch string) (string, error) { | ||||||
| 	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() { | ||||||
|  |  | ||||||
|  | @ -162,7 +162,7 @@ func TestPatch(pr *models.PullRequest) error { | ||||||
| 		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: " | ||||||
|  | @ -199,6 +199,7 @@ func TestPatch(pr *models.PullRequest) error { | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 				_ = stderrReader.Close() | 				_ = stderrReader.Close() | ||||||
|  | 				return nil | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -184,7 +184,7 @@ func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, new | ||||||
| 		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 { | ||||||
|  | @ -298,7 +298,7 @@ func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) | ||||||
| 		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 { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,11 @@ | ||||||
| <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> | ||||||
|  | @ -10,11 +14,20 @@ | ||||||
| 		</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> | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ | ||||||
| 	{{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> | ||||||
|  | @ -93,49 +94,69 @@ | ||||||
| 				</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))}}	 | 				{{$notAllOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .RequireSigned (not .WillSign)) (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}} | ||||||
| 				{{if or $.IsRepoAdmin (not $notAllOk)}} | 				{{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}} | ||||||
|  | @ -282,6 +303,11 @@ | ||||||
| 						<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> | ||||||
|  |  | ||||||
|  | @ -210,7 +210,7 @@ | ||||||
| 							<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}}> | ||||||
|  | @ -218,6 +218,13 @@ | ||||||
| 							<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> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -652,6 +652,9 @@ | ||||||
|                     margin-left: 10px; |                     margin-left: 10px; | ||||||
|                     margin-top: 10px; |                     margin-top: 10px; | ||||||
|                 } |                 } | ||||||
|  |                 .icon-octicon { | ||||||
|  |                     padding-left: 2px; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             .review-item { |             .review-item { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue