diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index b265cf475..2544b2939 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -967,6 +967,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi return } + // determine if the user viewing the pull request can edit the head branch + if ctx.Doer != nil && pull.HeadRepo != nil && !pull.HasMerged { + headRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer) + ctx.Data["SourceRepoLink"] = pull.HeadRepo.Link() + ctx.Data["HeadBranch"] = pull.HeadBranch + } + if ctx.IsSigned && ctx.Doer != nil { if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index be7c7e80f..05559fc9a 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -166,6 +166,9 @@ {{ctx.Locale.Tr "repo.diff.view_file"}} {{else}} {{ctx.Locale.Tr "repo.diff.view_file"}} + {{if and $.HeadBranchIsEditable (not $file.IsLFSFile)}} + {{ctx.Locale.Tr "repo.editor.edit_this_file"}} + {{end}} {{end}} {{end}} {{if $isReviewFile}} diff --git a/tests/integration/forgejo_git_test.go b/tests/integration/forgejo_git_test.go index f81521d73..bd0fb207e 100644 --- a/tests/integration/forgejo_git_test.go +++ b/tests/integration/forgejo_git_test.go @@ -134,6 +134,6 @@ func doActionsUserPR(ctx, doerCtx APITestContext, baseBranch, headBranch string) doerCtx.ExpectedCode = http.StatusCreated t.Run("AutoMergePR", doAPIAutoMergePullRequest(doerCtx, ctx.Username, ctx.Reponame, pr.Index)) // Ensure the PR page works - t.Run("EnsureCanSeePull", doEnsureCanSeePull(ctx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(ctx, pr, true)) } } diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 0c3a8616f..0afe9fa58 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -455,8 +455,19 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun assert.NoError(t, err) }) - // Ensure the PR page works - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + // Ensure the PR page works. + // For the base repository owner, the PR is not editable (maintainer edits are not enabled): + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) + // For the head repository owner, the PR is editable: + headSession := loginUser(t, "user2") + headToken := getTokenForLoggedInUser(t, headSession, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadUser) + headCtx := APITestContext{ + Session: headSession, + Token: headToken, + Username: baseCtx.Username, + Reponame: baseCtx.Reponame, + } + t.Run("EnsureCanSeePull", doEnsureCanSeePull(headCtx, pr, true)) // Then get the diff string var diffHash string @@ -470,7 +481,9 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun // Now: Merge the PR & make sure that doesn't break the PR page or change its diff t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + // for both users the PR is still visible but not editable anymore after it was merged + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(headCtx, pr, false)) t.Run("CheckPR", func(t *testing.T) { oldMergeBase := pr.MergeBase pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) @@ -481,12 +494,12 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) // Delete the head repository & make sure that doesn't break the PR page or change its diff t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) } } @@ -520,12 +533,19 @@ func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBr } } -func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) { +func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest, editable bool) func(t *testing.T) { return func(t *testing.T) { req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) ctx.Session.MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - ctx.Session.MakeRequest(t, req, http.StatusOK) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + editButtonCount := doc.doc.Find("div.diff-file-header-actions a[href*='/_edit/']").Length() + if editable { + assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + } else { + assert.Equal(t, 0, editButtonCount, "Expected not to find any buttons to edit files in PR diff view but there were some") + } req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) ctx.Session.MakeRequest(t, req, http.StatusOK) }