Skip to content

Commit 5d11ccc

Browse files
zeripathlafriks
andauthored
Handle push rejection message in Merge & Web Editor (#10373) (#10497)
Backport #10373 * Handle push rejection message in Merge * Fix sanitize, adjust message handling * Handle push-rejection in webeditor CRUD too Co-authored-by: Lauris BH <[email protected]> Co-authored-by: Lauris BH <[email protected]>
1 parent 93860af commit 5d11ccc

File tree

8 files changed

+137
-19
lines changed

8 files changed

+137
-19
lines changed

models/error.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package models
77

88
import (
99
"fmt"
10+
"strings"
1011

1112
"code.gitea.io/gitea/modules/git"
1213
)
@@ -1370,6 +1371,53 @@ func (err ErrMergePushOutOfDate) Error() string {
13701371
return fmt.Sprintf("Merge PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
13711372
}
13721373

1374+
// ErrPushRejected represents an error if merging fails due to rejection from a hook
1375+
type ErrPushRejected struct {
1376+
Style MergeStyle
1377+
Message string
1378+
StdOut string
1379+
StdErr string
1380+
Err error
1381+
}
1382+
1383+
// IsErrPushRejected checks if an error is a ErrPushRejected.
1384+
func IsErrPushRejected(err error) bool {
1385+
_, ok := err.(ErrPushRejected)
1386+
return ok
1387+
}
1388+
1389+
func (err ErrPushRejected) Error() string {
1390+
return fmt.Sprintf("Merge PushRejected Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
1391+
}
1392+
1393+
// GenerateMessage generates the remote message from the stderr
1394+
func (err *ErrPushRejected) GenerateMessage() {
1395+
messageBuilder := &strings.Builder{}
1396+
i := strings.Index(err.StdErr, "remote: ")
1397+
if i < 0 {
1398+
err.Message = ""
1399+
return
1400+
}
1401+
for {
1402+
if len(err.StdErr) <= i+8 {
1403+
break
1404+
}
1405+
if err.StdErr[i:i+8] != "remote: " {
1406+
break
1407+
}
1408+
i += 8
1409+
nl := strings.IndexByte(err.StdErr[i:], '\n')
1410+
if nl > 0 {
1411+
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
1412+
i = i + nl + 1
1413+
} else {
1414+
messageBuilder.WriteString(err.StdErr[i:])
1415+
i = len(err.StdErr)
1416+
}
1417+
}
1418+
err.Message = strings.TrimSpace(messageBuilder.String())
1419+
}
1420+
13731421
// ErrRebaseConflicts represents an error if rebase fails with a conflict
13741422
type ErrRebaseConflicts struct {
13751423
Style MergeStyle

modules/repofiles/temp_repo.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,30 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models
242242
func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, branch string) error {
243243
// Because calls hooks we need to pass in the environment
244244
env := models.PushingEnvironment(doer, t.repo)
245-
246-
if _, err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirWithEnv(t.basePath, env); err != nil {
247-
log.Error("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
248-
t.repo.FullName(), t.basePath, err)
245+
stdout := &strings.Builder{}
246+
stderr := &strings.Builder{}
247+
248+
if err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirTimeoutEnvPipeline(env, -1, t.basePath, stdout, stderr); err != nil {
249+
errString := stderr.String()
250+
if strings.Contains(errString, "non-fast-forward") {
251+
return models.ErrMergePushOutOfDate{
252+
StdOut: stdout.String(),
253+
StdErr: errString,
254+
Err: err,
255+
}
256+
} else if strings.Contains(errString, "! [remote rejected]") {
257+
log.Error("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
258+
t.repo.FullName(), t.basePath, stdout, errString, err)
259+
err := models.ErrPushRejected{
260+
StdOut: stdout.String(),
261+
StdErr: errString,
262+
Err: err,
263+
}
264+
err.GenerateMessage()
265+
return err
266+
}
267+
log.Error("Unable to push back to repo from temporary repo: %s (%s)\nStdout: %s\nError: %v",
268+
t.repo.FullName(), t.basePath, stdout, err)
249269
return fmt.Errorf("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
250270
t.repo.FullName(), t.basePath, err)
251271
}

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,8 @@ editor.commit_empty_file_header = Commit an empty file
776776
editor.commit_empty_file_text = The file you're about commit is empty. Proceed?
777777
editor.no_changes_to_show = There are no changes to show.
778778
editor.fail_to_update_file = Failed to update/create file '%s' with error: %v
779+
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check githooks.
780+
editor.push_rejected = The change was rejected by the server with the following message:<br>%s<br> Please check githooks.
779781
editor.add_subdir = Add a directory…
780782
editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v
781783
editor.upload_file_is_locked = File '%s' is locked by %s.
@@ -1075,6 +1077,8 @@ pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<
10751077
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
10761078
pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy
10771079
pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
1080+
pulls.push_rejected = Merge Failed: The push was rejected with the following message:<br>%s<br>Review the githooks for this repository
1081+
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the githooks for this repository
10781082
pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
10791083
pulls.status_checking = Some checks are pending
10801084
pulls.status_checks_success = All checks were successful

routers/api/v1/repo/pull.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,14 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
645645
} else if models.IsErrMergePushOutOfDate(err) {
646646
ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
647647
return
648+
} else if models.IsErrPushRejected(err) {
649+
errPushRej := err.(models.ErrPushRejected)
650+
if len(errPushRej.Message) == 0 {
651+
ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
652+
return
653+
}
654+
ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
655+
return
648656
}
649657
ctx.Error(http.StatusInternalServerError, "Merge", err)
650658
return

routers/repo/editor.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/modules/setting"
2323
"code.gitea.io/gitea/modules/upload"
2424
"code.gitea.io/gitea/modules/util"
25+
"code.gitea.io/gitea/routers/utils"
2526
)
2627

2728
const (
@@ -262,10 +263,17 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
262263
} else {
263264
ctx.Error(500, err.Error())
264265
}
265-
} else if models.IsErrCommitIDDoesNotMatch(err) {
266+
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
266267
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
268+
} else if models.IsErrPushRejected(err) {
269+
errPushRej := err.(models.ErrPushRejected)
270+
if len(errPushRej.Message) == 0 {
271+
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
272+
} else {
273+
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplEditFile, &form)
274+
}
267275
} else {
268-
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
276+
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, utils.SanitizeFlashErrorString(err.Error())), tplEditFile, &form)
269277
}
270278
}
271279

