Skip to content

Support choose email when creating a commit via web UI #33432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions models/user/email_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool {

return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}

func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) {
emails := make([]string, 0, 2)
if err := db.GetEngine(ctx).Table("email_address").Select("email").
Where("uid=? AND is_activated=?", uid, true).Asc("id").
Find(&emails); err != nil {
return nil, err
}
return emails, nil
}
2 changes: 1 addition & 1 deletion models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string {
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
}

// GetEmail returns an noreply email, if the user has set to keep his
// GetEmail returns a noreply email, if the user has set to keep his
// email address private, otherwise the primary email address.
func (u *User) GetEmail() string {
if u.KeepEmailPrivate {
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,8 @@ editor.new_branch_name_desc = New branch name…
editor.cancel = Cancel
editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s".
editor.commit_email = Commit email
editor.invalid_commit_email = The email for the commit is invalid.
editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
editor.branch_already_exists = Branch "%s" already exists in this repository.
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
Expand Down
32 changes: 16 additions & 16 deletions routers/api/v1/repo/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,12 @@ func ChangeFiles(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
GitUserName: apiOpts.Committer.Name,
GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
Name: apiOpts.Author.Name,
Email: apiOpts.Author.Email,
GitUserName: apiOpts.Author.Name,
GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
Expand Down Expand Up @@ -586,12 +586,12 @@ func CreateFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
GitUserName: apiOpts.Committer.Name,
GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
Name: apiOpts.Author.Name,
Email: apiOpts.Author.Email,
GitUserName: apiOpts.Author.Name,
GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
Expand Down Expand Up @@ -689,12 +689,12 @@ func UpdateFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
GitUserName: apiOpts.Committer.Name,
GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
Name: apiOpts.Author.Name,
Email: apiOpts.Author.Email,
GitUserName: apiOpts.Author.Name,
GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
Expand Down Expand Up @@ -848,12 +848,12 @@ func DeleteFile(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
GitUserName: apiOpts.Committer.Name,
GitUserEmail: apiOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
Name: apiOpts.Author.Name,
Email: apiOpts.Author.Email,
GitUserName: apiOpts.Author.Name,
GitUserEmail: apiOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: apiOpts.Dates.Author,
Expand Down
8 changes: 4 additions & 4 deletions routers/api/v1/repo/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ func ApplyDiffPatch(ctx *context.APIContext) {
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
GitUserName: apiOpts.Committer.Name,
GitUserEmail: apiOpts.Committer.Email,
},
Author: &files.IdentityOptions{
Name: apiOpts.Author.Name,
Email: apiOpts.Author.Email,
GitUserName: apiOpts.Author.Name,
GitUserEmail: apiOpts.Author.Email,
},
Dates: &files.CommitDateOptions{
Author: apiOpts.Dates.Author,
Expand Down
68 changes: 46 additions & 22 deletions routers/web/repo/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
Expand Down Expand Up @@ -102,10 +103,32 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
return treeNames, treePaths
}

func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsViewCode"] = true
func getCandidateEmailAddresses(ctx *context.Context) []string {
emails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
if err != nil {
log.Error("getCandidateEmailAddresses: GetActivatedEmailAddresses: %v", err)
}

if ctx.Doer.KeepEmailPrivate {
emails = append([]string{ctx.Doer.GetPlaceholderEmail()}, emails...)
}
return emails
}

func editFileCommon(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
ctx.Data["CommitCandidateEmails"] = getCandidateEmailAddresses(ctx)
ctx.Data["CommitDefaultEmail"] = ctx.Doer.GetEmail()
}

func editFile(ctx *context.Context, isNewFile bool) {
editFileCommon(ctx, isNewFile)
canCommit := renderCommitRights(ctx)

treePath := cleanUploadFileName(ctx.Repo.TreePath)
Expand Down Expand Up @@ -174,28 +197,19 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["FileContent"] = content
}
} else {
// Append filename from query, or empty string to allow user name the new file.
// Append filename from query, or empty string to allow username the new file.
treeNames = append(treeNames, fileName)
}

ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
if canCommit {
ctx.Data["commit_choice"] = frmCommitChoiceDirect
} else {
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
}
ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.Data["last_commit"] = ctx.Repo.CommitID
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)

ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)

ctx.HTML(http.StatusOK, tplEditFile)
}
Expand Down Expand Up @@ -224,36 +238,33 @@ func NewFile(ctx *context.Context) {
}

func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
editFileCommon(ctx, isNewFile)
ctx.Data["PageHasPosted"] = true

canCommit := renderCommitRights(ctx)
treeNames, treePaths := getParentTreeFields(form.TreePath)
branchName := ctx.Repo.BranchName
if form.CommitChoice == frmCommitChoiceNewBranch {
branchName = form.NewBranchName
}

