Skip to content

Commit fc53ca9

Browse files
committed
sync
1 parent 4fc626d commit fc53ca9

File tree

20 files changed

+361
-119
lines changed

20 files changed

+361
-119
lines changed

models/repo/repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
653653

654654
// CanEnableEditor returns true if repository meets the requirements of web editor.
655655
func (repo *Repository) CanEnableEditor() bool {
656-
return !repo.IsMirror
656+
return !repo.IsMirror && !repo.IsArchived
657657
}
658658

659659
// DescriptionHTML does special handles to description and return HTML string.

options/locale/locale_en-US.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,12 @@ editor.revert = Revert %s onto:
13981398
editor.failed_to_commit = Failed to commit changes.
13991399
editor.failed_to_commit_summary = Error Message:
14001400
1401+
editor.fork_create = Fork Repository to Propose Changes
1402+
editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request.
1403+
editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request.
1404+
editor.fork_not_editable = You have forked this repository but your fork is not editable.
1405+
editor.fork_failed_to_push_branch = Failed to push branch %s to your repository.
1406+
14011407
commits.desc = Browse source code change history.
14021408
commits.commits = Commits
14031409
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.

routers/web/repo/editor.go

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ package repo
55

66
import (
77
"bytes"
8+
gocontext "context"
9+
"errors"
810
"fmt"
911
"io"
1012
"net/http"
1113
"path"
1214
"strings"
1315

1416
git_model "code.gitea.io/gitea/models/git"
17+
repo_model "code.gitea.io/gitea/models/repo"
1518
"code.gitea.io/gitea/models/unit"
1619
"code.gitea.io/gitea/modules/charset"
1720
"code.gitea.io/gitea/modules/git"
1821
"code.gitea.io/gitea/modules/httplib"
22+
"code.gitea.io/gitea/modules/log"
1923
"code.gitea.io/gitea/modules/markup"
2024
"code.gitea.io/gitea/modules/setting"
2125
"code.gitea.io/gitea/modules/templates"
@@ -39,26 +43,36 @@ const (
3943
editorCommitChoiceNewBranch string = "commit-to-new-branch"
4044
)
4145

42-
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
46+
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
4347
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
4448
if cleanedTreePath != ctx.Repo.TreePath {
4549
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
4650
if ctx.Req.URL.RawQuery != "" {
4751
redirectTo += "?" + ctx.Req.URL.RawQuery
4852
}
4953
ctx.Redirect(redirectTo)
50-
return
54+
return nil
5155
}
5256

53-
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
57+
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
5458
if err != nil {
55-
ctx.ServerError("PrepareCommitFormBehaviors", err)
56-
return
59+
ctx.ServerError("PrepareCommitFormOptions", err)
60+
return nil
61+
}
62+
63+
if commitFormOptions.NeedFork {
64+
ForkToEdit(ctx)
65+
return nil
66+
}
67+
68+
if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
69+
ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
70+
ctx.NotFound(nil)
5771
}
5872

5973
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
6074
ctx.Data["TreePath"] = ctx.Repo.TreePath
61-
ctx.Data["CommitFormBehaviors"] = commitFormBehaviors
75+
ctx.Data["CommitFormOptions"] = commitFormOptions
6276

6377
// for online editor
6478
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
@@ -69,33 +83,47 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
6983
// form fields
7084
ctx.Data["commit_summary"] = ""
7185
ctx.Data["commit_message"] = ""
72-
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
73-
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
86+
ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
87+
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
7488
ctx.Data["last_commit"] = ctx.Repo.CommitID
89+
return commitFormOptions
7590
}
7691

7792
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
7893
// show the tree path fields in the "breadcrumb" and help users to edit the target tree path
7994
ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(treePath)
8095
}
8196

82-
type parsedEditorCommitForm[T any] struct {
83-
form T
84-
commonForm *forms.CommitCommonForm
85-
CommitFormBehaviors *context.CommitFormBehaviors
86-
TargetBranchName string
87-
GitCommitter *files_service.IdentityOptions
97+
type preparedEditorCommitForm[T any] struct {
98+
form T
99+
commonForm *forms.CommitCommonForm
100+
CommitFormOptions *context.CommitFormOptions
101+
TargetBranchName string
102+
GitCommitter *files_service.IdentityOptions
88103
}
89104

90-
func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
105+
func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
91106
commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
92107
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
93108
commitMessage += "\n\n" + body
94109
}
95110
return commitMessage
96111
}
97112