@@ -426,8 +434,15 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
426434
} else {
427435
ctx.Error(500, err.Error())
428436
}
429-
} else if models.IsErrCommitIDDoesNotMatch(err) {
437+
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
430438
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
439+
} else if models.IsErrPushRejected(err) {
440+
errPushRej := err.(models.ErrPushRejected)
441+
if len(errPushRej.Message) == 0 {
442+
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
443+
} else {
444+
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplDeleteFile, &form)
445+
}
431446
} else {
432447
ctx.ServerError("DeleteRepoFile", err)
433448
}

routers/repo/pull.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"container/list"
1111
"crypto/subtle"
1212
"fmt"
13-
"html"
1413
"net/http"
1514
"path"
1615
"strings"
@@ -25,6 +24,7 @@ import (
2524
"code.gitea.io/gitea/modules/repofiles"
2625
"code.gitea.io/gitea/modules/setting"
2726
"code.gitea.io/gitea/modules/util"
27+
"code.gitea.io/gitea/routers/utils"
2828
"code.gitea.io/gitea/services/gitdiff"
2929
pull_service "code.gitea.io/gitea/services/pull"
3030
repo_service "code.gitea.io/gitea/services/repository"
@@ -663,27 +663,18 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
663663
}
664664

665665
if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
666-
sanitize := func(x string) string {
667-
runes := []rune(x)
668-
669-
if len(runes) > 512 {
670-
x = "..." + string(runes[len(runes)-512:])
671-
}
672-
673-
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
674-
}
675666
if models.IsErrInvalidMergeStyle(err) {
676667
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
677668
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
678669
return
679670
} else if models.IsErrMergeConflicts(err) {
680671
conflictError := err.(models.ErrMergeConflicts)
681-
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
672+
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
682673
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
683674
return
684675
} else if models.IsErrRebaseConflicts(err) {
685676
conflictError := err.(models.ErrRebaseConflicts)
686-
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", sanitize(conflictError.CommitSHA), sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
677+
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA), utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
687678
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
688679
return
689680
} else if models.IsErrMergeUnrelatedHistories(err) {
@@ -696,6 +687,17 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
696687
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
697688
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
698689
return
690+
} else if models.IsErrPushRejected(err) {
691+
log.Debug("MergePushRejected error: %v", err)
692+
pushrejErr := err.(models.ErrPushRejected)
693+
message := pushrejErr.Message
694+
if len(message) == 0 {
695+
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
696+
} else {
697+
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected", utils.SanitizeFlashErrorString(pushrejErr.Message)))
698+
}
699+
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
700+
return
699701
}
700702
ctx.ServerError("Merge", err)
701703
return

routers/utils/utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package utils
66

77
import (
8+
"html"
89
"strings"
910
)
1011

@@ -34,3 +35,14 @@ func IsValidSlackChannel(channelName string) bool {
3435

3536
return true
3637
}
38+
39+
// SanitizeFlashErrorString will sanitize a flash error string
40+
func SanitizeFlashErrorString(x string) string {
41+
runes := []rune(x)
42+
43+
if len(runes) > 512 {
44+
x = "..." + string(runes[len(runes)-512:])
45+
}
46+
47+
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
48+
}

services/pull/merge.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
335335
StdErr: errbuf.String(),
336336
Err: err,
337337
}
338+
} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
339+
err := models.ErrPushRejected{
340+
Style: mergeStyle,
341+
StdOut: outbuf.String(),
342+
StdErr: errbuf.String(),
343+
Err: err,
344+
}
345+
err.GenerateMessage()
346+
return err
338347
}
339348
return fmt.Errorf("git push: %s", errbuf.String())
340349
}

0 commit comments

Comments
 (0)