feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -28,8 +28,8 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3"
act_model "github.com/nektos/act/pkg/model"
"go.yaml.in/yaml/v4"
)
const (
@@ -38,9 +38,10 @@ const (
tplViewActions templates.TplName = "repo/actions/view"
)
type Workflow struct {
Entry git.TreeEntry
ErrMsg string
type WorkflowInfo struct {
Entry git.TreeEntry
ErrMsg string
Workflow *act_model.Workflow
}
// MustEnableActions check if actions are enabled in settings
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
return
}
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
if ctx.Written() {
return
}
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
if ctx.Written() {
return
}
@@ -112,55 +117,46 @@ func WorkflowDispatchInputs(ctx *context.Context) {
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
return
}
prepareWorkflowDispatchTemplate(ctx, commit)
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
if ctx.Written() {
return
}
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
}
func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
workflowID := ctx.FormString("workflow")
ctx.Data["CurWorkflow"] = workflowID
ctx.Data["CurWorkflowExists"] = false
var curWorkflow *model.Workflow
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) {
curWorkflowID = ctx.FormString("workflow")
_, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return nil
return nil, ""
}
// Get all runner labels
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
RepoID: ctx.Repo.Repository.ID,
IsOnline: optional.Some(true),
WithAvailable: true,
})
if err != nil {
ctx.ServerError("FindRunners", err)
return nil
}
allRunnerLabels := make(container.Set[string])
for _, r := range runners {
allRunnerLabels.AddMultiple(r.AgentLabels...)
}
workflows = make([]Workflow, 0, len(entries))
workflows = make([]WorkflowInfo, 0, len(entries))
for _, entry := range entries {
workflow := Workflow{Entry: *entry}
workflow := WorkflowInfo{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.ServerError("GetContentFromEntry", err)
return nil
return nil, ""
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
if err := actions.ValidateWorkflowContent(content); err != nil {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
workflow.Workflow = wf
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
hasJobWithoutNeeds := false
// Check whether you have matching runner and a job without "needs"
@@ -173,22 +169,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
hasJobWithoutNeeds = true
}
runsOnList := j.RunsOn()
for _, ro := range runsOnList {
if strings.Contains(ro, "${{") {
// Skip if it contains expressions.
// The expressions could be very complex and could not be evaluated here,
// so just skip it, it's OK since it's just a tooltip message.
continue
}
if !allRunnerLabels.Contains(ro) {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break
}
}
if workflow.ErrMsg != "" {
break
}
}
if !hasJobWithoutNeeds {
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
@@ -197,61 +177,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
}
workflows = append(workflows, workflow)
if workflow.Entry.Name() == workflowID {
curWorkflow = wf
ctx.Data["CurWorkflowExists"] = true
}
}
ctx.Data["workflows"] = workflows
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
ctx.Data["ActionsConfig"] = actionsConfig
ctx.Data["CurWorkflow"] = curWorkflowID
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID)
if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
if !isWorkflowDisabled && curWorkflow != nil {
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
if workflowDispatchConfig != nil {
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: optional.Some(false),
ListOptions: db.ListOptions{
ListAll: true,
},
}
branches, err := git_model.FindBranchNames(ctx, branchOpts)
if err != nil {
ctx.ServerError("FindBranchNames", err)
return nil
}
// always put default branch on the top if it exists
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
}
ctx.Data["Branches"] = branches
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return nil
}
ctx.Data["Tags"] = tags
}
}
}
return workflows
return workflows, curWorkflowID
}
func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
return
}
var curWorkflow *act_model.Workflow
for _, workflowInfo := range workflowInfos {
if workflowInfo.Entry.Name() == curWorkflowID {
if workflowInfo.Workflow == nil {
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
return
}
curWorkflow = workflowInfo.Workflow
break
}
}
if curWorkflow == nil {
return
}
ctx.Data["CurWorkflowExists"] = true
curWfDispatchCfg := workflowDispatchConfig(curWorkflow)
if curWfDispatchCfg == nil {
return
}
ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: optional.Some(false),
ListOptions: db.ListOptions{
ListAll: true,
},
}
branches, err := git_model.FindBranchNames(ctx, branchOpts)
if err != nil {
ctx.ServerError("FindBranchNames", err)
return
}
// always put default branch on the top
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
ctx.Data["Branches"] = branches
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
}
ctx.Data["Tags"] = tags
}
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
workflowID := ctx.FormString("workflow")
@@ -302,6 +296,49 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
log.Error("LoadIsRefDeleted", err)
}
// Check for each run if there is at least one online runner that can run its jobs
runErrors := make(map[int64]string)
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
RepoID: ctx.Repo.Repository.ID,
IsOnline: optional.Some(true),
WithAvailable: true,
})
if err != nil {
ctx.ServerError("FindRunners", err)
return
}
for _, run := range runs {
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
continue
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.ServerError("GetRunJobsByRunID", err)
return
}
for _, job := range jobs {
if !job.Status.IsWaiting() {
continue
}
if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil {
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
break
}
hasOnlineRunner := false
for _, runner := range runners {
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
hasOnlineRunner = true
break
}
}
if !hasOnlineRunner {
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
break
}
}
}
ctx.Data["RunErrors"] = runErrors
ctx.Data["Runs"] = runs
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
@@ -313,7 +350,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale)
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
pager := context.NewPagination(total, opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
@@ -362,7 +399,7 @@ type WorkflowDispatch struct {
Inputs []WorkflowDispatchInput
}
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch {
switch w.RawOn.Kind {
case yaml.ScalarNode:
var val string
@@ -24,10 +24,11 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
@@ -36,43 +37,184 @@ import (
notify_service "code.gitea.io/gitea/services/notify"
"github.com/nektos/act/pkg/model"
"xorm.io/builder"
)
func getRunIndex(ctx *context_module.Context) int64 {
// if run param is "latest", get the latest run index
if ctx.PathParam("run") == "latest" {
if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil {
return run.Index
func findCurrentJobByPathParam(ctx *context_module.Context, jobs []*actions_model.ActionRunJob) (job *actions_model.ActionRunJob, hasPathParam bool) {
selectedJobID := ctx.PathParamInt64("job")
if selectedJobID <= 0 {
return nil, false
}
for _, job = range jobs {
if job.ID == selectedJobID {
return job, true
}
}
return ctx.PathParamInt64("run")
return nil, true
}
func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.ActionRun) {
var err error
// if run param is "latest", get the latest run id
if ctx.PathParam("run") == "latest" {
run, err = actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
} else {
run, err = actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("run"))
}
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound(nil)
} else if err != nil {
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
}
return run
}
// resolveCurrentRunForView resolves GET Actions page URLs and supports both ID-based and legacy index-based forms.
//
// By default, run summary pages (/actions/runs/{run}) use a best-effort ID-first fallback,
// and job pages (/actions/runs/{run}/jobs/{job}) try to confirm an ID-based URL first and prefer the ID-based interpretation when both are valid.
//
// `by_id=1` param explicitly forces the ID-based path, and `by_index=1` explicitly forces the legacy index-based path.
// If both are present, `by_id` takes precedence.
func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.ActionRun {
// `by_id` explicitly requests ID-based resolution, so the request skips the legacy index-based disambiguation logic and resolves the run by ID directly.
// It takes precedence over `by_index` when both query parameters are present.
if ctx.PathParam("run") == "latest" || ctx.FormBool("by_id") {
return getCurrentRunByPathParam(ctx)
}
runNum := ctx.PathParamInt64("run")
if runNum <= 0 {
ctx.NotFound(nil)
return nil
}
byIndex := ctx.FormBool("by_index")
if ctx.PathParam("job") == "" {
// The URL does not contain a {job} path parameter, so it cannot use the
// job-specific rules to disambiguate ID-based URLs from legacy index-based URLs.
// Because of that, this path is handled with a best-effort ID-first fallback by default.
//
// When the same repository contains:
// - a run whose ID matches runNum, and
// - a different run whose repo-scope index also matches runNum
// this path prefers the ID match and may show a different run than the old legacy URL originally intended,
// unless `by_index=1` explicitly forces the legacy index-based interpretation.
if !byIndex {
runByID, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runNum)
if err == nil {
return runByID
}
if !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
return nil
}
}
runByIndex, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
if err == nil {
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.RepoLink, runByIndex.ID), http.StatusFound)
return nil
}
if !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunByRepoAndIndex", err)
return nil
}
ctx.NotFound(nil)
return nil
}
jobNum := ctx.PathParamInt64("job")
if jobNum < 0 {
ctx.NotFound(nil)
return nil
}
// A job index should not be larger than MaxJobNumPerRun, so larger values can skip the legacy index-based path and be treated as job IDs directly.
if !byIndex && jobNum >= actions_model.MaxJobNumPerRun {
return getCurrentRunByPathParam(ctx)
}
var runByID, runByIndex *actions_model.ActionRun
var targetJobByIndex *actions_model.ActionRunJob
if !byIndex {
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunJobByRepoAndID", err)
return nil
}
if job != nil {
if err := job.LoadRun(ctx); err != nil {
ctx.ServerError("LoadRun", err)
return nil
}
if job.Run.ID == runNum {
runByID = job.Run
}
}
}
// Try to resolve the request as a legacy run-index/job-index URL.
{
run, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetRunByRepoAndIndex", err)
return nil
}
if run != nil {
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.ServerError("GetRunJobsByRunID", err)
return nil
}
if jobNum < int64(len(jobs)) {
runByIndex = run
targetJobByIndex = jobs[jobNum]
}
}
}
if runByID == nil && runByIndex == nil {
ctx.NotFound(nil)
return nil
}
if runByID != nil && runByIndex == nil {
return runByID
}
if runByID == nil && runByIndex != nil {
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d/jobs/%d", ctx.Repo.RepoLink, runByIndex.ID, targetJobByIndex.ID), http.StatusFound)
return nil
}
// Reaching this point means both ID-based and legacy index-based interpretations are valid. Prefer the ID-based interpretation by default.
// Use `by_index=1` query parameter to access the legacy index-based interpretation when necessary.
return runByID
}
func View(ctx *context_module.Context) {
ctx.Data["PageIsActions"] = true
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
ctx.Data["RunIndex"] = runIndex
ctx.Data["JobIndex"] = jobIndex
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
run := resolveCurrentRunForView(ctx)
if ctx.Written() {
return
}
ctx.Data["RunID"] = run.ID
ctx.Data["JobID"] = ctx.PathParamInt64("job") // it can be 0 when no job (e.g.: run summary view)
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
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)
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
if err != nil {
ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
@@ -115,6 +257,7 @@ type ViewResponse struct {
State struct {
Run struct {
RepoID int64 `json:"repoId"`
Link string `json:"link"`
Title string `json:"title"`
TitleHTML template.HTML `json:"titleHTML"`
@@ -122,6 +265,7 @@ type ViewResponse struct {
CanCancel bool `json:"canCancel"`
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
CanRerun bool `json:"canRerun"`
CanRerunFailed bool `json:"canRerunFailed"`
CanDeleteArtifact bool `json:"canDeleteArtifact"`
Done bool `json:"done"`
WorkflowID string `json:"workflowID"`
@@ -129,6 +273,10 @@ type ViewResponse struct {
IsSchedule bool `json:"isSchedule"`
Jobs []*ViewJob `json:"jobs"`
Commit ViewCommit `json:"commit"`
// Summary view: run duration and trigger time/event
Duration string `json:"duration"`
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
@@ -142,11 +290,13 @@ type ViewResponse struct {
}
type ViewJob struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
CanRerun bool `json:"canRerun"`
Duration string `json:"duration"`
ID int64 `json:"id"`
JobID string `json:"jobId,omitempty"`
Name string `json:"name"`
Status string `json:"status"`
CanRerun bool `json:"canRerun"`
Duration string `json:"duration"`
Needs []string `json:"needs,omitempty"`
}
type ViewCommit struct {
@@ -186,12 +336,8 @@ type ViewStepLogLine struct {
Timestamp float64 `json:"timestamp"`
}
func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
if err != nil {
return nil, err
}
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, repoID, runID)
if err != nil {
return nil, err
}
@@ -206,30 +352,36 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artif
}
func ViewPost(ctx *context_module.Context) {
req := web.GetForm(ctx).(*ViewRequest)
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
return
}
run := current.Run
if err := run.LoadAttributes(ctx); err != nil {
ctx.ServerError("run.LoadAttributes", err)
return
}
var err error
resp := &ViewResponse{}
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, runIndex)
fillViewRunResponseSummary(ctx, resp, run, jobs)
if ctx.Written() {
return
}
fillViewRunResponseCurrentJob(ctx, resp, run, jobs)
if ctx.Written() {
return
}
ctx.JSON(http.StatusOK, resp)
}
func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
var err error
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, run.ID)
if err != nil {
if !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("getActionsViewArtifacts", err)
return
}
ctx.ServerError("getActionsViewArtifacts", err)
return
}
resp.State.Run.RepoID = ctx.Repo.Repository.ID
// 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, ctx.Repo.Repository)
@@ -238,6 +390,14 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
if resp.State.Run.CanRerun {
for _, job := range jobs {
if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled {
resp.State.Run.CanRerunFailed = true
break
}
}
}
resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.WorkflowID = run.WorkflowID
resp.State.Run.WorkflowLink = run.WorkflowLink()
@@ -247,10 +407,12 @@ func ViewPost(ctx *context_module.Context) {
for _, v := range jobs {
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
ID: v.ID,
JobID: v.JobID,
Name: v.Name,
Status: v.Status.String(),
CanRerun: resp.State.Run.CanRerun,
Duration: v.Duration().String(),
Needs: v.Needs,
})
}
@@ -278,6 +440,20 @@ func ViewPost(ctx *context_module.Context) {
Pusher: pusher,
Branch: branch,
}
resp.State.Run.Duration = run.Duration().String()
resp.State.Run.TriggeredAt = run.Created.AsTime().Unix()
resp.State.Run.TriggerEvent = run.TriggerEvent
}
func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
req := web.GetForm(ctx).(*ViewRequest)
current, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
if current == nil {
if hasPathParam {
ctx.NotFound(nil)
}
return
}
var task *actions_model.ActionTask
if current.TaskID > 0 {
@@ -302,7 +478,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
if task != nil {
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
if err != nil {
ctx.ServerError("convertToViewModel", err)
return
@@ -310,11 +486,9 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...)
}
ctx.JSON(http.StatusOK, resp)
}
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
var viewJobs []*ViewJobStep
var logs []*ViewStepLog
@@ -344,7 +518,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
Lines: []*ViewStepLogLine{
{
Index: 1,
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
Message: locale.TrString("actions.runs.expire_log_message"),
// Timestamp doesn't mean anything when the log is expired.
// Set it to the task's updated time since it's probably the time when the log has expired.
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
@@ -396,246 +570,202 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
return viewJobs, logs, nil
}
// Rerun will rerun jobs in the given run
// If jobIndexStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndexStr := ctx.PathParam("job")
var jobIndex int64
if jobIndexStr != "" {
jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
}
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
ctx.ServerError("GetRunByIndex", err)
return
}
// rerun is not allowed if the run is not done
// checkRunRerunAllowed checks whether a rerun is permitted for the given run,
// writing the appropriate JSON error to ctx and returning false when it is not.
func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.ActionRun) bool {
if !run.Status.IsDone() {
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
return
return false
}
// can not rerun job when workflow is disabled
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(run.WorkflowID) {
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
return
return false
}
return true
}
// reset run's start and stop time
run.PreviousDuration = run.Duration()
run.Started = 0
run.Stopped = 0
run.Status = actions_model.StatusWaiting
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "status", "previous_duration"); err != nil {
ctx.ServerError("UpdateRun", err)
return
}
if err := run.LoadAttributes(ctx); err != nil {
ctx.ServerError("run.LoadAttributes", err)
return
}
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
// Rerun will rerun jobs in the given run
// If jobIDStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
return
}
if jobIndexStr == "" { // rerun all jobs
for _, j := range jobs {
// 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.ServerError("RerunJob", err)
return
}
}
ctx.JSONOK()
if !checkRunRerunAllowed(ctx, run) {
return
}
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
currentJob, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
if hasPathParam && currentJob == nil {
ctx.NotFound(nil)
return
}
for _, j := range rerunJobs {
// 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.ServerError("RerunJob", err)
return
}
var jobsToRerun []*actions_model.ActionRunJob
if currentJob != nil {
jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs)
} else {
jobsToRerun = jobs
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil {
ctx.ServerError("RerunWorkflowRunJobs", err)
return
}
ctx.JSONOK()
}
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status
if !status.IsDone() {
return nil
// RerunFailed reruns all failed jobs in the given run
func RerunFailed(ctx *context_module.Context) {
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
return
}
job.TaskID = 0
job.Status = actions_model.StatusWaiting
if shouldBlock {
job.Status = actions_model.StatusBlocked
}
job.Started = 0
job.Stopped = 0
if err := db.WithTx(ctx, func(ctx context.Context) error {
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
return err
}); err != nil {
return err
}
actions_service.CreateCommitStatus(ctx, job)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
return nil
}
func Logs(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
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)
if !checkRunRerunAllowed(ctx, run) {
return
}
if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
ctx.ServerError("RerunWorkflowRunJobs", err)
return
}
ctx.JSONOK()
}
func Logs(ctx *context_module.Context) {
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
jobID := ctx.PathParamInt64("job")
if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
}
}
func Cancel(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
_, jobs := getRunJobs(ctx, runIndex, -1)
run, jobs := getCurrentRunJobsByPathParam(ctx)
if ctx.Written() {
return
}
var updatedjobs []*actions_model.ActionRunJob
var updatedJobs []*actions_model.ActionRunJob
if err := db.WithTx(ctx, func(ctx context.Context) error {
for _, job := range jobs {
status := job.Status
if status.IsDone() {
continue
}
if job.TaskID == 0 {
job.Status = actions_model.StatusCancelled
job.Stopped = timeutil.TimeStampNow()
n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
return err
}
if n == 0 {
return errors.New("job has changed, try again")
}
if n > 0 {
updatedjobs = append(updatedjobs, job)
}
continue
}
if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
return err
}
cancelledJobs, err := actions_model.CancelJobs(ctx, jobs)
if err != nil {
return fmt.Errorf("cancel jobs: %w", err)
}
updatedJobs = append(updatedJobs, cancelledJobs...)
return nil
}); err != nil {
ctx.ServerError("StopTask", err)
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...)
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
for _, job := range updatedjobs {
for _, job := range updatedJobs {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
if len(updatedjobs) > 0 {
job := updatedjobs[0]
if len(updatedJobs) > 0 {
job := updatedJobs[0]
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
}
ctx.JSONOK()
}
func Approve(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
current, jobs := getRunJobs(ctx, runIndex, -1)
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
run := current.Run
doer := ctx.Doer
var updatedjobs []*actions_model.ActionRunJob
if err := db.WithTx(ctx, func(ctx context.Context) error {
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
for _, job := range jobs {
if len(job.Needs) == 0 && job.Status.IsBlocked() {
job.Status = actions_model.StatusWaiting
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
if n > 0 {
updatedjobs = append(updatedjobs, job)
}
}
}
return nil
}); err != nil {
ctx.ServerError("UpdateRunJob", err)
approveRuns(ctx, []int64{run.ID})
if ctx.Written() {
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
if len(updatedjobs) > 0 {
job := updatedjobs[0]
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
}
for _, job := range updatedjobs {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
ctx.JSONOK()
}
func Delete(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
repoID := ctx.Repo.Repository.ID
func approveRuns(ctx *context_module.Context, runIDs []int64) {
doer := ctx.Doer
repo := ctx.Repo.Repository
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.JSONErrorNotFound()
return
updatedJobs := make([]*actions_model.ActionRunJob, 0)
runMap := make(map[int64]*actions_model.ActionRun, len(runIDs))
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIDs))
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
for _, runID := range runIDs {
run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID)
if err != nil {
return err
}
runMap[run.ID] = run
run.Repo = repo
run.NeedApproval = false
run.ApprovedBy = doer.ID
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
return err
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
return err
}
runJobs[run.ID] = jobs
for _, job := range jobs {
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
return err
}
if job.Status == actions_model.StatusWaiting {
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
if n > 0 {
updatedJobs = append(updatedJobs, job)
}
}
}
}
ctx.ServerError("GetRunByIndex", err)
return nil
})
if err != nil {
ctx.NotFoundOrServerError("approveRuns", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
return
}
for runID, run := range runMap {
actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...)
}
if len(updatedJobs) > 0 {
job := updatedJobs[0]
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
}
for _, job := range updatedJobs {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
}
func Delete(ctx *context_module.Context) {
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
@@ -652,17 +782,11 @@ func Delete(ctx *context_module.Context) {
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.
func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound(nil)
return nil, nil
}
ctx.ServerError("GetRunByIndex", err)
// getRunJobs loads the run and its jobs for runID
// Any error will be written to the ctx, empty jobs will also result in 404 error, then the return values are all nil.
func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, []*actions_model.ActionRunJob) {
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return nil, nil
}
run.Repo = ctx.Repo.Repository
@@ -676,28 +800,19 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
return nil, nil
}
for _, v := range jobs {
v.Run = run
for _, job := range jobs {
job.Run = run
}
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
return jobs[jobIndex], jobs
}
return jobs[0], jobs
return run, jobs
}
func ArtifactsDeleteView(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
artifactName := ctx.PathParam("artifact_name")
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)
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
artifactName := ctx.PathParam("artifact_name")
if err := actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
ctx.ServerError("SetArtifactNeedDelete", err)
return
}
@@ -705,19 +820,12 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
}
func ArtifactsDownloadView(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
artifactName := ctx.PathParam("artifact_name")
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())
return
}
ctx.ServerError("GetRunByIndex", err)
run := getCurrentRunByPathParam(ctx)
if ctx.Written() {
return
}
artifactName := ctx.PathParam("artifact_name")
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RunID: run.ID,
ArtifactName: artifactName,
@@ -739,8 +847,9 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
}
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
// A v4 Artifact may only contain a single file
// Multiple files are uploaded as a single file archive
// All other cases fall back to the legacy v1v3 zip handling below
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
if err != nil {
@@ -752,39 +861,82 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
// Those need to be zipped for download
writer := zip.NewWriter(ctx.Resp)
defer writer.Close()
for _, art := range artifacts {
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
zipWriter := zip.NewWriter(ctx.Resp)
defer zipWriter.Close()
writeArtifactToZip := func(art *actions_model.ActionArtifact) error {
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
if err != nil {
ctx.ServerError("ActionsArtifacts.Open", err)
return
return fmt.Errorf("ActionsArtifacts.Open: %w", err)
}
defer f.Close()
var r io.ReadCloser
if art.ContentEncoding == "gzip" {
var r io.ReadCloser = f
if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip {
r, err = gzip.NewReader(f)
if err != nil {
ctx.ServerError("gzip.NewReader", err)
return
return fmt.Errorf("gzip.NewReader: %w", err)
}
} else {
r = f
}
defer r.Close()
w, err := writer.Create(art.ArtifactPath)
w, err := zipWriter.Create(art.ArtifactPath)
if err != nil {
ctx.ServerError("writer.Create", err)
return
return fmt.Errorf("zipWriter.Create: %w", err)
}
if _, err := io.Copy(w, r); err != nil {
ctx.ServerError("io.Copy", err)
_, err = io.Copy(w, r)
if err != nil {
return fmt.Errorf("io.Copy: %w", err)
}
return nil
}
for _, art := range artifacts {
err := writeArtifactToZip(art)
if err != nil {
ctx.ServerError("writeArtifactToZip", err)
return
}
}
}
func ApproveAllChecks(ctx *context_module.Context) {
repo := ctx.Repo.Repository
commitID := ctx.FormString("commit_id")
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return
}
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err)
return
}
runIDs := make([]int64, 0, len(runs))
for _, run := range runs {
if run.NeedApproval {
runIDs = append(runIDs, run.ID)
}
}
if len(runIDs) == 0 {
ctx.JSONOK()
return
}
approveRuns(ctx, runIDs)
if ctx.Written() {
return
}
ctx.Flash.Success(ctx.Tr("actions.approve_all_success"))
ctx.JSONOK()
}
func DisableWorkflowFile(ctx *context_module.Context) {
disableOrEnableWorkflowFile(ctx, false)
}
@@ -796,7 +948,7 @@ func EnableWorkflowFile(ctx *context_module.Context) {
func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
workflow := ctx.FormString("workflow")
if len(workflow) == 0 {
ctx.ServerError("workflow", nil)
ctx.JSONError("workflow is required")
return
}
@@ -809,7 +961,7 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
cfg.DisableWorkflow(workflow)
}
if err := repo_model.UpdateRepoUnit(ctx, cfgUnit); err != nil {
if err := repo_model.UpdateRepoUnitConfig(ctx, cfgUnit); err != nil {
ctx.ServerError("UpdateRepoUnit", err)
return
}
@@ -840,7 +992,7 @@ func Run(ctx *context_module.Context) {
ctx.ServerError("ref", nil)
return
}
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
_, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
@@ -0,0 +1,47 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertToViewModel(t *testing.T) {
task := &actions_model.ActionTask{
Status: actions_model.StatusSuccess,
Steps: []*actions_model.ActionTaskStep{
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
},
Stopped: timeutil.TimeStamp(20),
}
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
require.NoError(t, err)
expectedViewJobs := []*ViewJobStep{
{
Summary: "Set up job",
Duration: "0s",
Status: "success",
},
{
Summary: "Run step-name",
Duration: "4s",
Status: "success",
},
{
Summary: "Complete job",
Duration: "15s",
Status: "success",
},
}
assert.Equal(t, expectedViewJobs, viewJobSteps)
}
@@ -8,7 +8,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/git"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@@ -25,38 +25,33 @@ func Activity(ctx *context.Context) {
ctx.Data["PageIsPulse"] = true
ctx.Data["Period"] = ctx.PathParam("period")
timeUntil := time.Now()
var timeFrom time.Time
switch ctx.Data["Period"] {
period, timeFrom := "weekly", timeUntil.Add(-time.Hour*168)
switch ctx.PathParam("period") {
case "daily":
timeFrom = timeUntil.Add(-time.Hour * 24)
period, timeFrom = "daily", timeUntil.Add(-time.Hour*24)
case "halfweekly":
timeFrom = timeUntil.Add(-time.Hour * 72)
period, timeFrom = "halfweekly", timeUntil.Add(-time.Hour*72)
case "weekly":
timeFrom = timeUntil.Add(-time.Hour * 168)
period, timeFrom = "weekly", timeUntil.Add(-time.Hour*168)
case "monthly":
timeFrom = timeUntil.AddDate(0, -1, 0)
period, timeFrom = "monthly", timeUntil.AddDate(0, -1, 0)
case "quarterly":
timeFrom = timeUntil.AddDate(0, -3, 0)
period, timeFrom = "quarterly", timeUntil.AddDate(0, -3, 0)
case "semiyearly":
timeFrom = timeUntil.AddDate(0, -6, 0)
period, timeFrom = "semiyearly", timeUntil.AddDate(0, -6, 0)
case "yearly":
timeFrom = timeUntil.AddDate(-1, 0, 0)
default:
ctx.Data["Period"] = "weekly"
timeFrom = timeUntil.Add(-time.Hour * 168)
period, timeFrom = "yearly", timeUntil.AddDate(-1, 0, 0)
}
ctx.Data["DateFrom"] = timeFrom
ctx.Data["DateUntil"] = timeUntil
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
ctx.Data["Period"] = period
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + period)
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)
branchExist, _ := git_model.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)
@@ -6,34 +6,45 @@ package repo
import (
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
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/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
repo_service "code.gitea.io/gitea/services/repository"
)
func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) {
switch unitType {
case unit.TypeIssues, unit.TypePullRequests:
return auth_model.AccessTokenScopeReadIssue, true
case unit.TypeReleases:
return auth_model.AccessTokenScopeReadRepository, true
default:
return "", false
}
}
// UploadIssueAttachment response for Issue/PR attachments
func UploadIssueAttachment(ctx *context.Context) {
uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Attachment.AllowedTypes)
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
}
// UploadReleaseAttachment response for uploading release attachments
func UploadReleaseAttachment(ctx *context.Context) {
uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Repository.Release.AllowedTypes)
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForRelease)
}
// UploadAttachment response for uploading attachments
func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
func uploadAttachment(ctx *context.Context, repoID int64, uploadFunc attachment.UploadAttachmentFunc) {
if !setting.Attachment.Enabled {
ctx.HTTPError(http.StatusNotFound, "attachment is not enabled")
return
@@ -47,7 +58,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
defer file.Close()
uploaderFile := attachment.NewLimitedUploaderKnownSize(file, header.Size)
attach, err := attachment.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, allowedTypes, &repo_model.Attachment{
attach, err := uploadFunc(ctx, uploaderFile, &repo_model.Attachment{
Name: header.Filename,
UploaderID: ctx.Doer.ID,
RepoID: repoID,
@@ -57,7 +68,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
ctx.HTTPError(http.StatusBadRequest, err.Error())
return
}
ctx.ServerError("UploadAttachmentGeneralSizeLimit", err)
ctx.ServerError("uploadAttachment(uploadFunc)", err)
return
}
@@ -120,7 +131,7 @@ func DeleteAttachment(ctx *context.Context) {
})
}
// GetAttachment serve attachments with the given UUID
// ServeAttachment serve attachments with the given UUID
func ServeAttachment(ctx *context.Context, uuid string) {
attach, err := repo_model.GetAttachmentByUUID(ctx, uuid)
if err != nil {
@@ -151,16 +162,19 @@ func ServeAttachment(ctx *context.Context, uuid string) {
return
}
} else { // If we have the linked type, we need to check access
var perm access_model.Permission
if ctx.Repo.Repository == nil {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
var (
perm access_model.Permission
repo = ctx.Repo.Repository
)
if repo == nil {
repo, err = repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}
perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
} else {
@@ -171,6 +185,13 @@ func ServeAttachment(ctx *context.Context, uuid string) {
ctx.HTTPError(http.StatusNotFound)
return
}
if requiredScope, ok := attachmentReadScope(unitType); ok {
context.CheckTokenScopes(ctx, repo, requiredScope)
if ctx.Written() {
return
}
}
}
if err := attach.IncreaseDownloadCount(ctx); err != nil {
@@ -180,7 +201,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, ctx.Req.Method, nil)
u, err := storage.Attachments.ServeDirectURL(attach.RelativePath(), attach.Name, ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
@@ -188,7 +209,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
}
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`, attach.CreatedUnix.AsTimePtr()) {
return
}
@@ -200,7 +221,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
}
defer fr.Close()
common.ServeContentByReadSeeker(ctx.Base, attach.Name, util.ToPointer(attach.CreatedUnix.AsTime()), fr)
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, fr, httplib.ServeHeaderOptions{Filename: attach.Name})
}
// GetAttachment serve attachments
+64 -87
View File
@@ -4,19 +4,20 @@
package repo
import (
"bytes"
"fmt"
gotemplate "html/template"
"html/template"
"net/http"
"net/url"
"path"
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
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/gitrepo"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -26,24 +27,23 @@ import (
)
type blameRow struct {
RowNumber int
Avatar gotemplate.HTML
RepoLink string
PartSha string
RowNumber int
Avatar template.HTML
PreviousSha string
PreviousShaURL string
IsFirstCommit bool
CommitURL string
CommitMessage string
CommitSince gotemplate.HTML
Code gotemplate.HTML
EscapeStatus *charset.EscapeStatus
CommitSince template.HTML
Code template.HTML
EscapeStatus *charset.EscapeStatus
}
// RefBlame render blame page
func RefBlame(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["IsBlame"] = true
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
// Get current entry user currently looking at.
if ctx.Repo.TreePath == "" {
@@ -56,17 +56,6 @@ func RefBlame(ctx *context.Context) {
return
}
treeNames := strings.Split(ctx.Repo.TreePath, "/")
var paths []string
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["Paths"] = paths
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
blob := entry.Blob()
fileSize := blob.Size()
ctx.Data["FileSize"] = fileSize
@@ -111,7 +100,7 @@ func RefBlame(ctx *context.Context) {
}
type blameResult struct {
Parts []*git.BlamePart
Parts []*gitrepo.BlamePart
UsesIgnoreRevs bool
FaultyIgnoreRevsFile bool
}
@@ -119,7 +108,7 @@ type blameResult struct {
func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
objectFormat := ctx.Repo.GetObjectFormat()
blameReader, err := git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, bypassBlameIgnore)
blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, bypassBlameIgnore)
if err != nil {
return nil, err
}
@@ -135,7 +124,7 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
// try again without ignored revs
blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true)
blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, true)
if err != nil {
return nil, err
}
@@ -155,12 +144,12 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
return r, nil
}
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error {
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
previousHelper := make(map[string]*git.BlamePart)
previousHelper := make(map[string]*gitrepo.BlamePart)
r.Parts = make([]*git.BlamePart, 0, 5)
r.Parts = make([]*gitrepo.BlamePart, 0, 5)
for {
blamePart, err := br.NextPart()
if err != nil {
@@ -185,7 +174,7 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error {
return nil
}
func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit {
// store commit data by SHA to look up avatar info etc
commitNames := make(map[string]*user_model.UserCommit)
// and as blameParts can reference the same commits multiple
@@ -232,76 +221,64 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
return commitNames
}
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
if commit.User != nil {
br.Avatar = avatarUtils.Avatar(commit.User, 18)
} else {
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
}
br.PreviousSha = part.PreviousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
br.CommitMessage = commit.Message()
br.CommitSince = templates.TimeSince(commit.Author.When)
}
func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) {
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)
}
lines := make([]string, 0)
buf := &bytes.Buffer{}
rows := make([]*blameRow, 0)
avatarUtils := templates.NewAvatarUtils(ctx)
rowNumber := 0 // will be 1-based
for _, part := range blameParts {
for partLineIdx, line := range part.Lines {
rowNumber++
br := &blameRow{RowNumber: rowNumber}
rows = append(rows, br)
if int64(buf.Len()) < setting.UI.MaxDisplayFileSize {
buf.WriteString(line)
buf.WriteByte('\n')
}
if partLineIdx == 0 {
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
}
}
}
escapeStatus := &charset.EscapeStatus{}
var lexerName string
avatarUtils := templates.NewAvatarUtils(ctx)
i := 0
commitCnt := 0
for _, part := range blameParts {
for index, line := range part.Lines {
i++
lines = append(lines, line)
br := &blameRow{
RowNumber: i,
}
commit := commitNames[part.Sha]
if index == 0 {
// Count commit number
commitCnt++
// User avatar image
commitSince := templates.TimeSince(commit.Author.When)
var avatar string
if commit.User != nil {
avatar = string(avatarUtils.Avatar(commit.User, 18))
} else {
avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
}
br.Avatar = gotemplate.HTML(avatar)
br.RepoLink = repoLink
br.PartSha = part.Sha
br.PreviousSha = part.PreviousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
br.CommitMessage = commit.CommitMessage
br.CommitSince = commitSince
}
if i != len(lines)-1 {
line += "\n"
}
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
if lexerName == "" {
lexerName = lexerNameForLine
}
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
rows = append(rows, br)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
bufContent := buf.Bytes()
bufContent = charset.ToUTF8(bufContent, charset.ConvertOpts{})
highlighted, _, lexerDisplayName := highlight.RenderCodeSlowGuess(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlighted)
for i, br := range rows {
var line template.HTML
if i < len(unsafeLines) {
line = template.HTML(util.UnsafeBytesToString(unsafeLines[i]))
}
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
}
ctx.Data["EscapeStatus"] = escapeStatus
ctx.Data["BlameRows"] = rows
ctx.Data["CommitCnt"] = commitCnt
ctx.Data["LexerName"] = lexerName
ctx.Data["LexerName"] = lexerDisplayName
}
@@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -40,6 +41,7 @@ func Branches(ctx *context.Context) {
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx)
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
// TODO: Can be replaced by ctx.Repo.PullRequestCtx.CanCreateNewPull()
ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) ||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
ctx.Data["PageIsViewCode"] = true
@@ -82,7 +84,7 @@ func Branches(ctx *context.Context) {
ctx.Data["CommitStatus"] = commitStatus
ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["DefaultBranchBranch"] = defaultBranch
pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
pager := context.NewPagination(branchesCount, pageSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplBranch)
@@ -93,7 +95,7 @@ func DeleteBranchPost(ctx *context.Context) {
defer jsonRedirectBranches(ctx)
branchName := ctx.FormString("name")
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
@@ -133,8 +135,7 @@ func RestoreBranchPost(ctx *context.Context) {
return
}
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(),
if err := gitrepo.Push(ctx, ctx.Repo.Repository, ctx.Repo.Repository, git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name),
Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
}); err != nil {
@@ -194,9 +195,9 @@ func CreateBranch(ctx *context.Context) {
}
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
} else if ctx.Repo.RefFullName.IsBranch() {
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName)
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
} else {
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName)
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
}
if err != nil {
if release_service.IsErrProtectedTagName(err) {
+20 -13
View File
@@ -101,7 +101,7 @@ func Commits(ctx *context.Context) {
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
pager := context.NewPagination(commitsCount, pageSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
@@ -170,7 +170,7 @@ func Graph(ctx *context.Context) {
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 := context.NewPagination(graphCommitsCount, setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParamFromQuery(queryParams)
ctx.Data["Page"] = paginator
if divOnly {
@@ -222,7 +222,7 @@ func FileHistory(ctx *context.Context) {
return
}
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return
@@ -254,7 +254,7 @@ func FileHistory(ctx *context.Context) {
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
ctx.Data["CommitCount"] = commitsCount
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
@@ -276,20 +276,24 @@ func Diff(ctx *context.Context) {
userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name
commitID := ctx.PathParam("sha")
var (
gitRepo *git.Repository
err error
)
diffBlobExcerptData := &gitdiff.DiffBlobExcerptData{
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
DiffStyle: GetDiffViewStyle(ctx),
AfterCommitID: commitID,
}
gitRepo := ctx.Repo.GitRepo
var gitRepoStore gitrepo.Repository = ctx.Repo.Repository
if ctx.Data["PageIsWiki"] != nil {
gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
var err error
gitRepoStore = ctx.Repo.Repository.WikiStorageRepo()
gitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, gitRepoStore)
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
}
defer gitRepo.Close()
} else {
gitRepo = ctx.Repo.GitRepo
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
}
commit, err := gitRepo.GetCommit(commitID)
@@ -324,7 +328,7 @@ func Diff(ctx *context.Context) {
ctx.NotFound(err)
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(gitRepo, "", commitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, gitRepoStore, gitRepo, "", commitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return
@@ -360,6 +364,7 @@ func Diff(ctx *context.Context) {
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
if !fileOnly {
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID)
@@ -410,6 +415,8 @@ func Diff(ctx *context.Context) {
ctx.ServerError("PostProcessCommitMessage", err)
return
}
} else if !git.IsErrNotExist(err) {
log.Error("GetNote: %v", err)
}
pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
@@ -33,9 +33,9 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
opts.BaseRepo = ctx.Repo.Repository.BaseRepo
}
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
if err != nil {
log.Error("GetUserRepoPermission: %v", err)
log.Error("GetDoerRepoPermission: %v", err)
return
}
if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() {
+291 -398
View File
@@ -4,17 +4,16 @@
package repo
import (
"bufio"
gocontext "context"
"encoding/csv"
"errors"
"fmt"
"html"
"io"
"net/http"
"net/url"
"path/filepath"
"sort"
"strings"
"unicode"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@@ -41,8 +40,9 @@ import (
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull"
user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -148,9 +148,9 @@ func setCsvCompareContext(ctx *context.Context) {
if err != nil {
return nil, nil, err
}
var closer io.Closer = reader
csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader, charset.ConvertOpts{}))
return csvReader, reader, err
return csvReader, closer, err
}
baseReader, baseBlobCloser, err := csvReaderFromCommit(markup.NewRenderContext(ctx).WithRelativePath(diffFile.OldName), baseBlob)
@@ -191,146 +191,93 @@ func setCsvCompareContext(ctx *context.Context) {
}
// ParseCompareInfo parse compare info between two commit for preparing comparing references
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
baseRepo := ctx.Repo.Repository
ci := &common.CompareInfo{}
fileOnly := ctx.FormBool("file-only")
// Get compared branches information
// A full compare url is of the form:
//
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
//
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
// with the :baseRepo in ctx.Repo.
//
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
//
// How do we determine the :headRepo?
//
// 1. If :headOwner is not set then the :headRepo = :baseRepo
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
//
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
// 1 Parse compare router param
compareReq := common.ParseCompareRouterParam(ctx.PathParam("*"))
var (
isSameRepo bool
infoPath string
err error
)
infoPath = ctx.PathParam("*")
var infos []string
if infoPath == "" {
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
} else {
infos = strings.SplitN(infoPath, "...", 2)
if len(infos) != 2 {
if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
ci.DirectComparison = true
ctx.Data["PageIsComparePull"] = false
} else {
infos = []string{baseRepo.DefaultBranch, infoPath}
}
}
}
ctx.Data["BaseName"] = baseRepo.OwnerName
ci.BaseBranch = infos[0]
ctx.Data["BaseBranch"] = ci.BaseBranch
// If there is no head repository, it means compare between same repository.
headInfos := strings.Split(infos[1], ":")
if len(headInfos) == 1 {
isSameRepo = true
ci.HeadUser = ctx.Repo.Owner
ci.HeadBranch = headInfos[0]
} else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 {
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil
}
ci.HeadBranch = headInfos[1]
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
if isSameRepo {
ci.HeadRepo = baseRepo
}
} else {
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetRepositoryByOwnerAndName", err)
}
return nil
}
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil
}
ci.HeadBranch = headInfos[1]
ci.HeadUser = ci.HeadRepo.Owner
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
}
} else {
ctx.NotFound(nil)
// remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" {
ctx.HTTPError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
return nil
}
ctx.Data["HeadUser"] = ci.HeadUser
ctx.Data["HeadBranch"] = ci.HeadBranch
ctx.Repo.PullRequest.SameRepo = isSameRepo
// Check if base branch is valid.
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
baseIsBranch := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
// 2 get repository and owner for head
headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.HTTPError(http.StatusBadRequest, err.Error())
return nil
case errors.Is(err, util.ErrNotExist):
ctx.NotFound(nil)
return nil
case err != nil:
ctx.ServerError("GetHeadOwnerAndRepo", err)
return nil
}
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
ci.BaseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = ci.BaseBranch
baseIsCommit = true
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if isSameRepo {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
}
isSameRepo := baseRepo.ID == headRepo.ID
// 3 permission check
// base repository's code unit read permission check has been done on web.go
permBase := ctx.Repo.Permission
// If we're not merging from the same repo:
if !isSameRepo {
// Assert ctx.Doer has permission to read headRepo's codes
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetDoerRepoPermission", err)
return nil
} else {
}
if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
headRepo,
permHead)
}
ctx.NotFound(nil)
return nil
}
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
}
ctx.Data["BaseIsCommit"] = baseIsCommit
ctx.Data["BaseIsBranch"] = baseIsBranch
ctx.Data["BaseIsTag"] = baseIsTag
// 4 get base and head refs
baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx))
headRefName := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName)
if baseRef == "" {
ctx.NotFound(nil)
return nil
}
var headGitRepo *git.Repository
if isSameRepo {
headGitRepo = ctx.Repo.GitRepo
} else {
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
}
defer headGitRepo.Close()
}
headRef := headGitRepo.UnstableGuessRefByShortName(headRefName)
if headRef == "" {
ctx.NotFound(nil)
return nil
}
ctx.Data["BaseName"] = baseRepo.OwnerName
ctx.Data["BaseBranch"] = baseRef.ShortName() // for legacy templates
ctx.Data["HeadUser"] = headOwner
ctx.Data["HeadBranch"] = headRef.ShortName() // for legacy templates
ctx.Data["IsPull"] = true
// Now we have the repository that represents the base
context.InitRepoPullRequestCtx(ctx, baseRepo, headRepo)
// The current base and head repositories and branches may not
// actually be the intended branches that the user wants to
@@ -367,31 +314,31 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
}
has := ci.HeadRepo != nil
has := headRepo != nil
// 3. If the base is a forked from "RootRepo" and the owner of
// the "RootRepo" is the :headUser - set headRepo to that
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
ci.HeadRepo = rootRepo
if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
headRepo = rootRepo
has = true
}
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
// set the headRepo to the ownFork
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
ci.HeadRepo = ownForkRepo
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
headRepo = ownForkRepo
has = true
}
// 5. If the headOwner has a fork of the baseRepo - use that
if !has {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
has = ci.HeadRepo != nil
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
has = headRepo != nil
}
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
if !has && baseRepo.IsFork {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
has = ci.HeadRepo != nil
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
has = headRepo != nil
}
// 7. Otherwise if we're not the same repo and haven't found a repo give up
@@ -399,70 +346,15 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ctx.Data["PageIsComparePull"] = false
}
// 8. Finally open the git repo
if isSameRepo {
ci.HeadRepo = ctx.Repo.Repository
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has {
ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
if err != nil {
ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
return nil
}
} else {
ctx.NotFound(nil)
return nil
}
ctx.Data["HeadRepo"] = ci.HeadRepo
ctx.Data["HeadRepo"] = headRepo
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
// Now we need to assert that the ctx.Doer has permission to read
// the baseRepo's code and pulls
// (NOT headRepo's)
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if !permBase.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.Doer,
baseRepo,
permBase)
}
ctx.NotFound(nil)
return nil
}
// If we're not merging from the same repo:
if !isSameRepo {
// Assert ctx.Doer has permission to read headRepo's codes
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
ci.HeadRepo,
permHead)
}
ctx.NotFound(nil)
return nil
}
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
}
// If we have a rootRepo and it's different from:
// 1. the computed base
// 2. the computed head
// then get the branches of it
if rootRepo != nil &&
rootRepo.ID != ci.HeadRepo.ID &&
rootRepo.ID != headRepo.ID &&
rootRepo.ID != baseRepo.ID {
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
if canRead {
@@ -486,7 +378,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// 3. The rootRepo (if we have one)
// then get the branches from it.
if ownForkRepo != nil &&
ownForkRepo.ID != ci.HeadRepo.ID &&
ownForkRepo.ID != headRepo.ID &&
ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
@@ -504,28 +396,9 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
}
// Check if head branch is valid.
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
headIsBranch := gitrepo.IsBranchExist(ctx, ci.HeadRepo, ci.HeadBranch)
headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
ci.HeadBranch = headCommit.ID.String()
ctx.Data["HeadBranch"] = ci.HeadBranch
headIsCommit = true
} else {
ctx.NotFound(nil)
return nil
}
}
ctx.Data["HeadIsCommit"] = headIsCommit
ctx.Data["HeadIsBranch"] = headIsBranch
ctx.Data["HeadIsTag"] = headIsTag
// Treat as pull request if both references are branches
if ctx.Data["PageIsComparePull"] == nil {
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch && permBase.CanReadIssuesOrPulls(true)
ctx.Data["PageIsComparePull"] = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
}
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
@@ -539,41 +412,94 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
return nil
}
baseBranchRef := ci.BaseBranch
if baseIsBranch {
baseBranchRef = git.BranchPrefix + ci.BaseBranch
} else if baseIsTag {
baseBranchRef = git.TagPrefix + ci.BaseBranch
}
headBranchRef := ci.HeadBranch
if headIsBranch {
headBranchRef = git.BranchPrefix + ci.HeadBranch
} else if headIsTag {
headBranchRef = git.TagPrefix + ci.HeadBranch
}
ci.CompareInfo, err = pull_service.GetCompareInfo(ctx, baseRepo, ci.HeadRepo, ci.HeadGitRepo, baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Data["IsNoMergeBase"] = true
return compareInfo
}
ctx.ServerError("GetCompareInfo", err)
return nil
}
if ci.DirectComparison {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID
if compareReq.DirectComparison() {
ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
} else {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
}
return ci
return compareInfo
}
// autoTitleFromBranchName humanizes a branch name into a PR title.
func autoTitleFromBranchName(name string) string {
var buf strings.Builder
var prevIsSpace bool
runes := []rune(name)
for i, r := range runes {
isSpace := unicode.IsSpace(r)
if r == '-' || r == '_' || isSpace {
if !prevIsSpace {
buf.WriteRune(' ')
}
prevIsSpace = true
continue
}
if !prevIsSpace && unicode.IsUpper(r) {
needSpace := i > 0 && unicode.IsLower(runes[i-1]) || i < len(runes)-1 && unicode.IsLower(runes[i+1])
if needSpace {
buf.WriteRune(' ')
}
}
buf.WriteRune(unicode.ToLower(r))
prevIsSpace = isSpace
}
out := strings.TrimSpace(buf.String())
if out == "" {
return out
}
outRunes := []rune(out)
outRunes[0] = unicode.ToUpper(outRunes[0])
return string(outRunes)
}
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses, defaultTitleSource string) (title, content string) {
useFirstCommitAsTitle := len(commits) == 1 || (defaultTitleSource == setting.RepoPRTitleSourceFirstCommit && len(commits) > 0)
if useFirstCommitAsTitle {
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
c := commits[len(commits)-1]
title = strings.TrimSpace(c.UserCommit.Summary())
} else {
title = autoTitleFromBranchName(ci.HeadRef.ShortName())
}
if len(commits) == 1 {
c := commits[0]
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
content = strings.TrimSpace(content)
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
}
var titleTrailer string
// TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior
title, titleTrailer = util.EllipsisDisplayStringX(title, 255)
if titleTrailer != "" {
if content != "" {
content = titleTrailer + "\n\n" + content
} else {
content = titleTrailer + "\n"
}
}
return title, content
}
// PrepareCompareDiff renders compare diff page
func PrepareCompareDiff(
ctx *context.Context,
ci *common.CompareInfo,
ci *git_service.CompareInfo,
whitespaceBehavior gitcmd.TrustedCmdArgs,
) (nothingToCompare bool) {
repo := ctx.Repo.Repository
headCommitID := ci.CompareInfo.HeadCommitID
headCommitID := ci.HeadCommitID
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
ctx.Data["AfterCommitID"] = headCommitID
@@ -585,17 +511,15 @@ func PrepareCompareDiff(
ctx.Data["TitleQuery"] = newPrFormTitle
ctx.Data["BodyQuery"] = newPrFormBody
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID {
if (headCommitID == ci.MergeBase && !ci.DirectComparison()) ||
headCommitID == ci.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
ctx.Data["AllowEmptyPr"] = allowEmptyPr
return !allowEmptyPr
ctx.Data["AllowEmptyPr"] = !ci.IsSameRef()
return ci.IsSameRef()
}
ctx.Data["AllowEmptyPr"] = false
@@ -603,9 +527,9 @@ func PrepareCompareDiff(
return true
}
beforeCommitID := ci.CompareInfo.MergeBase
if ci.DirectComparison {
beforeCommitID = ci.CompareInfo.BaseCommitID
beforeCommitID := ci.MergeBase
if ci.DirectComparison() {
beforeCommitID = ci.BaseCommitID
}
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
@@ -625,19 +549,24 @@ func PrepareCompareDiff(
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: whitespaceBehavior,
DirectComparison: ci.DirectComparison,
DirectComparison: ci.DirectComparison(),
}, ctx.FormStrings("files")...)
if err != nil {
ctx.ServerError("GetDiff", err)
return false
}
diffShortStat, err := gitdiff.GetDiffShortStat(ci.HeadGitRepo, beforeCommitID, headCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ci.HeadRepo, ci.HeadGitRepo, beforeCommitID, headCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return false
}
ctx.Data["DiffShortStat"] = diffShortStat
ctx.Data["Diff"] = diff
ctx.Data["DiffBlobExcerptData"] = &gitdiff.DiffBlobExcerptData{
BaseLink: ci.HeadRepo.Link() + "/blob_excerpt",
DiffStyle: GetDiffViewStyle(ctx),
AfterCommitID: headCommitID,
}
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
if !fileOnly {
@@ -668,7 +597,7 @@ func PrepareCompareDiff(
return false
}
commits, err := processGitCommits(ctx, ci.CompareInfo.Commits)
commits, err := processGitCommits(ctx, ci.Commits)
if err != nil {
ctx.ServerError("processGitCommits", err)
return false
@@ -676,34 +605,11 @@ 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())
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
}
if len(title) > 255 {
var trailer string
title, trailer = util.EllipsisDisplayStringX(title, 255)
if len(trailer) > 0 {
if ctx.Data["content"] != nil {
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
} else {
ctx.Data["content"] = trailer + "\n"
}
}
}
ctx.Data["title"] = title
ctx.Data["Username"] = ci.HeadUser.Name
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits, setting.Repository.PullRequest.DefaultTitleSource)
ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name)
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
return false
}
@@ -733,17 +639,20 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["DirectComparison"] = ci.DirectComparison
ctx.Data["OtherCompareSeparator"] = ".."
ctx.Data["CompareSeparator"] = "..."
if ci.DirectComparison {
ctx.Data["CompareSeparator"] = ".."
ctx.Data["OtherCompareSeparator"] = "..."
}
ctx.Data["CompareInfo"] = ci
nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() {
return
var nothingToCompare bool
noMergeBase := ctx.Data["IsNoMergeBase"] == true
if noMergeBase {
ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true)
ctx.Data["PageIsComparePull"] = false
ctx.Data["CommitCount"] = 0
nothingToCompare = true
} else {
nothingToCompare = PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() {
return
}
}
baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
@@ -759,16 +668,13 @@ func CompareDiff(ctx *context.Context) {
return
}
headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
RepoID: ci.HeadRepo.ID,
ListOptions: db.ListOptionsAll,
IsDeletedBranch: optional.Some(false),
})
headBranches, headTags, err := getBranchesAndTagsForRepo(ctx, ci.HeadRepo)
if err != nil {
ctx.ServerError("GetBranches", err)
ctx.ServerError("GetBranchesAndTagsForRepo", err)
return
}
ctx.Data["HeadBranches"] = headBranches
ctx.Data["HeadTags"] = headTags
// For compare repo branches
PrepareBranchList(ctx)
@@ -776,15 +682,13 @@ func CompareDiff(ctx *context.Context) {
return
}
headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
if noMergeBase {
ctx.HTML(http.StatusOK, tplCompare)
return
}
ctx.Data["HeadTags"] = headTags
if ctx.Data["PageIsComparePull"] == true {
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
@@ -816,11 +720,7 @@ func CompareDiff(ctx *context.Context) {
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
afterCommitID := ctx.Data["AfterCommitID"].(string)
separator := "..."
if ci.DirectComparison {
separator = ".."
}
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + ci.CompareSeparator + base.ShortSha(afterCommitID)
ctx.Data["IsDiffCompare"] = true
@@ -865,125 +765,118 @@ func CompareDiff(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplCompare)
}
// attachCommentsToLines attaches comments to their corresponding diff lines
func attachCommentsToLines(section *gitdiff.DiffSection, lineComments map[int64][]*issues_model.Comment) {
for _, line := range section.Lines {
if comments, ok := lineComments[int64(line.LeftIdx*-1)]; ok {
line.Comments = append(line.Comments, comments...)
}
if comments, ok := lineComments[int64(line.RightIdx)]; ok {
line.Comments = append(line.Comments, comments...)
}
sort.SliceStable(line.Comments, func(i, j int) bool {
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
})
}
}
// attachHiddenCommentIDs calculates and attaches hidden comment IDs to expand buttons
func attachHiddenCommentIDs(section *gitdiff.DiffSection, lineComments map[int64][]*issues_model.Comment) {
for _, line := range section.Lines {
gitdiff.FillHiddenCommentIDsForDiffLine(line, lineComments)
}
}
// ExcerptBlob render blob excerpt contents
func ExcerptBlob(ctx *context.Context) {
commitID := ctx.PathParam("sha")
lastLeft := ctx.FormInt("last_left")
lastRight := ctx.FormInt("last_right")
idxLeft := ctx.FormInt("left")
idxRight := ctx.FormInt("right")
leftHunkSize := ctx.FormInt("left_hunk_size")
rightHunkSize := ctx.FormInt("right_hunk_size")
anchor := ctx.FormString("anchor")
direction := ctx.FormString("direction")
opts := gitdiff.BlobExcerptOptions{
LastLeft: ctx.FormInt("last_left"),
LastRight: ctx.FormInt("last_right"),
LeftIndex: ctx.FormInt("left"),
RightIndex: ctx.FormInt("right"),
LeftHunkSize: ctx.FormInt("left_hunk_size"),
RightHunkSize: ctx.FormInt("right_hunk_size"),
Direction: ctx.FormString("direction"),
Language: ctx.FormString("filelang"),
}
filePath := ctx.FormString("path")
gitRepo := ctx.Repo.GitRepo
diffBlobExcerptData := &gitdiff.DiffBlobExcerptData{
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
DiffStyle: GetDiffViewStyle(ctx),
AfterCommitID: commitID,
}
if ctx.Data["PageIsWiki"] == true {
var err error
gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
gitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
}
defer gitRepo.Close()
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
}
chunkSize := gitdiff.BlobExcerptChunkSize
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "GetCommit")
ctx.ServerError("GetCommit", err)
return
}
section := &gitdiff.DiffSection{
FileName: filePath,
}
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
idxLeft -= chunkSize
idxRight -= chunkSize
leftHunkSize += chunkSize
rightHunkSize += chunkSize
section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
lastLeft += chunkSize
lastRight += chunkSize
} else {
offset := -1
if direction == "down" {
offset = 0
}
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset)
leftHunkSize = 0
rightHunkSize = 0
idxLeft = lastLeft
idxRight = lastRight
}
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines")
return
}
if idxRight > lastRight {
lineText := " "
if rightHunkSize > 0 || leftHunkSize > 0 {
lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
}
lineText = html.EscapeString(lineText)
lineSection := &gitdiff.DiffLine{
Type: gitdiff.DiffLineSection,
Content: lineText,
SectionInfo: &gitdiff.DiffLineSectionInfo{
Path: filePath,
LastLeftIdx: lastLeft,
LastRightIdx: lastRight,
LeftIdx: idxLeft,
RightIdx: idxRight,
LeftHunkSize: leftHunkSize,
RightHunkSize: rightHunkSize,
},
}
switch direction {
case "up":
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
case "down":
section.Lines = append(section.Lines, lineSection)
}
}
ctx.Data["section"] = section
ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
ctx.Data["AfterCommitID"] = commitID
ctx.Data["Anchor"] = anchor
ctx.HTML(http.StatusOK, tplBlobExcerpt)
}
func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
blob, err := commit.Tree.GetBlobByPath(filePath)
if err != nil {
return nil, err
ctx.ServerError("GetBlobByPath", err)
return
}
reader, err := blob.DataAsync()
if err != nil {
return nil, err
ctx.ServerError("DataAsync", err)
return
}
defer reader.Close()
scanner := bufio.NewScanner(reader)
var diffLines []*gitdiff.DiffLine
for line := 0; line < idxRight+chunkSize; line++ {
if ok := scanner.Scan(); !ok {
break
}
if line < idxRight {
continue
}
lineText := scanner.Text()
diffLine := &gitdiff.DiffLine{
LeftIdx: idxLeft + (line - idxRight) + 1,
RightIdx: line + 1,
Type: gitdiff.DiffLinePlain,
Content: " " + lineText,
}
diffLines = append(diffLines, diffLine)
section, err := gitdiff.BuildBlobExcerptDiffSection(filePath, reader, opts)
if err != nil {
ctx.ServerError("BuildBlobExcerptDiffSection", err)
return
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("getExcerptLines scan: %w", err)
diffBlobExcerptData.PullIssueIndex = ctx.FormInt64("pull_issue_index")
if diffBlobExcerptData.PullIssueIndex > 0 {
if !ctx.Repo.CanRead(unit.TypePullRequests) {
ctx.NotFound(nil)
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, diffBlobExcerptData.PullIssueIndex)
if err != nil {
log.Error("GetIssueByIndex error: %v", err)
} else if issue.IsPull {
// FIXME: DIFF-CONVERSATION-DATA: the following data assignment is fragile
ctx.Data["Issue"] = issue
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
// and "diff/comment_form.tmpl" (reply comment) needs them
ctx.Data["PageIsPullFiles"] = true
ctx.Data["AfterCommitID"] = diffBlobExcerptData.AfterCommitID
allComments, err := issues_model.FetchCodeComments(ctx, issue, ctx.Doer, ctx.FormBool("show_outdated"))
if err != nil {
log.Error("FetchCodeComments error: %v", err)
} else {
if lineComments, ok := allComments[filePath]; ok {
attachCommentsToLines(section, lineComments)
attachHiddenCommentIDs(section, lineComments)
}
}
}
}
return diffLines, nil
ctx.Data["section"] = section
ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
ctx.HTML(http.StatusOK, tplBlobExcerpt)
}
@@ -0,0 +1,127 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"strings"
"testing"
"unicode/utf8"
asymkey_model "code.gitea.io/gitea/models/asymkey"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
"github.com/stretchr/testify/assert"
)
func TestAttachCommentsToLines(t *testing.T) {
section := &gitdiff.DiffSection{
Lines: []*gitdiff.DiffLine{
{LeftIdx: 5, RightIdx: 10},
{LeftIdx: 6, RightIdx: 11},
},
}
lineComments := map[int64][]*issues_model.Comment{
-5: {{ID: 100, CreatedUnix: 1000}}, // left side comment
10: {{ID: 200, CreatedUnix: 2000}}, // right side comment
11: {{ID: 300, CreatedUnix: 1500}, {ID: 301, CreatedUnix: 2500}}, // multiple comments
}
attachCommentsToLines(section, lineComments)
// First line should have left and right comments
assert.Len(t, section.Lines[0].Comments, 2)
assert.Equal(t, int64(100), section.Lines[0].Comments[0].ID)
assert.Equal(t, int64(200), section.Lines[0].Comments[1].ID)
// Second line should have two comments, sorted by creation time
assert.Len(t, section.Lines[1].Comments, 2)
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
}
func TestNewPullRequestTitleContent(t *testing.T) {
ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"}
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
return &git_model.SignCommitWithStatuses{
SignCommit: &asymkey_model.SignCommit{
UserCommit: &user_model.UserCommit{
Commit: &git.Commit{
CommitMessage: msg,
},
},
},
}
}
// no commit
title, content := prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "Head branch", title)
assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "Head branch", title)
assert.Empty(t, content)
// single commit
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "single-commit-title", title)
assert.Equal(t, "body", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "single-commit-title", title)
assert.Equal(t, "body", content)
// multiple commits
commits := []*git_model.SignCommitWithStatuses{
// ordered from newest to oldest
mockCommit("title2\nbody2"),
mockCommit("title1\nbody1"),
}
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "Head branch", title)
assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "title1", title)
assert.Empty(t, content)
// title string handling
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
assert.Equal(t, "…aaaaaaaaa\n", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
}
func TestAutoTitleFromBranchName(t *testing.T) {
cases := []struct {
branch string
want string
}{
{"fix/the-bug", "Fix/the bug"},
{"Already-Capitalized", "Already capitalized"},
{"ALL-CAPS-BRANCH", "All caps branch"},
{"FixHTMLBug", "Fix html bug"},
{"MixedCase-Name", "Mixed case name"},
{"fooBar-baz", "Foo bar baz"},
{"foo/BAR", "Foo/bar"},
{"_leading-underscore", "Leading underscore"},
{"CamelCase", "Camel case"},
{"foo--double-dash", "Foo double dash"},
{"123-fix", "123 fix"},
}
for _, c := range cases {
assert.Equal(t, c.want, autoTitleFromBranchName(c.branch), "branch: %q", c.branch)
}
}
@@ -7,76 +7,61 @@ package repo
import (
"time"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
)
func checkDownloadTokenScope(ctx *context.Context) bool {
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
return !ctx.Written()
}
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error {
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return nil
}
dataRc, err := blob.DataAsync()
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
if err != nil {
return err
}
closed := false
defer func() {
if closed {
return
}
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
}()
pointer, _ := lfs.ReadPointer(dataRc)
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
if pointer.IsValid() {
meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
if meta == nil {
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) {
return nil
}
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(), ctx.Req.Method, nil)
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return nil
}
}
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
return err
}
defer func() {
if err = lfsDataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
}()
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
defer lfsDataFile.Close()
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
return nil
}
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
}
@@ -109,6 +94,10 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
// SingleDownload download a file by repos path
func SingleDownload(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, lastModified := getBlobForEntry(ctx)
if blob == nil {
return
@@ -121,6 +110,10 @@ func SingleDownload(ctx *context.Context) {
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
func SingleDownloadOrLFS(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, lastModified := getBlobForEntry(ctx)
if blob == nil {
return
@@ -133,6 +126,10 @@ func SingleDownloadOrLFS(ctx *context.Context) {
// DownloadByID download a file by sha1 ID
func DownloadByID(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
@@ -149,6 +146,10 @@ func DownloadByID(ctx *context.Context) {
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
func DownloadByIDOrLFS(ctx *context.Context) {
if !checkDownloadTokenScope(ctx) {
return
}
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
+49 -22
View File
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/git"
"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/util"
@@ -41,7 +40,12 @@ const (
editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions {
prepareHomeTreeSideBarSwitch(ctx)
return prepareEditorPageFormOptions(ctx, editorAction)
}
func prepareEditorPageFormOptions(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))
@@ -73,8 +77,6 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *
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")
@@ -216,7 +218,8 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditor
}
// redirect to the newly updated file
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
redirectTo := ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(parsed.NewBranchName) + "/" + util.PathEscapeSegments(treePath)
redirectTo = strings.TrimSuffix(redirectTo, "/")
ctx.JSONRedirect(redirectTo)
}
@@ -283,7 +286,7 @@ func EditFile(ctx *context.Context) {
// 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))
prepareEditorCommitFormOptions(ctx, editorAction)
prepareEditorPage(ctx, editorAction)
if ctx.Written() {
return
}
@@ -312,17 +315,16 @@ func EditFile(ctx *context.Context) {
ctx.ServerError("ReadAll", err)
return
}
var fileContent string
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
fileContent = string(buf)
} else {
fileContent = string(content)
}
ctx.Data["FileContent"] = fileContent
ctx.Data["FileContent"] = string(charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true, ErrorReturnOrigin: true}))
}
}
ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
editorConfig := getCodeEditorConfigByEditorconfig(ctx, ctx.Repo.TreePath)
editorConfig.Autofocus = !isNewFile
if isNewFile {
editorConfig.Filename = ""
}
ctx.Data["CodeEditorConfig"] = editorConfig
ctx.HTML(http.StatusOK, tplEditFile)
}
@@ -378,15 +380,16 @@ func EditFilePost(ctx *context.Context) {
// DeleteFile render delete file page
func DeleteFile(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_delete")
prepareEditorPage(ctx, "_delete")
if ctx.Written() {
return
}
ctx.Data["PageIsDelete"] = true
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
ctx.HTML(http.StatusOK, tplDeleteFile)
}
// DeleteFilePost response for deleting file
// DeleteFilePost response for deleting file or directory
func DeleteFilePost(ctx *context.Context) {
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
if ctx.Written() {
@@ -394,17 +397,37 @@ func DeleteFilePost(ctx *context.Context) {
}
treePath := ctx.Repo.TreePath
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
if treePath == "" {
ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious
return
}
// Check if the path is a directory
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil {
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
var commitMessage string
if entry.IsDir() {
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
} else {
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", 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,
Operation: "delete",
TreePath: treePath,
DeleteRecursively: true,
},
},
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
Message: commitMessage,
Signoff: parsed.form.Signoff,
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
@@ -414,7 +437,11 @@ func DeleteFilePost(ctx *context.Context) {
return
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
if entry.IsDir() {
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
} else {
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
}
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
redirectForCommitChoice(ctx, parsed, redirectTreePath)
}
@@ -422,7 +449,7 @@ func DeleteFilePost(ctx *context.Context) {
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
opts := prepareEditorCommitFormOptions(ctx, "_upload")
opts := prepareEditorPage(ctx, "_upload")
if ctx.Written() {
return
}
@@ -14,12 +14,13 @@ import (
)
func NewDiffPatch(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_diffpatch")
prepareEditorPage(ctx, "_diffpatch")
if ctx.Written() {
return
}
ctx.Data["PageIsPatch"] = true
ctx.Data["CodeEditorConfig"] = CodeEditorConfig{Filename: "diff.patch"}
ctx.HTML(http.StatusOK, tplPatchFile)
}
@@ -9,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@@ -16,7 +17,7 @@ import (
)
func CherryPick(ctx *context.Context) {
prepareEditorCommitFormOptions(ctx, "_cherrypick")
prepareEditorPage(ctx, "_cherrypick")
if ctx.Written() {
return
}
@@ -64,9 +65,9 @@ func CherryPickPost(ctx *context.Context) {
// Drop through to the "apply" method
buf := &bytes.Buffer{}
if parsed.form.Revert {
err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
err = gitrepo.GetReverseRawDiff(ctx, ctx.Repo.Repository, fromCommitID, buf)
} else {
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, git.RawDiffPatch, buf)
}
if err == nil {
opts.Content = buf.String()
@@ -6,12 +6,13 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
)
func DiffPreviewPost(ctx *context.Context) {
content := ctx.FormString("content")
newContent := ctx.FormString("content")
treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
if treePath == "" {
ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
@@ -27,7 +28,12 @@ func DiffPreviewPost(ctx *context.Context) {
return
}
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
oldContent, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
ctx.ServerError("GetBlobContent", err)
return
}
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, oldContent, newContent)
if err != nil {
ctx.ServerError("GetDiffPreview", err)
return
@@ -7,15 +7,19 @@ import (
"context"
"fmt"
"path"
"strconv"
"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/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
context_service "code.gitea.io/gitea/services/context"
)
@@ -61,17 +65,39 @@ func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePa
return f(originTreePath, commit)
}
// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
// CodeEditorConfig is also used by frontend, defined in "codeeditor" module
type CodeEditorConfig struct {
Filename string `json:"filename"` // the base name, not full path
Autofocus bool `json:"autofocus"`
PreviewableExtensions []string `json:"previewableExtensions,omitempty"`
LineWrapExtensions []string `json:"lineWrapExtensions,omitempty"`
LineWrap bool `json:"lineWrap"`
Previewable bool `json:"previewable,omitempty"`
// the following can be read from .editorconfig if exists, or use default value
IndentStyle string `json:"indentStyle"` // in most cases, keep it empty by default, detected by the source code
IndentSize int `json:"indentSize"`
TabWidth int `json:"tabWidth"`
TrimTrailingWhitespace *bool `json:"trimTrailingWhitespace,omitempty"`
}
func getCodeEditorConfigByEditorconfig(ctx *context_service.Context, treePath string) CodeEditorConfig {
ret := CodeEditorConfig{Filename: path.Base(treePath)}
ret.PreviewableExtensions = markup.PreviewableExtensions()
ret.LineWrapExtensions = setting.Repository.Editor.LineWrapExtensions
ret.LineWrap = util.SliceContainsString(ret.LineWrapExtensions, path.Ext(treePath), true)
ret.Previewable = util.SliceContainsString(ret.PreviewableExtensions, path.Ext(treePath), true)
ec, _, err := ctx.Repo.GetEditorconfig()
if err == nil {
def, err := ec.GetDefinitionForFilename(treePath)
if err == nil {
jsonStr, _ := json.Marshal(def)
return string(jsonStr)
ret.IndentStyle = util.IfZero(def.IndentStyle, ret.IndentStyle)
ret.IndentSize, _ = strconv.Atoi(def.IndentSize)
ret.TabWidth = def.TabWidth
ret.TrimTrailingWhitespace = def.TrimTrailingWhitespace
}
}
return "null"
return ret
}
// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
@@ -102,8 +128,7 @@ func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) st
}
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(),
return gitrepo.Push(ctx, baseRepo, targetRepo, git.PushOptions{
Branch: baseBranchName + ":" + targetBranchName,
Env: repo_module.PushingEnvironment(doer, targetRepo),
})
@@ -1,24 +0,0 @@
// Copyright 2022 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/modules/util"
"code.gitea.io/gitea/services/context"
)
const (
tplFindFiles templates.TplName = "repo/find/files"
)
// FindFiles render the page to find repository files
func FindFiles(ctx *context.Context) {
path := ctx.PathParam("*")
ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
ctx.HTML(http.StatusOK, tplFindFiles)
}
+115 -152
View File
@@ -5,13 +5,11 @@
package repo
import (
"bytes"
"compress/gzip"
gocontext "context"
"fmt"
"net/http"
"os"
"path/filepath"
"path"
"regexp"
"slices"
"strconv"
@@ -19,7 +17,6 @@ import (
"sync"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@@ -27,6 +24,7 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -57,9 +55,9 @@ func CorsHandler() func(next http.Handler) http.Handler {
}
}
// httpBase implementation git smart HTTP protocol
// httpBase does the common work for git http services,
// including early response, authentication, repository lookup and permission check.
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
username := ctx.PathParam("username")
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if ctx.FormString("go-get") == "1" {
@@ -67,16 +65,23 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
var serviceType string
var isPull, receivePack bool
switch util.OptionalArg(optGitService) {
case "git-receive-pack":
serviceType = ServiceTypeReceivePack
receivePack = true
case "git-upload-pack":
serviceType = ServiceTypeUploadPack
isPull = true
case "git-upload-archive":
serviceType = ServiceTypeUploadArchive
isPull = true
default:
case "":
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
default: // unknown service
ctx.Resp.WriteHeader(http.StatusBadRequest)
return nil
}
var accessMode perm.AccessMode
@@ -124,10 +129,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string
)
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
// don't allow anonymous pulls if organization is not public
if isPublicPull {
@@ -160,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
@@ -177,56 +179,21 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
environ = []string{
repo_module.EnvRepoUsername + "=" + username,
repo_module.EnvRepoName + "=" + reponame,
repo_module.EnvPusherName + "=" + ctx.Doer.Name,
repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
repo_module.EnvAppURL + "=" + setting.AppURL,
}
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
if git.DefaultFeatures().SupportProcReceive {
// Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks.
if git.DefaultFeatures().SupportProcReceive && !isWiki {
accessMode = perm.AccessModeRead
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
ctx.ServerError("GetTaskByID", err)
return nil
}
if task.RepoID != repo.ID {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
p, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetDoerRepoPermission", err)
return nil
}
if task.IsForkPullRequest {
if accessMode > perm.AccessModeRead {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
} else {
if accessMode > perm.AccessModeWrite {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
}
} else {
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return nil
}
if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return nil
}
if !isPull && repo.IsMirror {
@@ -234,16 +201,6 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
}
if !ctx.Doer.KeepEmailPrivate {
environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
}
if isWiki {
environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
} else {
environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
}
}
if !repoExist {
@@ -287,17 +244,18 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
return nil
}
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
return nil
}
}
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
var environ []string
if !isPull {
// if not "pull", then must be "push", and doer must exist
environ = repo_module.DoerPushingEnvironment(ctx.Doer, repo, isWiki)
}
ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
return &serviceHandler{repo, isWiki, environ}
return &serviceHandler{serviceType, repo, isWiki, environ}
}
var (
@@ -319,7 +277,9 @@ func dummyInfoRefs(ctx *context.Context) {
return
}
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: tmpDir})
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").
WithDir(tmpDir).
RunStdBytes(ctx)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
@@ -338,16 +298,18 @@ func dummyInfoRefs(ctx *context.Context) {
}
type serviceHandler struct {
serviceType string
repo *repo_model.Repository
isWiki bool
environ []string
}
func (h *serviceHandler) getRepoDir() string {
func (h *serviceHandler) getStorageRepo() gitrepo.Repository {
if h.isWiki {
return h.repo.WikiPath()
return h.repo.WikiStorageRepo()
}
return h.repo.RepoPath()
return h.repo
}
func setHeaderNoCache(ctx *context.Context) {
@@ -358,7 +320,7 @@ func setHeaderNoCache(ctx *context.Context) {
func setHeaderCacheForever(ctx *context.Context) {
now := time.Now().Unix()
expires := now + 31536000
expires := now + 365*86400 // 365 days
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")
@@ -375,65 +337,58 @@ func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
if containsParentDirectorySeparator(file) {
log.Error("request file path contains invalid path: %v", file)
log.Debug("request file path contains invalid path: %v", file)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
reqFile := filepath.Join(h.getRepoDir(), file)
fi, err := os.Stat(reqFile)
if os.IsNotExist(err) {
ctx.Resp.WriteHeader(http.StatusNotFound)
return
}
fs := gitrepo.GetRepoFS(h.getStorageRepo())
ctx.Resp.Header().Set("Content-Type", contentType)
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)
http.ServeFileFS(ctx.Resp, ctx.Req, fs, path.Clean(file))
}
// one or more key=value pairs separated by colons
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
if service == "receive-pack" {
return gitcmd.NewCommand("receive-pack"), nil
func prepareGitCmdWithAllowedService(service string, allowedServices []string) *gitcmd.Command {
if !slices.Contains(allowedServices, service) {
return nil
}
if service == "upload-pack" {
return gitcmd.NewCommand("upload-pack"), nil
switch service {
case ServiceTypeReceivePack:
return gitcmd.NewCommand(ServiceTypeReceivePack)
case ServiceTypeUploadPack:
return gitcmd.NewCommand(ServiceTypeUploadPack)
case ServiceTypeUploadArchive:
return gitcmd.NewCommand(ServiceTypeUploadArchive)
default:
return nil
}
return nil, fmt.Errorf("service %q is not allowed", service)
}
func serviceRPC(ctx *context.Context, service string) {
defer func() {
if err := ctx.Req.Body.Close(); err != nil {
log.Error("serviceRPC: Close: %v", err)
}
}()
defer ctx.Req.Body.Close()
h := httpBase(ctx, "git-"+service)
if h == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
ctx.Resp.WriteHeader(http.StatusUnauthorized)
log.Debug("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
cmd, err := prepareGitCmdWithAllowedService(service)
if err != nil {
log.Error("Failed to prepareGitCmdWithService: %v", err)
ctx.Resp.WriteHeader(http.StatusUnauthorized)
cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack, ServiceTypeUploadArchive})
if cmd == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
// git upload-archive does not have a "--stateless-rpc" option
if service == ServiceTypeUploadPack || service == ServiceTypeReceivePack {
cmd.AddArguments("--stateless-rpc")
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
@@ -441,10 +396,10 @@ func serviceRPC(ctx *context.Context, service string) {
// Handle GZIP.
if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
var err error
reqBody, err = gzip.NewReader(reqBody)
if err != nil {
log.Error("Fail to create gzip reader: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
}
@@ -456,39 +411,35 @@ func serviceRPC(ctx *context.Context, service string) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
var stderr bytes.Buffer
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
if err := cmd.Run(ctx, &gitcmd.RunOpts{
Dir: h.getRepoDir(),
Env: append(os.Environ(), h.environ...),
Stdout: ctx.Resp,
Stdin: reqBody,
Stderr: &stderr,
UseContextTimeout: true,
}); err != nil {
if !git.IsErrCanceledOrKilled(err) {
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
if err := gitrepo.RunCmdWithStderr(ctx, h.getStorageRepo(), cmd.AddArguments(".").
WithEnv(append(os.Environ(), h.environ...)).
WithStdinCopy(reqBody).
WithStdoutCopy(ctx.Resp),
); err != nil {
if !gitcmd.IsErrorCanceledOrKilled(err) {
log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err)
}
return
}
}
const (
ServiceTypeUploadPack = "upload-pack"
ServiceTypeReceivePack = "receive-pack"
ServiceTypeUploadArchive = "upload-archive"
)
// ServiceUploadPack implements Git Smart HTTP protocol
func ServiceUploadPack(ctx *context.Context) {
serviceRPC(ctx, "upload-pack")
serviceRPC(ctx, ServiceTypeUploadPack)
}
// ServiceReceivePack implements Git Smart HTTP protocol
func ServiceReceivePack(ctx *context.Context) {
serviceRPC(ctx, "receive-pack")
serviceRPC(ctx, ServiceTypeReceivePack)
}
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
out, _, err := gitcmd.NewCommand("update-server-info").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: dir})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
}
return out
func ServiceUploadArchive(ctx *context.Context) {
serviceRPC(ctx, ServiceTypeUploadArchive)
}
func packetWrite(str string) []byte {
@@ -501,33 +452,45 @@ func packetWrite(str string) []byte {
// GetInfoRefs implements Git dumb HTTP
func GetInfoRefs(ctx *context.Context) {
service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
h := httpBase(ctx, "git-"+service)
h := httpBase(ctx, ctx.FormString("service")) // git http protocol: "?service=git-<service>"
if h == nil {
return
}
setHeaderNoCache(ctx)
cmd, err := prepareGitCmdWithAllowedService(service)
if err == nil {
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
if h.serviceType == "" {
// it's said that some legacy git clients will send requests to "/info/refs" without "service" parameter,
// although there should be no such case client in the modern days. TODO: not quite sure why we need this UpdateServerInfo logic
if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil {
ctx.ServerError("UpdateServerInfo", err)
return
}
h.environ = append(os.Environ(), h.environ...)
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
_, _ = ctx.Resp.Write([]byte("0000"))
_, _ = ctx.Resp.Write(refs)
} else {
updateServerInfo(ctx, h.getRepoDir())
h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
return
}
cmd := prepareGitCmdWithAllowedService(h.serviceType, []string{ServiceTypeUploadPack, ServiceTypeReceivePack})
if cmd == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
h.environ = append(os.Environ(), h.environ...)
cmd = cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").WithEnv(h.environ)
refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd)
if err != nil {
ctx.ServerError("RunGitServiceAdvertiseRefs", err)
return
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", h.serviceType))
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + h.serviceType + "\n"))
_, _ = ctx.Resp.Write([]byte("0000"))
_, _ = ctx.Resp.Write(refs)
}
// GetTextFile implements Git dumb HTTP
+9 -51
View File
@@ -14,13 +14,13 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional"
@@ -109,11 +109,6 @@ func MustAllowPulls(ctx *context.Context) {
ctx.NotFound(nil)
return
}
// User can send pull request if owns a forked repository.
if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
ctx.Repo.PullRequest.Allowed = true
}
}
func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
@@ -374,7 +369,7 @@ func UpdateIssueContent(ctx *context.Context) {
}
ctx.JSON(http.StatusOK, map[string]any{
"content": content,
"content": commentContentHTML(ctx, content),
"contentVersion": issue.ContentVersion,
"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
})
@@ -634,6 +629,13 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
return err
}
func commentContentHTML(ctx *context.Context, content template.HTML) template.HTML {
if strings.TrimSpace(string(content)) == "" {
return htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
}
return content
}
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
"ctxData": ctx.Data,
@@ -646,47 +648,3 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
}
return attachHTML
}
// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data
func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) {
// TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees"
// at the moment it is used on the issue list page, for the markdown editor mention
ctx.Data["Assignees"] = assignees
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
return
}
var isAdmin bool
var err error
var teams []*organization.Team
org := organization.OrgFromUser(ctx.Repo.Owner)
// Admin has super access.
if ctx.Doer.IsAdmin {
isAdmin = true
} else {
isAdmin, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("IsOwnedBy", err)
return
}
}
if isAdmin {
teams, err = org.LoadTeams(ctx)
if err != nil {
ctx.ServerError("LoadTeams", err)
return
}
} else {
teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
}
ctx.Data["MentionableTeams"] = teams
ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
}
@@ -9,19 +9,19 @@ import (
"html/template"
"net/http"
"strconv"
"strings"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
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/log"
"code.gitea.io/gitea/modules/markup/markdown"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -32,31 +32,22 @@ import (
// NewComment create a comment for issue
func NewComment(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateCommentForm)
issue := GetActionIssue(ctx)
if ctx.Written() {
if issue == nil {
return
}
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
issueType := "issues"
if issue.IsPull {
issueType = "pulls"
}
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Not logged in")
}
}
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}
form := web.GetForm(ctx).(*forms.CreateCommentForm)
issueType := util.Iif(issue.IsPull, "pulls", "issues")
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v", ctx.Doer, issue.PosterID, issueType, ctx.Repo.Repository, ctx.Repo.Permission)
ctx.HTTPError(http.StatusForbidden)
return
}
@@ -66,152 +57,134 @@ func NewComment(ctx *context.Context) {
return
}
var attachments []string
if setting.Attachment.Enabled {
attachments = form.Files
}
redirect := fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, issueType, issue.Index)
attachments := util.Iif(setting.Attachment.Enabled, form.Files, nil)
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}
var comment *issues_model.Comment
defer func() {
// Check if issue admin/poster changes the status of issue.
if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
(form.Status == "reopen" || form.Status == "close") &&
!(issue.IsPull && issue.PullRequest.HasMerged) {
// Duplication and conflict check should apply to reopen pull request.
var pr *issues_model.PullRequest
if form.Status == "reopen" && issue.IsPull {
pull := issue.PullRequest
var err error
pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
return
}
}
// Regenerate patch and test conflict.
if pr == nil {
issue.PullRequest.HeadCommitID = ""
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.GetGitHeadRefName()
if err := pull.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("Unable to load base repo", err)
return
}
prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
if err != nil {
ctx.ServerError("Get head commit Id of pr fail", err)
return
}
// get head commit of branch in the head repo
if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("Unable to load head repo", err)
return
}
if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok {
// todo localize
ctx.JSONError("The origin branch is delete, cannot reopen.")
return
}
headBranchRef := pull.GetGitHeadBranchRefName()
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
if err != nil {
ctx.ServerError("Get head commit Id of head branch fail", err)
return
}
err = pull.LoadIssue(ctx)
if err != nil {
ctx.ServerError("load the issue of pull request error", err)
return
}
if prHeadCommitID != headBranchCommitID {
// force push to base repo
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
Remote: pull.BaseRepo.RepoPath(),
Branch: pull.HeadBranch + ":" + prHeadRef,
Force: true,
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
})
if err != nil {
ctx.ServerError("force push error", err)
return
}
}
}
}
if pr != nil {
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
// allow empty content if there are attachments
if form.Content != "" || len(attachments) > 0 {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else {
if form.Status == "close" && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("CloseIssue: %v", err)
if issues_model.IsErrDependenciesLeft(err) {
if issue.IsPull {
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
} else {
ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
}
return
}
} else {
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
ctx.ServerError("stopTimerIfAvailable", err)
return
}
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
}
} else if form.Status == "reopen" && issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("ReopenIssue: %v", err)
ctx.ServerError("CreateIssueComment", err)
}
return
}
// redirect to the comment's hashtag
redirect += "#" + comment.HashTag()
} else if form.Status == "" {
// if no status change (close, reopen), it is a plain comment, and content is required
// "approve/reject" are handled differently in SubmitReview
ctx.JSONError(ctx.Tr("repo.issues.comment_no_content"))
return
}
// ATTENTION: From now on, do not use ctx.JSONError, don't return on user error, because the comment has been created.
// Always use ctx.Flash.Xxx and then redirect, then the message will be displayed
// TODO: need further refactoring to the code below
// Check if doer can change the status of issue (close, reopen).
if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
(form.Status == "reopen" || form.Status == "close") &&
!(issue.IsPull && issue.PullRequest.HasMerged) {
// Duplication and conflict check should apply to reopen pull request.
var branchOtherUnmergedPR *issues_model.PullRequest
var err error
if form.Status == "reopen" && issue.IsPull {
pull := issue.PullRequest
branchOtherUnmergedPR, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
}
}
if branchOtherUnmergedPR != nil {
ctx.Flash.Error(ctx.Tr("repo.pulls.open_unmerged_pull_exists", branchOtherUnmergedPR.Index))
} else {
// Regenerate patch and test conflict.
issue.PullRequest.HeadCommitID = ""
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 branchOtherUnmergedPR != nil && pull.Flow == issues_model.PullRequestFlowGithub {
prHeadRef := pull.GetGitHeadRefName()
if err := pull.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("Unable to load base repo", err)
return
}
prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef)
if err != nil {
ctx.ServerError("Get head commit Id of pr fail", err)
return
}
// get head commit of branch in the head repo
if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("Unable to load head repo", err)
return
}
if exist, _ := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.BaseBranch); !exist {
ctx.Flash.Error("The origin branch is delete, cannot reopen.")
return
}
headBranchRef := git.RefNameFromBranch(pull.HeadBranch)
headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String())
if err != nil {
ctx.ServerError("Get head commit Id of head branch fail", err)
return
}
err = pull.LoadIssue(ctx)
if err != nil {
ctx.ServerError("load the issue of pull request error", err)
return
}
if prHeadCommitID != headBranchCommitID {
// force push to base repo
err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{
Branch: pull.HeadBranch + ":" + prHeadRef,
Force: true,
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
})
if err != nil {
ctx.ServerError("force push error", err)
return
}
}
}
}
// Redirect to comment hashtag if there is any actual content.
typeName := "issues"
if issue.IsPull {
typeName = "pulls"
if form.Status == "close" && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("CloseIssue: %v", err)
if issues_model.IsErrDependenciesLeft(err) {
if issue.IsPull {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
} else {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
}
}
} else {
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
ctx.ServerError("stopTimerIfAvailable", err)
return
}
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
}
} else if form.Status == "reopen" && issue.IsClosed && branchOtherUnmergedPR == nil {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("ReopenIssue: %v", err)
ctx.Flash.Error("Unable to reopen.")
}
}
if comment != nil {
ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
} else {
ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
}
}()
} // end if: handle close or reopen
// Fix #321: Allow empty comments, as long as we have attachments.
if len(form.Content) == 0 && len(attachments) == 0 {
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
} else {
ctx.ServerError("CreateIssueComment", err)
}
return
}
log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
ctx.JSONRedirect(redirect)
}
// UpdateCommentContent change comment of issue's content
@@ -291,12 +264,8 @@ func UpdateCommentContent(ctx *context.Context) {
}
}
if strings.TrimSpace(string(renderedContent)) == "" {
renderedContent = htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
}
ctx.JSON(http.StatusOK, map[string]any{
"content": renderedContent,
"content": commentContentHTML(ctx, renderedContent),
"contentVersion": comment.ContentVersion,
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
})
@@ -6,13 +6,14 @@ package repo
import (
"bytes"
"html"
"html/template"
"net/http"
"strings"
"code.gitea.io/gitea/models/avatars"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@@ -53,29 +54,25 @@ func GetContentHistoryList(ctx *context.Context) {
// value is historyId
var results []map[string]any
for _, item := range items {
var actionText string
var actionHTML template.HTML
if item.IsDeleted {
actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
actionHTML = htmlutil.HTMLFormat(`<i data-history-is-deleted="1">%s</i>`, ctx.Locale.TrString("repo.issues.content_history.deleted"))
} else if item.IsFirstCreated {
actionText = ctx.Locale.TrString("repo.issues.content_history.created")
actionHTML = ctx.Locale.Tr("repo.issues.content_history.created")
} else {
actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
actionHTML = ctx.Locale.Tr("repo.issues.content_history.edited")
}
username := item.UserName
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
username = strings.TrimSpace(item.UserFullName)
var fullNameHTML template.HTML
userName, fullName := item.UserName, strings.TrimSpace(item.UserFullName)
if fullName != "" {
fullNameHTML = htmlutil.HTMLFormat(` (<span class="tw-inline-flex tw-max-w-[160px]"><span class="gt-ellipsis">%s</span></span>)`, fullName)
}
src := html.EscapeString(item.UserAvatarLink)
class := avatars.DefaultAvatarClass + " tw-mr-2"
name := html.EscapeString(username)
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
timeSinceHTML := string(templates.TimeSince(item.EditedUnix))
avatarHTML := templates.AvatarHTML(item.UserAvatarLink, 24, avatars.DefaultAvatarClass+" tw-mr-2", userName)
timeSinceHTML := templates.TimeSince(item.EditedUnix)
results = append(results, map[string]any{
"name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceHTML,
"name": htmlutil.HTMLFormat("%s <strong>%s</strong>%s %s %s", avatarHTML, userName, fullNameHTML, actionHTML, timeSinceHTML),
"value": item.HistoryID,
})
}
@@ -55,9 +55,9 @@ func AddDependency(ctx *context.Context) {
return
}
// Can ctx.Doer read issues in the dep repo?
depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer)
depRepoPerm, err := access_model.GetDoerRepoPermission(ctx, dep.Repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) {
@@ -119,7 +119,7 @@ func RemoveDependency(ctx *context.Context) {
case "blocking":
depType = issues_model.DependencyTypeBlocking
default:
ctx.HTTPError(http.StatusBadRequest, "GetDependecyType")
ctx.HTTPError(http.StatusBadRequest, "GetDependencyType")
return
}
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/shared/issue"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
@@ -45,15 +46,7 @@ func SearchIssues(ctx *context.Context) {
return
}
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = optional.Some(true)
case "all":
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
var (
repoIDs []int64
@@ -268,15 +261,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
return
}
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = optional.Some(true)
case "all":
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
@@ -477,14 +462,7 @@ func renderMilestones(ctx *context.Context) {
return
}
openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
for _, milestone := range milestones {
if milestone.IsClosed {
closedMilestones = append(closedMilestones, milestone)
} else {
openMilestones = append(openMilestones, milestone)
}
}
openMilestones, closedMilestones := issues_model.MilestoneList(milestones).SplitByOpenClosed()
ctx.Data["OpenMilestones"] = openMilestones
ctx.Data["ClosedMilestones"] = closedMilestones
}
@@ -580,17 +558,10 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
}
}
var isShowClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isShowClosed = optional.Some(true)
case "all":
isShowClosed = optional.None[bool]()
default:
isShowClosed = optional.Some(false)
}
isShowClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
// if there are closed issues and no open issues, default to showing all issues
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
if ctx.FormString("state") == "" && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
isShowClosed = optional.None[bool]()
}
@@ -604,9 +575,9 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
}
// prepare pager
total := int(issueStats.OpenCount + issueStats.ClosedCount)
total := issueStats.OpenCount + issueStats.ClosedCount
if isShowClosed.Has() {
total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
total = util.Iif(isShowClosed.Value(), issueStats.ClosedCount, issueStats.OpenCount)
}
page := max(ctx.FormInt("page"), 1)
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
@@ -691,10 +662,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
ctx.ServerError("GetRepoAssignees", err)
return
}
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
if ctx.Written() {
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
@@ -121,11 +121,9 @@ func NewIssue(ctx *context.Context) {
}
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
if pageMetaData.ProjectsData.SelectedProjectID > 0 {
if len(ctx.Req.URL.Query().Get("project")) > 0 {
ctx.Data["redirect_after_creation"] = "project"
}
pageMetaData.ProjectsData.SelectedProjectIDs, _ = base.StringsToInt64s(strings.Split(ctx.FormString("project"), ","))
if len(pageMetaData.ProjectsData.SelectedProjectIDs) == 1 {
ctx.Data["redirect_after_creation"] = "project"
}
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
@@ -273,7 +271,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
ctx.NotFound(nil)
return ret
}
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
pageMetaData.ProjectsData.SelectedProjectIDs = util.Iif(form.ProjectID > 0, []int64{form.ProjectID}, nil)
// prepare assignees
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
@@ -34,9 +34,14 @@ type issueSidebarAssigneesData struct {
}
type issueSidebarProjectsData struct {
SelectedProjectID int64
OpenProjects []*project_model.Project
ClosedProjects []*project_model.Project
SelectedProjectIDs []int64 // TODO: support multiple projects in the future
// the "selected" fields are only valid when len(SelectedProjectIDs)==1
SelectedProjectColumns []*project_model.Column
SelectedProjectColumn *project_model.Column
OpenProjects []*project_model.Project
ClosedProjects []*project_model.Project
}
type IssuePageMetaData struct {
@@ -92,6 +97,11 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
return data
}
data.retrieveProjectData(ctx)
if ctx.Written() {
return data
}
// TODO: the issue/pull permissions are quite complex and unclear
// A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch)
// A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
@@ -155,13 +165,36 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
}
d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
}
// FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees)
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
}
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
if d.Issue == nil || d.Issue.Project == nil {
return
}
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
columns, err := d.Issue.Project.GetColumns(ctx)
if err != nil {
ctx.ServerError("GetProjectColumns", err)
return
}
d.ProjectsData.SelectedProjectColumns = columns
columnID, err := d.Issue.ProjectColumnID(ctx)
if err != nil {
ctx.ServerError("ProjectColumnID", err)
return
}
for _, col := range columns {
if col.ID == columnID {
d.ProjectsData.SelectedProjectColumn = col
break
}
}
}
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
if d.Issue != nil && d.Issue.Project != nil {
d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
}
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
}
@@ -10,7 +10,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
)
@@ -34,7 +33,7 @@ func IssuePullPosters(ctx *context.Context) {
func issuePosters(ctx *context.Context, isPullList bool) {
repo := ctx.Repo.Repository
search := strings.TrimSpace(ctx.FormString("q"))
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search)
if err != nil {
ctx.JSON(http.StatusInternalServerError, err)
return
@@ -54,9 +53,7 @@ func issuePosters(ctx *context.Context, isPullList bool) {
resp.Results = make([]*userSearchInfo, len(posters))
for i, user := range posters {
resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
if setting.UI.DefaultShowFullName {
resp.Results[i].FullName = user.FullName
}
resp.Results[i].FullName = user.FullName
}
ctx.JSON(http.StatusOK, resp)
}
@@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) {
return
}
t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid"))
t, err := issues_model.GetTrackedTimeByID(c, issue.ID, c.PathParamInt64("timeid"))
if err != nil {
if db.IsErrNotExist(err) {
c.NotFound(err)
@@ -7,7 +7,6 @@ import (
"fmt"
"math/big"
"net/http"
"net/url"
"sort"
"strconv"
@@ -26,7 +25,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -34,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
@@ -58,7 +57,7 @@ func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *u
// Guess the role of the poster in the repo by permission
perm, hasPermCache := permsCache[poster.ID]
if !hasPermCache {
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
perm, err = access_model.GetIndividualUserRepoPermission(ctx, repo, poster)
if err != nil {
return roleDesc, err
}
@@ -146,9 +145,9 @@ func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.Depende
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return nil, nil
}
repoPerms[blocker.RepoID] = perm
@@ -193,7 +192,7 @@ func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
if err != nil {
return err
}
perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, ctx.Doer)
if err != nil {
return err
}
@@ -409,7 +408,7 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["Reference"] = issue.Ref
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
ctx.Data["SignInLink"] = middleware.RedirectLinkUserLogin(ctx.Req)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
@@ -437,6 +436,9 @@ func ViewIssue(ctx *context.Context) {
func ViewPullMergeBox(ctx *context.Context) {
issue := prepareIssueViewLoad(ctx)
if ctx.Written() {
return
}
if !issue.IsPull {
ctx.NotFound(nil)
return
@@ -467,7 +469,7 @@ func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model
ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
// Get Dependencies
blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
blockedBy, _, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
if err != nil {
ctx.ServerError("BlockedByDependencies", err)
return
@@ -493,7 +495,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.GetGitHeadRefName())
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo)
ctx.Data["WillSign"] = sign
ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
if err != nil {
@@ -563,8 +565,10 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue
pull := issue.PullRequest
isPullBranchDeletable := canDelete &&
pull.HeadRepo != nil &&
gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) &&
(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
if isPullBranchDeletable {
isPullBranchDeletable, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
}
if isPullBranchDeletable && pull.HasMerged {
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
@@ -841,9 +845,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
if err := pull.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
} else if pull.HeadRepo != nil {
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
if perm.CanWrite(unit.TypeCode) {
@@ -863,9 +867,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
}
perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
@@ -901,9 +905,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
// Check correct values and select default
if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
!prConfig.IsMergeStyleAllowed(ms) {
defaultMergeStyle := prConfig.GetDefaultMergeStyle()
if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
mergeStyle = defaultMergeStyle
if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) && !ok {
mergeStyle = prConfig.DefaultMergeStyle
} else if prConfig.AllowMerge {
mergeStyle = repo_model.MergeStyleMerge
} else if prConfig.AllowRebase {
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
shared_mention "code.gitea.io/gitea/routers/web/shared/mention"
"code.gitea.io/gitea/services/context"
)
// GetMentionsInRepo returns JSON data for mention autocomplete (assignees, participants, mentionable teams).
func GetMentionsInRepo(ctx *context.Context) {
c := shared_mention.NewCollector()
// Get participants if issue_index is provided
if issueIndex := ctx.FormInt64("issue_index"); issueIndex > 0 {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.ServerError("GetIssueByIndex", err)
return
}
if issue != nil {
userIDs, err := issue.GetParticipantIDsByIssue(ctx)
if err != nil {
ctx.ServerError("GetParticipantIDsByIssue", err)
return
}
users, err := user_model.GetUsersByIDs(ctx, userIDs)
if err != nil {
ctx.ServerError("GetUsersByIDs", err)
return
}
c.AddUsers(ctx, users)
}
}
// Get repo assignees
assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetRepoAssignees", err)
return
}
c.AddUsers(ctx, assignees)
// Get mentionable teams for org repos
if err := c.AddMentionableTeams(ctx, ctx.Doer, ctx.Repo.Owner); err != nil {
ctx.ServerError("AddMentionableTeams", err)
return
}
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(c.Result))
}
@@ -7,8 +7,11 @@ import (
"strconv"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
user_service "code.gitea.io/gitea/services/user"
)
@@ -28,36 +31,24 @@ func SetEditorconfigIfExists(ctx *context.Context) {
ctx.Data["Editorconfig"] = ec
}
func GetDiffViewStyle(ctx *context.Context) string {
return util.Iif(ctx.Data["IsSplitStyle"] == true, gitdiff.DiffStyleSplit, gitdiff.DiffStyleUnified)
}
// SetDiffViewStyle set diff style as render variable
func SetDiffViewStyle(ctx *context.Context) {
queryStyle := ctx.FormString("style")
if !ctx.IsSigned {
ctx.Data["IsSplitStyle"] = queryStyle == "split"
return
style := ctx.FormString("style")
if ctx.IsSigned {
style = util.IfZero(style, ctx.Doer.DiffViewStyle)
style = util.Iif(style == gitdiff.DiffStyleSplit, gitdiff.DiffStyleSplit, gitdiff.DiffStyleUnified)
if style != ctx.Doer.DiffViewStyle {
err := user_service.UpdateUser(ctx, ctx.Doer, &user_service.UpdateOptions{DiffViewStyle: optional.Some(style)})
if err != nil {
log.Error("UpdateUser DiffViewStyle: %v", err)
}
}
}
var (
userStyle = ctx.Doer.DiffViewStyle
style string
)
if queryStyle == "unified" || queryStyle == "split" {
style = queryStyle
} else if userStyle == "unified" || userStyle == "split" {
style = userStyle
} else {
style = "unified"
}
ctx.Data["IsSplitStyle"] = style == "split"
opts := &user_service.UpdateOptions{
DiffViewStyle: optional.Some(style),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
}
}
// SetWhitespaceBehavior set whitespace behavior as render variable
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/gitdiff"
"github.com/stretchr/testify/assert"
)
func TestDiffViewStyle(t *testing.T) {
unittest.PrepareTestEnv(t)
t.Run("AnonymousUser", func(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "/any")
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any?style=split")
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any")
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx)) // at the moment, anonymous users don't have a saved preference
})
t.Run("SignedInUser", func(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "/any")
contexttest.LoadUser(t, ctx, 2)
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any?style=split")
contexttest.LoadUser(t, ctx, 2)
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any")
contexttest.LoadUser(t, ctx, 2)
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any?style=unified")
contexttest.LoadUser(t, ctx, 2)
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
ctx, _ = contexttest.MockContext(t, "/any")
contexttest.LoadUser(t, ctx, 2)
SetDiffViewStyle(ctx)
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
})
}
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/services/task"
)
@@ -78,44 +79,44 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
switch {
case migrations.IsRateLimitError(err):
ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form)
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form)
case repo_model.IsErrReachLimitOfRepo(err):
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, tpl, form)
ctx.RenderWithErrDeprecated(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, 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"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
default:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
}
case db.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
case db.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.Data["Err_Auth"] = true
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
} else if strings.Contains(err.Error(), "fatal:") {
ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
} else {
ctx.ServerError(name, err)
}
@@ -127,24 +128,24 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsProtocolInvalid:
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
}
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
default:
log.Error("Error whilst updating url: %v", err)
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
}
} else {
log.Error("Error whilst updating url: %v", err)
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
}
}
@@ -192,7 +193,7 @@ func MigratePost(ctx *context.Context) {
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
if ep == nil {
ctx.Data["Err_LFSEndpoint"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
return
}
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
@@ -237,7 +238,7 @@ func MigratePost(ctx *context.Context) {
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
}
err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
err = repo_service.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
return
@@ -88,7 +88,7 @@ func Milestones(ctx *context.Context) {
ctx.Data["Keyword"] = keyword
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -118,7 +118,7 @@ func NewMilestonePost(ctx *context.Context) {
deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
if err != nil {
ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return
}
@@ -174,7 +174,7 @@ func EditMilestonePost(ctx *context.Context) {
deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
if err != nil {
ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return
}
@@ -64,7 +64,7 @@ func Packages(ctx *context.Context) {
ctx.Data["Total"] = total
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -66,13 +66,6 @@ func Projects(ctx *context.Context) {
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
var total int
if !isShowClosed {
total = repo.NumOpenProjects
} else {
total = repo.NumClosedProjects
}
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.IssuePagingNum,
@@ -111,12 +104,7 @@ func Projects(ctx *context.Context) {
ctx.Data["State"] = "open"
}
numPages := 0
if count > 0 {
numPages = (int(count) - 1/setting.UI.IssuePagingNum)
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
pager := context.NewPagination(count, setting.UI.IssuePagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -311,13 +299,25 @@ func ViewProject(ctx *context.Context) {
}
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
if ctx.Written() {
return
}
assigneeID := ctx.FormString("assignee")
milestoneID := ctx.FormInt64("milestone")
var milestoneIDs []int64
if milestoneID > 0 {
milestoneIDs = []int64{milestoneID}
} else if milestoneID == db.NoConditionID {
milestoneIDs = []int64{db.NoConditionID}
}
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
RepoIDs: []int64{ctx.Repo.Repository.ID},
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
AssigneeID: assigneeID,
RepoIDs: []int64{ctx.Repo.Repository.ID},
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
AssigneeID: assigneeID,
MilestoneIDs: milestoneIDs,
})
if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err)
@@ -408,6 +408,12 @@ func ViewProject(ctx *context.Context) {
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
ctx.Data["AssigneeID"] = assigneeID
renderMilestones(ctx)
if ctx.Written() {
return
}
ctx.Data["MilestoneID"] = milestoneID
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
project.RenderedContent, err = markdown.RenderString(rctx, project.Description)
if err != nil {
+165 -95
View File
@@ -23,6 +23,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
@@ -35,15 +36,18 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
@@ -91,9 +95,9 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
return nil
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return nil
}
@@ -202,7 +206,7 @@ func GetPullDiffStats(ctx *context.Context) {
log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
@@ -235,7 +239,8 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
}
if commitSHA != "" {
// Get immediate parent of the first commit in the patch, grab history back
parentCommit, _, err = gitcmd.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(ctx, &gitcmd.RunOpts{Dir: ctx.Repo.GitRepo.Path})
parentCommit, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
gitcmd.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA))
if err == nil {
parentCommit = strings.TrimSpace(parentCommit)
}
@@ -255,7 +260,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
return baseCommit
}
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
if !issue.IsPull {
return nil
}
@@ -266,7 +271,7 @@ func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *p
}
// prepareMergedViewPullInfo show meta information for a merged pull request view page
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
pull := issue.PullRequest
setMergeTarget(ctx, pull)
@@ -274,10 +279,10 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
baseCommit := GetMergedBaseCommitID(ctx, issue)
compareInfo, err := pull_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
baseCommit, pull.GetGitHeadRefName(), false, false)
compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
git.RefName(baseCommit), git.RefName(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") {
if gitcmd.IsStdErrorNotValidObjectName(err) || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
@@ -311,8 +316,65 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
return compareInfo
}
type pullCommitStatusCheckData struct {
MissingRequiredChecks []string // list of missing required checks
IsContextRequired func(string) bool // function to check whether a context is required
RequireApprovalRunCount int // number of workflow runs that require approval
CanApprove bool // whether the user can approve workflow runs
ApproveLink string // link to approve all checks
RequiredChecksState commitstatus.CommitStatusState
LatestCommitStatus *git_model.CommitStatus
}
func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string {
if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 {
return locale.TrString("repo.pulls.status_checking")
} else if d.RequiredChecksState.IsSuccess() {
if d.LatestCommitStatus != nil && d.LatestCommitStatus.State.IsFailure() {
return locale.TrString("repo.pulls.status_checks_failure_optional")
}
return locale.TrString("repo.pulls.status_checks_success")
} else if d.RequiredChecksState.IsWarning() {
return locale.TrString("repo.pulls.status_checks_warning")
} else if d.RequiredChecksState.IsFailure() {
return locale.TrString("repo.pulls.status_checks_failure_required")
} else if d.RequiredChecksState.IsError() {
return locale.TrString("repo.pulls.status_checks_error")
}
return locale.TrString("repo.pulls.status_checking")
}
func getViewPullHeadBranchInfo(ctx *context.Context, pull *issues_model.PullRequest, baseGitRepo *git.Repository) (headCommitID string, headCommitExists bool, err error) {
if pull.HeadRepo == nil {
return "", false, nil
}
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
if err != nil {
return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
}
defer closer.Close()
if pull.Flow == issues_model.PullRequestFlowGithub {
headCommitExists, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
} else {
headCommitExists = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
}
if headCommitExists {
if pull.Flow != issues_model.PullRequestFlowGithub {
headCommitID, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
} else {
headCommitID, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
}
if err != nil {
return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
}
}
return headCommitID, headCommitExists, nil
}
// prepareViewPullInfo show meta information for a pull request preview page
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
repo := ctx.Repo.Repository
@@ -349,7 +411,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
defer baseGitRepo.Close()
}
if !gitrepo.IsBranchExist(ctx, pull.BaseRepo, pull.BaseBranch) {
statusCheckData := &pullCommitStatusCheckData{}
if exist, _ := git_model.IsBranchExist(ctx, pull.BaseRepo.ID, pull.BaseBranch); !exist {
ctx.Data["BaseBranchNotExist"] = true
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
@@ -369,15 +433,16 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
}
statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses)
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus
}
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
pull.MergeBase, pull.GetGitHeadRefName(), false, false)
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
if gitcmd.IsStdErrorNotValidObjectName(err) {
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
@@ -394,34 +459,10 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
return compareInfo
}
var headBranchExist bool
var headBranchSha string
// HeadRepo may be missing
if pull.HeadRepo != nil {
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
if err != nil {
ctx.ServerError("RepositoryFromContextOrOpen", err)
return nil
}
defer closer.Close()
if pull.Flow == issues_model.PullRequestFlowGithub {
headBranchExist = gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch)
} else {
headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
}
if headBranchExist {
if pull.Flow != issues_model.PullRequestFlowGithub {
headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
} else {
headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
}
if err != nil {
ctx.ServerError("GetBranchCommitID", err)
return nil
}
}
headBranchSha, headBranchExist, err := getViewPullHeadBranchInfo(ctx, pull, baseGitRepo)
if err != nil {
ctx.ServerError("getViewPullHeadBranchInfo", err)
return nil
}
if headBranchExist {
@@ -456,6 +497,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
return nil
}
ctx.Data["StatusCheckData"] = statusCheckData
statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), sha)
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
@@ -465,9 +509,24 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
}
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err)
return nil
}
for _, run := range runs {
if run.NeedApproval {
statusCheckData.RequireApprovalRunCount++
}
}
if statusCheckData.RequireApprovalRunCount > 0 {
statusCheckData.CanApprove = ctx.Repo.CanWrite(unit.TypeActions)
}
statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses)
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus
}
if pb != nil && pb.EnableStatusCheck {
@@ -486,9 +545,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
missingRequiredChecks = append(missingRequiredChecks, requiredContext)
}
}
ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
statusCheckData.MissingRequiredChecks = missingRequiredChecks
ctx.Data["is_context_required"] = func(context string) bool {
statusCheckData.IsContextRequired = func(context string) bool {
for _, c := range pb.StatusCheckContexts {
if c == context {
return true
@@ -504,7 +563,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
}
return false
}
ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
}
ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
@@ -522,10 +581,10 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
}
}
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
git.BranchPrefix+pull.BaseBranch, pull.GetGitHeadRefName(), false, false)
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
if gitcmd.IsStdErrorNotValidObjectName(err) {
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
@@ -655,6 +714,8 @@ func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
// ViewPullFiles render pull request changed files list page
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
var err error
ctx.Data["PageIsPullList"] = true
ctx.Data["PageIsPullFiles"] = true
@@ -681,43 +742,53 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
}
isSingleCommit := beforeCommitID == "" && afterCommitID != ""
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
// FIXME: when afterCommitID==headCommitID, isSingleCommit and isShowAllCommits can be both true, which doesn't seem right
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
if afterCommitID == "" || afterCommitID == headCommitID {
afterCommitID = headCommitID
}
// "commits list" is half-open, half-closed: (base, head]
// * base commit is not in the list
// * if the PR is empty, the list is also empty (head commit is not in the list)
afterCommitID = util.IfZero(afterCommitID, headCommitID)
afterCommit := indexCommit(prInfo.Commits, afterCommitID)
if afterCommit == nil && afterCommitID == headCommitID {
afterCommit, err = gitRepo.GetCommit(afterCommitID)
if err != nil {
ctx.ServerError("GetCommit(afterCommitID)", err)
return
}
}
if afterCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
ctx.NotFound(nil)
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 {
beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
if beforeCommit == nil {
ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
return
}
}
} else {
if isSingleCommit {
beforeCommit, err = afterCommit.Parent(0)
if err != nil {
ctx.ServerError("Parent", err)
ctx.ServerError("afterCommit.Parent", err)
return
}
beforeCommitID = beforeCommit.ID.String()
} else {
beforeCommitID = util.IfZero(beforeCommitID, prInfo.MergeBase)
beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
if beforeCommit == nil && 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(beforeCommitID)", err)
return
}
}
}
if beforeCommit == nil {
ctx.NotFound(nil)
return
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
@@ -766,7 +837,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
}
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return
@@ -832,6 +903,12 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
}
ctx.Data["Diff"] = diff
ctx.Data["DiffBlobExcerptData"] = &gitdiff.DiffBlobExcerptData{
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
PullIssueIndex: pull.Index,
DiffStyle: GetDiffViewStyle(ctx),
AfterCommitID: afterCommitID,
}
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
if ctx.IsSigned && ctx.Doer != nil {
@@ -848,10 +925,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
ctx.ServerError("GetRepoAssignees", err)
return
}
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
if ctx.Written() {
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
if err != nil && !issues_model.IsErrReviewNotExist(err) {
@@ -896,13 +970,13 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
if pull.HeadRepo != nil {
if !pull.HasMerged && ctx.Doer != nil {
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
headPerm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
if issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pull.HeadBranch, ctx.Doer) {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
@@ -1121,7 +1195,7 @@ func MergePullRequest(ctx *context.Context) {
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if pull_service.IsErrMergeConflicts(err) {
@@ -1304,6 +1378,10 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if ctx.Written() {
return
}
if ctx.Data["IsNoMergeBase"] == true {
ctx.JSONError(ctx.Tr("repo.pulls.no_common_history"))
return
}
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
if ctx.Written() {
@@ -1327,7 +1405,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
}
// Check if a pull request already exists with the same head and base branch.
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
@@ -1357,11 +1435,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
pullRequest := &issues_model.PullRequest{
HeadRepoID: ci.HeadRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: ci.HeadBranch,
BaseBranch: ci.BaseBranch,
HeadBranch: ci.HeadRef.ShortName(),
BaseBranch: ci.BaseRef.ShortName(),
HeadRepo: ci.HeadRepo,
BaseRepo: repo,
MergeBase: ci.CompareInfo.MergeBase,
MergeBase: ci.MergeBase,
Type: issues_model.PullRequestGitea,
AllowMaintainerEdit: form.AllowMaintainerEdit,
}
@@ -1376,6 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
AssigneeIDs: assigneeIDs,
Reviewers: validateRet.Reviewers,
TeamReviewers: validateRet.TeamReviewers,
ProjectID: projectID,
}
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
switch {
@@ -1427,15 +1506,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
if !errors.Is(err, util.ErrPermissionDenied) {
ctx.ServerError("IssueAssignOrRemoveProject", err)
return
}
}
}
log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
ctx.JSONRedirect(pullIssue.Link())
}
@@ -30,7 +30,7 @@ func TestRenderConversation(t *testing.T) {
run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) {
t.Run(name, func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
contexttest.LoadUser(t, ctx, pr.Issue.PosterID)
contexttest.LoadRepo(t, ctx, pr.BaseRepoID)
contexttest.LoadGitRepo(t, ctx)
@@ -5,6 +5,7 @@
package repo
import (
stdCtx "context"
"errors"
"fmt"
"net/http"
@@ -40,7 +41,7 @@ const (
)
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
target := release.Target
if target == "" {
target = repoCtx.Repository.DefaultBranch
@@ -60,7 +61,7 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err)
}
}
countCache[target], err = commit.CommitsCount()
countCache[target], err = gitrepo.CommitsCountOfCommit(ctx, repoCtx.Repository, commit.ID.String())
if err != nil {
return fmt.Errorf("CommitsCount: %w", err)
}
@@ -103,13 +104,9 @@ 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.GetPossibleUserByID(ctx, r.PublisherID)
r.PublisherID, r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
} else {
return nil, err
}
return nil, err
}
cacheUsers[r.PublisherID] = r.Publisher
}
@@ -123,7 +120,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
if !r.IsDraft {
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
if err := calReleaseNumCommitsBehind(ctx, ctx.Repo, r, countCache); err != nil {
return nil, err
}
}
@@ -186,7 +183,7 @@ func Releases(ctx *context.Context) {
ctx.Data["Releases"] = releases
numReleases := ctx.Data["NumReleases"].(int64)
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
pager := context.NewPagination(numReleases, listOptions.PageSize, listOptions.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplReleasesList)
@@ -238,7 +235,7 @@ func TagsList(ctx *context.Context) {
ctx.Data["Releases"] = releases
ctx.Data["TagCount"] = count
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases)
@@ -391,6 +388,32 @@ func NewRelease(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplReleaseNew)
}
// GenerateReleaseNotes builds release notes content for the given tag and base.
func GenerateReleaseNotes(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.GenerateReleaseNotesForm)
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}
content, err := release_service.GenerateReleaseNotes(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, release_service.GenerateReleaseNotesOptions{
TagName: form.TagName,
TagTarget: form.TagTarget,
PreviousTag: form.PreviousTag,
})
if err != nil {
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
ctx.JSONError(errTr.Translate(ctx.Locale))
} else {
ctx.ServerError("GenerateReleaseNotes", err)
}
return
}
ctx.JSON(http.StatusOK, map[string]any{"content": content})
}
// NewReleasePost response for creating a release
func NewReleasePost(ctx *context.Context) {
newReleaseCommon(ctx)
@@ -424,14 +447,15 @@ func NewReleasePost(ctx *context.Context) {
return
}
if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Target) {
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
form.Target = util.IfZero(form.Target, ctx.Repo.Repository.DefaultBranch)
if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist {
ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
return
}
if !form.TagOnly && form.Title == "" {
// if not "tag only", then the title of the release cannot be empty
ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form)
return
}
@@ -439,13 +463,13 @@ func NewReleasePost(ctx *context.Context) {
ctx.Data["Err_TagName"] = true
switch {
case release_service.IsErrTagAlreadyExists(err):
ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form)
case repo_model.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case release_service.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
case release_service.IsErrProtectedTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
default:
ctx.ServerError("handleTagReleaseError", err)
}
@@ -498,7 +522,7 @@ func NewReleasePost(ctx *context.Context) {
// add new logic: if tag-only, do not convert the tag to a release
if form.TagOnly || !rel.IsTag {
ctx.Data["Err_TagName"] = true
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
return
}
@@ -519,11 +543,13 @@ func NewReleasePost(ctx *context.Context) {
// EditRelease render release edit page
func EditRelease(ctx *context.Context) {
newReleaseCommon(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "release")
tagName := ctx.PathParam("*")
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
@@ -535,6 +561,11 @@ func EditRelease(ctx *context.Context) {
}
return
}
if rel.IsTag {
ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release
return
}
ctx.Data["ID"] = rel.ID
ctx.Data["tag_name"] = rel.TagName
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
@@ -564,8 +595,13 @@ func EditRelease(ctx *context.Context) {
// EditReleasePost response for edit release
func EditReleasePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditReleaseForm)
newReleaseCommon(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
tagName := ctx.PathParam("*")
@@ -579,7 +615,7 @@ func EditReleasePost(ctx *context.Context) {
return
}
if rel.IsTag {
ctx.NotFound(err)
ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release
return
}
ctx.Data["tag_name"] = rel.TagName
@@ -158,7 +158,7 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) {
countCache := make(map[string]int64)
for _, release := range releases {
err := calReleaseNumCommitsBehind(ctx.Repo, release, countCache)
err := calReleaseNumCommitsBehind(ctx, ctx.Repo, release, countCache)
assert.NoError(t, err)
}
@@ -32,24 +32,21 @@ func RenderFile(ctx *context.Context) {
return
}
dataRc, err := blob.DataAsync()
blobReader, err := blob.DataAsync()
if err != nil {
ctx.ServerError("DataAsync", err)
return
}
defer dataRc.Close()
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType == "" {
http.Error(ctx.Resp, "Unsupported file type render", http.StatusBadRequest)
return
}
defer blobReader.Close()
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true)
renderer, err := markup.FindRendererByContext(rctx)
}).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{
CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(),
RenderQueryString: ctx.Req.URL.RawQuery,
})
renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader)
if err != nil {
http.Error(ctx.Resp, "Unable to find renderer", http.StatusBadRequest)
return
@@ -71,7 +68,7 @@ func RenderFile(ctx *context.Context) {
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
}
err = markup.RenderWithRenderer(rctx, renderer, dataRc, ctx.Resp)
err = markup.RenderWithRenderer(rctx, renderer, rendererInput, ctx.Resp)
if err != nil {
log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)
+36 -20
View File
@@ -70,7 +70,7 @@ func CommitInfoCache(ctx *context.Context) {
ctx.ServerError("GetBranchCommit", err)
return
}
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
@@ -186,28 +186,28 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error,
case repo_model.IsErrReachLimitOfRepo(err):
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, tpl, form)
ctx.RenderWithErrDeprecated(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, 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"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
default:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
}
case db.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
case db.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
ctx.ServerError(name, err)
}
@@ -254,7 +254,7 @@ func CreatePost(ctx *context.Context) {
}
if !opts.IsValid() {
ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.one_item"), tplCreate, form)
return
}
@@ -264,7 +264,7 @@ func CreatePost(ctx *context.Context) {
}
if !templateRepo.IsTemplate {
ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.invalid"), tplCreate, form)
return
}
@@ -364,31 +364,47 @@ func RedirectDownload(ctx *context.Context) {
// Download an archive of a repository
func Download(ctx *context.Context) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
if !checkDownloadTokenScope(ctx) {
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path"))
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.HTTPError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
} else if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound, err.Error())
} else {
ctx.ServerError("archiver_service.NewRequest", err)
}
return
}
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq)
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.HTTPError(http.StatusBadRequest, err.Error())
} else {
ctx.ServerError("archiver_service.ServeRepoArchive", err)
}
}
}
// InitiateDownload will enqueue an archival request, as needed. It may submit
// a request that's already in-progress, but the archiver service will just
// kind of drop it on the floor if this is the case.
func InitiateDownload(ctx *context.Context) {
if setting.Repository.StreamArchives {
if !checkDownloadTokenScope(ctx) {
return
}
paths := ctx.FormStrings("path")
if setting.Repository.StreamArchives || len(paths) > 0 {
ctx.JSON(http.StatusOK, map[string]any{
"complete": true,
})
return
}
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), paths)
if err != nil {
ctx.HTTPError(http.StatusBadRequest, "invalid archive request")
return
@@ -398,7 +414,7 @@ func InitiateDownload(ctx *context.Context) {
return
}
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.Repo.ID, aReq.Type, aReq.CommitID)
if err != nil {
ctx.ServerError("archiver_service.StartArchive", err)
return
@@ -518,9 +534,9 @@ func SearchRepo(ctx *context.Context) {
ctx.SetTotalCountHeader(count)
latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
latestCommitStatuses, err := commitstatus_service.FindReposLatestCommitStatuses(ctx, repos)
if err != nil {
log.Error("FindReposLastestCommitStatuses: %v", err)
log.Error("FindReposLatestCommitStatuses: %v", err)
ctx.JSON(http.StatusInternalServerError, nil)
return
}
@@ -32,7 +32,7 @@ func Search(ctx *context.Context) {
page = 1
}
var total int
var total int64
var searchResults []*code_indexer.Result
var searchResultLanguages []*code_indexer.SearchResultLanguages
if setting.Indexer.RepoIndexerEnabled {
@@ -0,0 +1,191 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/actions"
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/templates"
"code.gitea.io/gitea/modules/util"
shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
const tplRepoActionsGeneralSettings templates.TplName = "repo/settings/actions"
func ActionsGeneralSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.general")
ctx.Data["PageType"] = "general"
ctx.Data["PageIsActionsSettingsGeneral"] = true
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
ctx.ServerError("GetUnit", err)
return
}
if actionsUnit == nil { // no actions unit
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
return
}
actionsCfg := actionsUnit.ActionsConfig()
// Token permission settings
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
// Follow owner config (only for repos in orgs)
ctx.Data["OverrideOwnerConfig"] = actionsCfg.OverrideOwnerConfig
if actionsCfg.OverrideOwnerConfig {
ctx.Data["MaxTokenPermissions"] = actionsCfg.GetMaxTokenPermissions()
ctx.Data["TokenPermissionMode"] = actionsCfg.TokenPermissionMode
ctx.Data["EnableMaxTokenPermissions"] = actionsCfg.MaxTokenPermissions != nil
} else {
ownerActionsConfig, err := actions.GetOwnerActionsConfig(ctx, ctx.Repo.Repository.OwnerID)
if err != nil {
ctx.ServerError("GetOwnerActionsConfig", err)
return
}
ctx.Data["MaxTokenPermissions"] = ownerActionsConfig.GetMaxTokenPermissions()
ctx.Data["TokenPermissionMode"] = ownerActionsConfig.TokenPermissionMode
ctx.Data["EnableMaxTokenPermissions"] = ownerActionsConfig.MaxTokenPermissions != nil
}
if ctx.Repo.Repository.IsPrivate {
collaborativeOwnerIDs := actionsCfg.CollaborativeOwnerIDs
collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs)
if err != nil {
ctx.ServerError("GetUsersByIDs", err)
return
}
ctx.Data["CollaborativeOwners"] = collaborativeOwners
}
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
}
func ActionsUnitPost(ctx *context.Context) {
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"
enableActionsUnit := ctx.FormBool("enable_actions")
repo := ctx.Repo.Repository
var err error
if enableActionsUnit && !unit_model.TypeActions.UnitGlobalDisabled() {
err = repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{newRepoUnit(repo, unit_model.TypeActions, nil)}, nil)
} else if !unit_model.TypeActions.UnitGlobalDisabled() {
err = repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions})
}
if err != nil {
ctx.ServerError("UpdateRepositoryUnits", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(redirectURL)
}
func AddCollaborativeOwner(ctx *context.Context) {
collUser, err := user_model.GetUserByName(ctx, ctx.FormString("collaborative_owner"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.JSONError(ctx.Tr("form.user_not_exist"))
} else {
ctx.ServerError("GetUserByName", err)
}
return
}
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
if err != nil {
ctx.ServerError("GetUnit", err)
return
}
actionsCfg := actionsUnit.ActionsConfig()
actionsCfg.AddCollaborativeOwner(collUser.ID)
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
ctx.ServerError("UpdateRepoUnitConfig", err)
return
}
ctx.JSONOK()
}
func DeleteCollaborativeOwner(ctx *context.Context) {
ownerID := ctx.FormInt64("id")
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
if err != nil {
ctx.ServerError("GetUnit", err)
return
}
actionsCfg := actionsUnit.ActionsConfig()
if !actionsCfg.IsCollaborativeOwner(ownerID) {
ctx.Flash.Error(ctx.Tr("actions.general.collaborative_owner_not_exist"))
ctx.JSONErrorNotFound()
return
}
actionsCfg.RemoveCollaborativeOwner(ownerID)
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
ctx.ServerError("UpdateRepoUnitConfig", err)
return
}
ctx.JSONOK()
}
// UpdateTokenPermissions updates the token permission settings for the repository
func UpdateTokenPermissions(ctx *context.Context) {
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
if err != nil {
ctx.ServerError("GetUnit", err)
return
}
actionsCfg := actionsUnit.ActionsConfig()
// Update Override Owner Config (for repos in orgs)
// If checked, it means we WANT to override (opt-out of following)
actionsCfg.OverrideOwnerConfig = ctx.FormBool("override_owner_config")
// Update permission mode (only if overriding owner config)
shouldUpdate := actionsCfg.OverrideOwnerConfig
if shouldUpdate {
permissionMode, permissionModeValid := util.EnumValue(repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode")))
if !permissionModeValid {
ctx.Flash.Error("Invalid token permission mode")
ctx.Redirect(redirectURL)
return
}
actionsCfg.TokenPermissionMode = permissionMode
}
// Update Maximum Permissions (radio buttons: none/read/write)
enableMaxPermissions := ctx.FormBool("enable_max_permissions")
if shouldUpdate {
if enableMaxPermissions {
actionsCfg.MaxTokenPermissions = shared_actions.ParseMaxTokenPermissions(ctx)
} else {
// If not enabled, ensure any sent permissions are ignored and set to nil
actionsCfg.MaxTokenPermissions = nil
}
}
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
ctx.ServerError("UpdateRepoUnitConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(redirectURL)
}
@@ -208,7 +208,7 @@ func DeleteTeam(ctx *context.Context) {
}
if err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID); err != nil {
ctx.ServerError("team.RemoveRepositorys", err)
ctx.ServerError("team.RemoveRepositories", err)
return
}
@@ -76,16 +76,16 @@ func DeployKeysPost(ctx *context.Context) {
switch {
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
case asymkey_model.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
default:
ctx.ServerError("AddDeployKey", err)
}
@@ -7,6 +7,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context"
)
@@ -41,6 +42,7 @@ func GitHooksEdit(ctx *context.Context) {
return
}
ctx.Data["Hook"] = hook
ctx.Data["CodeEditorConfig"] = repo.CodeEditorConfig{Filename: name + ".sh", IndentStyle: "tab", TabWidth: 4}
ctx.HTML(http.StatusOK, tplGithookEdit)
}
@@ -20,6 +20,7 @@ import (
"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/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -53,7 +54,7 @@ func LFSFiles(ctx *context.Context) {
}
ctx.Data["Total"] = total
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
ctx.Data["PageIsSettingsLFS"] = true
lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
@@ -82,7 +83,7 @@ func LFSLocks(ctx *context.Context) {
}
ctx.Data["Total"] = total
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
ctx.Data["PageIsSettingsLFS"] = true
lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
@@ -112,7 +113,7 @@ func LFSLocks(ctx *context.Context) {
}
defer cleanup()
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
if err := gitrepo.CloneRepoToLocal(ctx, ctx.Repo.Repository, tmpBasePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
}); err != nil {
@@ -300,13 +301,13 @@ func LFSFileGet(ctx *context.Context) {
if index != len(lines)-1 {
line += "\n"
}
output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
fmt.Fprintf(&output, `<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line)
}
ctx.Data["FileContent"] = gotemplate.HTML(output.String())
output.Reset()
for i := 0; i < len(lines); i++ {
output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
fmt.Fprintf(&output, `<span id="L%d">%d</span>`, i+1, i+1)
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
@@ -406,7 +407,9 @@ func LFSPointerFiles(ctx *context.Context) {
err = func() error {
pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1)
go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
go func() {
errChan <- lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan)
}()
numPointers := 0
var numAssociated, numNoExist, numAssociatable int
@@ -482,11 +485,6 @@ func LFSPointerFiles(ctx *context.Context) {
results = append(results, result)
}
err, has := <-errChan
if has {
return err
}
ctx.Data["Pointers"] = results
ctx.Data["NumPointers"] = numPointers
ctx.Data["NumAssociated"] = numAssociated
@@ -494,7 +492,8 @@ func LFSPointerFiles(ctx *context.Context) {
ctx.Data["NumNoExist"] = numNoExist
ctx.Data["NumNotAssociated"] = numPointers - numAssociated
return nil
err := <-errChan
return err
}()
if err != nil {
ctx.ServerError("LFSPointerFiles", err)
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
@@ -312,10 +313,14 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
}
func UpdateBranchProtectionPriories(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ProtectBranchPriorityForm)
repo := ctx.Repo.Repository
if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil {
var form struct {
IDs []int64 `json:"ids"`
}
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.JSONError("invalid argument")
return
}
if err := git_model.UpdateProtectBranchPriorities(ctx, ctx.Repo.Repository, form.IDs); err != nil {
ctx.ServerError("UpdateProtectBranchPriorities", err)
return
}
@@ -336,7 +341,7 @@ func RenameBranchPost(ctx *context.Context) {
return
}
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, form.From, form.To)
if err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
@@ -7,7 +7,6 @@ import (
"errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
@@ -46,7 +45,7 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsOrgSettings"] == true {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return nil, nil
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
}
return &secretsCtx{
OwnerID: ctx.ContextUser.ID,
@@ -74,7 +73,6 @@ func Secrets(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.actions")
ctx.Data["PageType"] = "secrets"
ctx.Data["PageIsSharedSettingsSecrets"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
sCtx, err := getSecretsCtx(ctx)
if err != nil {
@@ -28,8 +28,8 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
repo_router "code.gitea.io/gitea/routers/web/repo"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
@@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) {
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())
signing, _ := gitrepo.GetSigningKey(ctx)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -89,6 +89,11 @@ func SettingsCtxData(ctx *context.Context) {
return
}
ctx.Data["PushMirrors"] = pushMirrors
repo_router.PrepareBranchList(ctx)
if ctx.Written() {
return
}
}
// Settings show a repository's settings page
@@ -105,7 +110,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
signing, _ := gitrepo.GetSigningKey(ctx)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -176,23 +181,23 @@ func handleSettingsPostUpdate(ctx *context.Context) {
ctx.Data["Err_RepoName"] = true
switch {
case repo_model.IsErrRepoAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
default:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
default:
ctx.ServerError("ChangeRepositoryName", err)
}
@@ -208,11 +213,6 @@ func handleSettingsPostUpdate(ctx *context.Context) {
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
@@ -247,7 +247,7 @@ func handleSettingsPostMirror(ctx *context.Context) {
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)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
return
}
@@ -298,7 +298,7 @@ func handleSettingsPostMirror(ctx *context.Context) {
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
if ep == nil {
ctx.Data["Err_LFSEndpoint"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
return
}
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
@@ -369,7 +369,7 @@ func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
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{})
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
return
}
@@ -445,7 +445,7 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
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)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
return
}
@@ -614,12 +614,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
}
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)
}
if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
@@ -634,6 +628,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
}))
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
@@ -738,7 +733,7 @@ func handleSettingsPostConvert(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
return
}
@@ -772,7 +767,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
return
}
@@ -808,14 +803,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
return
}
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)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
ctx.ServerError("IsUserExist", err)
@@ -825,7 +820,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
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)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
}
@@ -839,14 +834,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(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)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
} else {
ctx.ServerError("TransferOwnership", err)
}
@@ -900,7 +895,7 @@ func handleSettingsPostDelete(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
return
}
@@ -927,7 +922,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) {
return
}
if repo.Name != form.RepoName {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
return
}
@@ -1004,39 +999,33 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
}
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")
ctx.JSONError(ctx.Tr("repo.settings.visibility.fork_error"))
return
}
var err error
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
// 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)
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.repository_force_private"))
return
}
if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
return
}
if repo.IsPrivate {
err = repo_service.MakeRepoPublic(ctx, repo)
} else {
err = repo_service.MakeRepoPrivate(ctx, repo)
}
err := repo_service.MakeRepoPrivate(ctx, repo, private)
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")
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
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")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
@@ -1044,21 +1033,21 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsProtocolInvalid:
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
}
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
default:
ctx.ServerError("Unknown error", err)
}
return
}
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
}
@@ -234,6 +234,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
w := &webhook.Webhook{
RepoID: orCtx.RepoID,
URL: params.URL,
Name: strings.TrimSpace(params.WebhookForm.Name),
HTTPMethod: params.HTTPMethod,
ContentType: params.ContentType,
Secret: params.WebhookForm.Secret,
@@ -288,6 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) {
}
w.URL = params.URL
w.Name = strings.TrimSpace(params.WebhookForm.Name)
w.ContentType = params.ContentType
w.Secret = params.WebhookForm.Secret
w.HookEvent = ParseHookEvent(params.WebhookForm)
@@ -448,12 +450,21 @@ func MatrixHooksEditPost(ctx *context.Context) {
editWebhook(ctx, matrixHookParams(ctx))
}
func matrixRoomIDEncode(roomID string) string {
// See https://spec.matrix.org/latest/appendices/#room-ids
// Some (unrelated) demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation
// API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room
// Some of their examples show links like: "PUT /rooms/!roomid:domain/state/m.example.event"
return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID))
}
func matrixHookParams(ctx *context.Context) webhookParams {
form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
// TODO: need to migrate to the latest (v3) API: https://spec.matrix.org/v1.18/client-server-api/
return webhookParams{
Type: webhook_module.MATRIX,
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)),
ContentType: webhook.ContentTypeJSON,
HTTPMethod: http.MethodPut,
WebhookForm: form.WebhookForm,
@@ -0,0 +1,15 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWebhookMatrix(t *testing.T) {
assert.Equal(t, "!roomid:domain", matrixRoomIDEncode("!roomid:domain"))
assert.Equal(t, "!room%23id:domain", matrixRoomIDEncode("!room#id:domain")) // maybe it should never really happen in real world
}
@@ -33,7 +33,7 @@ func TreeList(ctx *context.Context) {
ctx.ServerError("ListEntriesRecursiveFast", err)
return
}
entries.CustomSort(base.NaturalSortLess)
entries.CustomSort(base.NaturalSortCompare)
files := make([]string, 0, len(entries))
for _, entry := range entries {
+29 -37
View File
@@ -151,12 +151,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
return true
}
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
renderer, err := markup.FindRendererByContext(renderCtx)
if err != nil {
return nil, "", err
}
func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, renderer markup.Renderer, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
@@ -164,7 +159,7 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i
go func() {
sb := &strings.Builder{}
if markup.RendererNeedPostProcess(renderer) {
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.EscapeOptionsForView())
} else {
escaped = &charset.EscapeStatus{}
_, _ = io.Copy(sb, markupRd)
@@ -245,27 +240,17 @@ func LastCommit(ctx *context.Context) {
return
}
// The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info
// It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag).
// So we need to get the ref name from the query parameter "refSubUrl".
// TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this
refSubURL := path.Clean(ctx.FormString("refSubUrl"))
prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL()))
renderDirectoryFiles(ctx, 0)
if ctx.Written() {
return
}
var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["BranchLink"] = branchLink
ctx.HTML(http.StatusOK, tplRepoViewList)
}
@@ -289,7 +274,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
return nil
}
ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
// TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details
lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL())
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
@@ -308,7 +295,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
ctx.ServerError("ListEntries", err)
return nil
}
allEntries.CustomSort(base.NaturalSortLess)
allEntries.CustomSort(base.NaturalSortCompare)
commitInfoCtx := gocontext.Context(ctx)
if timeout > 0 {
@@ -322,6 +309,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
ctx.ServerError("GetCommitsInfo", err)
return nil
}
{
if timeout != 0 && !setting.IsProd && !setting.IsInTesting {
log.Debug("first call to get directory file commit info")
clearFilesCommitInfo := func() {
log.Warn("clear directory file commit info to force async loading on frontend")
for i := range files {
files[i].Commit = nil
}
}
_ = clearFilesCommitInfo
// clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests
}
}
ctx.Data["Files"] = files
prepareDirectoryFileIcons(ctx, files)
for _, f := range files {
@@ -334,16 +336,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
if !loadLatestCommitData(ctx, latestCommit) {
return nil
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
treeLink := branchLink
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Data["TreeLink"] = treeLink
return allEntries
}
@@ -353,7 +345,7 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp
if page <= 0 {
page = 1
}
pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
pager := context.NewPagination(int64(total), setting.ItemsPerPage, page, 5)
ctx.Data["Page"] = pager
items, err := getter(db.ListOptions{
@@ -411,7 +403,7 @@ func Forks(ctx *context.Context) {
return
}
pager := context.NewPagination(int(total), pageSize, page, 5)
pager := context.NewPagination(total, pageSize, page, 5)
ctx.Data["ShowRepoOwnerAvatar"] = true
ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
@@ -23,12 +23,9 @@ import (
"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"
"github.com/nektos/act/pkg/model"
)
func prepareLatestCommitInfo(ctx *context.Context) bool {
@@ -60,14 +57,19 @@ func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool)
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
func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Reader io.Reader) bool {
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).WithRelativePath(ctx.Repo.TreePath)
renderer := rctx.DetectMarkupRenderer(prefetchBuf)
if renderer == nil {
return false // not supported markup
}
metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
rctx.WithMetas(metas)
ctx.Data["HasSourceRenderedToggle"] = true
@@ -75,27 +77,22 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy
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)
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader)
if err != nil {
ctx.ServerError("Render", err)
return true
}
opts, ok := markup.GetExternalRendererOptions(renderer)
usingIframe := ok && opts.DisplayInIframe
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
ctx.Data["RenderAsMarkup"] = util.Iif(usingIframe, "markup-iframe", "markup-inplace")
return true
}
func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
func handleFileViewRenderSource(ctx *context.Context, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
filename := ctx.Repo.TreePath
if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
return false
}
@@ -124,12 +121,8 @@ func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *at
}
language := attrs.GetLanguage().Value()
fileContent, lexerName, err := highlight.File(filename, language, buf)
fileContent, lexerName := highlight.RenderFullFile(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 {
@@ -193,8 +186,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
if err != nil {
log.Error("actions.GetContentFromEntry: %v", err)
}
_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
if workFlowErr != nil {
if workFlowErr := actions.ValidateWorkflowContent(content); workFlowErr != nil {
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
}
} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
@@ -246,10 +238,8 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
switch {
case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize:
ctx.Data["IsFileTooLarge"] = true
case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, contentReader):
// it also sets ctx.Data["FileContent"] and more
ctx.Data["IsMarkup"] = true
case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, contentReader):
case handleFileViewRenderMarkup(ctx, buf, contentReader):
case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader):
// it also sets ctx.Data["FileContent"] and more
ctx.Data["IsDisplayingSource"] = true
case handleFileViewRenderImage(ctx, fInfo, buf):
@@ -6,7 +6,6 @@ package repo
import (
"errors"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
@@ -70,21 +69,26 @@ func prepareHomeSidebarRepoTopics(ctx *context.Context) {
func prepareOpenWithEditorApps(ctx *context.Context) {
var tmplApps []map[string]any
apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
if len(apps) == 0 {
apps = setting.DefaultOpenWithEditorApps()
}
for _, app := range apps {
schema, _, _ := strings.Cut(app.OpenURL, ":")
var iconHTML template.HTML
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
iconHTML = svg.RenderHTML("gitea-"+schema, 16)
} else {
iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future
var iconName string
switch schema {
case "vscode":
iconName = "octicon-vscode"
case "vscodium":
iconName = "gitea-vscodium"
case "jetbrains":
iconName = "gitea-jetbrains"
default:
// TODO: it could support user's customized icon in the future
iconName = "gitea-git"
}
tmplApps = append(tmplApps, map[string]any{
"DisplayName": app.DisplayName,
"OpenURL": app.OpenURL,
"IconHTML": iconHTML,
"IconHTML": svg.RenderHTML(iconName, 16),
})
}
ctx.Data["OpenWithEditorApps"] = tmplApps
@@ -278,7 +282,7 @@ func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.Comm
ctx.Data["NotFoundPrompt"] = redirectLink
ctx.NotFound(nil)
} else {
ctx.Redirect(submoduleWebLink.CommitWebLink)
ctx.RedirectToCurrentSite(redirectLink)
}
}
@@ -355,6 +359,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b
return false
}
func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) {
// for: home, file list, file view, blame
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item
// prepare the tree path navigation
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL
treeLink := branchLink
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
}
// Home render repository home page
func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) {
@@ -376,8 +406,7 @@ func Home(ctx *context.Context) {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
// empty or broken repositories need to be handled differently
@@ -398,26 +427,6 @@ func Home(ctx *context.Context) {
return
}
// prepare the tree path
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
treeLink := branchLink
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
// some UI components are only shown when the tree path is root
isTreePathRoot := ctx.Repo.TreePath == ""
@@ -448,7 +457,7 @@ func Home(ctx *context.Context) {
if isViewHomeOnlyContent(ctx) {
ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 {
} else if ctx.Repo.TreePath != "" {
ctx.HTML(http.StatusOK, tplRepoView)
} else {
ctx.HTML(http.StatusOK, tplRepoHome)
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
@@ -67,7 +66,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
for _, entry := range entries {
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
fullPath := path.Join(parentDir, entry.Name())
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob().Name()) < 0 {
if entry.IsLink() {
res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
@@ -103,7 +102,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
return "", nil, err
}
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, path.Join(parentDir, subTreeEntry.Name()), childEntries, false)
if err != nil && !git.IsErrNotExist(err) {
return "", nil, err
}
@@ -190,25 +189,22 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Dir(readmeFullPath),
}).
WithMarkupType(markupType).
WithRelativePath(readmeFullPath)
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
CurrentTreePath: path.Dir(readmeFullPath),
}).WithRelativePath(readmeFullPath)
renderer := rctx.DetectMarkupRenderer(buf)
if renderer != nil {
ctx.Data["RenderAsMarkup"] = "markup-inplace"
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
delete(ctx.Data, "IsMarkup")
delete(ctx.Data, "RenderAsMarkup")
}
}
if ctx.Data["IsMarkup"] != true {
if ctx.Data["RenderAsMarkup"] == nil {
ctx.Data["IsPlainText"] = true
content, err := io.ReadAll(rd)
if err != nil {
@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"path"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindReadmeFileInEntriesWithSymlinkInSubfolder(t *testing.T) {
for _, subdir := range []string{".github", ".gitea", "docs"} {
t.Run(subdir, func(t *testing.T) {
repoPath := t.TempDir()
stdin := fmt.Sprintf(`commit refs/heads/master
author Test <test@example.com> 1700000000 +0000
committer Test <test@example.com> 1700000000 +0000
data <<EOT
initial
EOT
M 100644 inline target.md
data <<EOT
target-content
EOT
M 120000 inline %s/README.md
data 12
../target.md
`, subdir)
var err error
err = gitcmd.NewCommand("init", "--bare", ".").WithDir(repoPath).RunWithStderr(t.Context())
require.NoError(t, err)
err = gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(stdin)).RunWithStderr(t.Context())
require.NoError(t, err)
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
require.NoError(t, err)
defer gitRepo.Close()
commit, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
entries, err := commit.ListEntries()
require.NoError(t, err)
ctx, _ := contexttest.MockContext(t, "/")
ctx.Repo.Commit = commit
foundDir, foundReadme, err := findReadmeFileInEntries(ctx, "", entries, true)
require.NoError(t, err)
require.NotNil(t, foundReadme)
assert.Equal(t, subdir, foundDir)
assert.Equal(t, "README.md", foundReadme.Name())
assert.True(t, foundReadme.IsLink())
// Verify that it can follow the link
res, err := git.EntryFollowLinks(commit, path.Join(foundDir, foundReadme.Name()), foundReadme)
require.NoError(t, err)
assert.Equal(t, "target.md", res.TargetFullPath)
})
}
}
+16 -19
View File
@@ -10,7 +10,7 @@ import (
"io"
"net/http"
"net/url"
"path/filepath"
"path"
"strings"
"code.gitea.io/gitea/models/renderhelper"
@@ -238,7 +238,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if isRaw {
ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/raw/" + string(pageName))
}
if entry == nil || ctx.Written() {
return nil, nil
@@ -258,8 +258,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
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)
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.EscapeOptionsForView())
output = template.HTML(buf.String())
buf.Reset()
close(done)
@@ -277,12 +276,10 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil
}
if rctx.SidebarTocNode != nil {
if rctx.TocShowInSection == markup.TocShowInSidebar && len(rctx.TocHeadingItems) > 0 {
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)
}
ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String())
markup.RenderTocHeadingItems(rctx, map[string]string{"open": ""}, &sb)
ctx.Data["WikiSidebarTocHTML"] = template.HTML(sb.String())
}
if !isSideBar {
@@ -310,7 +307,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
}
// get commit count - wiki revisions
commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
return wikiGitRepo, entry
@@ -350,7 +347,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
}
// get commit count - wiki revisions
commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
@@ -373,7 +370,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
return nil, nil
}
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -492,9 +489,9 @@ func Wiki(ctx *context.Context) {
}
wikiPath := entry.Name()
if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
ext := strings.ToUpper(filepath.Ext(wikiPath))
ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
detectedRender := markup.DetectRendererTypeByFilename(wikiPath)
if detectedRender == nil || detectedRender.Name() != markdown.MarkupName {
ctx.Data["FormatWarning"] = "File extension " + path.Ext(wikiPath) + " is not supported at the moment. Rendered as Markdown."
}
// Get last change information.
lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
@@ -567,7 +564,7 @@ func WikiPages(ctx *context.Context) {
ctx.ServerError("ListEntries", err)
return
}
allEntries.CustomSort(base.NaturalSortLess)
allEntries.CustomSort(base.NaturalSortCompare)
entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
if err != nil {
@@ -670,7 +667,7 @@ func NewWikiPost(ctx *context.Context) {
}
if util.IsEmptyString(form.Title) {
ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
return
}
@@ -683,10 +680,10 @@ func NewWikiPost(ctx *context.Context) {
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
} else {
ctx.ServerError("AddWikiPage", err)
}