mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-28 14:43:52 +03:00
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