98-
func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] {
113+
func parseBaseRepoBranch(ctx gocontext.Context, input string) (*repo_model.Repository, string, error) {
114+
baseRepoFullName, baseBranchName, ok := strings.Cut(input, ":")
115+
if !ok {
116+
return nil, "", util.NewInvalidArgumentErrorf("invalid base repo name: %s", input)
117+
}
118+
baseOwnerName, baseRepoName, ok := strings.Cut(baseRepoFullName, "/")
119+
if !ok {
120+
return nil, "", util.NewInvalidArgumentErrorf("invalid base repo name: %s", input)
121+
}
122+
baseRepo, err := repo_model.GetRepositoryByOwnerAndName(ctx, baseOwnerName, baseRepoName)
123+
return baseRepo, baseBranchName, err
124+
}
125+
126+
func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
99127
form := web.GetForm(ctx).(T)
100128
if ctx.HasError() {
101129
ctx.JSONError(ctx.GetErrMsg())
@@ -105,15 +133,20 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
105133
commonForm := form.GetCommitCommonForm()
106134
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
107135

108-
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
136+
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
109137
if err != nil {
110-
ctx.ServerError("PrepareCommitFormBehaviors", err)
138+
ctx.ServerError("PrepareCommitFormOptions", err)
139+
return nil
140+
}
141+
if commitFormOptions.NeedFork {
142+
// It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
143+
ctx.JSONError(ctx.Locale.TrString("error.not_found"))
111144
return nil
112145
}
113146

114147
// check commit behavior
115148
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
116-
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
149+
if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
117150
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
118151
return nil
119152
}
@@ -125,28 +158,46 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
125158
return nil
126159
}
127160

128-
return &parsedEditorCommitForm[T]{
129-
form: form,
130-
commonForm: commonForm,
131-
CommitFormBehaviors: commitFormBehaviors,
132-
TargetBranchName: targetBranchName,
133-
GitCommitter: gitCommitter,
161+
fromBase := ctx.FormString("from_base")
162+
if fromBase != "" {
163+
baseRepo, baseBranchName, err := parseBaseRepoBranch(ctx, fromBase)
164+
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
165+
ctx.JSONError(ctx.Tr("error.not_found"))
166+
return nil
167+
} else if err != nil {
168+
ctx.ServerError("parseBaseRepoBranch", err)
169+
return nil
170+
}
171+
err = editorPushBranchToForkedRepository(ctx, ctx.Doer, baseRepo, baseBranchName, ctx.Repo.Repository, ctx.Repo.RefFullName.BranchName())
172+
if err != nil {
173+
log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
174+
ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
175+
return nil
176+
}
177+
}
178+
179+
return &preparedEditorCommitForm[T]{
180+
form: form,
181+
commonForm: commonForm,
182+
CommitFormOptions: commitFormOptions,
183+
TargetBranchName: targetBranchName,
184+
GitCommitter: gitCommitter,
134185
}
135186
}
136187

