aboutsummaryrefslogtreecommitdiffstats
path: root/routers/web/repo
diff options
context:
space:
mode:
Diffstat (limited to 'routers/web/repo')
-rw-r--r--routers/web/repo/actions/actions.go17
-rw-r--r--routers/web/repo/actions/badge.go25
-rw-r--r--routers/web/repo/actions/view.go173
-rw-r--r--routers/web/repo/activity.go19
-rw-r--r--routers/web/repo/attachment.go2
-rw-r--r--routers/web/repo/blame.go10
-rw-r--r--routers/web/repo/branch.go9
-rw-r--r--routers/web/repo/cherry_pick.go192
-rw-r--r--routers/web/repo/code_frequency.go2
-rw-r--r--routers/web/repo/commit.go52
-rw-r--r--routers/web/repo/common_recentbranches.go73
-rw-r--r--routers/web/repo/compare.go53
-rw-r--r--routers/web/repo/download.go2
-rw-r--r--routers/web/repo/editor.go1054
-rw-r--r--routers/web/repo/editor_apply_patch.go51
-rw-r--r--routers/web/repo/editor_cherry_pick.go86
-rw-r--r--routers/web/repo/editor_error.go82
-rw-r--r--routers/web/repo/editor_fork.go31
-rw-r--r--routers/web/repo/editor_preview.go41
-rw-r--r--routers/web/repo/editor_test.go79
-rw-r--r--routers/web/repo/editor_uploader.go61
-rw-r--r--routers/web/repo/editor_util.go110
-rw-r--r--routers/web/repo/fork.go59
-rw-r--r--routers/web/repo/githttp.go28
-rw-r--r--routers/web/repo/githttp_test.go2
-rw-r--r--routers/web/repo/issue.go19
-rw-r--r--routers/web/repo/issue_comment.go38
-rw-r--r--routers/web/repo/issue_content_history.go7
-rw-r--r--routers/web/repo/issue_label.go48
-rw-r--r--routers/web/repo/issue_label_test.go104
-rw-r--r--routers/web/repo/issue_list.go125
-rw-r--r--routers/web/repo/issue_lock.go5
-rw-r--r--routers/web/repo/issue_new.go9
-rw-r--r--routers/web/repo/issue_stopwatch.go78
-rw-r--r--routers/web/repo/issue_view.go94
-rw-r--r--routers/web/repo/milestone.go7
-rw-r--r--routers/web/repo/packages.go5
-rw-r--r--routers/web/repo/patch.go125
-rw-r--r--routers/web/repo/projects.go19
-rw-r--r--routers/web/repo/pull.go252
-rw-r--r--routers/web/repo/pull_review.go11
-rw-r--r--routers/web/repo/recent_commits.go15
-rw-r--r--routers/web/repo/release.go15
-rw-r--r--routers/web/repo/repo.go32
-rw-r--r--routers/web/repo/setting/lfs.go54
-rw-r--r--routers/web/repo/setting/protected_branch.go36
-rw-r--r--routers/web/repo/setting/protected_tag.go3
-rw-r--r--routers/web/repo/setting/public_access.go155
-rw-r--r--routers/web/repo/setting/secrets.go5
-rw-r--r--routers/web/repo/setting/setting.go1498
-rw-r--r--routers/web/repo/setting/settings_test.go46
-rw-r--r--routers/web/repo/setting/webhook.go8
-rw-r--r--routers/web/repo/treelist.go99
-rw-r--r--routers/web/repo/treelist_test.go68
-rw-r--r--routers/web/repo/view.go78
-rw-r--r--routers/web/repo/view_file.go422
-rw-r--r--routers/web/repo/view_home.go140
-rw-r--r--routers/web/repo/view_home_test.go37
-rw-r--r--routers/web/repo/view_readme.go74
-rw-r--r--routers/web/repo/wiki.go201
-rw-r--r--routers/web/repo/wiki_test.go39
61 files changed, 3192 insertions, 3092 deletions
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index d07d195713..202da407d2 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -6,6 +6,7 @@ package actions
import (
"bytes"
stdCtx "context"
+ "errors"
"net/http"
"slices"
"strings"
@@ -67,7 +68,11 @@ func List(ctx *context.Context) {
ctx.Data["PageIsActions"] = true
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+ ctx.NotFound(nil)
+ return
+ } else if err != nil {
ctx.ServerError("GetBranchCommit", err)
return
}
@@ -121,7 +126,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
var curWorkflow *model.Workflow
- entries, err := actions.ListWorkflows(commit)
+ _, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return nil
@@ -312,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
+
+ ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
}
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
@@ -370,10 +377,8 @@ func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
if !decodeNode(w.RawOn, &val) {
return nil
}
- for _, v := range val {
- if v == "workflow_dispatch" {
- return &WorkflowDispatch{}
- }
+ if slices.Contains(val, "workflow_dispatch") {
+ return &WorkflowDispatch{}
}
case yaml.MappingNode:
var val map[string]yaml.Node
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index e920ecaf58..d268a8df8a 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -5,35 +5,38 @@ package actions
import (
"errors"
- "fmt"
"net/http"
"path/filepath"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
func GetWorkflowBadge(ctx *context.Context) {
workflowFile := ctx.PathParam("workflow_name")
- branch := ctx.Req.URL.Query().Get("branch")
- if branch == "" {
- branch = ctx.Repo.Repository.DefaultBranch
- }
- branchRef := fmt.Sprintf("refs/heads/%s", branch)
- event := ctx.Req.URL.Query().Get("event")
+ branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch)
+ event := ctx.FormString("event")
+ style := ctx.FormString("style")
- badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+ branchRef := git.RefNameFromBranch(branch)
+ b, err := getWorkflowBadge(ctx, workflowFile, branchRef.String(), event)
if err != nil {
ctx.ServerError("GetWorkflowBadge", err)
return
}
- ctx.Data["Badge"] = badge
+ ctx.Data["Badge"] = b
ctx.RespHeader().Set("Content-Type", "image/svg+xml")
- ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+ switch style {
+ case badge.StyleFlatSquare:
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square")
+ default: // defaults to badge.StyleFlat
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
+ }
}
func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
@@ -48,7 +51,7 @@ func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event stri
return badge.Badge{}, err
}
- color, ok := badge.StatusColorMap[run.Status]
+ color, ok := badge.GlobalVars().StatusColorMap[run.Status]
if !ok {
return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 41f0d2d0ec..52b2e9995e 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -14,7 +14,6 @@ import (
"net/http"
"net/url"
"strconv"
- "strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
@@ -31,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/common"
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
@@ -64,6 +64,36 @@ func View(ctx *context_module.Context) {
ctx.HTML(http.StatusOK, tplViewActions)
}
+func ViewWorkflowFile(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ rpath, entries, err := actions.ListWorkflows(commit)
+ if err != nil {
+ ctx.ServerError("ListWorkflows", err)
+ return
+ }
+ for _, entry := range entries {
+ if entry.Name() == run.WorkflowID {
+ ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID)))
+ return
+ }
+ }
+ ctx.NotFound(nil)
+}
+
type LogCursor struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
@@ -200,13 +230,9 @@ func ViewPost(ctx *context_module.Context) {
}
}
- // TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
- // need to be refactored together in the future
- metas := ctx.Repo.Repository.ComposeMetas(ctx)
-
// the title for the "run" is from the commit message
resp.State.Run.Title = run.Title
- resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas)
+ resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository)
resp.State.Run.Link = run.Link()
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
@@ -223,7 +249,7 @@ func ViewPost(ctx *context_module.Context) {
ID: v.ID,
Name: v.Name,
Status: v.Status.String(),
- CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
+ CanRerun: resp.State.Run.CanRerun,
Duration: v.Duration().String(),
})
}
@@ -278,7 +304,7 @@ func ViewPost(ctx *context_module.Context) {
if task != nil {
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("convertToViewModel", err)
return
}
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
@@ -382,7 +408,7 @@ func Rerun(ctx *context_module.Context) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -400,7 +426,7 @@ func Rerun(ctx *context_module.Context) {
run.Started = 0
run.Stopped = 0
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRun", err)
return
}
}
@@ -415,11 +441,11 @@ func Rerun(ctx *context_module.Context) {
// if the job has needs, it should be set to "blocked" status to wait for other jobs
shouldBlock := len(j.Needs) > 0
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
- ctx.JSON(http.StatusOK, struct{}{})
+ ctx.JSONOK()
return
}
@@ -429,17 +455,17 @@ func Rerun(ctx *context_module.Context) {
// jobs other than the specified one should be set to "blocked" status
shouldBlock := j.JobID != job.JobID
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
- ctx.JSON(http.StatusOK, struct{}{})
+ ctx.JSONOK()
}
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status
- if !status.IsDone() {
+ if !status.IsDone() || !job.Run.Status.IsDone() {
return nil
}
@@ -459,7 +485,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
}
actions_service.CreateCommitStatus(ctx, job)
- _ = job.LoadAttributes(ctx)
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
return nil
@@ -469,49 +495,19 @@ func Logs(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
- job, _ := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
- if job.TaskID == 0 {
- ctx.HTTPError(http.StatusNotFound, "job is not started")
- return
- }
-
- err := job.LoadRun(ctx)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- return
- }
-
- task, err := actions_model.GetTaskByID(ctx, job.TaskID)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- return
- }
- if task.LogExpired {
- ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up")
- return
- }
-
- reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
return
}
- defer reader.Close()
- workflowName := job.Run.WorkflowID
- if p := strings.Index(workflowName, "."); p > 0 {
- workflowName = workflowName[0:p]
+ if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
+ ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
}
- ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
- Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
- ContentLength: &task.LogSize,
- ContentType: "text/plain",
- ContentTypeCharset: "utf-8",
- Disposition: "attachment",
- })
}
func Cancel(ctx *context_module.Context) {
@@ -538,7 +534,7 @@ func Cancel(ctx *context_module.Context) {
return err
}
if n == 0 {
- return fmt.Errorf("job has changed, try again")
+ return errors.New("job has changed, try again")
}
if n > 0 {
updatedjobs = append(updatedjobs, job)
@@ -551,7 +547,7 @@ func Cancel(ctx *context_module.Context) {
}
return nil
}); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("StopTask", err)
return
}
@@ -561,7 +557,11 @@ func Cancel(ctx *context_module.Context) {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
-
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
ctx.JSON(http.StatusOK, struct{}{})
}
@@ -597,12 +597,18 @@ func Approve(ctx *context_module.Context) {
}
return nil
}); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRunJob", err)
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
+
for _, job := range updatedjobs {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
@@ -611,6 +617,33 @@ func Approve(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{})
}
+func Delete(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ repoID := ctx.Repo.Repository.ID
+
+ run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ }
+ ctx.ServerError("GetRunByIndex", err)
+ return
+ }
+
+ if !run.Status.IsDone() {
+ ctx.JSONError(ctx.Tr("actions.runs.not_done"))
+ return
+ }
+
+ if err := actions_service.DeleteRun(ctx, run); err != nil {
+ ctx.ServerError("DeleteRun", err)
+ return
+ }
+
+ ctx.JSONOK()
+}
+
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx.
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
@@ -618,20 +651,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.HTTPError(http.StatusNotFound, err.Error())
+ ctx.NotFound(nil)
return nil, nil
}
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return nil, nil
}
run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunJobsByRunID", err)
return nil, nil
}
if len(jobs) == 0 {
- ctx.HTTPError(http.StatusNotFound)
+ ctx.NotFound(nil)
return nil, nil
}
@@ -657,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
return
}
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("SetArtifactNeedDelete", err)
return
}
ctx.JSON(http.StatusOK, struct{}{})
@@ -673,7 +706,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ctx.HTTPError(http.StatusNotFound, err.Error())
return
}
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -682,7 +715,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ArtifactName: artifactName,
})
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("FindArtifacts", err)
return
}
if len(artifacts) == 0 {
@@ -703,7 +736,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("DownloadArtifactV4", err)
return
}
return
@@ -716,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
for _, art := range artifacts {
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("ActionsArtifacts.Open", err)
return
}
@@ -724,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
if art.ContentEncoding == "gzip" {
r, err = gzip.NewReader(f)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("gzip.NewReader", err)
return
}
} else {
@@ -734,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
w, err := writer.Create(art.ArtifactPath)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("writer.Create", err)
return
}
if _, err := io.Copy(w, r); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("io.Copy", err)
return
}
}
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 1d809ad8e9..8232f0cc04 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -8,6 +8,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
ctx.Data["DateUntil"] = timeUntil
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
+ canReadCode := ctx.Repo.CanRead(unit.TypeCode)
+ if canReadCode {
+ // GetActivityStats needs to read the default branch to get some information
+ branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
+ if !branchExist {
+ ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+ ctx.NotFound(nil)
+ return
+ }
+ }
+
var err error
- if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
+ // TODO: refactor these arguments to a struct
+ ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
ctx.Repo.CanRead(unit.TypeReleases),
ctx.Repo.CanRead(unit.TypeIssues),
ctx.Repo.CanRead(unit.TypePullRequests),
- ctx.Repo.CanRead(unit.TypeCode)); err != nil {
+ canReadCode,
+ )
+ if err != nil {
ctx.ServerError("GetActivityStats", err)
return
}
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 9eda926dad..f696669196 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -129,7 +129,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
if setting.Attachment.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, nil)
+ u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index efd85b9452..e304633f95 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -8,6 +8,7 @@ import (
gotemplate "html/template"
"net/http"
"net/url"
+ "path"
"strconv"
"strings"
@@ -15,13 +16,13 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/languagestats"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
- files_service "code.gitea.io/gitea/services/repository/files"
)
type blameRow struct {
@@ -69,7 +70,7 @@ func RefBlame(ctx *context.Context) {
blob := entry.Blob()
fileSize := blob.Size()
ctx.Data["FileSize"] = fileSize
- ctx.Data["FileName"] = blob.Name()
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
tplName := tplRepoViewContent
if !ctx.FormBool("only_content") {
@@ -234,7 +235,7 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
- language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+ language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
if err != nil {
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
@@ -285,8 +286,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
if i != len(lines)-1 {
line += "\n"
}
- fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
- line, lexerNameForLine := highlight.Code(fileName, language, line)
+ line, lexerNameForLine := highlight.Code(path.Base(ctx.Repo.TreePath), language, line)
// set lexer name to the first detected lexer. this is certainly suboptimal and
// we should instead highlight the whole file at once
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 5d963eff66..96d1d87836 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -45,10 +45,7 @@ func Branches(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsBranches"] = true
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := setting.Git.BranchesRangeSize
kw := ctx.FormString("q")
@@ -261,10 +258,10 @@ func CreateBranch(ctx *context.Context) {
func MergeUpstream(ctx *context.Context) {
branchName := ctx.FormString("branch")
- _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
+ _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.JSONError(ctx.Tr("error.not_found"))
+ ctx.JSONErrorNotFound()
return
} else if pull_service.IsErrMergeConflicts(err) {
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
deleted file mode 100644
index ec50e1435e..0000000000
--- a/routers/web/repo/cherry_pick.go
+++ /dev/null
@@ -1,192 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "bytes"
- "errors"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-var tplCherryPick templates.TplName = "repo/editor/cherry_pick"
-
-// CherryPick handles cherrypick GETs
-func CherryPick(ctx *context.Context) {
- ctx.Data["SHA"] = ctx.PathParam("sha")
- cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha"))
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(err)
- return
- }
- ctx.ServerError("GetCommit", err)
- return
- }
-
- if ctx.FormString("cherry-pick-type") == "revert" {
- ctx.Data["CherryPickType"] = "revert"
- ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
- ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
- ctx.Data["commit_summary"] = splits[0]
- ctx.Data["commit_message"] = splits[1]
- }
-
- canCommit := renderCommitRights(ctx)
- ctx.Data["TreePath"] = ""
-
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(200, tplCherryPick)
-}
-
-// CherryPickPost handles cherrypick POSTs
-func CherryPickPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CherryPickForm)
-
- sha := ctx.PathParam("sha")
- ctx.Data["SHA"] = sha
- if form.Revert {
- ctx.Data["CherryPickType"] = "revert"
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- }
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- if ctx.HasError() {
- ctx.HTML(200, tplCherryPick)
- return
- }
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if message == "" {
- if form.Revert {
- message = ctx.Locale.TrString("repo.commit.revert-header", sha)
- } else {
- message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
- }
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form)
- return
- }
- opts := &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Author: gitCommitter,
- Committer: gitCommitter,
- }
-
- // First lets try the simple plain read-tree -m approach
- opts.Content = sha
- if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- // Drop through to the apply technique
-
- buf := &bytes.Buffer{}
- if form.Revert {
- if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- } else {
- if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- }
-
- opts.Content = buf.String()
- ctx.Data["FileContent"] = opts.Content
-
- if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
- }
-}
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index e212d3b60c..2b2dd5744a 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted)
return
}
- ctx.ServerError("GetCodeFrequencyData", err)
+ ctx.ServerError("GetContributorStats", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index bbdcf9875e..0c60abcecd 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -15,12 +15,14 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -66,10 +68,7 @@ func Commits(ctx *context.Context) {
commitsCount := ctx.Repo.CommitsCount
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := ctx.FormInt("limit")
if pageSize <= 0 {
@@ -77,7 +76,7 @@ func Commits(ctx *context.Context) {
}
// Both `git log branchName` and `git log commitId` work.
- commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "")
+ commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
if err != nil {
ctx.ServerError("CommitsByRange", err)
return
@@ -168,10 +167,13 @@ func Graph(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+ divOnly := ctx.FormBool("div-only")
+ queryParams := ctx.Req.URL.Query()
+ queryParams.Del("div-only")
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
- paginator.AddParamFromRequest(ctx.Req)
+ paginator.AddParamFromQuery(queryParams)
ctx.Data["Page"] = paginator
- if ctx.FormBool("div-only") {
+ if divOnly {
ctx.HTML(http.StatusOK, tplGraphDiv)
return
}
@@ -215,13 +217,12 @@ func SearchCommits(ctx *context.Context) {
// FileHistory show a file's reversions
func FileHistory(ctx *context.Context) {
- fileName := ctx.Repo.TreePath
- if len(fileName) == 0 {
+ if ctx.Repo.TreePath == "" {
Commits(ctx)
return
}
- commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), fileName) // FIXME: legacy code used ShortName
+ commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return
@@ -230,15 +231,12 @@ func FileHistory(ctx *context.Context) {
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
- File: fileName,
+ File: ctx.Repo.TreePath,
Page: page,
})
if err != nil {
@@ -253,7 +251,7 @@ func FileHistory(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- ctx.Data["FileName"] = fileName
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
ctx.Data["CommitCount"] = commitsCount
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
@@ -284,7 +282,7 @@ func Diff(ctx *context.Context) {
)
if ctx.Data["PageIsWiki"] != nil {
- gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
@@ -314,7 +312,7 @@ func Diff(ctx *context.Context) {
maxLines, maxFiles = -1, -1
}
- diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{
+ diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
@@ -370,10 +368,14 @@ func Diff(ctx *context.Context) {
return
}
- ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -410,6 +412,11 @@ func Diff(ctx *context.Context) {
}
}
+ pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
+ if pr != nil {
+ ctx.Data["MergedPRIssueNumber"] = pr.Index
+ }
+
ctx.HTML(http.StatusOK, tplCommitPage)
}
@@ -417,7 +424,7 @@ func Diff(ctx *context.Context) {
func RawDiff(ctx *context.Context) {
var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil {
- wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
@@ -453,6 +460,9 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_m
}
if !ctx.Repo.CanRead(unit_model.TypeActions) {
for _, commit := range commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
diff --git a/routers/web/repo/common_recentbranches.go b/routers/web/repo/common_recentbranches.go
new file mode 100644
index 0000000000..c2083dec73
--- /dev/null
+++ b/routers/web/repo/common_recentbranches.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ git_model "code.gitea.io/gitea/models/git"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ unit_model "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+type RecentBranchesPromptDataStruct struct {
+ RecentlyPushedNewBranches []*git_model.RecentlyPushedNewBranch
+}
+
+func prepareRecentlyPushedNewBranches(ctx *context.Context) {
+ if ctx.Doer == nil {
+ return
+ }
+ if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
+ log.Error("GetBaseRepo: %v", err)
+ return
+ }
+
+ opts := git_model.FindRecentlyPushedNewBranchesOptions{
+ Repo: ctx.Repo.Repository,
+ BaseRepo: ctx.Repo.Repository,
+ }
+ if ctx.Repo.Repository.IsFork {
+ opts.BaseRepo = ctx.Repo.Repository.BaseRepo
+ }
+
+ baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission: %v", err)
+ return
+ }
+ if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() {
+ return
+ }
+ if !opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || !baseRepoPerm.CanRead(unit_model.TypePullRequests) {
+ return
+ }
+
+ var finalBranches []*git_model.RecentlyPushedNewBranch
+ branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
+ if err != nil {
+ log.Error("FindRecentlyPushedNewBranches failed: %v", err)
+ return
+ }
+
+ for _, branch := range branches {
+ divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
+ branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
+ opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
+ )
+ if err != nil {
+ log.Error("GetBranchDivergingInfo failed: %v", err)
+ continue
+ }
+ branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
+ baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
+ if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
+ finalBranches = append(finalBranches, branch)
+ }
+ }
+ if len(finalBranches) > 0 {
+ ctx.Data["RecentBranchesPromptData"] = RecentBranchesPromptDataStruct{finalBranches}
+ }
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 6cea95e387..c771b30e5f 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -401,12 +402,11 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.HeadRepo = ctx.Repo.Repository
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has {
- ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
+ ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
if err != nil {
- ctx.ServerError("OpenRepository", err)
+ ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
return nil
}
- defer ci.HeadGitRepo.Close()
} else {
ctx.NotFound(nil)
return nil
@@ -569,20 +569,20 @@ func PrepareCompareDiff(
ctx *context.Context,
ci *common.CompareInfo,
whitespaceBehavior git.TrustedCmdArgs,
-) bool {
- var (
- repo = ctx.Repo.Repository
- err error
- title string
- )
-
- // Get diff information.
- ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
-
+) (nothingToCompare bool) {
+ repo := ctx.Repo.Repository
headCommitID := ci.CompareInfo.HeadCommitID
+ ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
ctx.Data["AfterCommitID"] = headCommitID
+ // follow GitHub's behavior: autofill the form and expand
+ newPrFormTitle := ctx.FormTrim("title")
+ newPrFormBody := ctx.FormTrim("body")
+ ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != ""
+ ctx.Data["TitleQuery"] = newPrFormTitle
+ ctx.Data["BodyQuery"] = newPrFormBody
+
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true
@@ -614,7 +614,7 @@ func PrepareCompareDiff(
fileOnly := ctx.FormBool("file-only")
- diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo,
+ diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadRepo.Link(), ci.HeadGitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: beforeCommitID,
AfterCommitID: headCommitID,
@@ -645,7 +645,11 @@ func PrepareCompareDiff(
return false
}
- ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
@@ -670,6 +674,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
+ title := ci.HeadBranch
if len(commits) == 1 {
c := commits[0]
title = strings.TrimSpace(c.UserCommit.Summary())
@@ -678,9 +683,8 @@ func PrepareCompareDiff(
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
- } else {
- title = ci.HeadBranch
}
+
if len(title) > 255 {
var trailer string
title, trailer = util.EllipsisDisplayStringX(title, 255)
@@ -727,11 +731,6 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
// CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) {
ci := ParseCompareInfo(ctx)
- defer func() {
- if ci != nil && ci.HeadGitRepo != nil {
- ci.HeadGitRepo.Close()
- }
- }()
if ctx.Written() {
return
}
@@ -745,8 +744,7 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["OtherCompareSeparator"] = "..."
}
- nothingToCompare := PrepareCompareDiff(ctx, ci,
- gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+ nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() {
return
}
@@ -885,7 +883,7 @@ func ExcerptBlob(ctx *context.Context) {
gitRepo := ctx.Repo.GitRepo
if ctx.Data["PageIsWiki"] == true {
var err error
- gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
@@ -945,9 +943,10 @@ func ExcerptBlob(ctx *context.Context) {
RightHunkSize: rightHunkSize,
},
}
- if direction == "up" {
+ switch direction {
+ case "up":
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
- } else if direction == "down" {
+ case "down":
section.Lines = append(section.Lines, lineSection)
}
}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 020cebf196..6f394aae27 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -54,7 +54,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage, blob storage), redirect to this directly.
- u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil)
+ u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return nil
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 113622f872..2a5ac10282 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -4,6 +4,7 @@
package repo
import (
+ "bytes"
"fmt"
"io"
"net/http"
@@ -11,19 +12,17 @@ import (
"strings"
git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
@@ -35,871 +34,422 @@ const (
tplEditDiffPreview templates.TplName = "repo/editor/diff_preview"
tplDeleteFile templates.TplName = "repo/editor/delete"
tplUploadFile templates.TplName = "repo/editor/upload"
+ tplPatchFile templates.TplName = "repo/editor/patch"
+ tplCherryPick templates.TplName = "repo/editor/cherry_pick"
- frmCommitChoiceDirect string = "direct"
- frmCommitChoiceNewBranch string = "commit-to-new-branch"
+ editorCommitChoiceDirect string = "direct"
+ editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
-func canCreateBasePullRequest(ctx *context.Context) bool {
- baseRepo := ctx.Repo.Repository.BaseRepo
- return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
-}
-
-func renderCommitRights(ctx *context.Context) bool {
- canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
- if err != nil {
- log.Error("CanCommitToBranch: %v", err)
- }
- ctx.Data["CanCommitToBranch"] = canCommitToBranch
- ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
-
- return canCommitToBranch.CanCommitToBranch
-}
-
-// redirectForCommitChoice redirects after committing the edit to a branch
-func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
- if commitChoice == frmCommitChoiceNewBranch {
- // Redirect to a pull request when possible
- redirectToPullRequest := false
- repo := ctx.Repo.Repository
- baseBranch := ctx.Repo.BranchName
- headBranch := newBranchName
- if repo.UnitEnabled(ctx, unit.TypePullRequests) {
- redirectToPullRequest = true
- } else if canCreateBasePullRequest(ctx) {
- redirectToPullRequest = true
- baseBranch = repo.BaseRepo.DefaultBranch
- headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
- repo = repo.BaseRepo
- }
-
- if redirectToPullRequest {
- ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
- return
+func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
+ cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if cleanedTreePath != ctx.Repo.TreePath {
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
+ if ctx.Req.URL.RawQuery != "" {
+ redirectTo += "?" + ctx.Req.URL.RawQuery
}
+ ctx.Redirect(redirectTo)
+ return nil
}
- returnURI := ctx.FormString("return_uri")
-
- ctx.RedirectToCurrentSite(
- returnURI,
- ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
- )
-}
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
+ }
-// getParentTreeFields returns list of parent tree names and corresponding tree paths
-// based on given tree path.
-func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
- if len(treePath) == 0 {
- return treeNames, treePaths
+ if commitFormOptions.NeedFork {
+ ForkToEdit(ctx)
+ return nil
}
- treeNames = strings.Split(treePath, "/")
- treePaths = make([]string, len(treeNames))
- for i := range treeNames {
- treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
+ ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
+ ctx.NotFound(nil)
}
- return treeNames, treePaths
-}
-func editFileCommon(ctx *context.Context, isNewFile bool) {
- ctx.Data["PageIsEdit"] = true
- ctx.Data["IsNewFile"] = isNewFile
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["TreePath"] = ctx.Repo.TreePath
+ ctx.Data["CommitFormOptions"] = commitFormOptions
+
+ // for online editor
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
-}
-
-func editFile(ctx *context.Context, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- if isNewFile {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- } else {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- }
- return
- }
-
- // Check if the filename (and additional path) is specified in the querystring
- // (filename is a misnomer, but kept for compatibility with GitHub)
- filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
- filePath = strings.Trim(filePath, "/")
- treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
-
- if !isNewFile {
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
- if err != nil {
- HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
- return
- }
-
- // No way to edit a directory online.
- if entry.IsDir() {
- ctx.NotFound(nil)
- return
- }
-
- blob := entry.Blob()
- if blob.Size() >= setting.UI.MaxDisplayFileSize {
- ctx.NotFound(err)
- return
- }
-
- dataRc, err := blob.DataAsync()
- if err != nil {
- ctx.NotFound(err)
- return
- }
-
- defer dataRc.Close()
-
- ctx.Data["FileSize"] = blob.Size()
- ctx.Data["FileName"] = blob.Name()
-
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(dataRc, buf)
- buf = buf[:n]
-
- // Only some file types are editable online as text.
- if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
- ctx.NotFound(nil)
- return
- }
-
- d, _ := io.ReadAll(dataRc)
-
- buf = append(buf, d...)
- if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
- log.Error("ToUTF8: %v", err)
- ctx.Data["FileContent"] = string(buf)
- } else {
- ctx.Data["FileContent"] = content
- }
- } else {
- // Append filename from query, or empty string to allow username the new file.
- treeNames = append(treeNames, fileName)
- }
-
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
+ // form fields
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
- ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
+ ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
+ ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
-
- ctx.HTML(http.StatusOK, tplEditFile)
+ return commitFormOptions
}
-// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
-func GetEditorConfig(ctx *context.Context, treePath string) string {
- ec, _, err := ctx.Repo.GetEditorconfig()
- if err == nil {
- def, err := ec.GetDefinitionForFilename(treePath)
- if err == nil {
- jsonStr, _ := json.Marshal(def)
- return string(jsonStr)
- }
- }
- return "null"
+func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
+ // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
+ ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
}
-// EditFile render edit file page
-func EditFile(ctx *context.Context) {
- editFile(ctx, false)
+type preparedEditorCommitForm[T any] struct {
+ form T
+ commonForm *forms.CommitCommonForm
+ CommitFormOptions *context.CommitFormOptions
+ OldBranchName string
+ NewBranchName string
+ GitCommitter *files_service.IdentityOptions
}
-// NewFile render create file page
-func NewFile(ctx *context.Context) {
- editFile(ctx, true)
+func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
+ commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
+ if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
+ commitMessage += "\n\n" + body
+ }
+ return commitMessage
}
-func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- ctx.Data["PageHasPosted"] = true
-
- canCommit := renderCommitRights(ctx)
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
-
+func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
+ form := web.GetForm(ctx).(T)
if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplEditFile)
- return
+ ctx.JSONError(ctx.GetErrMsg())
+ return nil
}
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
+ commonForm := form.GetCommitCommonForm()
+ commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- if isNewFile {
- message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
- } else {
- message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
- }
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
}
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+ if commitFormOptions.NeedFork {
+ // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
+ ctx.JSONError(ctx.Locale.TrString("error.not_found"))
+ return nil
}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
- return
+ // check commit behavior
+ fromBaseBranch := ctx.FormString("from_base_branch")
+ commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
+ targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
+ if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
+ ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
+ return nil
}
- operation := "update"
- if isNewFile {
- operation = "create"
+ if !issues.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, targetBranchName, ctx.Doer) {
+ ctx.NotFound(nil)
+ return nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: operation,
- FromTreePath: ctx.Repo.TreePath,
- TreePath: form.TreePath,
- ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
- },
- },
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
- if git.IsErrNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
- } else if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
- default:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form)
- }
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- // For when a user adds/updates a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
+ // Committer user info
+ gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail)
+ if !valid {
+ ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
+ return nil
+ }
+
+ if commitToNewBranch {
+ // if target branch exists, we should stop
+ targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
+ if err != nil {
+ ctx.ServerError("IsBranchExist", err)
+ return nil
+ } else if targetBranchExists {
+ if fromBaseBranch != "" {
+ ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
} else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
}
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
- "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
- "Details": utils.SanitizeFlashErrorString(err.Error()),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
+ return nil
}
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
+ oldBranchName := ctx.Repo.BranchName
+ if fromBaseBranch != "" {
+ err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
+ if err != nil {
+ log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
+ ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
+ return nil
}
+ // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
+ oldBranchName = targetBranchName
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
-
-// EditFilePost response for editing file
-func EditFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, false)
-}
-
-// NewFilePost response for creating file
-func NewFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, true)
-}
-
-// DiffPreviewPost render preview diff page
-func DiffPreviewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if len(treePath) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid")
- return
+ return &preparedEditorCommitForm[T]{
+ form: form,
+ commonForm: commonForm,
+ CommitFormOptions: commitFormOptions,
+ OldBranchName: oldBranchName,
+ NewBranchName: targetBranchName,
+ GitCommitter: gitCommitter,
}
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
- return
- } else if entry.IsDir() {
- ctx.HTTPError(http.StatusUnprocessableEntity)
- return
- }
-
- diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
- return
- }
-
- if len(diff.Files) != 0 {
- ctx.Data["File"] = diff.Files[0]
- }
-
- ctx.HTML(http.StatusOK, tplEditDiffPreview)
}
-// DeleteFile render delete file page
-func DeleteFile(ctx *context.Context) {
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
-
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+// redirectForCommitChoice redirects after committing the edit to a branch
+func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
+ // when editing a file in a PR, it should return to the origin location
+ if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
+ ctx.JSONRedirect(returnURI)
return
}
- ctx.Data["TreePath"] = treePath
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+ if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
+ // Redirect to a pull request when possible
+ redirectToPullRequest := false
+ repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
+ if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
+ redirectToPullRequest = true
+ baseBranch = repo.BaseRepo.DefaultBranch
+ headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
+ repo = repo.BaseRepo
+ } else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
+ redirectToPullRequest = true
+ }
+ if redirectToPullRequest {
+ ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
+ return
+ }
}
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.HTML(http.StatusOK, tplDeleteFile)
+ // redirect to the newly updated file
+ redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
+ ctx.JSONRedirect(redirectTo)
}
-// DeleteFilePost response for deleting file
-func DeleteFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["TreePath"] = ctx.Repo.TreePath
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplDeleteFile)
- return
- }
-
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
- }
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+ if err != nil {
+ HandleGitError(ctx, "GetTreeEntryByPath", err)
+ return nil, nil, nil
}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form)
- return
+ // No way to edit a directory online.
+ if entry.IsDir() {
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: "delete",
- TreePath: ctx.Repo.TreePath,
- },
- },
- Message: message,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
- if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
- default:
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else {
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else if git.IsErrBranchNotExist(err) {
- // For when a user deletes a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("DeleteFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplDeleteFile, &form)
- }
+ blob := entry.Blob()
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound(err)
} else {
- ctx.ServerError("DeleteRepoFile", err)
+ ctx.ServerError("getFileReader", err)
}
- return
+ return nil, nil, nil
}
- ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
- treePath := path.Dir(ctx.Repo.TreePath)
- if treePath == "." {
- treePath = "" // the file deleted was in the root, so we return the user to the root directory
- }
- if len(treePath) > 0 {
- // Need to get the latest commit since it changed
- commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
- if err == nil && commit != nil {
- // We have the comment, now find what directory we can return the user to
- // (must have entries)
- treePath = GetClosestParentWithFiles(treePath, commit)
- } else {
- treePath = "" // otherwise return them to the root of the repo
+ if fInfo.isLFSFile() {
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ if err != nil {
+ _ = dataRc.Close()
+ ctx.ServerError("GetTreePathLock", err)
+ return nil, nil, nil
+ } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
+ _ = dataRc.Close()
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
+ return buf, dataRc, fInfo
}
-// UploadFile render upload file page
-func UploadFile(ctx *context.Context) {
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- return
- }
- ctx.Repo.TreePath = treePath
-
- treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
- }
-
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
- ctx.HTML(http.StatusOK, tplUploadFile)
-}
-
-// UploadFilePost response for uploading file
-func UploadFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
-
- oldBranchName := ctx.Repo.BranchName
- branchName := oldBranchName
-
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- form.TreePath = cleanUploadFileName(form.TreePath)
+func EditFile(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ ctx.Data["IsNewFile"] = isNewFile
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
+ // Check if the filename (and additional path) is specified in the querystring
+ // (filename is a misnomer, but kept for compatibility with GitHub)
+ urlQuery := ctx.Req.URL.Query()
+ queryFilename := urlQuery.Get("filename")
+ if queryFilename != "" {
+ newTreePath := path.Join(ctx.Repo.TreePath, queryFilename)
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))
+ urlQuery.Del("filename")
+ if newQueryParams := urlQuery.Encode(); newQueryParams != "" {
+ redirectTo += "?" + newQueryParams
+ }
+ ctx.Redirect(redirectTo)
+ return
}
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = branchName
+ // on the "New File" page, we should add an empty path field to make end users could input a new name
+ prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplUploadFile)
+ prepareEditorCommitFormOptions(ctx, editorAction)
+ if ctx.Written() {
return
}
- if oldBranchName != branchName {
- if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err == nil {
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
+ if !isNewFile {
+ prefetch, dataRc, fInfo := editFileOpenExisting(ctx)
+ if ctx.Written() {
return
}
- } else if !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
- return
- }
+ defer dataRc.Close()
- if !ctx.Repo.Repository.IsEmpty {
- var newTreePath string
- for _, part := range treeNames {
- newTreePath = path.Join(newTreePath, part)
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
+ ctx.Data["FileSize"] = fInfo.fileSize
+
+ // Only some file types are editable online as text.
+ if fInfo.isLFSFile() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
+ } else if !fInfo.st.IsRepresentableAsText() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
+ } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
+ }
+
+ if ctx.Data["NotEditableReason"] == nil {
+ buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc))
if err != nil {
- if git.IsErrNotExist(err) {
- break // Means there is no item with that name, so we're good
- }
- ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
+ ctx.ServerError("ReadAll", err)
return
}
-
- // User can only upload files to a directory, the directory name shouldn't be an existing file.
- if !entry.IsDir() {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
- return
+ if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
+ ctx.Data["FileContent"] = string(buf)
+ } else {
+ ctx.Data["FileContent"] = content
}
}
}
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- dir := form.TreePath
- if dir == "" {
- dir = "/"
- }
- message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
+ ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
+ ctx.HTML(http.StatusOK, tplEditFile)
+}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form)
+func EditFilePost(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
- LastCommitID: ctx.Repo.CommitID,
- OldBranch: oldBranchName,
- NewBranch: branchName,
- TreePath: form.TreePath,
- Message: message,
- Files: form.Files,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- fileErr := err.(files_service.ErrFilePathInvalid)
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
- default:
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- branchErr := err.(git.ErrBranchNotExist)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("UploadFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplUploadFile, &form)
- }
- } else {
- // os.ErrNotExist - upload file missing in the intervening time?!
- log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
- ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
- }
+ defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath))
+
+ var operation string
+ if isNewFile {
+ operation = "create"
+ } else if parsed.form.Content.Has() {
+ // The form content only has data if the file is representable as text, is not too large and not in lfs.
+ operation = "update"
+ } else if ctx.Repo.TreePath != parsed.form.TreePath {
+ // If it doesn't have data, the only possible operation is a "rename"
+ operation = "rename"
+ } else {
+ // It should never happen, just in case
+ ctx.JSONError(ctx.Tr("error.occurred"))
return
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
- }
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: operation,
+ FromTreePath: ctx.Repo.TreePath,
+ TreePath: parsed.form.TreePath,
+ ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")),
+ },
+ },
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
-func cleanUploadFileName(name string) string {
- // Rebase the filename
- name = util.PathJoinRel(name)
- // Git disallows any filenames to have a .git directory in them.
- for _, part := range strings.Split(name, "/") {
- if strings.ToLower(part) == ".git" {
- return ""
- }
+// DeleteFile render delete file page
+func DeleteFile(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_delete")
+ if ctx.Written() {
+ return
}
- return name
+ ctx.Data["PageIsDelete"] = true
+ ctx.HTML(http.StatusOK, tplDeleteFile)
}
-// UploadFileToServer upload file to server file dir not git
-func UploadFileToServer(ctx *context.Context) {
- file, header, err := ctx.Req.FormFile("file")
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+// DeleteFilePost response for deleting file
+func DeleteFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- defer file.Close()
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(file, buf)
- if n > 0 {
- buf = buf[:n]
- }
-
- err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ treePath := ctx.Repo.TreePath
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ TreePath: treePath,
+ },
+ },
+ Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
if err != nil {
- ctx.HTTPError(http.StatusBadRequest, err.Error())
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
- name := cleanUploadFileName(header.Filename)
- if len(name) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid")
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
+ redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
+ redirectForCommitChoice(ctx, parsed, redirectTreePath)
+}
- upload, err := repo_model.NewUpload(ctx, name, buf, file)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
+func UploadFile(ctx *context.Context) {
+ ctx.Data["PageIsUpload"] = true
+ prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
+ opts := prepareEditorCommitFormOptions(ctx, "_upload")
+ if ctx.Written() {
return
}
+ upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
- log.Trace("New file uploaded: %s", upload.UUID)
- ctx.JSON(http.StatusOK, map[string]string{
- "uuid": upload.UUID,
- })
+ ctx.HTML(http.StatusOK, tplUploadFile)
}
-// RemoveUploadFileFromServer remove file from server file dir
-func RemoveUploadFileFromServer(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
- if len(form.File) == 0 {
- ctx.Status(http.StatusNoContent)
+func UploadFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
+ err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ TreePath: parsed.form.TreePath,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: parsed.form.Files,
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
-
- log.Trace("Upload file removed: %s", form.File)
- ctx.Status(http.StatusNoContent)
-}
-
-// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
-// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
-// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
-// type in the branch name themselves (will be an empty field)
-func GetUniquePatchBranchName(ctx *context.Context) string {
- prefix := ctx.Doer.LowerName + "-patch-"
- for i := 1; i <= 1000; i++ {
- branchName := fmt.Sprintf("%s%d", prefix, i)
- if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err != nil {
- if git.IsErrBranchNotExist(err) {
- return branchName
- }
- log.Error("GetUniquePatchBranchName: %v", err)
- return ""
- }
- }
- return ""
-}
-
-// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
-// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
-// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
-func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
- if len(treePath) == 0 || treePath == "." {
- return ""
- }
- // see if the tree has entries
- if tree, err := commit.SubTree(treePath); err != nil {
- // failed to get tree, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
- // no files in this dir, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- }
- return treePath
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go
new file mode 100644
index 0000000000..bd2811cc5f
--- /dev/null
+++ b/routers/web/repo/editor_apply_patch.go
@@ -0,0 +1,51 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func NewDiffPatch(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_diffpatch")
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["PageIsPatch"] = true
+ ctx.HTML(http.StatusOK, tplPatchFile)
+}
+
+// NewDiffPatchPost response for sending patch page
+func NewDiffPatchPost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
+ _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go
new file mode 100644
index 0000000000..10c2741b1c
--- /dev/null
+++ b/routers/web/repo/editor_cherry_pick.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func CherryPick(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_cherrypick")
+ if ctx.Written() {
+ return
+ }
+
+ fromCommitID := ctx.PathParam("sha")
+ ctx.Data["FromCommitID"] = fromCommitID
+ cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
+ if err != nil {
+ HandleGitError(ctx, "GetCommit", err)
+ return
+ }
+
+ if ctx.FormString("cherry-pick-type") == "revert" {
+ ctx.Data["CherryPickType"] = "revert"
+ ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
+ ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
+ } else {
+ ctx.Data["CherryPickType"] = "cherry-pick"
+ splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
+ ctx.Data["commit_summary"] = splits[0]
+ ctx.Data["commit_message"] = splits[1]
+ }
+
+ ctx.HTML(http.StatusOK, tplCherryPick)
+}
+
+func CherryPickPost(ctx *context.Context) {
+ fromCommitID := ctx.PathParam("sha")
+ parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
+ opts := &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ }
+
+ // First try the simple plain read-tree -m approach
+ opts.Content = fromCommitID
+ if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
+ // Drop through to the "apply" method
+ buf := &bytes.Buffer{}
+ if parsed.form.Revert {
+ err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
+ } else {
+ err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
+ }
+ if err == nil {
+ opts.Content = buf.String()
+ _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go
new file mode 100644
index 0000000000..245226a039
--- /dev/null
+++ b/routers/web/repo/editor_error.go
@@ -0,0 +1,82 @@
+// Copyright 2025 Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/utils"
+ context_service "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func errorAs[T error](v error) (e T, ok bool) {
+ if errors.As(v, &e) {
+ return e, true
+ }
+ return e, false
+}
+
+func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": message,
+ "Summary": summary,
+ "Details": utils.SanitizeFlashErrorString(details),
+ })
+ if err == nil {
+ ctx.JSONError(flashError)
+ } else {
+ log.Error("RenderToHTML: %v", err)
+ ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
+ }
+}
+
+func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
+ if errAs := util.ErrorAsLocale(err); errAs != nil {
+ ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
+ } else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
+ } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName))
+ } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok {
+ switch errAs.Type {
+ case git.EntryModeSymlink:
+ ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
+ case git.EntryModeTree:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
+ case git.EntryModeBlob:
+ ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
+ default:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ }
+ } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
+ } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
+ } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
+ } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok {
+ if errAs.Message == "" {
+ ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
+ } else {
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
+ }
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONError(ctx.Tr("error.not_found"))
+ } else {
+ setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
+ }
+}
diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go
new file mode 100644
index 0000000000..b78a634c00
--- /dev/null
+++ b/routers/web/repo/editor_fork.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const tplEditorFork templates.TplName = "repo/editor/fork"
+
+func ForkToEdit(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, tplEditorFork)
+}
+
+func ForkToEditPost(ctx *context.Context) {
+ ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
+ BaseRepo: ctx.Repo.Repository,
+ Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
+ Description: ctx.Repo.Repository.Description,
+ SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork?
+ })
+ if ctx.Written() {
+ return
+ }
+ ctx.JSONRedirect("") // reload the page, the new fork should be editable now
+}
diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go
new file mode 100644
index 0000000000..14be5b72b6
--- /dev/null
+++ b/routers/web/repo/editor_preview.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func DiffPreviewPost(ctx *context.Context) {
+ content := ctx.FormString("content")
+ treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if treePath == "" {
+ ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
+ return
+ }
+
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ ctx.ServerError("GetTreeEntryByPath", err)
+ return
+ } else if entry.IsDir() {
+ ctx.HTTPError(http.StatusUnprocessableEntity)
+ return
+ }
+
+ diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
+ if err != nil {
+ ctx.ServerError("GetDiffPreview", err)
+ return
+ }
+
+ if len(diff.Files) != 0 {
+ ctx.Data["File"] = diff.Files[0]
+ }
+
+ ctx.HTML(http.StatusOK, tplEditDiffPreview)
+}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 566db31693..6e2c1d6219 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -6,76 +6,27 @@ package repo
import (
"testing"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
-func TestCleanUploadName(t *testing.T) {
+func TestEditorUtils(t *testing.T) {
unittest.PrepareTestEnv(t)
-
- kases := map[string]string{
- ".git/refs/master": "",
- "/root/abc": "root/abc",
- "./../../abc": "abc",
- "a/../.git": "",
- "a/../../../abc": "abc",
- "../../../acd": "acd",
- "../../.git/abc": "",
- "..\\..\\.git/abc": "..\\..\\.git/abc",
- "..\\../.git/abc": "",
- "..\\../.git": "",
- "abc/../def": "def",
- ".drone.yml": ".drone.yml",
- ".abc/def/.drone.yml": ".abc/def/.drone.yml",
- "..drone.yml.": "..drone.yml.",
- "..a.dotty...name...": "..a.dotty...name...",
- "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
- }
- for k, v := range kases {
- assert.EqualValues(t, cleanUploadFileName(k), v)
- }
-}
-
-func TestGetUniquePatchBranchName(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- expectedBranchName := "user2-patch-1"
- branchName := GetUniquePatchBranchName(ctx)
- assert.Equal(t, expectedBranchName, branchName)
-}
-
-func TestGetClosestParentWithFiles(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- branch := repo.DefaultBranch
- gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
- defer gitRepo.Close()
- commit, _ := gitRepo.GetBranchCommit(branch)
- var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
- for _, deletedFile := range []string{
- "dir1/dir2/dir3/file.txt",
- "file.txt",
- } {
- treePath := GetClosestParentWithFiles(deletedFile, commit)
- assert.Equal(t, expectedTreePath, treePath)
- }
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ t.Run("getUniquePatchBranchName", func(t *testing.T) {
+ branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
+ assert.Equal(t, "user2-patch-1", branchName)
+ })
+ t.Run("getClosestParentWithFiles", func(t *testing.T) {
+ gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
+ defer gitRepo.Close()
+ treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
+ assert.Equal(t, "docs", treePath)
+ treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
+ assert.Empty(t, treePath)
+ })
}
diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go
new file mode 100644
index 0000000000..1ce9a1aca4
--- /dev/null
+++ b/routers/web/repo/editor_uploader.go
@@ -0,0 +1,61 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+// UploadFileToServer upload file to server file dir not git
+func UploadFileToServer(ctx *context.Context) {
+ file, header, err := ctx.Req.FormFile("file")
+ if err != nil {
+ ctx.ServerError("FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(file, buf)
+ if n > 0 {
+ buf = buf[:n]
+ }
+
+ err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ if err != nil {
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ name := files_service.CleanGitTreePath(header.Filename)
+ if len(name) == 0 {
+ ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
+ return
+ }
+
+ uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
+ if err != nil {
+ ctx.ServerError("NewUpload", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
+}
+
+// RemoveUploadFileFromServer remove file from server file dir
+func RemoveUploadFileFromServer(ctx *context.Context) {
+ fileUUID := ctx.FormString("file")
+ if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
+ ctx.ServerError("DeleteUploadByUUID", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go
new file mode 100644
index 0000000000..f910f0bd40
--- /dev/null
+++ b/routers/web/repo/editor_util.go
@@ -0,0 +1,110 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "strings"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ context_service "code.gitea.io/gitea/services/context"
+)
+
+// getUniquePatchBranchName Gets a unique branch name for a new patch branch
+// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
+// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
+// type in the branch name themselves (will be an empty field)
+func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
+ prefix := prefixName + "-patch-"
+ for i := 1; i <= 1000; i++ {
+ branchName := fmt.Sprintf("%s%d", prefix, i)
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
+ log.Error("getUniquePatchBranchName: %v", err)
+ return ""
+ } else if !exist {
+ return branchName
+ }
+ }
+ return ""
+}
+
+// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
+// deleted. It returns "" for the tree root if no parents other than the root have files.
+func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
+ var f func(treePath string, commit *git.Commit) string
+ f = func(treePath string, commit *git.Commit) string {
+ if treePath == "" || treePath == "." {
+ return ""
+ }
+ // see if the tree has entries
+ if tree, err := commit.SubTree(treePath); err != nil {
+ return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
+ } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
+ return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
+ }
+ return treePath
+ }
+ commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
+ if err != nil {
+ log.Error("GetBranchCommit: %v", err)
+ return ""
+ }
+ return f(originTreePath, commit)
+}
+
+// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
+func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
+ ec, _, err := ctx.Repo.GetEditorconfig()
+ if err == nil {
+ def, err := ec.GetDefinitionForFilename(treePath)
+ if err == nil {
+ jsonStr, _ := json.Marshal(def)
+ return string(jsonStr)
+ }
+ }
+ return "null"
+}
+
+// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
+// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
+// or: []{""}, []{""} for the root treePath
+func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
+ treeNames = strings.Split(treePath, "/")
+ treePaths = make([]string, len(treeNames))
+ for i := range treeNames {
+ treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ }
+ return treeNames, treePaths
+}
+
+// getUniqueRepositoryName Gets a unique repository name for a user
+// It will append a -<num> 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 36e64bfee3..c2694e540f 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -91,12 +91,17 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
ctx.Data["CanForkToUser"] = canForkToUser
ctx.Data["Orgs"] = orgs
+ // TODO: this message should only be shown for the "current doer" when it is selected, just like the "new repo" page.
+ // msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", ctx.Doer.MaxCreationLimit())
+
if canForkToUser {
ctx.Data["ContextUser"] = ctx.Doer
+ ctx.Data["CanForkRepoInNewOwner"] = true
} else if len(orgs) > 0 {
ctx.Data["ContextUser"] = orgs[0]
+ ctx.Data["CanForkRepoInNewOwner"] = true
} else {
- ctx.Data["CanForkRepo"] = false
+ ctx.Data["CanForkRepoInNewOwner"] = false
ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
return nil
}
@@ -120,15 +125,6 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
// Fork render repository fork page
func Fork(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_fork")
-
- if ctx.Doer.CanForkRepo() {
- ctx.Data["CanForkRepo"] = true
- } else {
- maxCreationLimit := ctx.Doer.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg, true)
- }
-
getForkRepository(ctx)
if ctx.Written() {
return
@@ -141,7 +137,6 @@ func Fork(ctx *context.Context) {
func ForkPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateRepoForm)
ctx.Data["Title"] = ctx.Tr("new_fork")
- ctx.Data["CanForkRepo"] = true
ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() {
@@ -156,7 +151,7 @@ func ForkPost(ctx *context.Context) {
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
+ ctx.JSONError(ctx.GetErrMsg())
return
}
@@ -164,12 +159,12 @@ func ForkPost(ctx *context.Context) {
traverseParentRepo := forkRepo
for {
if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
return
}
repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
if repo != nil {
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
return
}
if !traverseParentRepo.IsFork {
@@ -194,44 +189,50 @@ 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.RenderWithErr(msg, tplFork, &form)
+ ctx.JSONError(msg)
case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
case repo_model.IsErrRepoFilesAlreadyExist(err):
switch {
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
}
case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name))
case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern))
case errors.Is(err, user_model.ErrBlockedUser):
- ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+ ctx.JSONError(ctx.Tr("repo.fork.blocked_user"))
default:
ctx.ServerError("ForkPost", err)
}
- return
+ return nil
}
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ return repo
}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index f93d7fc66a..deb3ae4f3a 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "slices"
"strconv"
"strings"
"sync"
@@ -29,7 +30,6 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
@@ -78,7 +78,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true
} else {
- isPull = ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
+ isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
}
var accessMode perm.AccessMode
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
- askAuth = !isPublicPull || setting.Service.RequireSignInView
+ askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string
)
@@ -303,17 +303,12 @@ var (
func dummyInfoRefs(ctx *context.Context) {
infoRefsOnce.Do(func() {
- tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-info-refs-cache")
+ tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-info-refs-cache")
if err != nil {
log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
return
}
-
- defer func() {
- if err := util.RemoveAll(tmpDir); err != nil {
- log.Error("RemoveAll: %v", err)
- }
- }()
+ defer cleanup()
if err := git.InitRepository(ctx, tmpDir, true, git.Sha1ObjectFormat.Name()); err != nil {
log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
@@ -360,8 +355,8 @@ func setHeaderNoCache(ctx *context.Context) {
func setHeaderCacheForever(ctx *context.Context) {
now := time.Now().Unix()
expires := now + 31536000
- ctx.Resp.Header().Set("Date", fmt.Sprintf("%d", now))
- ctx.Resp.Header().Set("Expires", fmt.Sprintf("%d", expires))
+ ctx.Resp.Header().Set("Date", strconv.FormatInt(now, 10))
+ ctx.Resp.Header().Set("Expires", strconv.FormatInt(expires, 10))
ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
@@ -369,12 +364,7 @@ func containsParentDirectorySeparator(v string) bool {
if !strings.Contains(v, "..") {
return false
}
- for _, ent := range strings.FieldsFunc(v, isSlashRune) {
- if ent == ".." {
- return true
- }
- }
- return false
+ return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..")
}
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
@@ -394,7 +384,7 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
}
ctx.Resp.Header().Set("Content-Type", contentType)
- ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
+ ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
diff --git a/routers/web/repo/githttp_test.go b/routers/web/repo/githttp_test.go
index 5ba8de3d63..0164b11f66 100644
--- a/routers/web/repo/githttp_test.go
+++ b/routers/web/repo/githttp_test.go
@@ -37,6 +37,6 @@ func TestContainsParentDirectorySeparator(t *testing.T) {
}
for i := range tests {
- assert.EqualValues(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
+ assert.Equal(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
}
}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index dbbe29a3c3..54b7e5df2a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -43,7 +43,8 @@ const (
tplIssueChoose templates.TplName = "repo/issue/choose"
tplIssueView templates.TplName = "repo/issue/view"
- tplReactions templates.TplName = "repo/issue/view_content/reactions"
+ tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
+ tplReactions templates.TplName = "repo/issue/view_content/reactions"
issueTemplateKey = "IssueTemplate"
issueTemplateTitleKey = "IssueTemplateTitle"
@@ -211,7 +212,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList {
return nil
}
issueIDs := make([]int64, 0, 10)
- for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+ for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") {
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
if err != nil {
ctx.ServerError("ParseInt", err)
@@ -363,7 +364,9 @@ func UpdateIssueContent(ctx *context.Context) {
}
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0",
+ })
content, err := markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -417,6 +420,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
continue
}
issue.MilestoneID = milestoneID
+ if milestoneID > 0 {
+ var err error
+ issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
+ if err != nil {
+ ctx.ServerError("GetMilestoneByRepoID", err)
+ return
+ }
+ } else {
+ issue.Milestone = nil
+ }
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.ServerError("ChangeMilestoneAssign", err)
return
diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go
index 45463200f6..cb5b2d8019 100644
--- a/routers/web/repo/issue_comment.go
+++ b/routers/web/repo/issue_comment.go
@@ -8,6 +8,7 @@ import (
"fmt"
"html/template"
"net/http"
+ "strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
@@ -96,13 +97,13 @@ func NewComment(ctx *context.Context) {
// Regenerate patch and test conflict.
if pr == nil {
issue.PullRequest.HeadCommitID = ""
- pull_service.AddToTaskQueue(ctx, issue.PullRequest)
+ pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest)
}
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
// get head commit of PR
if pull.Flow == issues_model.PullRequestFlowGithub {
- prHeadRef := pull.GetGitRefName()
+ prHeadRef := pull.GetGitHeadRefName()
if err := pull.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("Unable to load base repo", err)
return
@@ -239,21 +240,28 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
- oldContent := comment.Content
newContent := ctx.FormString("content")
contentVersion := ctx.FormInt("content_version")
+ if contentVersion != comment.ContentVersion {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ return
+ }
- // allow to save empty content
- comment.Content = newContent
- if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
- } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
- ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
- } else {
- ctx.ServerError("UpdateComment", err)
+ if newContent != comment.Content {
+ // allow to save empty content
+ oldContent := comment.Content
+ comment.Content = newContent
+
+ if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
+ return
}
- return
}
if err := comment.LoadAttachments(ctx); err != nil {
@@ -271,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) {
var renderedContent template.HTML
if comment.Content != "" {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
renderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index c2c208736c..3602f4ec8a 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -157,15 +157,16 @@ func GetContentHistoryDetail(ctx *context.Context) {
diffHTMLBuf := bytes.Buffer{}
diffHTMLBuf.WriteString("<pre class='chroma'>")
for _, it := range diff {
- if it.Type == diffmatchpatch.DiffInsert {
+ switch it.Type {
+ case diffmatchpatch.DiffInsert:
diffHTMLBuf.WriteString("<span class='gi'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
- } else if it.Type == diffmatchpatch.DiffDelete {
+ case diffmatchpatch.DiffDelete:
diffHTMLBuf.WriteString("<span class='gd'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
- } else {
+ default:
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
}
}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 62c0128f19..72a316e98d 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -4,6 +4,7 @@
package repo
import (
+ "errors"
"net/http"
"code.gitea.io/gitea/models/db"
@@ -13,7 +14,9 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
@@ -100,54 +103,53 @@ func RetrieveLabelsForList(ctx *context.Context) {
// NewLabel create new label for repository
func NewLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
- ctx.Data["Title"] = ctx.Tr("repo.labels")
- ctx.Data["PageIsLabels"] = true
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
return
}
l := &issues_model.Label{
- RepoID: ctx.Repo.Repository.ID,
- Name: form.Title,
- Exclusive: form.Exclusive,
- Description: form.Description,
- Color: form.Color,
+ RepoID: ctx.Repo.Repository.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ ExclusiveOrder: form.ExclusiveOrder,
+ Description: form.Description,
+ Color: form.Color,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
+ return
+ }
+
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
- if err != nil {
- switch {
- case issues_model.IsErrRepoLabelNotExist(err):
- ctx.HTTPError(http.StatusNotFound)
- default:
- ctx.ServerError("UpdateLabel", err)
- }
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLabelInRepoByID", err)
return
}
+
l.Name = form.Title
l.Exclusive = form.Exclusive
+ l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color
-
l.SetArchived(form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// DeleteLabel delete a label
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 486c2e35a2..f4eca26f8e 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -6,10 +6,12 @@ package repo
import (
"net/http"
"strconv"
+ "strings"
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -19,26 +21,27 @@ import (
"github.com/stretchr/testify/assert"
)
-func int64SliceToCommaSeparated(a []int64) string {
- s := ""
- for i, n := range a {
- if i > 0 {
- s += ","
- }
- s += strconv.Itoa(int(n))
- }
- return s
+func TestIssueLabel(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ t.Run("RetrieveLabels", testRetrieveLabels)
+ t.Run("NewLabel", testNewLabel)
+ t.Run("NewLabelInvalidColor", testNewLabelInvalidColor)
+ t.Run("UpdateLabel", testUpdateLabel)
+ t.Run("UpdateLabelInvalidColor", testUpdateLabelInvalidColor)
+ t.Run("UpdateIssueLabelClear", testUpdateIssueLabelClear)
+ t.Run("UpdateIssueLabelToggle", testUpdateIssueLabelToggle)
+ t.Run("InitializeLabels", testInitializeLabels)
+ t.Run("DeleteLabel", testDeleteLabel)
}
-func TestInitializeLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testInitializeLabels(t *testing.T) {
assert.NoError(t, repository.LoadRepoConfig())
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/initialize")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2,
Name: "enhancement",
@@ -47,8 +50,7 @@ func TestInitializeLabels(t *testing.T) {
assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
}
-func TestRetrieveLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testRetrieveLabels(t *testing.T) {
for _, testCase := range []struct {
RepoID int64
Sort string
@@ -68,15 +70,14 @@ func TestRetrieveLabels(t *testing.T) {
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
- assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
+ assert.Equal(t, testCase.ExpectedLabelIDs[i], label.ID)
}
}
}
}
-func TestNewLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -84,17 +85,32 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestUpdateLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ Title: "newlabel-x",
+ Color: "bad-label-code",
+ })
+ NewLabel(ctx)
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertNotExistsBean(t, &issues_model.Label{
+ Name: "newlabel-x",
+ })
+}
+
+func testUpdateLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -104,43 +120,62 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true,
})
UpdateLabel(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestDeleteLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ ID: 1,
+ Title: "label1",
+ Color: "bad-label-code",
+ })
+
+ UpdateLabel(ctx)
+
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
+ ID: 1,
+ Name: "label1",
+ Color: "#abcdef",
+ })
+}
+
+func testDeleteLabel(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/delete")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
-func TestUpdateIssueLabel_Clear(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateIssueLabelClear(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
unittest.CheckConsistencyFor(t, &issues_model.Label{})
}
-func TestUpdateIssueLabel_Toggle(t *testing.T) {
+func testUpdateIssueLabelToggle(t *testing.T) {
for _, testCase := range []struct {
Action string
IssueIDs []int64
@@ -156,11 +191,12 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
- ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
+
+ ctx.Req.Form.Set("issue_ids", strings.Join(base.Int64sToStrings(testCase.IssueIDs), ","))
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
for _, issueID := range testCase.IssueIDs {
if testCase.ExpectedAdd {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})
diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go
index a65ae77795..fd34422cfc 100644
--- a/routers/web/repo/issue_list.go
+++ b/routers/web/repo/issue_list.go
@@ -5,8 +5,10 @@ package repo
import (
"bytes"
- "fmt"
+ "maps"
"net/http"
+ "slices"
+ "sort"
"strconv"
"strings"
@@ -18,6 +20,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ db_indexer "code.gitea.io/gitea/modules/indexer/issues/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@@ -30,14 +33,6 @@ import (
pull_service "code.gitea.io/gitea/services/pull"
)
-func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
- ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
- if err != nil {
- return nil, fmt.Errorf("SearchIssues: %w", err)
- }
- return ids, nil
-}
-
func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
}
@@ -66,7 +61,7 @@ func SearchIssues(ctx *context.Context) {
)
{
// find repos user can access (for issue search)
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
@@ -208,10 +203,10 @@ func SearchIssues(ctx *context.Context) {
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- searchOpt.PosterID = optional.Some(ctxUserID)
+ searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
}
if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = optional.Some(ctxUserID)
+ searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
}
if ctx.FormBool("mentioned") {
searchOpt.MentionID = optional.Some(ctxUserID)
@@ -373,10 +368,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
}
if createdByID > 0 {
- searchOpt.PosterID = optional.Some(createdByID)
+ searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
}
if assignedByID > 0 {
- searchOpt.AssigneeID = optional.Some(assignedByID)
+ searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
}
if mentionedByID > 0 {
searchOpt.MentionID = optional.Some(mentionedByID)
@@ -459,6 +454,19 @@ func UpdateIssueStatus(ctx *context.Context) {
ctx.JSONOK()
}
+func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*issues_model.Label) {
+ scopeSet := make(map[string]bool)
+ for _, label := range allLabels {
+ scope := label.ExclusiveScope()
+ if len(scope) > 0 && label.ExclusiveOrder > 0 {
+ scopeSet[scope] = true
+ }
+ }
+ scopes := slices.Collect(maps.Keys(scopeSet))
+ sort.Strings(scopes)
+ ctx.Data["ExclusiveLabelScopes"] = scopes
+}
+
func renderMilestones(ctx *context.Context) {
// Get milestones
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
@@ -481,7 +489,7 @@ func renderMilestones(ctx *context.Context) {
ctx.Data["ClosedMilestones"] = closedMilestones
}
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
+func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
var err error
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
@@ -490,7 +498,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
viewType = "all"
}
- assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
+ assigneeID := ctx.FormString("assignee")
posterUsername := ctx.FormString("poster")
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
var mentionedID, reviewRequestedID, reviewedID int64
@@ -498,11 +506,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
if ctx.IsSigned {
switch viewType {
case "created_by":
- posterUserID = optional.Some(ctx.Doer.ID)
+ posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
case "mentioned":
mentionedID = ctx.Doer.ID
case "assigned":
- assigneeID = ctx.Doer.ID
+ assigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
case "review_requested":
reviewRequestedID = ctx.Doer.ID
case "reviewed_by":
@@ -521,18 +529,21 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
mileIDs = []int64{milestoneID}
}
- labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
+ preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
if ctx.Written() {
return
}
+ prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
+
+ var keywordMatchedIssueIDs []int64
var issueStats *issues_model.IssueStats
statsOpts := &issues_model.IssuesOptions{
RepoIDs: []int64{repo.ID},
- LabelIDs: labelIDs,
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
MilestoneIDs: mileIDs,
ProjectID: projectID,
- AssigneeID: optional.Some(assigneeID),
+ AssigneeID: assigneeID,
MentionedID: mentionedID,
PosterID: posterUserID,
ReviewRequestedID: reviewRequestedID,
@@ -541,7 +552,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
IssueIDs: nil,
}
if keyword != "" {
- allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
+ keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
if err != nil {
if issue_indexer.IsAvailable(ctx) {
ctx.ServerError("issueIDsFromSearch", err)
@@ -550,14 +561,17 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["IssueIndexerUnavailable"] = true
return
}
- statsOpts.IssueIDs = allIssueIDs
+ if len(keywordMatchedIssueIDs) == 0 {
+ // It did search with the keyword, but no issue found, just set issueStats to empty, then no need to do query again.
+ issueStats = &issues_model.IssueStats{}
+ // set keywordMatchedIssueIDs to empty slice, so we can distinguish it from "nil"
+ keywordMatchedIssueIDs = []int64{}
+ }
+ statsOpts.IssueIDs = keywordMatchedIssueIDs
}
- if keyword != "" && len(statsOpts.IssueIDs) == 0 {
- // So it did search with the keyword, but no issue found.
- // Just set issueStats to empty.
- issueStats = &issues_model.IssueStats{}
- } else {
- // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
+
+ if issueStats == nil {
+ // Either it did search with the keyword, and found some issues, it needs to get issueStats of these issues.
// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
if err != nil {
@@ -589,31 +603,27 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["TotalTrackedTime"] = totalTrackedTime
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
-
- var total int
- switch {
- case isShowClosed.Value():
- total = int(issueStats.ClosedCount)
- case !isShowClosed.Has():
- total = int(issueStats.OpenCount + issueStats.ClosedCount)
- default:
- total = int(issueStats.OpenCount)
+ // prepare pager
+ total := int(issueStats.OpenCount + issueStats.ClosedCount)
+ if isShowClosed.Has() {
+ total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
}
+ page := max(ctx.FormInt("page"), 1)
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+ // prepare real issue list:
var issues issues_model.IssueList
- {
- ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
+ if keywordMatchedIssueIDs == nil || len(keywordMatchedIssueIDs) > 0 {
+ // Either it did search with the keyword, and found some issues, then keywordMatchedIssueIDs is not null, it needs to use db indexer.
+ // Or the keyword is empty, it also needs to usd db indexer.
+ // In either case, no need to use keyword anymore
+ searchResult, err := db_indexer.GetIndexer().FindWithIssueOptions(ctx, &issues_model.IssuesOptions{
Paginator: &db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo.ID},
- AssigneeID: optional.Some(assigneeID),
+ AssigneeID: assigneeID,
PosterID: posterUserID,
MentionedID: mentionedID,
ReviewRequestedID: reviewRequestedID,
@@ -622,18 +632,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ProjectID: projectID,
IsClosed: isShowClosed,
IsPull: isPullOption,
- LabelIDs: labelIDs,
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
SortType: sortType,
+ IssueIDs: keywordMatchedIssueIDs,
})
if err != nil {
- if issue_indexer.IsAvailable(ctx) {
- ctx.ServerError("issueIDsFromSearch", err)
- return
- }
- ctx.Data["IssueIndexerUnavailable"] = true
+ ctx.ServerError("DBIndexer.Search", err)
return
}
- issues, err = issues_model.GetIssuesByIDs(ctx, ids, true)
+ issueIDs := issue_indexer.SearchResultToIDSlice(searchResult)
+ issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
if err != nil {
ctx.ServerError("GetIssuesByIDs", err)
return
@@ -696,9 +704,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -727,7 +736,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["IssueStats"] = issueStats
ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount
- ctx.Data["SelLabelIDs"] = labelIDs
+ ctx.Data["SelLabelIDs"] = preparedLabelFilter.SelectedLabelIDs
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["MilestoneID"] = milestoneID
@@ -758,6 +767,10 @@ func Issues(ctx *context.Context) {
}
ctx.Data["Title"] = ctx.Tr("repo.pulls")
ctx.Data["PageIsPullList"] = true
+ prepareRecentlyPushedNewBranches(ctx)
+ if ctx.Written() {
+ return
+ }
} else {
MustEnableIssues(ctx)
if ctx.Written() {
@@ -768,7 +781,7 @@ func Issues(ctx *context.Context) {
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
}
- issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
+ prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
if ctx.Written() {
return
}
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index 1d5fc8a5f3..bc8aabd90b 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -24,11 +24,6 @@ func LockIssue(ctx *context.Context) {
return
}
- if !form.HasValidReason() {
- ctx.JSONError(ctx.Tr("repo.issues.lock.unknown_reason"))
- return
- }
-
if err := issues_model.LockIssue(ctx, &issues_model.IssueLockOptions{
Doer: ctx.Doer,
Issue: issue,
diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go
index 9f52396414..887019b146 100644
--- a/routers/web/repo/issue_new.go
+++ b/routers/web/repo/issue_new.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"html/template"
+ "maps"
"net/http"
"slices"
"sort"
@@ -136,9 +137,7 @@ func NewIssue(ctx *context.Context) {
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
- for k, v := range errs {
- ret.TemplateErrors[k] = v
- }
+ maps.Copy(ret.TemplateErrors, errs)
if ctx.Written() {
return
}
@@ -223,11 +222,11 @@ func DeleteIssue(ctx *context.Context) {
}
if issue.IsPull {
- ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
+ ctx.Redirect(ctx.Repo.Repository.Link()+"/pulls", http.StatusSeeOther)
return
}
- ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
+ ctx.Redirect(ctx.Repo.Repository.Link()+"/issues", http.StatusSeeOther)
}
func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(ItemType) KeyType) container.Set[KeyType] {
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 73e279e0a6..2de3a7cfec 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -4,41 +4,53 @@
package repo
import (
- "strings"
-
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/services/context"
)
-// IssueStopwatch creates or stops a stopwatch for the given issue.
-func IssueStopwatch(c *context.Context) {
+// IssueStartStopwatch creates a stopwatch for the given issue.
+func IssueStartStopwatch(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
- var showSuccessMessage bool
-
- if !issues_model.StopwatchExists(c, c.Doer.ID, issue.ID) {
- showSuccessMessage = true
- }
-
if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
c.NotFound(nil)
return
}
- if err := issues_model.CreateOrStopIssueStopwatch(c, c.Doer, issue); err != nil {
- c.ServerError("CreateOrStopIssueStopwatch", err)
+ if ok, err := issues_model.CreateIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("CreateIssueStopwatch", err)
return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_created"))
+ } else {
+ c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
}
+ c.JSONRedirect("")
+}
- if showSuccessMessage {
- c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
+// IssueStopStopwatch stops a stopwatch for the given issue.
+func IssueStopStopwatch(c *context.Context) {
+ issue := GetActionIssue(c)
+ if c.Written() {
+ return
}
+ if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
+ c.NotFound(nil)
+ return
+ }
+
+ if ok, err := issues_model.FinishIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("FinishIssueStopwatch", err)
+ return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_stopped"))
+ }
c.JSONRedirect("")
}
@@ -53,7 +65,7 @@ func CancelStopwatch(c *context.Context) {
return
}
- if err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
+ if _, err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
c.ServerError("CancelStopwatch", err)
return
}
@@ -72,39 +84,3 @@ func CancelStopwatch(c *context.Context) {
c.JSONRedirect("")
}
-
-// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
-func GetActiveStopwatch(ctx *context.Context) {
- if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
- return
- }
-
- if !ctx.IsSigned {
- return
- }
-
- _, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("HasUserStopwatch", err)
- return
- }
-
- if sw == nil || sw.ID == 0 {
- return
- }
-
- ctx.Data["ActiveStopwatch"] = StopwatchTmplInfo{
- issue.Link(),
- issue.Repo.FullName(),
- issue.Index,
- sw.Seconds() + 1, // ensure time is never zero in ui
- }
-}
-
-// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
-type StopwatchTmplInfo struct {
- IssueLink string
- RepoSlug string
- IssueIndex int64
- Seconds int64
-}
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index b312f1260a..d0064e763e 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"sort"
+ "strconv"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@@ -31,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars"
+ "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
@@ -271,14 +273,29 @@ func combineLabelComments(issue *issues_model.Issue) {
}
}
-// ViewIssue render issue view page
-func ViewIssue(ctx *context.Context) {
+func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
+ return nil
+ }
+ issue.Repo = ctx.Repo.Repository
+ ctx.Data["Issue"] = issue
+
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ ctx.ServerError("LoadPullRequest", err)
+ return nil
+ }
+ return issue
+}
+
+func handleViewIssueRedirectExternal(ctx *context.Context) {
if ctx.PathParam("type") == "issues" {
// If issue was requested we check if repo has external tracker and redirect
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil && extIssueUnit != nil {
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
- metas := ctx.Repo.Repository.ComposeMetas(ctx)
+ metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
metas["index"] = ctx.PathParam("index")
res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
if err != nil {
@@ -294,18 +311,18 @@ func ViewIssue(ctx *context.Context) {
return
}
}
+}
- issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
- if err != nil {
- if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound(err)
- } else {
- ctx.ServerError("GetIssueByIndex", err)
- }
+// ViewIssue render issue view page
+func ViewIssue(ctx *context.Context) {
+ handleViewIssueRedirectExternal(ctx)
+ if ctx.Written() {
return
}
- if issue.Repo == nil {
- issue.Repo = ctx.Repo.Repository
+
+ issue := prepareIssueViewLoad(ctx)
+ if ctx.Written() {
+ return
}
// Make sure type and URL matches.
@@ -337,12 +354,12 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
- if err = issue.LoadAttributes(ctx); err != nil {
+ if err := issue.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
- if err = filterXRefComments(ctx, issue); err != nil {
+ if err := filterXRefComments(ctx, issue); err != nil {
ctx.ServerError("filterXRefComments", err)
return
}
@@ -351,7 +368,7 @@ func ViewIssue(ctx *context.Context) {
if ctx.IsSigned {
// Update issue-user.
- if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
+ if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
@@ -365,15 +382,13 @@ func ViewIssue(ctx *context.Context) {
prepareFuncs := []func(*context.Context, *issues_model.Issue){
prepareIssueViewContent,
- func(ctx *context.Context, issue *issues_model.Issue) {
- preparePullViewPullInfo(ctx, issue)
- },
prepareIssueViewCommentsAndSidebarParticipants,
- preparePullViewReviewAndMerge,
prepareIssueViewSidebarWatch,
prepareIssueViewSidebarTimeTracker,
prepareIssueViewSidebarDependency,
prepareIssueViewSidebarPin,
+ func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
+ preparePullViewReviewAndMerge,
}
for _, prepareFunc := range prepareFuncs {
@@ -412,9 +427,29 @@ func ViewIssue(ctx *context.Context) {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
+ if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
+ ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
+ }
+
ctx.HTML(http.StatusOK, tplIssueView)
}
+func ViewPullMergeBox(ctx *context.Context) {
+ issue := prepareIssueViewLoad(ctx)
+ if !issue.IsPull {
+ ctx.NotFound(nil)
+ return
+ }
+ preparePullViewPullInfo(ctx, issue)
+ preparePullViewReviewAndMerge(ctx, issue)
+ ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
+
+ // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly
+ ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ ctx.HTML(http.StatusOK, tplPullMergeBox)
+}
+
func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
ctx.Data["IssueDependencySearchType"] = "pulls"
@@ -457,7 +492,7 @@ func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
pull := issue.PullRequest
ctx.Data["WillSign"] = false
if ctx.Doer != nil {
- sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
+ sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName())
ctx.Data["WillSign"] = sign
ctx.Data["SigningKey"] = key
if err != nil {
@@ -594,7 +629,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
comment.Issue = issue
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -670,7 +707,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
}
} else if comment.Type.HasContentSupport() {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -727,6 +766,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
if !ctx.Repo.CanRead(unit.TypeActions) {
for _, commit := range comment.Commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
@@ -792,6 +834,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
allowMerge := false
canWriteToHeadRepo := false
+ pull_service.StartPullRequestCheckOnView(ctx, pull)
+
if ctx.IsSigned {
if err := pull.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
@@ -838,6 +882,7 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
}
}
+ ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
ctx.Data["AllowMerge"] = allowMerge
@@ -948,7 +993,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
var err error
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content
+ })
issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -958,5 +1005,4 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
ctx.ServerError("roleDescriptor", err)
return
}
- ctx.Data["Issue"] = issue
}
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index f1d0a857ea..dd53b1d3f1 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -38,10 +38,7 @@ func Milestones(ctx *context.Context) {
isShowClosed := ctx.FormString("state") == "closed"
sortType := ctx.FormString("sort")
keyword := ctx.FormTrim("q")
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
@@ -263,7 +260,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone
- issues(ctx, milestoneID, projectID, optional.None[bool]())
+ prepareIssueFilterAndList(ctx, milestoneID, projectID, optional.None[bool]())
ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 65a340a799..d09a57c03f 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -21,10 +21,7 @@ const (
// Packages displays a list of all packages in the repository
func Packages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
deleted file mode 100644
index 120b3469f6..0000000000
--- a/routers/web/repo/patch.go
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-const (
- tplPatchFile templates.TplName = "repo/editor/patch"
-)
-
-// NewDiffPatch render create patch page
-func NewDiffPatch(ctx *context.Context) {
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["PageIsPatch"] = true
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(200, tplPatchFile)
-}
-
-// NewDiffPatchPost response for sending patch page
-func NewDiffPatchPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["PageIsPatch"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-
- if ctx.HasError() {
- ctx.HTML(200, tplPatchFile)
- return
- }
-
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
-
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.patch")
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form)
- return
- }
-
- fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Content: strings.ReplaceAll(form.Content, "\r", ""),
- Author: gitCommitter,
- Committer: gitCommitter,
- })
- if err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
- }
-}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 5b81a5e4d1..a57976b4ca 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -61,10 +61,7 @@ func Projects(ctx *context.Context) {
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
keyword := ctx.FormTrim("q")
repo := ctx.Repo.Repository
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
@@ -313,14 +310,14 @@ func ViewProject(ctx *context.Context) {
return
}
- labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
+ preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
- assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
+ assigneeID := ctx.FormString("assignee")
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
RepoIDs: []int64{ctx.Repo.Repository.ID},
- LabelIDs: labelIDs,
- AssigneeID: optional.Some(assigneeID),
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
+ AssigneeID: assigneeID,
})
if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err)
@@ -381,8 +378,8 @@ func ViewProject(ctx *context.Context) {
}
// Get the exclusive scope for every label ID
- labelExclusiveScopes := make([]string, 0, len(labelIDs))
- for _, labelID := range labelIDs {
+ labelExclusiveScopes := make([]string, 0, len(preparedLabelFilter.SelectedLabelIDs))
+ for _, labelID := range preparedLabelFilter.SelectedLabelIDs {
foundExclusiveScope := false
for _, label := range labels {
if label.ID == labelID || label.ID == -labelID {
@@ -397,7 +394,7 @@ func ViewProject(ctx *context.Context) {
}
for _, l := range labels {
- l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
+ l.LoadSelectedLabelsAfterClick(preparedLabelFilter.SelectedLabelIDs, labelExclusiveScopes)
}
ctx.Data["Labels"] = labels
ctx.Data["NumLabels"] = len(labels)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index e12798f93d..bc58efeb6f 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -24,8 +24,10 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/graceful"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -181,6 +183,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
+ // FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@@ -188,21 +191,19 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
-
if mergeBaseCommitID == "" {
- ctx.NotFound(nil)
- return
+ return // no merge base, do nothing, do not stop the route handler, see below
}
- headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
+ // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view.
+ headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
- ctx.ServerError("GetRefCommitID", err)
+ log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
-
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
- ctx.ServerError("GetDiffShortStat", err)
+ log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
@@ -217,13 +218,13 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
if pull.MergeBase == "" {
var commitSHA, parentCommit string
// If there is a head or a patch file, and it is readable, grab info
- commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
+ commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
// Head File does not exist, try the patch
commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index)
if err == nil {
// Recreate pull head in files for next time
- if err := ctx.Repo.GitRepo.SetReference(pull.GetGitRefName(), commitSHA); err != nil {
+ if err := ctx.Repo.GitRepo.SetReference(pull.GetGitHeadRefName(), commitSHA); err != nil {
log.Error("Could not write head file", err)
}
} else {
@@ -273,7 +274,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
baseCommit := GetMergedBaseCommitID(ctx, issue)
compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
- baseCommit, pull.GetGitRefName(), false, false)
+ baseCommit, pull.GetGitHeadRefName(), false, false)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
ctx.Data["IsPullRequestBroken"] = true
@@ -291,7 +292,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -353,12 +354,12 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["HeadTarget"] = pull.HeadBranch
- sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+ sha, err := baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
- ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+ ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitHeadRefName()), err)
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -373,7 +374,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
}
compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
- pull.MergeBase, pull.GetGitRefName(), false, false)
+ pull.MergeBase, pull.GetGitHeadRefName(), false, false)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
ctx.Data["IsPullRequestBroken"] = true
@@ -406,12 +407,12 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
if pull.Flow == issues_model.PullRequestFlowGithub {
headBranchExist = gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch)
} else {
- headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitRefName())
+ headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
}
if headBranchExist {
if pull.Flow != issues_model.PullRequestFlowGithub {
- headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+ headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
} else {
headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
}
@@ -434,7 +435,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["GetCommitMessages"] = ""
}
- sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+ sha, err := baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
if git.IsErrNotExist(err) {
ctx.Data["IsPullRequestBroken"] = true
@@ -450,11 +451,11 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["NumFiles"] = 0
return nil
}
- ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+ ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitHeadRefName()), err)
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -521,7 +522,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
}
compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
- git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName(), false, false)
+ git.BranchPrefix+pull.BaseBranch, pull.GetGitHeadRefName(), false, false)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
ctx.Data["IsPullRequestBroken"] = true
@@ -580,7 +581,7 @@ func GetPullCommits(ctx *context.Context) {
}
resp := &pullCommitList{}
- commits, lastReviewCommitSha, err := pull_service.GetPullCommits(ctx, issue)
+ commits, lastReviewCommitSha, err := pull_service.GetPullCommits(ctx, ctx.Repo.GitRepo, ctx.Doer, issue)
if err != nil {
ctx.JSON(http.StatusInternalServerError, err)
return
@@ -642,8 +643,17 @@ func ViewPullCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplPullCommits)
}
+func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
+ for i := range commits {
+ if commits[i].ID.String() == commitID {
+ return commits[i]
+ }
+ }
+ return nil
+}
+
// ViewPullFiles render pull request changed files list page
-func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) {
+func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
ctx.Data["PageIsPullList"] = true
ctx.Data["PageIsPullFiles"] = true
@@ -653,11 +663,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
pull := issue.PullRequest
- var (
- startCommitID string
- endCommitID string
- gitRepo = ctx.Repo.GitRepo
- )
+ gitRepo := ctx.Repo.GitRepo
prInfo := preparePullViewPullInfo(ctx, issue)
if ctx.Written() {
@@ -667,77 +673,68 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return
}
- // Validate the given commit sha to show (if any passed)
- if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
- foundStartCommit := len(specifiedStartCommit) == 0
- foundEndCommit := len(specifiedEndCommit) == 0
-
- if !(foundStartCommit && foundEndCommit) {
- for _, commit := range prInfo.Commits {
- if commit.ID.String() == specifiedStartCommit {
- foundStartCommit = true
- }
- if commit.ID.String() == specifiedEndCommit {
- foundEndCommit = true
- }
-
- if foundStartCommit && foundEndCommit {
- break
- }
- }
- }
-
- if !(foundStartCommit && foundEndCommit) {
- ctx.NotFound(nil)
- return
- }
- }
-
- if ctx.Written() {
- return
- }
-
- headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+ headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
- ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
+ isSingleCommit := beforeCommitID == "" && afterCommitID != ""
+ ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
+ isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
+ ctx.Data["IsShowingAllCommits"] = isShowAllCommits
- if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
- if len(specifiedEndCommit) > 0 {
- endCommitID = specifiedEndCommit
- } else {
- endCommitID = headCommitID
- }
- if len(specifiedStartCommit) > 0 {
- startCommitID = specifiedStartCommit
+ if afterCommitID == "" || afterCommitID == headCommitID {
+ afterCommitID = headCommitID
+ }
+ afterCommit := indexCommit(prInfo.Commits, afterCommitID)
+ if afterCommit == nil {
+ ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
+ return
+ }
+
+ var beforeCommit *git.Commit
+ if !isSingleCommit {
+ if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase {
+ beforeCommitID = prInfo.MergeBase
+ // mergebase commit is not in the list of the pull request commits
+ beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
+ if err != nil {
+ ctx.ServerError("GetCommit", err)
+ return
+ }
} else {
- startCommitID = prInfo.MergeBase
+ beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
+ if beforeCommit == nil {
+ ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
+ return
+ }
}
- ctx.Data["IsShowingAllCommits"] = false
} else {
- endCommitID = headCommitID
- startCommitID = prInfo.MergeBase
- ctx.Data["IsShowingAllCommits"] = true
+ beforeCommit, err = afterCommit.Parent(0)
+ if err != nil {
+ ctx.ServerError("Parent", err)
+ return
+ }
+ beforeCommitID = beforeCommit.ID.String()
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- ctx.Data["AfterCommitID"] = endCommitID
- ctx.Data["BeforeCommitID"] = startCommitID
-
- fileOnly := ctx.FormBool("file-only")
+ ctx.Data["MergeBase"] = prInfo.MergeBase
+ ctx.Data["AfterCommitID"] = afterCommitID
+ ctx.Data["BeforeCommitID"] = beforeCommitID
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
files := ctx.FormStrings("files")
+ fileOnly := ctx.FormBool("file-only")
if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1
}
diffOptions := &gitdiff.DiffOptions{
- AfterCommitID: endCommitID,
+ BeforeCommitID: beforeCommitID,
+ AfterCommitID: afterCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
@@ -745,11 +742,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
}
- if !willShowSpecifiedCommit {
- diffOptions.BeforeCommitID = startCommitID
- }
-
- diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
+ diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...)
if err != nil {
ctx.ServerError("GetDiff", err)
return
@@ -759,19 +752,16 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
// have to load only the diff and not get the viewed information
// as the viewed information is designed to be loaded only on latest PR
// diff and if you're signed in.
- shouldGetUserSpecificDiff := false
- if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
- // do nothing
- } else {
- shouldGetUserSpecificDiff = true
- err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions, files...)
+ var reviewState *pull_model.ReviewState
+ if ctx.IsSigned && isShowAllCommits {
+ reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
if err != nil {
ctx.ServerError("SyncUserSpecificDiff", err)
return
}
}
- diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, startCommitID, endCommitID)
+ diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return
@@ -818,39 +808,26 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if !fileOnly {
// note: use mergeBase is set to false because we already have the merge base from the pull request info
- diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID)
+ diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffTree", err)
return
}
-
- filesViewedState := make(map[string]pull_model.ViewedState)
- if shouldGetUserSpecificDiff {
- // This sort of sucks because we already fetch this when getting the diff
- review, err := pull_model.GetNewestReviewState(ctx, ctx.Doer.ID, issue.ID)
- if err == nil && review != nil && review.UpdatedFiles != nil {
- // If there wasn't an error and we have a review with updated files, use that
- filesViewedState = review.UpdatedFiles
- }
+ var filesViewedState map[string]pull_model.ViewedState
+ if reviewState != nil {
+ filesViewedState = reviewState.UpdatedFiles
}
- ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState)
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
- baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
- if err != nil {
- ctx.ServerError("GetCommit", err)
- return
- }
- commit, err := gitRepo.GetCommit(endCommitID)
- if err != nil {
- ctx.ServerError("GetCommit", err)
- return
- }
-
if ctx.IsSigned && ctx.Doer != nil {
if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
@@ -858,7 +835,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}
- setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+ setCompareContext(ctx, beforeCommit, afterCommit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
@@ -905,7 +882,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
- if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
+ if isShowAllCommits && pull.Flow == issues_model.PullRequestFlowGithub {
if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return
@@ -934,19 +911,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
func ViewPullFilesForSingleCommit(ctx *context.Context) {
- viewPullFiles(ctx, "", ctx.PathParam("sha"), true, true)
+ // it doesn't support showing files from mergebase to the special commit
+ // otherwise it will be ambiguous
+ viewPullFiles(ctx, "", ctx.PathParam("sha"))
}
func ViewPullFilesForRange(ctx *context.Context) {
- viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"), true, false)
-}
-
-func ViewPullFilesStartingFromCommit(ctx *context.Context) {
- viewPullFiles(ctx, "", ctx.PathParam("sha"), true, false)
+ viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"))
}
func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) {
- viewPullFiles(ctx, "", "", false, false)
+ viewPullFiles(ctx, "", "")
}
// UpdatePullRequest merge PR's baseBranch into headBranch
@@ -991,7 +966,9 @@ func UpdatePullRequest(ctx *context.Context) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
- if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
+ // The update process should not be cancelled by the user
+ // so we set the context to be a background context
+ if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
@@ -1063,7 +1040,7 @@ func MergePullRequest(ctx *context.Context) {
} else {
ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
}
- case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
+ case errors.Is(err, pull_service.ErrNoPermissionToMerge):
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
case errors.Is(err, pull_service.ErrHasMerged):
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
@@ -1071,7 +1048,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
case errors.Is(err, pull_service.ErrNotMergeableState):
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
- case pull_service.IsErrDisallowedToMerge(err):
+ case errors.Is(err, pull_service.ErrNotReadyToMerge):
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
case asymkey_service.IsErrWontSign(err):
ctx.JSONError(err.Error()) // has no translation ...
@@ -1260,13 +1237,23 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
}
func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error {
- if issues_model.StopwatchExists(ctx, user.ID, issue.ID) {
- if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil {
- return err
+ _, err := issues_model.FinishIssueStopwatch(ctx, user, issue)
+ return err
+}
+
+func PullsNewRedirect(ctx *context.Context) {
+ branch := ctx.PathParam("*")
+ redirectRepo := ctx.Repo.Repository
+ repo := ctx.Repo.Repository
+ if repo.IsFork {
+ if err := repo.GetBaseRepo(ctx); err != nil {
+ ctx.ServerError("GetBaseRepo", err)
+ return
}
+ redirectRepo = repo.BaseRepo
+ branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
}
-
- return nil
+ ctx.Redirect(fmt.Sprintf("%s/compare/%s...%s?expand=1", redirectRepo.Link(), util.PathEscapeSegments(redirectRepo.DefaultBranch), util.PathEscapeSegments(branch)))
}
// CompareAndPullRequestPost response for creating pull request
@@ -1286,11 +1273,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
)
ci := ParseCompareInfo(ctx)
- defer func() {
- if ci != nil && ci.HeadGitRepo != nil {
- ci.HeadGitRepo.Close()
- }
- }()
if ctx.Written() {
return
}
@@ -1506,7 +1488,7 @@ func CleanUpPullRequest(ctx *context.Context) {
}()
// Check if branch has no new commits
- headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
+ headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index fb92d24394..18e14e9b22 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -49,7 +49,7 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
ctx.Data["PageIsPullFiles"] = true
ctx.Data["Issue"] = issue
ctx.Data["CurrentReview"] = currentReview
- pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
+ pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitHeadRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
@@ -199,7 +199,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
ctx.ServerError("comment.Issue.LoadPullRequest", err)
return
}
- pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
+ pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitHeadRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
@@ -209,11 +209,12 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
- if origin == "diff" {
+ switch origin {
+ case "diff":
ctx.HTML(http.StatusOK, tplDiffConversation)
- } else if origin == "timeline" {
+ case "timeline":
ctx.HTML(http.StatusOK, tplTimelineConversation)
- } else {
+ default:
ctx.HTTPError(http.StatusBadRequest, "Unknown origin: "+origin)
}
}
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index 228eb0dbac..2660116062 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -4,12 +4,10 @@
package repo
import (
- "errors"
"net/http"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
- contributors_service "code.gitea.io/gitea/services/repository"
)
const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits)
}
-
-// RecentCommitsData returns JSON of recent commits data
-func RecentCommitsData(ctx *context.Context) {
- if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
- if errors.Is(err, contributors_service.ErrAwaitGeneration) {
- ctx.Status(http.StatusAccepted)
- return
- }
- ctx.ServerError("RecentCommitsData", err)
- } else {
- ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
- }
-}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 553bdbf6e5..36ea20c23e 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strconv"
"strings"
"code.gitea.io/gitea/models/db"
@@ -102,7 +103,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
releaseInfos := make([]*ReleaseInfo, 0, len(releases))
for _, r := range releases {
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
- r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
+ r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
@@ -113,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
cacheUsers[r.PublisherID] = r.Publisher
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(r.ID, 10),
+ })
r.RenderedNote, err = markdown.RenderString(rctx, r.Note)
if err != nil {
return nil, err
@@ -130,7 +133,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
if canReadActions {
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
if err != nil {
return nil, err
}
@@ -378,7 +381,7 @@ func NewRelease(ctx *context.Context) {
ctx.Data["ShowCreateTagOnlyButton"] = false
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments
@@ -534,7 +537,7 @@ func EditRelease(ctx *context.Context) {
}
ctx.Data["ID"] = rel.ID
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
@@ -580,7 +583,7 @@ func EditReleasePost(ctx *context.Context) {
return
}
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 73baf683ed..1b700aa6da 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -87,17 +87,13 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
return nil
}
- if !ctx.Doer.IsAdmin {
- orgsAvailable := []*organization.Organization{}
- for i := 0; i < len(orgs); i++ {
- if orgs[i].CanCreateRepo() {
- orgsAvailable = append(orgsAvailable, orgs[i])
- }
+ var orgsAvailable []*organization.Organization
+ for i := range orgs {
+ if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
+ orgsAvailable = append(orgsAvailable, orgs[i])
}
- ctx.Data["Orgs"] = orgsAvailable
- } else {
- ctx.Data["Orgs"] = orgs
}
+ ctx.Data["Orgs"] = orgsAvailable
// Not equal means current user is an organization.
if uid == ctx.Doer.ID || uid == 0 {
@@ -154,8 +150,8 @@ func createCommon(ctx *context.Context) {
ctx.Data["Licenses"] = repo_module.Licenses
ctx.Data["Readmes"] = repo_module.Readmes
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
- ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
- ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
+ ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepoIn(ctx.Doer)
+ ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit()
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
}
@@ -305,11 +301,15 @@ func CreatePost(ctx *context.Context) {
}
func handleActionError(ctx *context.Context, err error) {
- if errors.Is(err, user_model.ErrBlockedUser) {
+ switch {
+ case errors.Is(err, user_model.ErrBlockedUser):
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
- } else if errors.Is(err, util.ErrPermissionDenied) {
+ case repo_service.IsRepositoryLimitReached(err):
+ limit := err.(repo_service.LimitReachedError).Limit
+ ctx.Flash.Error(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))
+ case errors.Is(err, util.ErrPermissionDenied):
ctx.HTTPError(http.StatusNotFound)
- } else {
+ default:
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
}
}
@@ -398,7 +398,7 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
rPath := archiver.RelativePath()
if setting.RepoArchive.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
- u, err := storage.RepoArchives.URL(rPath, downloadName, nil)
+ u, err := storage.RepoArchives.URL(rPath, downloadName, ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
@@ -461,7 +461,7 @@ func SearchRepo(ctx *context.Context) {
if page <= 0 {
page = 1
}
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 655291d25c..af6708e841 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -44,10 +45,7 @@ func LFSFiles(ctx *context.Context) {
ctx.NotFound(nil)
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSFiles", err)
@@ -76,10 +74,7 @@ func LFSLocks(ctx *context.Context) {
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
@@ -109,17 +104,13 @@ func LFSLocks(ctx *context.Context) {
}
// Clone base repo.
- tmpBasePath, err := repo_module.CreateTemporaryPath("locks")
+ tmpBasePath, cleanup, err := repo_module.CreateTemporaryPath("locks")
if err != nil {
log.Error("Failed to create temporary path: %v", err)
ctx.ServerError("LFSLocks", err)
return
}
- defer func() {
- if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
- log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
- }
- }()
+ defer cleanup()
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
Bare: true,
@@ -138,39 +129,24 @@ func LFSLocks(ctx *context.Context) {
}
defer gitRepo.Close()
- filenames := make([]string, len(lfsLocks))
-
- for i, lock := range lfsLocks {
- filenames[i] = lock.Path
- }
-
- if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
- log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
- ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err))
- return
- }
-
- name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
- Attributes: []string{"lockable"},
- Filenames: filenames,
- CachedOnly: true,
- })
+ checker, err := attribute.NewBatchChecker(gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable})
if err != nil {
log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", err)
return
}
+ defer checker.Close()
lockables := make([]bool, len(lfsLocks))
+ filenames := make([]string, len(lfsLocks))
for i, lock := range lfsLocks {
- attribute2info, has := name2attribute2info[lock.Path]
- if !has {
- continue
- }
- if attribute2info["lockable"] != "set" {
+ filenames[i] = lock.Path
+ attrs, err := checker.CheckPath(lock.Path)
+ if err != nil {
+ log.Error("Unable to check attributes in %s: %s (%v)", tmpBasePath, lock.Path, err)
continue
}
- lockables[i] = true
+ lockables[i] = attrs.Get(attribute.Lockable).ToBool().Value()
}
ctx.Data["Lockables"] = lockables
@@ -291,8 +267,10 @@ func LFSFileGet(ctx *context.Context) {
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
+ // FIXME: there is no IsPlainText set, but template uses it
ctx.Data["IsTextFile"] = st.IsText()
ctx.Data["FileSize"] = meta.Size
+ // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct"
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
switch {
case st.IsRepresentableAsText():
@@ -333,8 +311,6 @@ func LFSFileGet(ctx *context.Context) {
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
- case st.IsPDF():
- ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 75de2ba1e7..0eea5e3f34 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@@ -88,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["recent_status_checks"] = contexts
if c.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return
@@ -110,7 +112,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
var protectBranch *git_model.ProtectedBranch
if f.RuleName == "" {
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_rule_name"))
- ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/branches/edit")
return
}
@@ -283,32 +285,32 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
func DeleteProtectedBranchRulePost(ctx *context.Context) {
ruleID := ctx.PathParamInt64("id")
if ruleID <= 0 {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID)
if err != nil {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
if rule == nil {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository, ruleID); err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", rule.RuleName))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
}
func UpdateBranchProtectionPriories(ctx *context.Context) {
@@ -332,7 +334,7 @@ func RenameBranchPost(ctx *context.Context) {
if ctx.HasError() {
ctx.Flash.Error(ctx.GetErrMsg())
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
@@ -341,13 +343,13 @@ func RenameBranchPost(ctx *context.Context) {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error"))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
case git_model.IsErrBranchAlreadyExists(err):
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed"))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
default:
ctx.ServerError("RenameBranch", err)
}
@@ -356,16 +358,16 @@ func RenameBranchPost(ctx *context.Context) {
if msg == "target_exist" {
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_exist", form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
if msg == "from_not_exist" {
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_not_exist", form.From))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.rename_branch_success", form.From, form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
}
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 33692778d5..50f5a28c4c 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
ctx.Data["Users"] = users
if ctx.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return err
diff --git a/routers/web/repo/setting/public_access.go b/routers/web/repo/setting/public_access.go
new file mode 100644
index 0000000000..368d34294a
--- /dev/null
+++ b/routers/web/repo/setting/public_access.go
@@ -0,0 +1,155 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+ "slices"
+ "strconv"
+
+ "code.gitea.io/gitea/models/perm"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+)
+
+const tplRepoSettingsPublicAccess templates.TplName = "repo/settings/public_access"
+
+func parsePublicAccessMode(permission string, allowed []string) (ret struct {
+ AnonymousAccessMode, EveryoneAccessMode perm.AccessMode
+},
+) {
+ ret.AnonymousAccessMode = perm.AccessModeNone
+ ret.EveryoneAccessMode = perm.AccessModeNone
+
+ // if site admin forces repositories to be private, then do not allow any other access mode,
+ // otherwise the "force private" setting would be bypassed
+ if setting.Repository.ForcePrivate {
+ return ret
+ }
+ if !slices.Contains(allowed, permission) {
+ return ret
+ }
+ switch permission {
+ case paAnonymousRead:
+ ret.AnonymousAccessMode = perm.AccessModeRead
+ case paEveryoneRead:
+ ret.EveryoneAccessMode = perm.AccessModeRead
+ case paEveryoneWrite:
+ ret.EveryoneAccessMode = perm.AccessModeWrite
+ }
+ return ret
+}
+
+const (
+ paNotSet = "not-set"
+ paAnonymousRead = "anonymous-read"
+ paEveryoneRead = "everyone-read"
+ paEveryoneWrite = "everyone-write"
+)
+
+type repoUnitPublicAccess struct {
+ UnitType unit.Type
+ FormKey string
+ DisplayName string
+ PublicAccessTypes []string
+ UnitPublicAccess string
+}
+
+func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess {
+ accesses := []*repoUnitPublicAccess{
+ {
+ UnitType: unit.TypeCode,
+ DisplayName: ctx.Locale.TrString("repo.code"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeIssues,
+ DisplayName: ctx.Locale.TrString("issues"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypePullRequests,
+ DisplayName: ctx.Locale.TrString("pull_requests"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeReleases,
+ DisplayName: ctx.Locale.TrString("repo.releases"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeWiki,
+ DisplayName: ctx.Locale.TrString("repo.wiki"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead, paEveryoneWrite},
+ },
+ {
+ UnitType: unit.TypeProjects,
+ DisplayName: ctx.Locale.TrString("repo.projects"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypePackages,
+ DisplayName: ctx.Locale.TrString("repo.packages"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeActions,
+ DisplayName: ctx.Locale.TrString("repo.actions"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ }
+ for _, ua := range accesses {
+ ua.FormKey = "repo-unit-access-" + strconv.Itoa(int(ua.UnitType))
+ for _, u := range ctx.Repo.Repository.Units {
+ if u.Type == ua.UnitType {
+ ua.UnitPublicAccess = paNotSet
+ switch {
+ case u.EveryoneAccessMode == perm.AccessModeWrite:
+ ua.UnitPublicAccess = paEveryoneWrite
+ case u.EveryoneAccessMode == perm.AccessModeRead:
+ ua.UnitPublicAccess = paEveryoneRead
+ case u.AnonymousAccessMode == perm.AccessModeRead:
+ ua.UnitPublicAccess = paAnonymousRead
+ }
+ break
+ }
+ }
+ }
+ return slices.DeleteFunc(accesses, func(ua *repoUnitPublicAccess) bool {
+ return ua.UnitPublicAccess == ""
+ })
+}
+
+func PublicAccess(ctx *context.Context) {
+ ctx.Data["PageIsSettingsPublicAccess"] = true
+ ctx.Data["RepoUnitPublicAccesses"] = repoUnitPublicAccesses(ctx)
+ ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate
+ if setting.Repository.ForcePrivate {
+ ctx.Flash.Error(ctx.Tr("form.repository_force_private"), true)
+ }
+ ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess)
+}
+
+func PublicAccessPost(ctx *context.Context) {
+ accesses := repoUnitPublicAccesses(ctx)
+ for _, ua := range accesses {
+ formVal := ctx.FormString(ua.FormKey)
+ parsed := parsePublicAccessMode(formVal, ua.PublicAccessTypes)
+ err := repo.UpdateRepoUnitPublicAccess(ctx, &repo.RepoUnit{
+ RepoID: ctx.Repo.Repository.ID,
+ Type: ua.UnitType,
+ AnonymousAccessMode: parsed.AnonymousAccessMode,
+ EveryoneAccessMode: parsed.EveryoneAccessMode,
+ })
+ if err != nil {
+ ctx.ServerError("UpdateRepoUnitPublicAccess", err)
+ return
+ }
+ }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/public_access")
+}
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index 46cb875f9b..c6e2d18249 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -44,9 +44,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
}
if ctx.Data["PageIsOrgSettings"] == true {
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return nil, nil
}
return &secretsCtx{
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index ac7eb768fa..0865d9d7c0 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -6,14 +6,12 @@ package setting
import (
"errors"
- "fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -37,6 +35,8 @@ import (
mirror_service "code.gitea.io/gitea/services/mirror"
repo_service "code.gitea.io/gitea/services/repository"
wiki_service "code.gitea.io/gitea/services/wiki"
+
+ "xorm.io/xorm/convert"
)
const (
@@ -48,15 +48,6 @@ const (
tplDeployKeys templates.TplName = "repo/settings/deploy_keys"
)
-func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode {
- // if site admin forces repositories to be private, then do not allow any other access mode,
- // otherwise the "force private" setting would be bypassed
- if setting.Repository.ForcePrivate {
- return perm.AccessModeNone
- }
- return perm.ParseAccessMode(permission, allowed...)
-}
-
// SettingsCtxData is a middleware that sets all the general context data for the
// settings template.
func SettingsCtxData(ctx *context.Context) {
@@ -68,9 +59,10 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
+ ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -105,8 +97,6 @@ func Settings(ctx *context.Context) {
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RepoSettingForm)
-
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
@@ -115,871 +105,937 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- repo := ctx.Repo.Repository
-
switch ctx.FormString("action") {
case "update":
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplSettingsOptions)
- return
- }
+ handleSettingsPostUpdate(ctx)
+ case "mirror":
+ handleSettingsPostMirror(ctx)
+ case "mirror-sync":
+ handleSettingsPostMirrorSync(ctx)
+ case "push-mirror-sync":
+ handleSettingsPostPushMirrorSync(ctx)
+ case "push-mirror-update":
+ handleSettingsPostPushMirrorUpdate(ctx)
+ case "push-mirror-remove":
+ handleSettingsPostPushMirrorRemove(ctx)
+ case "push-mirror-add":
+ handleSettingsPostPushMirrorAdd(ctx)
+ case "advanced":
+ handleSettingsPostAdvanced(ctx)
+ case "signing":
+ handleSettingsPostSigning(ctx)
+ case "admin":
+ handleSettingsPostAdmin(ctx)
+ case "admin_index":
+ handleSettingsPostAdminIndex(ctx)
+ case "convert":
+ handleSettingsPostConvert(ctx)
+ case "convert_fork":
+ handleSettingsPostConvertFork(ctx)
+ case "transfer":
+ handleSettingsPostTransfer(ctx)
+ case "cancel_transfer":
+ handleSettingsPostCancelTransfer(ctx)
+ case "delete":
+ handleSettingsPostDelete(ctx)
+ case "delete-wiki":
+ handleSettingsPostDeleteWiki(ctx)
+ case "archive":
+ handleSettingsPostArchive(ctx)
+ case "unarchive":
+ handleSettingsPostUnarchive(ctx)
+ case "visibility":
+ handleSettingsPostVisibility(ctx)
+ default:
+ ctx.NotFound(nil)
+ }
+}
- newRepoName := form.RepoName
- // Check if repository name has been changed.
- if repo.LowerName != strings.ToLower(newRepoName) {
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
- if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+func handleSettingsPostUpdate(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplSettingsOptions)
+ return
+ }
+
+ newRepoName := form.RepoName
+ // Check if repository name has been changed.
+ if !strings.EqualFold(repo.LowerName, newRepoName) {
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
+ if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case repo_model.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
+ case db.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+ case repo_model.IsErrRepoFilesAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
switch {
- case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
- case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- case repo_model.IsErrRepoFilesAlreadyExist(err):
- ctx.Data["Err_RepoName"] = true
- switch {
- case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
- case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
- case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
- default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
- }
- case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
+ case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
+ case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
default:
- ctx.ServerError("ChangeRepositoryName", err)
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
}
- return
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ default:
+ ctx.ServerError("ChangeRepositoryName", err)
}
-
- log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
- }
- // In case it's just a case change.
- repo.Name = newRepoName
- repo.LowerName = strings.ToLower(newRepoName)
- repo.Description = form.Description
- repo.Website = form.Website
- repo.IsTemplate = form.Template
-
- // Visibility of forked repository is forced sync with base repository.
- if repo.IsFork {
- form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
- }
-
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
return
}
- log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
+ }
+ // In case it's just a case change.
+ repo.Name = newRepoName
+ repo.LowerName = strings.ToLower(newRepoName)
+ repo.Description = form.Description
+ repo.Website = form.Website
+ repo.IsTemplate = form.Template
+
+ // Visibility of forked repository is forced sync with base repository.
+ if repo.IsFork {
+ form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
+ }
- case "mirror":
- if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
- ctx.NotFound(nil)
- return
- }
+ if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
+ return
+ }
+ log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
- if err == repo_model.ErrMirrorNotExist {
- ctx.NotFound(nil)
- return
- }
- if err != nil {
- ctx.ServerError("GetMirrorByRepoID", err)
- return
- }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
-
- interval, err := time.ParseDuration(form.Interval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.Data["Err_Interval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- pullMirror.EnablePrune = form.EnablePrune
- pullMirror.Interval = interval
- pullMirror.ScheduleNextUpdate()
- if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
- ctx.ServerError("UpdateMirror", err)
- return
- }
+func handleSettingsPostMirror(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
- form.MirrorPassword, _ = u.User.Password()
- }
+ pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err == repo_model.ErrMirrorNotExist {
+ ctx.NotFound(nil)
+ return
+ }
+ if err != nil {
+ ctx.ServerError("GetMirrorByRepoID", err)
+ return
+ }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
+
+ interval, err := time.ParseDuration(form.Interval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Data["Err_Interval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ return
+ }
- address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
- if err == nil {
- err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
- }
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ pullMirror.EnablePrune = form.EnablePrune
+ pullMirror.Interval = interval
+ pullMirror.ScheduleNextUpdate()
+ if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
+ }
- if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
- ctx.ServerError("UpdateAddress", err)
- return
- }
+ u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+ if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
+ form.MirrorPassword, _ = u.User.Password()
+ }
- remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- pullMirror.RemoteAddress = remoteAddress
+ address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+ }
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- form.LFS = form.LFS && setting.LFS.StartServer
+ if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
+ ctx.ServerError("UpdateAddress", err)
+ return
+ }
- if len(form.LFSEndpoint) > 0 {
- ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
- if ep == nil {
- ctx.Data["Err_LFSEndpoint"] = true
- ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
- return
- }
- err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
- if err != nil {
- ctx.Data["Err_LFSEndpoint"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- }
+ remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+ pullMirror.RemoteAddress = remoteAddress
- pullMirror.LFS = form.LFS
- pullMirror.LFSEndpoint = form.LFSEndpoint
- if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
- ctx.ServerError("UpdateMirror", err)
+ form.LFS = form.LFS && setting.LFS.StartServer
+
+ if len(form.LFSEndpoint) > 0 {
+ ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
+ if ep == nil {
+ ctx.Data["Err_LFSEndpoint"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
return
}
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
-
- case "mirror-sync":
- if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
- ctx.NotFound(nil)
+ err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
+ if err != nil {
+ ctx.Data["Err_LFSEndpoint"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
return
}
+ }
- mirror_service.AddPullMirrorToQueue(repo.ID)
+ pullMirror.LFS = form.LFS
+ pullMirror.LFSEndpoint = form.LFSEndpoint
+ if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
+ }
- ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
- ctx.Redirect(repo.Link() + "/settings")
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- case "push-mirror-sync":
- if !setting.Mirror.Enabled {
- ctx.NotFound(nil)
- return
- }
+func handleSettingsPostMirrorSync(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound(nil)
- return
- }
+ mirror_service.AddPullMirrorToQueue(repo.ID)
- mirror_service.AddPushMirrorToQueue(m.ID)
+ ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
- ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostPushMirrorSync(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- case "push-mirror-update":
- if !setting.Mirror.Enabled || repo.IsArchived {
- ctx.NotFound(nil)
- return
- }
+ if !setting.Mirror.Enabled {
+ ctx.NotFound(nil)
+ return
+ }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- interval, err := time.ParseDuration(form.PushMirrorInterval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
- return
- }
+ mirror_service.AddPushMirrorToQueue(m.ID)
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound(nil)
- return
- }
+ ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- m.Interval = interval
- if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
- ctx.ServerError("UpdatePushMirrorInterval", err)
- return
- }
- // Background why we are adding it to Queue
- // If we observed its implementation in the context of `push-mirror-sync` where it
- // is evident that pushing to the queue is necessary for updates.
- // So, there are updates within the given interval, it is necessary to update the queue accordingly.
- if !ctx.FormBool("push_mirror_defer_sync") {
- // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
- mirror_service.AddPushMirrorToQueue(m.ID)
- }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- case "push-mirror-remove":
- if !setting.Mirror.Enabled || repo.IsArchived {
- ctx.NotFound(nil)
- return
- }
+ if !setting.Mirror.Enabled || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound(nil)
- return
- }
+ interval, err := time.ParseDuration(form.PushMirrorInterval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
+ return
+ }
- if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
- ctx.ServerError("RemovePushMirrorRemote", err)
- return
- }
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
- ctx.ServerError("DeletePushMirrorByID", err)
- return
- }
+ m.Interval = interval
+ if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
+ ctx.ServerError("UpdatePushMirrorInterval", err)
+ return
+ }
+ // Background why we are adding it to Queue
+ // If we observed its implementation in the context of `push-mirror-sync` where it
+ // is evident that pushing to the queue is necessary for updates.
+ // So, there are updates within the given interval, it is necessary to update the queue accordingly.
+ if !ctx.FormBool("push_mirror_defer_sync") {
+ // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
+ mirror_service.AddPushMirrorToQueue(m.ID)
+ }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- case "push-mirror-add":
- if setting.Mirror.DisableNewPush || repo.IsArchived {
- ctx.NotFound(nil)
- return
- }
+ if !setting.Mirror.Enabled || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- interval, err := time.ParseDuration(form.PushMirrorInterval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.Data["Err_PushMirrorInterval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
- if err == nil {
- err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
- }
- if err != nil {
- ctx.Data["Err_PushMirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
+ ctx.ServerError("RemovePushMirrorRemote", err)
+ return
+ }
- remoteSuffix, err := util.CryptoRandomString(10)
- if err != nil {
- ctx.ServerError("RandomString", err)
- return
- }
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ ctx.ServerError("DeletePushMirrorByID", err)
+ return
+ }
- remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
- if err != nil {
- ctx.Data["Err_PushMirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- m := &repo_model.PushMirror{
- RepoID: repo.ID,
- Repo: repo,
- RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
- SyncOnCommit: form.PushMirrorSyncOnCommit,
- Interval: interval,
- RemoteAddress: remoteAddress,
- }
- if err := db.Insert(ctx, m); err != nil {
- ctx.ServerError("InsertPushMirror", err)
- return
- }
+func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
- if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
- log.Error("DeletePushMirrors %v", err)
- }
- ctx.ServerError("AddPushMirrorRemote", err)
- return
- }
+ if setting.Mirror.DisableNewPush || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- case "advanced":
- var repoChanged bool
- var units []repo_model.RepoUnit
- var deleteUnitTypes []unit_model.Type
+ interval, err := time.ParseDuration(form.PushMirrorInterval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Data["Err_PushMirrorInterval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ return
+ }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+ }
+ if err != nil {
+ ctx.Data["Err_PushMirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
- repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
- repoChanged = true
- }
+ remoteSuffix, err := util.CryptoRandomString(10)
+ if err != nil {
+ ctx.ServerError("RandomString", err)
+ return
+ }
- if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeCode,
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
- })
- } else if !unit_model.TypeCode.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
- }
+ remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
+ if err != nil {
+ ctx.Data["Err_PushMirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalWikiURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
+ m := &repo_model.PushMirror{
+ RepoID: repo.ID,
+ Repo: repo,
+ RemoteName: "remote_mirror_" + remoteSuffix,
+ SyncOnCommit: form.PushMirrorSyncOnCommit,
+ Interval: interval,
+ RemoteAddress: remoteAddress,
+ }
+ if err := db.Insert(ctx, m); err != nil {
+ ctx.ServerError("InsertPushMirror", err)
+ return
+ }
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeExternalWiki,
- Config: &repo_model.ExternalWikiConfig{
- ExternalWikiURL: form.ExternalWikiURL,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
- } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeWiki,
- Config: new(repo_model.UnitConfig),
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
- } else {
- if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
- }
- if !unit_model.TypeWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
- }
+ if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ log.Error("DeletePushMirrors %v", err)
}
+ ctx.ServerError("AddPushMirrorRemote", err)
+ return
+ }
- if form.DefaultWikiBranch != "" {
- if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
- log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
- ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
- }
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
- ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeExternalTracker,
- Config: &repo_model.ExternalTrackerConfig{
- ExternalTrackerURL: form.ExternalTrackerURL,
- ExternalTrackerFormat: form.TrackerURLFormat,
- ExternalTrackerStyle: form.TrackerIssueStyle,
- ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
- } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeIssues,
- Config: &repo_model.IssuesConfig{
- EnableTimetracker: form.EnableTimetracker,
- AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
- EnableDependencies: form.EnableIssueDependencies,
- },
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
- } else {
- if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
- }
- if !unit_model.TypeIssues.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
- }
+func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config convert.Conversion) repo_model.RepoUnit {
+ repoUnit := repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, Config: config}
+ for _, u := range repo.Units {
+ if u.Type == unitType {
+ repoUnit.EveryoneAccessMode = u.EveryoneAccessMode
+ repoUnit.AnonymousAccessMode = u.AnonymousAccessMode
}
+ }
+ return repoUnit
+}
- if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeProjects,
- Config: &repo_model.ProjectsConfig{
- ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
- },
- })
- } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
- }
+func handleSettingsPostAdvanced(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ var repoChanged bool
+ var units []repo_model.RepoUnit
+ var deleteUnitTypes []unit_model.Type
- if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeReleases,
- })
- } else if !unit_model.TypeReleases.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
- }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
+
+ if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
+ repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
+ repoChanged = true
+ }
+
+ if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
+ } else if !unit_model.TypeCode.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
+ }
- if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypePackages,
- })
- } else if !unit_model.TypePackages.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
+ if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalWikiURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
}
- if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeActions,
- })
- } else if !unit_model.TypeActions.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
+ units = append(units, newRepoUnit(repo, unit_model.TypeExternalWiki, &repo_model.ExternalWikiConfig{
+ ExternalWikiURL: form.ExternalWikiURL,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
+ } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
+ } else {
+ if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
}
+ if !unit_model.TypeWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
+ }
+ }
- if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypePullRequests,
- Config: &repo_model.PullRequestsConfig{
- IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
- AllowMerge: form.PullsAllowMerge,
- AllowRebase: form.PullsAllowRebase,
- AllowRebaseMerge: form.PullsAllowRebaseMerge,
- AllowSquash: form.PullsAllowSquash,
- AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
- AllowManualMerge: form.PullsAllowManualMerge,
- AutodetectManualMerge: form.EnableAutodetectManualMerge,
- AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
- DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
- DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
- DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
- },
- })
- } else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
+ if form.DefaultWikiBranch != "" {
+ if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+ log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+ ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
}
+ }
- if len(units) == 0 {
- ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
return
}
-
- if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
- ctx.ServerError("UpdateRepositoryUnits", err)
+ if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
+ ctx.Redirect(repo.Link() + "/settings")
return
}
- if repoChanged {
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
+ units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{
+ ExternalTrackerURL: form.ExternalTrackerURL,
+ ExternalTrackerFormat: form.TrackerURLFormat,
+ ExternalTrackerStyle: form.TrackerIssueStyle,
+ ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
+ } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
+ EnableTimetracker: form.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+ EnableDependencies: form.EnableIssueDependencies,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
+ } else {
+ if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
- log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if !unit_model.TypeIssues.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
+ }
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeProjects, &repo_model.ProjectsConfig{
+ ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+ }))
+ } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
+ }
- case "signing":
- changed := false
- trustModel := repo_model.ToTrustModel(form.TrustModel)
- if trustModel != repo.TrustModel {
- repo.TrustModel = trustModel
- changed = true
- }
+ if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
+ } else if !unit_model.TypeReleases.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
+ }
- if changed {
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
- }
- log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypePackages, nil))
+ } else if !unit_model.TypePackages.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil))
+ } else if !unit_model.TypeActions.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
+ }
- case "admin":
- if !ctx.Doer.IsAdmin {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
+ if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
+ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
+ AllowMerge: form.PullsAllowMerge,
+ AllowRebase: form.PullsAllowRebase,
+ AllowRebaseMerge: form.PullsAllowRebaseMerge,
+ AllowSquash: form.PullsAllowSquash,
+ AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
+ AllowManualMerge: form.PullsAllowManualMerge,
+ AutodetectManualMerge: form.EnableAutodetectManualMerge,
+ AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
+ DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
+ DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
+ DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
+ }))
+ } else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
+ }
- if repo.IsFsckEnabled != form.EnableHealthCheck {
- repo.IsFsckEnabled = form.EnableHealthCheck
- }
+ if len(units) == 0 {
+ ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
+ if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
+ ctx.ServerError("UpdateRepositoryUnits", err)
+ return
+ }
+ if repoChanged {
if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
+ }
+ log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "admin_index":
- if !ctx.Doer.IsAdmin {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- switch form.RequestReindexType {
- case "stats":
- if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
- ctx.ServerError("UpdateStatsRepondexer", err)
- return
- }
- case "code":
- if !setting.Indexer.RepoIndexerEnabled {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
- code.UpdateRepoIndexer(ctx.Repo.Repository)
- default:
- ctx.NotFound(nil)
+func handleSettingsPostSigning(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ trustModel := repo_model.ToTrustModel(form.TrustModel)
+ if trustModel != repo.TrustModel {
+ repo.TrustModel = trustModel
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "trust_model"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
+ log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ }
- log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+func handleSettingsPostAdmin(ctx *context.Context) {
+ if !ctx.Doer.IsAdmin {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- case "convert":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ repo := ctx.Repo.Repository
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ if repo.IsFsckEnabled != form.EnableHealthCheck {
+ repo.IsFsckEnabled = form.EnableHealthCheck
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fsck_enabled"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
+ log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ }
- if !repo.IsMirror {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
- repo.IsMirror = false
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
- ctx.ServerError("CleanUpMigrateInfo", err)
- return
- } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("DeleteMirrorByRepoID", err)
- return
- }
- log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
- ctx.Redirect(repo.Link())
+func handleSettingsPostAdminIndex(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Doer.IsAdmin {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- case "convert_fork":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
+ switch form.RequestReindexType {
+ case "stats":
+ if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
+ ctx.ServerError("UpdateStatsRepondexer", err)
return
}
- if err := repo.LoadOwner(ctx); err != nil {
- ctx.ServerError("Convert Fork", err)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ case "code":
+ if !setting.Indexer.RepoIndexerEnabled {
+ ctx.HTTPError(http.StatusForbidden)
return
}
+ code.UpdateRepoIndexer(ctx.Repo.Repository)
+ default:
+ ctx.NotFound(nil)
+ return
+ }
- if !repo.IsFork {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
+ log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
- if !ctx.Repo.Owner.CanCreateRepo() {
- maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg)
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
- log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
- ctx.ServerError("Convert Fork", err)
- return
- }
+func handleSettingsPostConvert(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- log.Trace("Repository converted from fork to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
- ctx.Redirect(repo.Link())
+ if !repo.IsMirror {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ repo.IsMirror = false
- case "transfer":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
+ if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
+ ctx.ServerError("CleanUpMigrateInfo", err)
+ return
+ } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("DeleteMirrorByRepoID", err)
+ return
+ }
+ log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+ ctx.Redirect(repo.Link())
+}
- newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
- if err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- ctx.ServerError("IsUserExist", err)
- return
- }
+func handleSettingsPostConvertFork(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if err := repo.LoadOwner(ctx); err != nil {
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- if newOwner.Type == user_model.UserTypeOrganization {
- if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
- // The user shouldn't know about this organization
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- }
+ if !repo.IsFork {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
+ if !ctx.Doer.CanForkRepoIn(ctx.Repo.Owner) {
+ maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.Flash.Error(msg)
+ ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
- oldFullname := repo.FullName()
- if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
- if repo_model.IsErrRepoAlreadyExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
- } else if repo_model.IsErrRepoTransferInProgress(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
- } else if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
- } else {
- ctx.ServerError("TransferOwnership", err)
- }
+ if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
+ log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
- return
- }
+ log.Trace("Repository converted from fork to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
+ ctx.Redirect(repo.Link())
+}
- if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
- log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
- } else {
- log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
- }
- ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostTransfer(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- case "cancel_transfer":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
+ newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
+ ctx.ServerError("IsUserExist", err)
+ return
+ }
- repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
- if err != nil {
- if repo_model.IsErrNoPendingTransfer(err) {
- ctx.Flash.Error("repo.settings.transfer_abort_invalid")
- ctx.Redirect(repo.Link() + "/settings")
- } else {
- ctx.ServerError("GetPendingRepositoryTransfer", err)
- }
+ if newOwner.Type == user_model.UserTypeOrganization {
+ if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
+ // The user shouldn't know about this organization
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
+ }
- if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
- ctx.ServerError("CancelRepositoryTransfer", err)
- return
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
+
+ oldFullname := repo.FullName()
+ if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
+ if repo_model.IsErrRepoAlreadyExist(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
+ } else if repo_model.IsErrRepoTransferInProgress(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+ } else if repo_service.IsRepositoryLimitReached(err) {
+ limit := err.(repo_service.LimitReachedError).Limit
+ ctx.RenderWithErr(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
+ } else {
+ ctx.ServerError("TransferOwnership", err)
}
- log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
- ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
- case "delete":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
+ if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
+ log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
+ } else {
+ log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
+ }
+ ctx.Redirect(repo.Link() + "/settings")
+}
- // Close the gitrepository before doing this.
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
+func handleSettingsPostCancelTransfer(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
- if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
- ctx.ServerError("DeleteRepository", err)
- return
+ repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
+ if err != nil {
+ if repo_model.IsErrNoPendingTransfer(err) {
+ ctx.Flash.Error("repo.settings.transfer_abort_invalid")
+ ctx.Redirect(repo.Link() + "/settings")
+ } else {
+ ctx.ServerError("GetPendingRepositoryTransfer", err)
}
- log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
- ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+ if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
+ ctx.ServerError("CancelRepositoryTransfer", err)
+ return
+ }
- case "delete-wiki":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
+ log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- err := wiki_service.DeleteWiki(ctx, repo)
- if err != nil {
- log.Error("Delete Wiki: %v", err.Error())
- }
- log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+func handleSettingsPostDelete(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ // Close the gitrepository before doing this.
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ }
- case "archive":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
+ if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
+ ctx.ServerError("DeleteRepository", err)
+ return
+ }
+ log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- if repo.IsMirror {
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+ ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+}
- if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
- log.Error("Tried to archive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+func handleSettingsPostDeleteWiki(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
- log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
- }
+ err := wiki_service.DeleteWiki(ctx, repo)
+ if err != nil {
+ log.Error("Delete Wiki: %v", err.Error())
+ }
+ log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- // update issue indexer
- issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+ ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
+func handleSettingsPostArchive(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if repo.IsMirror {
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- case "unarchive":
- if !ctx.Repo.IsOwner() {
- ctx.HTTPError(http.StatusForbidden)
- return
- }
+ if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
+ log.Error("Tried to archive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
- log.Error("Tried to unarchive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+ if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
- if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
- if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
- log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
- }
- }
+ // update issue indexer
+ issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
- // update issue indexer
- issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+ ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
- ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
+ log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
+
+func handleSettingsPostUnarchive(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
+ log.Error("Tried to unarchive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- case "visibility":
- if repo.IsFork {
- ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
+ }
- var err error
+ // update issue indexer
+ issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
- // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
- if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
- ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
- if repo.IsPrivate {
- err = repo_service.MakeRepoPublic(ctx, repo)
- } else {
- err = repo_service.MakeRepoPrivate(ctx, repo)
- }
+ log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if err != nil {
- log.Error("Tried to change the visibility of the repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+func handleSettingsPostVisibility(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if repo.IsFork {
+ ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+ var err error
- log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
+ if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
+ ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
+ return
+ }
- default:
- ctx.NotFound(nil)
+ if repo.IsPrivate {
+ err = repo_service.MakeRepoPublic(ctx, repo)
+ } else {
+ err = repo_service.MakeRepoPrivate(ctx, repo)
+ }
+
+ if err != nil {
+ log.Error("Tried to change the visibility of the repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
}
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+
+ log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index ad33dac514..15ebea888c 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest"
@@ -24,23 +25,8 @@ import (
"github.com/stretchr/testify/assert"
)
-func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
- tmpDir := t.TempDir()
-
- oldPath := setting.SSH.RootPath
- setting.SSH.RootPath = tmpDir
-
- return func() {
- setting.SSH.RootPath = oldPath
- }
-}
-
func TestAddReadOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
@@ -54,7 +40,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -64,11 +50,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
@@ -84,7 +66,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -121,7 +103,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@@ -147,7 +129,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -179,7 +161,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@@ -188,7 +170,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -210,7 +192,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -250,7 +232,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
@@ -290,7 +272,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -331,7 +313,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -364,7 +346,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo
AddTeamPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index d3151a86a2..f107449749 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
webhook_module.HookEventRepository: form.Repository,
webhook_module.HookEventPackage: form.Package,
webhook_module.HookEventStatus: form.Status,
+ webhook_module.HookEventWorkflowRun: form.WorkflowRun,
webhook_module.HookEventWorkflowJob: form.WorkflowJob,
},
BranchFilter: form.BranchFilter,
@@ -197,7 +198,6 @@ type webhookParams struct {
URL string
ContentType webhook.HookContentType
- Secret string
HTTPMethod string
WebhookForm forms.WebhookForm
Meta any
@@ -236,7 +236,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
URL: params.URL,
HTTPMethod: params.HTTPMethod,
ContentType: params.ContentType,
- Secret: params.Secret,
+ Secret: params.WebhookForm.Secret,
HookEvent: ParseHookEvent(params.WebhookForm),
IsActive: params.WebhookForm.Active,
Type: params.Type,
@@ -289,7 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) {
w.URL = params.URL
w.ContentType = params.ContentType
- w.Secret = params.Secret
+ w.Secret = params.WebhookForm.Secret
w.HookEvent = ParseHookEvent(params.WebhookForm)
w.IsActive = params.WebhookForm.Active
w.HTTPMethod = params.HTTPMethod
@@ -335,7 +335,6 @@ func giteaHookParams(ctx *context.Context) webhookParams {
Type: webhook_module.GITEA,
URL: form.PayloadURL,
ContentType: contentType,
- Secret: form.Secret,
HTTPMethod: form.HTTPMethod,
WebhookForm: form.WebhookForm,
}
@@ -363,7 +362,6 @@ func gogsHookParams(ctx *context.Context) webhookParams {
Type: webhook_module.GOGS,
URL: form.PayloadURL,
ContentType: contentType,
- Secret: form.Secret,
WebhookForm: form.WebhookForm,
}
}
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index ab74741e61..340b2bc091 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -4,10 +4,14 @@
package repo
import (
+ "html/template"
"net/http"
+ "path"
+ "strings"
pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
@@ -56,41 +60,94 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
return false
}
-type FileDiffFile struct {
- Name string
+// WebDiffFileItem is used by frontend, check the field names in frontend before changing
+type WebDiffFileItem struct {
+ FullName string
+ DisplayName string
NameHash string
- IsSubmodule bool
+ DiffStatus string
+ EntryMode string
IsViewed bool
- Status string
+ Children []*WebDiffFileItem
+ FileIcon template.HTML
}
-// transformDiffTreeForUI transforms a DiffTree into a slice of FileDiffFile for UI rendering
+// WebDiffFileTree is used by frontend, check the field names in frontend before changing
+type WebDiffFileTree struct {
+ TreeRoot WebDiffFileItem
+}
+
+// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
// it also takes a map of file names to their viewed state, which is used to mark files as viewed
-func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile {
- files := make([]FileDiffFile, 0, len(diffTree.Files))
+func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
+ dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
+ addItem := func(item *WebDiffFileItem) {
+ var parentPath string
+ pos := strings.LastIndexByte(item.FullName, '/')
+ if pos == -1 {
+ item.DisplayName = item.FullName
+ } else {
+ parentPath = item.FullName[:pos]
+ item.DisplayName = item.FullName[pos+1:]
+ }
+ parentNode, parentExists := dirNodes[parentPath]
+ if !parentExists {
+ parentNode = &dft.TreeRoot
+ fields := strings.Split(parentPath, "/")
+ for idx, field := range fields {
+ nodePath := strings.Join(fields[:idx+1], "/")
+ node, ok := dirNodes[nodePath]
+ if !ok {
+ node = &WebDiffFileItem{EntryMode: "tree", DisplayName: field, FullName: nodePath}
+ dirNodes[nodePath] = node
+ parentNode.Children = append(parentNode.Children, node)
+ }
+ parentNode = node
+ }
+ }
+ parentNode.Children = append(parentNode.Children, item)
+ }
for _, file := range diffTree.Files {
- nameHash := git.HashFilePathForWebUI(file.HeadPath)
- isSubmodule := file.HeadMode == git.EntryModeCommit
- isViewed := filesViewedState[file.HeadPath] == pull_model.Viewed
-
- files = append(files, FileDiffFile{
- Name: file.HeadPath,
- NameHash: nameHash,
- IsSubmodule: isSubmodule,
- IsViewed: isViewed,
- Status: file.Status,
- })
+ item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
+ item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
+ item.NameHash = git.HashFilePathForWebUI(item.FullName)
+ item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{BaseName: path.Base(file.HeadPath), EntryMode: file.HeadMode})
+
+ switch file.HeadMode {
+ case git.EntryModeTree:
+ item.EntryMode = "tree"
+ case git.EntryModeCommit:
+ item.EntryMode = "commit" // submodule
+ default:
+ // default to empty, and will be treated as "blob" file because there is no "symlink" support yet
+ }
+ addItem(item)
}
- return files
+ var mergeSingleDir func(node *WebDiffFileItem)
+ mergeSingleDir = func(node *WebDiffFileItem) {
+ if len(node.Children) == 1 {
+ if child := node.Children[0]; child.EntryMode == "tree" {
+ node.FullName = child.FullName
+ node.DisplayName = node.DisplayName + "/" + child.DisplayName
+ node.Children = child.Children
+ mergeSingleDir(node)
+ }
+ }
+ }
+ for _, node := range dft.TreeRoot.Children {
+ mergeSingleDir(node)
+ }
+ return dft
}
func TreeViewNodes(ctx *context.Context) {
- results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
if err != nil {
ctx.ServerError("GetTreeViewNodes", err)
return
}
- ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results})
+ ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results, "renderedIconPool": renderedIconPool.IconSVGs})
}
diff --git a/routers/web/repo/treelist_test.go b/routers/web/repo/treelist_test.go
new file mode 100644
index 0000000000..94ba60661b
--- /dev/null
+++ b/routers/web/repo/treelist_test.go
@@ -0,0 +1,68 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "html/template"
+ "testing"
+
+ pull_model "code.gitea.io/gitea/models/pull"
+ "code.gitea.io/gitea/modules/fileicon"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/gitdiff"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTransformDiffTreeForWeb(t *testing.T) {
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
+ {
+ Status: "changed",
+ HeadPath: "dir-a/dir-a-x/file-deep",
+ HeadMode: git.EntryModeBlob,
+ },
+ {
+ Status: "added",
+ HeadPath: "file1",
+ HeadMode: git.EntryModeBlob,
+ },
+ }}, map[string]pull_model.ViewedState{
+ "dir-a/dir-a-x/file-deep": pull_model.Viewed,
+ })
+
+ mockIconForFile := func(id string) template.HTML {
+ return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
+ }
+ assert.Equal(t, WebDiffFileTree{
+ TreeRoot: WebDiffFileItem{
+ Children: []*WebDiffFileItem{
+ {
+ EntryMode: "tree",
+ DisplayName: "dir-a/dir-a-x",
+ FullName: "dir-a/dir-a-x",
+ Children: []*WebDiffFileItem{
+ {
+ EntryMode: "",
+ DisplayName: "file-deep",
+ FullName: "dir-a/dir-a-x/file-deep",
+ NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
+ DiffStatus: "changed",
+ IsViewed: true,
+ FileIcon: mockIconForFile(`svg-mfi-file`),
+ },
+ },
+ },
+ {
+ EntryMode: "",
+ DisplayName: "file1",
+ FullName: "file1",
+ NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
+ DiffStatus: "added",
+ FileIcon: mockIconForFile(`svg-mfi-file`),
+ },
+ },
+ },
+ }, ret)
+}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 6ed5801d10..e47bc56d08 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/url"
+ "path"
"strings"
"time"
@@ -29,6 +30,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -58,60 +60,63 @@ const (
)
type fileInfo struct {
- isTextFile bool
- isLFSFile bool
- fileSize int64
- lfsMeta *lfs.Pointer
- st typesniffer.SniffedType
+ fileSize int64
+ lfsMeta *lfs.Pointer
+ st typesniffer.SniffedType
}
-func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
- dataRc, err := blob.DataAsync()
+func (fi *fileInfo) isLFSFile() bool {
+ return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
+}
+
+func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
+ dataRc, err = blob.DataAsync()
if err != nil {
return nil, nil, nil, err
}
- buf := make([]byte, 1024)
+ const prefetchSize = lfs.MetaFileMaxSize
+
+ buf = make([]byte, prefetchSize)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf)
- isTextFile := st.IsText()
+ fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
// FIXME: what happens when README file is an image?
- if !isTextFile || !setting.LFS.StartServer {
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !fi.st.IsText() || !setting.LFS.StartServer {
+ return buf, dataRc, fi, nil
}
pointer, _ := lfs.ReadPointerFromBuffer(buf)
- if !pointer.IsValid() { // fallback to plain file
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !pointer.IsValid() { // fallback to a plain file
+ return buf, dataRc, fi, nil
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
- if err != nil { // fallback to plain file
+ if err != nil { // fallback to a plain file
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ return buf, dataRc, fi, nil
}
- dataRc.Close()
-
+ // close the old dataRc and open the real LFS target
+ _ = dataRc.Close()
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
return nil, nil, nil, err
}
- buf = make([]byte, 1024)
+ buf = make([]byte, prefetchSize)
n, err = util.ReadAtMost(dataRc, buf)
if err != nil {
- dataRc.Close()
- return nil, nil, nil, err
+ _ = dataRc.Close()
+ return nil, nil, fi, err
}
buf = buf[:n]
-
- st = typesniffer.DetectContentType(buf)
-
- return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
+ fi.st = typesniffer.DetectContentType(buf)
+ fi.fileSize = blob.Size()
+ fi.lfsMeta = &meta.Pointer
+ return buf, dataRc, fi, nil
}
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
@@ -130,7 +135,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -252,6 +257,19 @@ func LastCommit(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRepoViewList)
}
+func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ fileIcons := map[string]template.HTML{}
+ for _, f := range files {
+ fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
+ entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry)
+ fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
+ }
+ fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.Data["FileIcons"] = fileIcons
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
+}
+
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
if err != nil {
@@ -287,12 +305,13 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
defer cancel()
}
- files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
+ files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil
}
ctx.Data["Files"] = files
+ prepareDirectoryFileIcons(ctx, files)
for _, f := range files {
if f.Commit == nil {
ctx.Data["HasFilesWithoutLatestCommit"] = true
@@ -381,9 +400,10 @@ func Forks(ctx *context.Context) {
}
pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["ShowRepoOwnerAvatar"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
-
- ctx.Data["Forks"] = forks
+ ctx.Data["Repos"] = forks
ctx.HTML(http.StatusOK, tplForks)
}
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index 4ce7a8e3a4..2d5bddd939 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -18,44 +18,165 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
- files_service "code.gitea.io/gitea/services/repository/files"
"github.com/nektos/act/pkg/model"
)
-func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
- ctx.Data["IsViewFile"] = true
- ctx.Data["HideRepoInfo"] = true
- blob := entry.Blob()
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+func prepareLatestCommitInfo(ctx *context.Context) bool {
+ commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
if err != nil {
- ctx.ServerError("getFileReader", err)
- return
+ ctx.ServerError("GetCommitByPath", err)
+ return false
}
- defer dataRc.Close()
- ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
- ctx.Data["FileIsSymlink"] = entry.IsLink()
- ctx.Data["FileName"] = blob.Name()
- ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+ return loadLatestCommitData(ctx, commit)
+}
- commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
+func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) {
+ attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
+ Filenames: []string{ctx.Repo.TreePath},
+ Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
+ })
if err != nil {
- ctx.ServerError("GetCommitByPath", err)
- return
+ ctx.ServerError("attribute.CheckAttributes", err)
+ return nil, false
+ }
+ attrs := attrsMap[ctx.Repo.TreePath]
+ if attrs == nil {
+ // this case shouldn't happen, just in case.
+ setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
+ attrs = attribute.NewAttributes()
+ }
+ ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
+ return attrs, true
+}
+
+func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
+ markupType := markup.DetectMarkupTypeByFileName(filename)
+ if markupType == "" {
+ markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
+ }
+ if markupType == "" {
+ return false
+ }
+
+ ctx.Data["HasSourceRenderedToggle"] = true
+
+ if ctx.FormString("display") == "source" {
+ return false
+ }
+
+ ctx.Data["MarkupType"] = markupType
+ metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
+ metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
+ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
+ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ }).
+ WithMarkupType(markupType).
+ WithRelativePath(ctx.Repo.TreePath).
+ WithMetas(metas)
+
+ var err error
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
+ if err != nil {
+ ctx.ServerError("Render", err)
+ return true
+ }
+ // to prevent iframe from loading third-party url
+ ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
+ return true
+}
+
+func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
+ if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
+ return false
+ }
+
+ if !fInfo.st.IsText() {
+ if ctx.FormString("display") == "" {
+ // not text but representable as text, e.g. SVG
+ // since there is no "display" is specified, let other renders to handle
+ return false
+ }
+ ctx.Data["HasSourceRenderedToggle"] = true
+ }
+
+ buf, _ := io.ReadAll(utf8Reader)
+ // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
+ // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
+ // Gitea uses the definition (like most modern editors):
+ // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
+ // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
+ // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
+ // This NumLines is only used for the display on the UI: "xxx lines"
+ if len(buf) == 0 {
+ ctx.Data["NumLines"] = 0
+ } else {
+ ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
+ }
+
+ language := attrs.GetLanguage().Value()
+ fileContent, lexerName, err := highlight.File(filename, language, buf)
+ ctx.Data["LexerName"] = lexerName
+ if err != nil {
+ log.Error("highlight.File failed, fallback to plain text: %v", err)
+ fileContent = highlight.PlainText(buf)
+ }
+ status := &charset.EscapeStatus{}
+ statuses := make([]*charset.EscapeStatus, len(fileContent))
+ for i, line := range fileContent {
+ statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
+ status = status.Or(statuses[i])
}
+ ctx.Data["EscapeStatus"] = status
+ ctx.Data["FileContent"] = fileContent
+ ctx.Data["LineEscapeStatus"] = statuses
+ return true
+}
+
+func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool {
+ if !fInfo.st.IsImage() {
+ return false
+ }
+ if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled {
+ return false
+ }
+ if fInfo.st.IsSvgImage() {
+ ctx.Data["HasSourceRenderedToggle"] = true
+ } else {
+ img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf))
+ if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
+ ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
+ }
+ }
+ return true
+}
- if !loadLatestCommitData(ctx, commit) {
+func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
+ ctx.Data["IsViewFile"] = true
+ ctx.Data["HideRepoInfo"] = true
+
+ if !prepareLatestCommitInfo(ctx) {
return
}
+ blob := entry.Blob()
+
+ ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
+ ctx.Data["FileIsSymlink"] = entry.IsLink()
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
+ ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+
if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if editorconfigWarning != nil {
@@ -87,226 +208,103 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
}
}
- isDisplayingSource := ctx.FormString("display") == "source"
- isDisplayingRendered := !isDisplayingSource
+ // Don't call any other repository functions depends on git.Repository until the dataRc closed to
+ // avoid creating an unnecessary temporary cat file.
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ ctx.ServerError("getFileReader", err)
+ return
+ }
+ defer dataRc.Close()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
- isRepresentableAsText := fInfo.st.IsRepresentableAsText()
- if !isRepresentableAsText {
- // If we can't show plain text, always try to render.
- isDisplayingSource = false
- isDisplayingRendered = true
+ if !prepareFileViewEditorButtons(ctx) {
+ return
}
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsTextFile"] = fInfo.isTextFile
- ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
- ctx.Data["IsDisplayingSource"] = isDisplayingSource
- ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
+ ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText()
ctx.Data["IsExecutable"] = entry.IsExecutable()
+ ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage()
- isTextSource := fInfo.isTextFile || isDisplayingSource
- ctx.Data["IsTextSource"] = isTextSource
- if isTextSource {
- ctx.Data["CanCopyContent"] = true
- }
-
- // Check LFS Lock
- lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
- ctx.Data["LFSLock"] = lfsLock
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
+ attrs, ok := prepareFileViewLfsAttrs(ctx)
+ if !ok {
return
}
- if lfsLock != nil {
- u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
- return
- }
- ctx.Data["LFSLockOwner"] = u.Name
- ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
- ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
- }
- // Assume file is not editable first.
- if fInfo.isLFSFile {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
- } else if !isRepresentableAsText {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
- }
+ // TODO: in the future maybe we need more accurate flags, for example:
+ // * IsRepresentableAsText: some files are text, some are not
+ // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
+ // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
+ utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
switch {
- case isRepresentableAsText:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
- if fInfo.st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
- ctx.Data["HasSourceRenderedToggle"] = true
- }
-
- rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
-
- shouldRenderSource := ctx.FormString("display") == "source"
- readmeExist := util.IsReadmeFileName(blob.Name())
- ctx.Data["ReadmeExist"] = readmeExist
-
- markupType := markup.DetectMarkupTypeByFileName(blob.Name())
- if markupType == "" {
- markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
- }
- if markupType != "" {
- ctx.Data["HasSourceRenderedToggle"] = true
- }
- if markupType != "" && !shouldRenderSource {
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
- metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
- metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath).
- WithMetas(metas)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- // to prevent iframe load third-party url
- ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
- } else {
- buf, _ := io.ReadAll(rd)
-
- // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
- // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
- // Gitea uses the definition (like most modern editors):
- // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
- // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
- // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
- // This NumLines is only used for the display on the UI: "xxx lines"
- if len(buf) == 0 {
- ctx.Data["NumLines"] = 0
- } else {
- ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
- }
-
- language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
- if err != nil {
- log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
- }
-
- fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
- ctx.Data["LexerName"] = lexerName
- if err != nil {
- log.Error("highlight.File failed, fallback to plain text: %v", err)
- fileContent = highlight.PlainText(buf)
- }
- status := &charset.EscapeStatus{}
- statuses := make([]*charset.EscapeStatus, len(fileContent))
- for i, line := range fileContent {
- statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
- status = status.Or(statuses[i])
- }
- ctx.Data["EscapeStatus"] = status
- ctx.Data["FileContent"] = fileContent
- ctx.Data["LineEscapeStatus"] = statuses
- }
- if !fInfo.isLFSFile {
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanEditFile"] = false
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanEditFile"] = true
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
- }
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
- }
- }
-
- case fInfo.st.IsPDF():
- ctx.Data["IsPDFFile"] = true
+ case fInfo.fileSize >= setting.UI.MaxDisplayFileSize:
+ ctx.Data["IsFileTooLarge"] = true
+ case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsMarkup"] = true
+ case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsDisplayingSource"] = true
+ case handleFileViewRenderImage(ctx, fInfo, buf):
+ ctx.Data["IsImageFile"] = true
case fInfo.st.IsVideo():
ctx.Data["IsVideoFile"] = true
case fInfo.st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
default:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
+ // unable to render anything, show the "view raw" or let frontend handle it
+ }
+}
- // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
- // It is used by "external renders", markupRender will execute external programs to get rendered content.
- if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
- rd := io.MultiReader(bytes.NewReader(buf), dataRc)
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
-
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- }
+func prepareFileViewEditorButtons(ctx *context.Context) bool {
+ // archived or mirror repository, the buttons should not be shown
+ if !ctx.Repo.Repository.CanEnableEditor() {
+ return true
}
- if ctx.Repo.GitRepo != nil {
- checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
- if checker != nil {
- defer deferable()
- attrs, err := checker.CheckPath(ctx.Repo.TreePath)
- if err == nil {
- ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
- ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
- }
- }
+ // The buttons should not be shown if it's not a branch
+ if !ctx.Repo.RefFullName.IsBranch() {
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ return true
}
- if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
- img, _, err := image.DecodeConfig(bytes.NewReader(buf))
- if err == nil {
- // There are Image formats go can't decode
- // Instead of throwing an error in that case, we show the size only when we can decode
- ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
- }
+ 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 true
}
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanDeleteFile"] = false
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanDeleteFile"] = true
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ ctx.Data["LFSLock"] = lfsLock
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
+ }
+ if lfsLock != nil {
+ u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
}
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+ ctx.Data["LFSLockOwner"] = u.Name
+ ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
+ ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}
+
+ // it's a lfs file and the user is not the owner of the lock
+ isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID
+ ctx.Data["CanEditFile"] = !isLFSLocked
+ ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file"))
+ ctx.Data["CanDeleteFile"] = !isLFSLocked
+ ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file"))
+ return true
}
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index d538406035..f475e93f60 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -15,11 +15,13 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
- access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/htmlutil"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -76,7 +78,7 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
schema, _, _ := strings.Cut(app.OpenURL, ":")
var iconHTML template.HTML
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
- iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16)
+ iconHTML = svg.RenderHTML("gitea-"+schema, 16)
} else {
iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future
}
@@ -140,7 +142,7 @@ func prepareToRenderDirectory(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
}
- subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
+ subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
if err != nil {
ctx.ServerError("findReadmeFileInEntries", err)
return
@@ -193,56 +195,6 @@ func prepareUpstreamDivergingInfo(ctx *context.Context) {
ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo
}
-func prepareRecentlyPushedNewBranches(ctx *context.Context) {
- if ctx.Doer != nil {
- if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil {
- ctx.ServerError("GetBaseRepo", err)
- return
- }
-
- opts := &git_model.FindRecentlyPushedNewBranchesOptions{
- Repo: ctx.Repo.Repository,
- BaseRepo: ctx.Repo.Repository,
- }
- if ctx.Repo.Repository.IsFork {
- opts.BaseRepo = ctx.Repo.Repository.BaseRepo
- }
-
- baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
-
- if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
- opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
- baseRepoPerm.CanRead(unit_model.TypePullRequests) {
- var finalBranches []*git_model.RecentlyPushedNewBranch
- branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
- if err != nil {
- log.Error("FindRecentlyPushedNewBranches failed: %v", err)
- }
-
- for _, branch := range branches {
- divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
- branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
- opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
- )
- if err != nil {
- log.Error("GetBranchDivergingInfo failed: %v", err)
- continue
- }
- branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
- baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
- if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
- finalBranches = append(finalBranches, branch)
- }
- }
- ctx.Data["RecentlyPushedNewBranches"] = finalBranches
- }
- }
-}
-
func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
return
@@ -259,6 +211,10 @@ func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status re
func handleRepoEmptyOrBroken(ctx *context.Context) {
showEmpty := true
+ if ctx.Repo.GitRepo == nil {
+ // in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
+ ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
+ }
if ctx.Repo.GitRepo != nil {
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
if err != nil {
@@ -269,7 +225,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
} else if reallyEmpty {
showEmpty = true // the repo is really empty
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
- } else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 {
+ } else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(0, 1); len(branches) == 0 {
showEmpty = true // it is not really empty, but there is no branch
// at the moment, other repo units like "actions" are not able to handle such case,
// so we just mark the repo as empty to prevent from displaying these units.
@@ -302,12 +258,44 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
ctx.Redirect(link)
}
+func isViewHomeOnlyContent(ctx *context.Context) bool {
+ return ctx.FormBool("only_content")
+}
+
+func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
+ submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
+ if submoduleWebLink == nil {
+ ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
+ ctx.NotFound(nil)
+ return
+ }
+
+ redirectLink := submoduleWebLink.CommitWebLink
+ if isViewHomeOnlyContent(ctx) {
+ ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
+ } else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
+ // don't auto-redirect to external URL, to avoid open redirect or phishing
+ ctx.Data["NotFoundPrompt"] = redirectLink
+ ctx.NotFound(nil)
+ } else {
+ ctx.Redirect(submoduleWebLink.CommitWebLink)
+ }
+}
+
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) {
- if entry.IsDir() {
+ if entry.IsSubModule() {
+ commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
+ if err != nil {
+ HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
+ return
+ }
+ handleRepoViewSubmodule(ctx, commitSubmoduleFile)
+ } else if entry.IsDir() {
prepareToRenderDirectory(ctx)
} else {
- prepareToRenderFile(ctx, entry)
+ prepareFileView(ctx, entry)
}
}
}
@@ -343,11 +331,39 @@ func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
ctx.Data["UserSettingCodeViewShowFileTree"] = showFileTree
}
+func redirectSrcToRaw(ctx *context.Context) bool {
+ // GitHub redirects a tree path with "?raw=1" to the raw path
+ // It is useful to embed some raw contents into Markdown files,
+ // then viewing the Markdown in "src" path could embed the raw content correctly.
+ if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
+ ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
+ return true
+ }
+ return false
+}
+
+func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool {
+ if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") {
+ return false
+ }
+ if treePathEntry.IsLink() {
+ if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
+ redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
+ ctx.Redirect(redirect)
+ return true
+ } // else: don't handle the links we cannot resolve, so ignore the error
+ }
+ return false
+}
+
// Home render repository home page
func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) {
return
}
+ if redirectSrcToRaw(ctx) {
+ return
+ }
// Check whether the repo is viewable: not in migration, and the code unit should be enabled
// Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
@@ -356,10 +372,8 @@ func Home(ctx *context.Context) {
return
}
- prepareHomeTreeSideBarSwitch(ctx)
-
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
- if len(ctx.Repo.Repository.Description) > 0 {
+ if ctx.Repo.Repository.Description != "" {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
@@ -372,6 +386,8 @@ func Home(ctx *context.Context) {
return
}
+ prepareHomeTreeSideBarSwitch(ctx)
+
// get the current git entry which doer user is currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
@@ -379,6 +395,10 @@ func Home(ctx *context.Context) {
return
}
+ if redirectFollowSymlink(ctx, entry) {
+ return
+ }
+
// prepare the tree path
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
@@ -427,7 +447,7 @@ func Home(ctx *context.Context) {
}
}
- if ctx.FormBool("only_content") {
+ if isViewHomeOnlyContent(ctx) {
ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 {
ctx.HTML(http.StatusOK, tplRepoView)
diff --git a/routers/web/repo/view_home_test.go b/routers/web/repo/view_home_test.go
new file mode 100644
index 0000000000..dd74ae560b
--- /dev/null
+++ b/routers/web/repo/view_home_test.go
@@ -0,0 +1,37 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ git_module "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewHomeSubmoduleRedirect(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+
+ ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
+ submodule := git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
+ handleRepoViewSubmodule(ctx, submodule)
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "/user2/repo-other/tree/any-ref-id", ctx.Resp.Header().Get("Location"))
+
+ ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
+ submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "https://other/user2/repo-other.git", "any-ref-id")
+ handleRepoViewSubmodule(ctx, submodule)
+ // do not auto-redirect for external URLs, to avoid open redirect or phishing
+ assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
+
+ ctx, respWriter := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule?only_content=true")
+ submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
+ handleRepoViewSubmodule(ctx, submodule)
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, `<a href="/user2/repo-other/tree/any-ref-id">/user2/repo-other/tree/any-ref-id</a>`, respWriter.Body.String())
+}
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index 48befe47f8..ba03febff3 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -32,15 +32,7 @@ import (
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
//
// FIXME: There has to be a more efficient way of doing this
-func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
- // Create a list of extensions in priority order
- // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
- // 2. Txt files - e.g. README.txt
- // 3. No extension - e.g. README
- exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
- extCount := len(exts)
- readmeFiles := make([]*git.TreeEntry, extCount+1)
-
+func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
for _, entry := range entries {
if tryWellKnownDirs && entry.IsDir() {
@@ -62,16 +54,23 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
docsEntries[2] = entry
}
}
- continue
}
+ }
+
+ // Create a list of extensions in priority order
+ // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
+ // 2. Txt files - e.g. README.txt
+ // 3. No extension - e.g. README
+ exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
+ extCount := len(exts)
+ readmeFiles := make([]*git.TreeEntry, extCount+1)
+ for _, entry := range entries {
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
- log.Debug("Potential readme file: %s", entry.Name())
+ fullPath := path.Join(parentDir, entry.Name())
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
if entry.IsLink() {
- target, err := entry.FollowLinks()
- if err != nil && !git.IsErrBadLink(err) {
- return "", nil, err
- } else if target != nil && (target.IsExecutable() || target.IsRegular()) {
+ res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
+ if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
readmeFiles[i] = entry
}
} else {
@@ -80,6 +79,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
}
}
}
+
var readmeFile *git.TreeEntry
for _, f := range readmeFiles {
if f != nil {
@@ -103,7 +103,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
return "", nil, err
}
- subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
+ subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
if err != nil && !git.IsErrNotExist(err) {
return "", nil, err
}
@@ -139,46 +139,52 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
}
func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
- target := readmeFile
- if readmeFile != nil && readmeFile.IsLink() {
- target, _ = readmeFile.FollowLinks()
- }
- if target == nil {
- // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
- // simply skip rendering the README
+ if readmeFile == nil {
return
}
+ readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
+ readmeTargetEntry := readmeFile
+ if readmeFile.IsLink() {
+ if res, err := git.EntryFollowLinks(ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil {
+ readmeTargetEntry = res.TargetEntry
+ } else {
+ readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error
+ }
+ }
+ if readmeTargetEntry == nil {
+ return // if no valid README entry found, skip rendering the README
+ }
+
ctx.Data["RawFileLink"] = ""
- ctx.Data["ReadmeInList"] = true
+ ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path
ctx.Data["ReadmeExist"] = true
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob())
if err != nil {
ctx.ServerError("getFileReader", err)
return
}
defer dataRc.Close()
- ctx.Data["FileIsText"] = fInfo.isTextFile
- ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
+ ctx.Data["FileIsText"] = fInfo.st.IsText()
+ ctx.Data["FileTreePath"] = readmeFullPath
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
}
- if !fInfo.isTextFile {
+ if !fInfo.st.IsText() {
return
}
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
// Pretend that this is a normal text file to display 'This file is too large to be shown'
ctx.Data["IsFileTooLarge"] = true
- ctx.Data["IsTextFile"] = true
return
}
@@ -190,10 +196,10 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
+ CurrentTreePath: path.Dir(readmeFullPath),
}).
WithMarkupType(markupType).
- WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
+ WithRelativePath(readmeFullPath)
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
if err != nil {
@@ -212,7 +218,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/repo/wiki.go b/routers/web/repo/wiki.go
index 0f8e1223c6..a35b7b86e1 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -6,8 +6,7 @@ package repo
import (
"bytes"
- gocontext "context"
- "fmt"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -62,9 +61,9 @@ func MustEnableWiki(ctx *context.Context) {
return
}
- unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
+ repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
if err == nil {
- ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
+ ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL)
return
}
}
@@ -96,7 +95,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
- wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errGitRepo != nil {
ctx.ServerError("OpenRepository", errGitRepo)
return nil, nil, errGitRepo
@@ -105,12 +104,12 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if git.IsErrNotExist(errCommit) {
// if the default branch recorded in database is out of sync, then re-sync it
- gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository)
+ gitRepoDefaultBranch, errBranch := gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errBranch != nil {
return wikiGitRepo, nil, errBranch
}
// update the default branch in the database
- errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+ errDb := repo_model.UpdateRepositoryColsNoAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
if errDb != nil {
return wikiGitRepo, nil, errDb
}
@@ -179,23 +178,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_
}
func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // Get page list.
+ // get the wiki pages list.
entries, err := commit.ListEntries()
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("ListEntries", err)
return nil, nil
}
@@ -209,9 +202,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("WikiFilenameToName", err)
return nil, nil
} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
@@ -250,58 +240,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- // get filecontent
+ // get page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- var sidebarContent []byte
- if !isSideBar {
- sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- sidebarContent = data
- }
-
- var footerContent []byte
- if !isFooter {
- footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- footerContent = data
- }
-
rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
- buf := &strings.Builder{}
- renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
+ renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) {
+ buf := &strings.Builder{}
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
done := make(chan struct{})
go func() {
// We allow NBSP here this is rendered
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
- output = buf.String()
+ output = template.HTML(buf.String())
buf.Reset()
close(done)
}()
@@ -312,75 +270,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return escaped, output, err
}
- ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data)
+ ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
if rctx.SidebarTocNode != nil {
- sb := &strings.Builder{}
- err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode)
- if err != nil {
+ sb := strings.Builder{}
+ if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil {
log.Error("Failed to render wiki sidebar TOC: %v", err)
- } else {
- ctx.Data["sidebarTocContent"] = sb.String()
}
+ ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String())
}
if !isSideBar {
- buf.Reset()
- ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent)
+ sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["sidebarPresent"] = sidebarContent != nil
- } else {
- ctx.Data["sidebarPresent"] = false
}
if !isFooter {
- buf.Reset()
- ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent)
+ footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["footerPresent"] = footerContent != nil
- } else {
- ctx.Data["footerPresent"] = false
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -395,53 +339,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- // lookup filename in wiki - get filecontent, gitTree entry , real filename
- data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+ // lookup filename in wiki - get page content, gitTree entry , real filename
+ _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
-
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
// get Commit Count
- commitsHistory, err := wikiRepo.CommitsByFileAndRange(
+ commitsHistory, err := wikiGitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("CommitsByFileAndRange", err)
return nil, nil
}
ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("ConvertFromGitCommit", err)
return nil, nil
}
@@ -450,16 +376,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderEditPage(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
@@ -467,7 +388,7 @@ func renderEditPage(ctx *context.Context) {
return
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -491,17 +412,13 @@ func renderEditPage(ctx *context.Context) {
return
}
- // get filecontent
+ // get wiki page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
return
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
+ ctx.Data["WikiEditContent"] = string(data)
}
// WikiPost renders post of wiki page
@@ -563,12 +480,7 @@ func Wiki(ctx *context.Context) {
return
}
- wikiRepo, entry := renderViewPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
+ wikiGitRepo, entry := renderViewPage(ctx)
if ctx.Written() {
return
}
@@ -581,10 +493,10 @@ func Wiki(ctx *context.Context) {
wikiPath := entry.Name()
if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
ext := strings.ToUpper(filepath.Ext(wikiPath))
- ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
+ ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
}
// Get last change information.
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -604,13 +516,7 @@ func WikiRevision(ctx *context.Context) {
return
}
- wikiRepo, entry := renderRevisionPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ wikiGitRepo, entry := renderRevisionPage(ctx)
if ctx.Written() {
return
}
@@ -622,7 +528,7 @@ func WikiRevision(ctx *context.Context) {
// Get last change information.
wikiPath := entry.Name()
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -642,12 +548,7 @@ func WikiPages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
return
@@ -667,7 +568,7 @@ func WikiPages(ctx *context.Context) {
}
allEntries.CustomSort(base.NaturalSortLess)
- entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath)
+ entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return
@@ -701,13 +602,7 @@ func WikiPages(ctx *context.Context) {
// WikiRaw outputs raw blob requested by user (image for example)
func WikiRaw(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(nil)
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 99114c93e0..59bf6ed79b 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -29,7 +29,7 @@ const (
)
func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry {
- wikiRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo)
+ wikiRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
assert.NoError(t, err)
defer wikiRepo.Close()
commit, err := wikiRepo.GetBranchCommit("master")
@@ -71,7 +71,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
require.Len(t, pageMetas, len(expectedNames))
for i, pageMeta := range pageMetas {
- assert.EqualValues(t, expectedNames[i], pageMeta.Name)
+ assert.Equal(t, expectedNames[i], pageMeta.Name)
}
}
@@ -82,7 +82,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "Home")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
@@ -90,7 +90,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location"))
}
@@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
}
@@ -111,7 +111,7 @@ func TestNewWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
}
@@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
}
@@ -149,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
@@ -162,16 +162,16 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
- assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
+ assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"])
ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit")
ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
- assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
}
func TestEditWikiPost(t *testing.T) {
@@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message,
})
EditWikiPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" {
@@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
@@ -228,10 +228,10 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx)
if filetype == "" {
- assert.EqualValues(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
} else {
- assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
- assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
+ assert.Equal(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
}
}
}
@@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) {
assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
// repo with wiki
- assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+ assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(
+ db.DefaultContext,
+ &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"},
+ "default_wiki_branch",
+ ),
+ )
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
ctx.SetPathParam("*", "Home")