postfix if the name is already taken
+func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
+	uniqueName := name
+	for i := 1; i < 1000; i++ {
+		_, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
+		if err != nil || repo_model.IsErrRepoNotExist(err) {
+			return uniqueName
+		}
+		uniqueName = fmt.Sprintf("%s-%d", name, i)
+		i++
+	}
+	return ""
+}
+
+func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
+	return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
+		Remote: targetRepo.RepoPath(),
+		Branch: baseBranchName + ":" + targetBranchName,
+		Env:    repo_module.PushingEnvironment(doer, targetRepo),
+	})
+}
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
index 9f5cda10c28c3..c2694e540f7d0 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) {
 		}
 	}
 
-	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+	repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
 		BaseRepo:     forkRepo,
 		Name:         form.RepoName,
 		Description:  form.Description,
 		SingleBranch: form.ForkSingleBranch,
 	})
+	if ctx.Written() {
+		return
+	}
+	ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
+
+func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
+	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
 	if err != nil {
 		ctx.Data["Err_RepoName"] = true
 		switch {
 		case repo_model.IsErrReachLimitOfRepo(err):
-			maxCreationLimit := ctxUser.MaxCreationLimit()
+			maxCreationLimit := owner.MaxCreationLimit()
 			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
 			ctx.JSONError(msg)
 		case repo_model.IsErrRepoAlreadyExist(err):
@@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
 		default:
 			ctx.ServerError("ForkPost", err)
 		}
-		return
+		return nil
 	}
-
-	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
-	ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+	return repo
 }
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index ec0ad02828c57..5606a8e6ecdc4 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
 
 func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
 	// archived or mirror repository, the buttons should not be shown
-	if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() {
+	if !ctx.Repo.Repository.CanEnableEditor() {
 		return
 	}
 
@@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
 	}
 
 	if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
+		ctx.Data["CanEditFile"] = true
 		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
+		ctx.Data["CanDeleteFile"] = true
 		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
 		return
 	}
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index 7af6ad450e402..4ce22d79db5ab 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
 	}
 
-	if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
+	if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() {
 		ctx.Data["CanEditReadmeFile"] = true
 	}
 }
diff --git a/routers/web/web.go b/routers/web/web.go
index 3040375def2c6..4b5d68b260f6b 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1313,23 +1313,35 @@ func registerWebRoutes(m *web.Router) {
 	}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
 	// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
 