ctx.Data["PageIsEdit"] = true
ctx.Data["PageHasPosted"] = true
ctx.Data["IsNewFile"] = isNewFile
ctx.Data["TreePath"] = form.TreePath
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
ctx.Data["FileContent"] = form.Content
ctx.Data["commit_summary"] = form.CommitSummary
ctx.Data["commit_message"] = form.CommitMessage
ctx.Data["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = form.NewBranchName
ctx.Data["last_commit"] = ctx.Repo.CommitID
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)

if ctx.HasError() {
ctx.HTML(http.StatusOK, tplEditFile)
return
}

// Cannot commit to a an existing branch if user doesn't have rights
// Cannot commit to an existing branch if user doesn't have rights
if branchName == ctx.Repo.BranchName && !canCommit {
ctx.Data["Err_NewBranchName"] = true
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
Expand All @@ -276,6 +287,17 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message += "\n\n" + form.CommitMessage
}

gitCommitter := &files_service.IdentityOptions{}
if form.CommitEmail != "" {
if util.SliceContainsString(getCandidateEmailAddresses(ctx), form.CommitEmail, true) {
gitCommitter.GitUserEmail = form.CommitEmail
} else {
ctx.Data["Err_CommitEmail"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
return
}
}

operation := "update"
if isNewFile {
operation = "create"
Expand All @@ -294,7 +316,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
},
},
Signoff: form.Signoff,
Signoff: form.Signoff,
Author: gitCommitter,
Committer: gitCommitter,
}); err != nil {
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
if git.IsErrNotExist(err) {
Expand Down
1 change: 1 addition & 0 deletions services/forms/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ type EditRepoFileForm struct {
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
LastCommit string
Signoff bool
CommitEmail string
}

// Validate validates the fields
Expand Down
10 changes: 7 additions & 3 deletions services/packages/cargo/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"io"
"path"
"strconv"
"time"

packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
Expand Down Expand Up @@ -296,8 +295,13 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re
return err
}

now := time.Now()
commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now)
commitOpts := &files_service.CommitTreeUserOptions{
ParentCommitID: lastCommitID,
TreeHash: treeHash,
CommitMessage: commitMessage,
DoerUser: doer,
}
commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return err
}
Expand Down
21 changes: 14 additions & 7 deletions services/repository/files/cherry_pick.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
}

// CherryPick cherrypicks or reverts a commit to the given repository
// CherryPick cherry-picks or reverts a commit to the given repository
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
if err := opts.Validate(ctx, repo, doer); err != nil {
return nil, err
}
message := strings.TrimSpace(opts.Message)

author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)

t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
Expand Down Expand Up @@ -112,12 +110,21 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
}

// Now commit the tree
var commitHash string
commitOpts := &CommitTreeUserOptions{
ParentCommitID: "HEAD",
TreeHash: treeHash,
CommitMessage: message,
SignOff: opts.Signoff,
DoerUser: doer,
AuthorIdentity: opts.Author,
AuthorTime: nil,
CommitterIdentity: opts.Committer,
CommitterTime: nil,
}
if opts.Dates != nil {
commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
} else {
commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
commitHash, err := t.CommitTree(commitOpts)
if err != nil {
return nil, err
}
Expand Down
46 changes: 0 additions & 46 deletions services/repository/files/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -111,51 +110,6 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap
return fileCommit, nil
}

// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_model.User) (authorUser, committerUser *user_model.User) {
// Committer and author are optional. If they are not the doer (not same email address)
// then we use bogus User objects for them to store their FullName and Email.
// If only one of the two are provided, we set both of them to it.
// If neither are provided, both are the doer.
if committer != nil && committer.Email != "" {
if doer != nil && strings.EqualFold(doer.Email, committer.Email) {
committerUser = doer // the committer is the doer, so will use their user object
if committer.Name != "" {
committerUser.FullName = committer.Name
}
} else {
committerUser = &user_model.User{
FullName: committer.Name,
Email: committer.Email,
}
}
}
if author != nil && author.Email != "" {
if doer != nil && strings.EqualFold(doer.Email, author.Email) {
authorUser = doer // the author is the doer, so will use their user object
if authorUser.Name != "" {
authorUser.FullName = author.Name
}
} else {
authorUser = &user_model.User{
FullName: author.Name,
Email: author.Email,
}
}
}
if authorUser == nil {
if committerUser != nil {
authorUser = committerUser // No valid author was given so use the committer
} else if doer != nil {
authorUser = doer // No valid author was given and no valid committer so use the doer
}
}
if committerUser == nil {
committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
}
return authorUser, committerUser
}

// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
Expand Down
Loading