137188
// redirectForCommitChoice redirects after committing the edit to a branch
138-
func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) {
189+
func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
139190
if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
140191
// Redirect to a pull request when possible
141192
redirectToPullRequest := false
142193
repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
143-
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
144-
redirectToPullRequest = true
145-
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
194+
if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
146195
redirectToPullRequest = true
147196
baseBranch = repo.BaseRepo.DefaultBranch
148197
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
149198
repo = repo.BaseRepo
199+
} else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
200+
redirectToPullRequest = true
150201
}
151202
if redirectToPullRequest {
152203
ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
@@ -268,7 +319,7 @@ func EditFile(ctx *context.Context) {
268319
func EditFilePost(ctx *context.Context) {
269320
editorAction := ctx.PathParam("editor_action")
270321
isNewFile := editorAction == "_new"
271-
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
322+
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
272323
if ctx.Written() {
273324
return
274325
}
@@ -327,7 +378,7 @@ func DeleteFile(ctx *context.Context) {
327378

328379
// DeleteFilePost response for deleting file
329380
func DeleteFilePost(ctx *context.Context) {
330-
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
381+
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
331382
if ctx.Written() {
332383
return
333384
}
@@ -360,18 +411,18 @@ func DeleteFilePost(ctx *context.Context) {
360411

361412
func UploadFile(ctx *context.Context) {
362413
ctx.Data["PageIsUpload"] = true
363-
upload.AddUploadContext(ctx, "repo")
364414
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
365-
366-
prepareEditorCommitFormOptions(ctx, "_upload")
415+
opts := prepareEditorCommitFormOptions(ctx, "_upload")
367416
if ctx.Written() {
368417
return
369418
}
419+
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
420+
370421
ctx.HTML(http.StatusOK, tplUploadFile)
371422
}
372423

373424
func UploadFilePost(ctx *context.Context) {
374-
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
425+
parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
375426
if ctx.Written() {
376427
return
377428
}

routers/web/repo/editor_apply_patch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
2525

2626
// NewDiffPatchPost response for sending patch page
2727
func NewDiffPatchPost(ctx *context.Context) {
28-
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
28+
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
2929
if ctx.Written() {
3030
return
3131
}

routers/web/repo/editor_cherry_pick.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
4545

4646
func CherryPickPost(ctx *context.Context) {
4747
fromCommitID := ctx.PathParam("sha")
48-
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
48+
parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
4949
if ctx.Written() {
5050
return
5151
}

routers/web/repo/editor_fork.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
9+
"code.gitea.io/gitea/modules/templates"
10+
"code.gitea.io/gitea/services/context"
11+
repo_service "code.gitea.io/gitea/services/repository"
12+
)
13+
14+
const tplEditorFork templates.TplName = "repo/editor/fork"
15+
16+
func ForkToEdit(ctx *context.Context) {
17+
ctx.HTML(http.StatusOK, tplEditorFork)
18+
}
19+
20+
func ForkToEditPost(ctx *context.Context) {
21+
ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
22+
BaseRepo: ctx.Repo.Repository,
23+
Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
24+
Description: ctx.Repo.Repository.Description,
25+
SingleBranch: ctx.Repo.Repository.DefaultBranch, // many we only need the default branch in the fork?
26+
})
27+
if ctx.Written() {
28+
return
29+
}
30+
ctx.JSONRedirect("") // reload the page, the new fork should be editable now
31+
}

routers/web/repo/editor_util.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111

1212
git_model "code.gitea.io/gitea/models/git"
1313
repo_model "code.gitea.io/gitea/models/repo"
14+
user_model "code.gitea.io/gitea/models/user"
1415
"code.gitea.io/gitea/modules/git"
1516
"code.gitea.io/gitea/modules/json"
1617
"code.gitea.io/gitea/modules/log"
18+
repo_module "code.gitea.io/gitea/modules/repository"
1719
context_service "code.gitea.io/gitea/services/context"
1820
)
1921

@@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
8385
}
8486
return treeNames, treePaths
8587
}
88+
89+
// getUniqueRepositoryName Gets a unique repository name for a user
90+
// It will append a -<num> postfix if the name is already taken
91+
func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
92+
uniqueName := name
93+
for i := 1; i < 1000; i++ {
94+
_, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
95+
if err != nil || repo_model.IsErrRepoNotExist(err) {
96+
return uniqueName
97+
}
98+
uniqueName = fmt.Sprintf("%s-%d", name, i)
99+
i++
100+
}
101+
return ""
102+
}
103+
104+
func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
105+
return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
106+
Remote: targetRepo.RepoPath(),
107+
Branch: baseBranchName + ":" + targetBranchName,
108+
Env: repo_module.PushingEnvironment(doer, targetRepo),
109+
})
110+
}

routers/web/repo/fork.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) {
189189
}
190190
}
191191

192-
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
192+
repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
193193
BaseRepo: forkRepo,
194194
Name: form.RepoName,
195195
Description: form.Description,
196196
SingleBranch: form.ForkSingleBranch,
197197
})
198+
if ctx.Written() {
199+
return
200+
}
201+
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
202+
}
203+
204+
func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
205+
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
198206
if err != nil {
199207
ctx.Data["Err_RepoName"] = true
200208
switch {
201209
case repo_model.IsErrReachLimitOfRepo(err):
202-
maxCreationLimit := ctxUser.MaxCreationLimit()
210+
maxCreationLimit := owner.MaxCreationLimit()
203211
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
204212
ctx.JSONError(msg)
205213
case repo_model.IsErrRepoAlreadyExist(err):
@@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
224232
default:
225233
ctx.ServerError("ForkPost", err)
226234
}
227-
return
235+
return nil
228236
}
229-
230-
log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
231-
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
237+
return repo
232238
}

0 commit comments

Comments
 (0)