Skip to content

Commit 256b94e

Browse files
authored
Support choose email when creating a commit via web UI (#33432)
Initial PR for #24469
1 parent ac2d97c commit 256b94e

File tree

19 files changed

+353
-179
lines changed

19 files changed

+353
-179
lines changed

models/user/email_address.go

+10
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool {
542542

543543
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
544544
}
545+
546+
func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) {
547+
emails := make([]string, 0, 2)
548+
if err := db.GetEngine(ctx).Table("email_address").Select("email").
549+
Where("uid=? AND is_activated=?", uid, true).Asc("id").
550+
Find(&emails); err != nil {
551+
return nil, err
552+
}
553+
return emails, nil
554+
}

models/user/user.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string {
214214
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
215215
}
216216

217-
// GetEmail returns an noreply email, if the user has set to keep his
217+
// GetEmail returns a noreply email, if the user has set to keep his
218218
// email address private, otherwise the primary email address.
219219
func (u *User) GetEmail() string {
220220
if u.KeepEmailPrivate {

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,8 @@ editor.new_branch_name_desc = New branch name…
13451345
editor.cancel = Cancel
13461346
editor.filename_cannot_be_empty = The filename cannot be empty.
13471347
editor.filename_is_invalid = The filename is invalid: "%s".
1348+
editor.commit_email = Commit email
1349+
editor.invalid_commit_email = The email for the commit is invalid.
13481350
editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
13491351
editor.branch_already_exists = Branch "%s" already exists in this repository.
13501352
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.

routers/api/v1/repo/file.go

+16-16
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,12 @@ func ChangeFiles(ctx *context.APIContext) {
489489
OldBranch: apiOpts.BranchName,
490490
NewBranch: apiOpts.NewBranchName,
491491
Committer: &files_service.IdentityOptions{
492-
Name: apiOpts.Committer.Name,
493-
Email: apiOpts.Committer.Email,
492+
GitUserName: apiOpts.Committer.Name,
493+
GitUserEmail: apiOpts.Committer.Email,
494494
},
495495
Author: &files_service.IdentityOptions{
496-
Name: apiOpts.Author.Name,
497-
Email: apiOpts.Author.Email,
496+
GitUserName: apiOpts.Author.Name,
497+
GitUserEmail: apiOpts.Author.Email,
498498
},
499499
Dates: &files_service.CommitDateOptions{
500500
Author: apiOpts.Dates.Author,
@@ -586,12 +586,12 @@ func CreateFile(ctx *context.APIContext) {
586586
OldBranch: apiOpts.BranchName,
587587
NewBranch: apiOpts.NewBranchName,
588588
Committer: &files_service.IdentityOptions{
589-
Name: apiOpts.Committer.Name,
590-
Email: apiOpts.Committer.Email,
589+
GitUserName: apiOpts.Committer.Name,
590+
GitUserEmail: apiOpts.Committer.Email,
591591
},
592592
Author: &files_service.IdentityOptions{
593-
Name: apiOpts.Author.Name,
594-
Email: apiOpts.Author.Email,
593+
GitUserName: apiOpts.Author.Name,
594+
GitUserEmail: apiOpts.Author.Email,
595595
},
596596
Dates: &files_service.CommitDateOptions{
597597
Author: apiOpts.Dates.Author,
@@ -689,12 +689,12 @@ func UpdateFile(ctx *context.APIContext) {
689689
OldBranch: apiOpts.BranchName,
690690
NewBranch: apiOpts.NewBranchName,
691691
Committer: &files_service.IdentityOptions{
692-
Name: apiOpts.Committer.Name,
693-
Email: apiOpts.Committer.Email,
692+
GitUserName: apiOpts.Committer.Name,
693+
GitUserEmail: apiOpts.Committer.Email,
694694
},
695695
Author: &files_service.IdentityOptions{
696-
Name: apiOpts.Author.Name,
697-
Email: apiOpts.Author.Email,
696+
GitUserName: apiOpts.Author.Name,
697+
GitUserEmail: apiOpts.Author.Email,
698698
},
699699
Dates: &files_service.CommitDateOptions{
700700
Author: apiOpts.Dates.Author,
@@ -848,12 +848,12 @@ func DeleteFile(ctx *context.APIContext) {
848848
OldBranch: apiOpts.BranchName,
849849
NewBranch: apiOpts.NewBranchName,
850850
Committer: &files_service.IdentityOptions{
851-
Name: apiOpts.Committer.Name,
852-
Email: apiOpts.Committer.Email,
851+
GitUserName: apiOpts.Committer.Name,
852+
GitUserEmail: apiOpts.Committer.Email,
853853
},
854854
Author: &files_service.IdentityOptions{
855-
Name: apiOpts.Author.Name,
856-
Email: apiOpts.Author.Email,
855+
GitUserName: apiOpts.Author.Name,
856+
GitUserEmail: apiOpts.Author.Email,
857857
},
858858
Dates: &files_service.CommitDateOptions{
859859
Author: apiOpts.Dates.Author,

routers/api/v1/repo/patch.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ func ApplyDiffPatch(ctx *context.APIContext) {
5858
OldBranch: apiOpts.BranchName,
5959
NewBranch: apiOpts.NewBranchName,
6060
Committer: &files.IdentityOptions{
61-
Name: apiOpts.Committer.Name,
62-
Email: apiOpts.Committer.Email,
61+
GitUserName: apiOpts.Committer.Name,
62+
GitUserEmail: apiOpts.Committer.Email,
6363
},
6464
Author: &files.IdentityOptions{
65-
Name: apiOpts.Author.Name,
66-
Email: apiOpts.Author.Email,
65+
GitUserName: apiOpts.Author.Name,
66+
GitUserEmail: apiOpts.Author.Email,
6767
},
6868
Dates: &files.CommitDateOptions{
6969
Author: apiOpts.Dates.Author,

routers/web/repo/editor.go

+46-22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
git_model "code.gitea.io/gitea/models/git"
1414
repo_model "code.gitea.io/gitea/models/repo"
1515
"code.gitea.io/gitea/models/unit"
16+
user_model "code.gitea.io/gitea/models/user"
1617
"code.gitea.io/gitea/modules/charset"
1718
"code.gitea.io/gitea/modules/git"
1819
"code.gitea.io/gitea/modules/json"
@@ -102,10 +103,32 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
102103
return treeNames, treePaths
103104
}
104105

105-
func editFile(ctx *context.Context, isNewFile bool) {
106-
ctx.Data["PageIsViewCode"] = true
106+
func getCandidateEmailAddresses(ctx *context.Context) []string {
107+
emails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
108+
if err != nil {
109+
log.Error("getCandidateEmailAddresses: GetActivatedEmailAddresses: %v", err)
110+
}
111+
112+
if ctx.Doer.KeepEmailPrivate {
113+
emails = append([]string{ctx.Doer.GetPlaceholderEmail()}, emails...)
114+
}
115+
return emails
116+
}
117+
118+
func editFileCommon(ctx *context.Context, isNewFile bool) {
107119
ctx.Data["PageIsEdit"] = true
108120
ctx.Data["IsNewFile"] = isNewFile
121+
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
122+
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
123+
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
124+
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
125+
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
126+
ctx.Data["CommitCandidateEmails"] = getCandidateEmailAddresses(ctx)
127+
ctx.Data["CommitDefaultEmail"] = ctx.Doer.GetEmail()
128+
}
129+
130+
func editFile(ctx *context.Context, isNewFile bool) {
131+
editFileCommon(ctx, isNewFile)
109132
canCommit := renderCommitRights(ctx)
110133

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

181204
ctx.Data["TreeNames"] = treeNames
182205
ctx.Data["TreePaths"] = treePaths
183-
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
184206
ctx.Data["commit_summary"] = ""
185207
ctx.Data["commit_message"] = ""
186-
if canCommit {
187-
ctx.Data["commit_choice"] = frmCommitChoiceDirect
188-
} else {
189-
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
190-
}
208+
ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
191209
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
192210
ctx.Data["last_commit"] = ctx.Repo.CommitID
193-
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
194-
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
195-
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
196211

197-
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
198-
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
212+
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
199213

200214
ctx.HTML(http.StatusOK, tplEditFile)
201215
}
@@ -224,36 +238,33 @@ func NewFile(ctx *context.Context) {
224238
}
225239

226240
func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
241+
editFileCommon(ctx, isNewFile)
242+
ctx.Data["PageHasPosted"] = true
243+
227244
canCommit := renderCommitRights(ctx)
228245
treeNames, treePaths := getParentTreeFields(form.TreePath)
229246
branchName := ctx.Repo.BranchName
230247
if form.CommitChoice == frmCommitChoiceNewBranch {
231248
branchName = form.NewBranchName
232249
}
233250

234-
ctx.Data["PageIsEdit"] = true
235-
ctx.Data["PageHasPosted"] = true
236-
ctx.Data["IsNewFile"] = isNewFile
237251
ctx.Data["TreePath"] = form.TreePath
238252
ctx.Data["TreeNames"] = treeNames
239253
ctx.Data["TreePaths"] = treePaths
240-
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
241254
ctx.Data["FileContent"] = form.Content
242255
ctx.Data["commit_summary"] = form.CommitSummary
243256
ctx.Data["commit_message"] = form.CommitMessage
244257
ctx.Data["commit_choice"] = form.CommitChoice
245258
ctx.Data["new_branch_name"] = form.NewBranchName
246259
ctx.Data["last_commit"] = ctx.Repo.CommitID
247-
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
248-
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
249260
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
250261

251262
if ctx.HasError() {
252263
ctx.HTML(http.StatusOK, tplEditFile)
253264
return
254265
}
255266

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

290+
gitCommitter := &files_service.IdentityOptions{}
291+
if form.CommitEmail != "" {
292+
if util.SliceContainsString(getCandidateEmailAddresses(ctx), form.CommitEmail, true) {
293+
gitCommitter.GitUserEmail = form.CommitEmail
294+
} else {
295+
ctx.Data["Err_CommitEmail"] = true
296+
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
297+
return
298+
}
299+
}
300+
279301
operation := "update"
280302
if isNewFile {
281303
operation = "create"
@@ -294,7 +316,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
294316
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
295317
},
296318
},
297-
Signoff: form.Signoff,
319+
Signoff: form.Signoff,
320+
Author: gitCommitter,
321+
Committer: gitCommitter,
298322
}); err != nil {
299323
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
300324
if git.IsErrNotExist(err) {

services/forms/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ type EditRepoFileForm struct {
720720
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
721721
LastCommit string
722722
Signoff bool
723+
CommitEmail string
723724
}
724725

725726
// Validate validates the fields

services/packages/cargo/index.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"io"
1212
"path"
1313
"strconv"
14-
"time"
1514

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

299-
now := time.Now()
300-
commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now)
298+
commitOpts := &files_service.CommitTreeUserOptions{
299+
ParentCommitID: lastCommitID,
300+
TreeHash: treeHash,
301+
CommitMessage: commitMessage,
302+
DoerUser: doer,
303+
}
304+
commitHash, err := t.CommitTree(commitOpts)
301305
if err != nil {
302306
return err
303307
}

services/repository/files/cherry_pick.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
3232
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
3333
}
3434

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

42-
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
43-
4442
t, err := NewTemporaryUploadRepository(ctx, repo)
4543
if err != nil {
4644
log.Error("NewTemporaryUploadRepository failed: %v", err)
@@ -112,12 +110,21 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
112110
}
113111

114112
// Now commit the tree
115-
var commitHash string
113+
commitOpts := &CommitTreeUserOptions{
114+
ParentCommitID: "HEAD",
115+
TreeHash: treeHash,
116+
CommitMessage: message,
117+
SignOff: opts.Signoff,
118+
DoerUser: doer,
119+
AuthorIdentity: opts.Author,
120+
AuthorTime: nil,
121+
CommitterIdentity: opts.Committer,
122+
CommitterTime: nil,
123+
}
116124
if opts.Dates != nil {
117-
commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
118-
} else {
119-
commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
125+
commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
120126
}
127+
commitHash, err := t.CommitTree(commitOpts)
121128
if err != nil {
122129
return nil, err
123130
}

services/repository/files/file.go

-46
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"time"
1212

1313
repo_model "code.gitea.io/gitea/models/repo"
14-
user_model "code.gitea.io/gitea/models/user"
1514
"code.gitea.io/gitea/modules/git"
1615
api "code.gitea.io/gitea/modules/structs"
1716
"code.gitea.io/gitea/modules/util"
@@ -111,51 +110,6 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap
111110
return fileCommit, nil
112111
}
113112

114-
// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
115-
func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_model.User) (authorUser, committerUser *user_model.User) {
116-
// Committer and author are optional. If they are not the doer (not same email address)
117-
// then we use bogus User objects for them to store their FullName and Email.
118-
// If only one of the two are provided, we set both of them to it.
119-
// If neither are provided, both are the doer.
120-
if committer != nil && committer.Email != "" {
121-
if doer != nil && strings.EqualFold(doer.Email, committer.Email) {
122-
committerUser = doer // the committer is the doer, so will use their user object
123-
if committer.Name != "" {
124-
committerUser.FullName = committer.Name
125-
}
126-
} else {
127-
committerUser = &user_model.User{
128-
FullName: committer.Name,
129-
Email: committer.Email,
130-
}
131-
}
132-
}
133-
if author != nil && author.Email != "" {
134-
if doer != nil && strings.EqualFold(doer.Email, author.Email) {
135-
authorUser = doer // the author is the doer, so will use their user object
136-
if authorUser.Name != "" {
137-
authorUser.FullName = author.Name
138-
}
139-
} else {
140-
authorUser = &user_model.User{
141-
FullName: author.Name,
142-
Email: author.Email,
143-
}
144-
}
145-
}
146-
if authorUser == nil {
147-
if committerUser != nil {
148-
authorUser = committerUser // No valid author was given so use the committer
149-
} else if doer != nil {
150-
authorUser = doer // No valid author was given and no valid committer so use the doer
151-
}
152-
}
153-
if committerUser == nil {
154-
committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
155-
}
156-
return authorUser, committerUser
157-
}
158-
159113
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
160114
type ErrFilenameInvalid struct {
161115
Path string

0 commit comments

Comments
 (0)