-	m.Group("/{username}/{reponame}", func() { // repo code
+	m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
 		m.Group("", func() {
 			m.Group("", func() {
-				m.Post("/_preview/*", repo.DiffPreviewPost)
-				m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
-					Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
-				m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
-					Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
-				m.Combo("/_delete/*").Get(repo.DeleteFile).
-					Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
-				m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
-					Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
-				m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
-					Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
-				m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
-					Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
-			}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
+				// "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
+				// Because reader can "fork and edit"
+				canWriteToBranch := context.CanWriteToBranch()
+				m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
+				m.Post("/_fork/*", repo.ForkToEditPost)     // read-only, fork to own repo, fine with "code reader"
+
+				// the path params are used in PrepareCommitFormOptions to construct the correct form action URL
+				m.Combo("/{editor_action:_edit}/*").
+					Get(repo.EditFile).
+					Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+				m.Combo("/{editor_action:_new}/*").
+					Get(repo.EditFile).
+					Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+				m.Combo("/{editor_action:_delete}/*").
+					Get(repo.DeleteFile).
+					Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
+				m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
+					Get(repo.UploadFile).
+					Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
+				m.Combo("/{editor_action:_diffpatch}/*").
+					Get(repo.NewDiffPatch).
+					Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
+				m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
+					Get(repo.CherryPick).
+					Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
+			}, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
 			m.Group("", func() {
 				m.Post("/upload-file", repo.UploadFileToServer)
 				m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
diff --git a/services/context/repo.go b/services/context/repo.go
index c28ae7e8fd786..572211712bacf 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User
 	return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
 }
 
-// CanEnableEditor returns true if repository is editable and user has proper access level.
-func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool {
-	return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived
-}
-
 // CanCreateBranch returns true if repository is editable and user has proper access level.
 func (r *Repository) CanCreateBranch() bool {
 	return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
@@ -94,9 +89,13 @@ func RepoMustNotBeArchived() func(ctx *Context) {
 	}
 }
 
-type CommitFormBehaviors struct {
+type CommitFormOptions struct {
+	NeedFork bool
+
+	TargetRepo               *repo_model.Repository
+	TargetFormAction         string
+	WillSubmitToFork         bool
 	CanCommitToBranch        bool
-	EditorEnabled            bool
 	UserCanPush              bool
 	RequireSigned            bool
 	WillSign                 bool
@@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
 	CanCreateBasePullRequest bool
 }
 
-func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
-	protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
+func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
+	if !refName.IsBranch() {
+		// it shouldn't happen because middleware already checks
+		return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
+	}
+
+	originRepo := targetRepo
+	branchName := refName.ShortName()
+	// TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does
+	if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) {
+		targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID)
+		if targetRepo == nil {
+			return &CommitFormOptions{NeedFork: true}, nil
+		}
+		// now, we get our own forked repo; it must be writable by us.
+	}
+	submitToForkedRepo := targetRepo.ID != originRepo.ID
+	err := targetRepo.GetBaseRepo(ctx)
 	if err != nil {
 		return nil, err
 	}
-	userCanPush := true
-	requireSigned := false
+
+	protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
+	if err != nil {
+		return nil, err
+	}
+	canPushWithProtection := true
+	protectionRequireSigned := false
 	if protectedBranch != nil {
-		protectedBranch.Repo = r.Repository
-		userCanPush = protectedBranch.CanUserPush(ctx, doer)
-		requireSigned = protectedBranch.RequireSignedCommits
+		protectedBranch.Repo = targetRepo
+		canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
+		protectionRequireSigned = protectedBranch.RequireSignedCommits
 	}
 
-	sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
-
-	canEnableEditor := r.CanEnableEditor(ctx, doer)
-	canCommit := canEnableEditor && userCanPush
-	if requireSigned {
-		canCommit = canCommit && sign
-	}
+	willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
 	wontSignReason := ""
-	if err != nil {
-		if asymkey_service.IsErrWontSign(err) {
-			wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
-			err = nil
-		} else {
-			wontSignReason = "error"
-		}
+	if asymkey_service.IsErrWontSign(err) {
+		wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
+	} else if err != nil {
+		return nil, err
+	}
+
+	canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
+	if protectionRequireSigned {
+		canCommitToBranch = canCommitToBranch && willSign
 	}
 
-	canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
-	canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
+	canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
+	canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
 
-	return &CommitFormBehaviors{
-		CanCommitToBranch: canCommit,
-		EditorEnabled:     canEnableEditor,
-		UserCanPush:       userCanPush,
-		RequireSigned:     requireSigned,
-		WillSign:          sign,
-		SigningKey:        keyID,
+	opts := &CommitFormOptions{
+		TargetRepo:        targetRepo,
+		WillSubmitToFork:  submitToForkedRepo,
+		CanCommitToBranch: canCommitToBranch,
+		UserCanPush:       canPushWithProtection,
+		RequireSigned:     protectionRequireSigned,
+		WillSign:          willSign,
+		SigningKey:        signKeyID,
 		WontSignReason:    wontSignReason,
 
 		CanCreatePullRequest:     canCreatePullRequest,
 		CanCreateBasePullRequest: canCreateBasePullRequest,
-	}, err
+	}
+	editorAction := ctx.PathParam("editor_action")
+	editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+	if submitToForkedRepo {
+		// there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo
+		editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName)
+	}
+	if editorAction == "_cherrypick" {
+		opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining
+	} else {
+		opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining
+	}
+	if ctx.Req.URL.RawQuery != "" {
+		opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery
+	}
+	return opts, nil
 }
 
 // CanUseTimetracker returns whether a user can use the timetracker.
diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go
index 303e7da38b113..23707950d4a5c 100644
--- a/services/context/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -11,7 +11,9 @@ import (
 	"regexp"
 	"strings"
 
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/reqctx"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/services/context"
 )
@@ -106,14 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
 		ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
 		ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
 		ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
-	case "repo":
-		ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
-		ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
-		ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
-		ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
-		ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
-		ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
 	default:
 		setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
 	}
 }
+
+func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) {
+	ctxData, repoLink := ctx.GetData(), repo.Link()
+	ctxData["UploadUrl"] = repoLink + "/upload-file"
+	ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove"
+	ctxData["UploadLinkUrl"] = repoLink + "/upload-file"
+	ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
+	ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
+	ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
+}
diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl
index f850ebf916e40..7981fd076186a 100644
--- a/templates/repo/editor/cherry_pick.tmpl
+++ b/templates/repo/editor/cherry_pick.tmpl
@@ -3,8 +3,9 @@
 	{{template "repo/header" .}}
 	
 		{{template "base/alert" .}}
-