From 574ff84967485339baf377b62adf6bfa655a4f3b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 12 Jan 2025 02:46:37 -0800 Subject: [PATCH 01/13] Fix mirror bug (#33224) Fix #33200 Co-authored-by: wxiaoguang (cherry picked from commit be4e961240883778c44d9651eaaf9ab8723bbbb0) --- modules/git/ref_test.go | 2 ++ services/mirror/mirror_pull.go | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/git/ref_test.go b/modules/git/ref_test.go index 58f679b7d6..1fd33b5163 100644 --- a/modules/git/ref_test.go +++ b/modules/git/ref_test.go @@ -20,6 +20,8 @@ func TestRefName(t *testing.T) { // Test pull names assert.Equal(t, "1", RefName("refs/pull/1/head").PullName()) + assert.True(t, RefName("refs/pull/1/head").IsPull()) + assert.True(t, RefName("refs/pull/1/merge").IsPull()) assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName()) // Test for branch names diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index bce8386f54..5ca18eb5cd 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -96,6 +96,7 @@ type mirrorSyncResult struct { /* // * [new tag] v0.1.8 -> v0.1.8 // * [new branch] master -> origin/master +// * [new ref] refs/pull/2/head -> refs/pull/2/head" // - [deleted] (none) -> origin/test // delete a branch // - [deleted] (none) -> 1 // delete a tag // 957a993..a87ba5f test -> origin/test @@ -126,6 +127,11 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { refName: git.RefNameFromBranch(refName), oldCommitID: gitShortEmptySha, }) + case strings.HasPrefix(lines[i], " * [new ref]"): // new reference + results = append(results, &mirrorSyncResult{ + refName: git.RefName(refName), + oldCommitID: gitShortEmptySha, + }) case strings.HasPrefix(lines[i], " - "): // Delete reference isTag := !strings.HasPrefix(refName, remoteName+"/") var refFullName git.RefName @@ -168,8 +174,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { log.Error("Expect two SHAs but not what found: %q", lines[i]) continue } + var refFullName git.RefName + if strings.HasPrefix(refName, "refs/") { + refFullName = git.RefName(refName) + } else { + refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")) + } + results = append(results, &mirrorSyncResult{ - refName: git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")), + refName: refFullName, oldCommitID: shas[0], newCommitID: shas[1], }) From 6ffa4549795b53444f9bd0e22d99a1b18db8d011 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 19 Jan 2025 10:00:06 +0100 Subject: [PATCH 02/13] Partial revert "Do not report warning when git shows new reference (#6540)" This reverts commit af5d96e2cc8d92f13f3fd17763562f3f1ef4c863. Except for the test case because Fix mirror bug (#33224) implements it now. --- services/mirror/mirror_pull.go | 1 - services/mirror/mirror_test.go | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 5ca18eb5cd..b180cf498f 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -187,7 +187,6 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { newCommitID: shas[1], }) - case strings.HasPrefix(lines[i], " * [new ref]"): // new reference - nothing to do default: log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i]) } diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 860470522e..e26204e2ec 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -21,7 +21,7 @@ func Test_parseRemoteUpdateOutput(t *testing.T) { * [new ref] refs/pull/516/head -> refs/pull/516/head ` results := parseRemoteUpdateOutput(output, "origin") - assert.Len(t, results, 6) + assert.Len(t, results, 8) assert.EqualValues(t, "refs/tags/v0.1.8", results[0].refName.String()) assert.EqualValues(t, gitShortEmptySha, results[0].oldCommitID) assert.EqualValues(t, "", results[0].newCommitID) @@ -45,4 +45,12 @@ func Test_parseRemoteUpdateOutput(t *testing.T) { assert.EqualValues(t, "refs/heads/test3", results[5].refName.String()) assert.EqualValues(t, "957a993", results[5].oldCommitID) assert.EqualValues(t, "a87ba5f", results[5].newCommitID) + + assert.EqualValues(t, "refs/pull/27/merge", results[6].refName.String()) + assert.EqualValues(t, gitShortEmptySha, results[6].oldCommitID) + assert.EqualValues(t, "", results[6].newCommitID) + + assert.EqualValues(t, "refs/pull/516/head", results[7].refName.String()) + assert.EqualValues(t, gitShortEmptySha, results[7].oldCommitID) + assert.EqualValues(t, "", results[7].newCommitID) } From 06dfcc1a451bd9ef6a9fad13367ebd90beba3dbc Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Wed, 15 Jan 2025 21:34:30 +0800 Subject: [PATCH 03/13] Move some Actions related functions from `routers` to `services` (#33280) Move the main logic of `generateTaskContext` and `findTaskNeeds` to the `services` layer. This is a part of #32751, since we need the git context and `needs` to parse the concurrency expressions. --------- Co-authored-by: Lunny Xiao Co-authored-by: wxiaoguang (cherry picked from commit d0962ce3da424f0a1df2acf595b20066d6e55128) Conflicts: routers/api/actions/runner/main_test.go routers/api/actions/runner/utils.go services/actions/context_test.go services/actions/init_test.go tests/integration/actions_job_test.go simple conflicts related to ref_type": string(refName.RefType()), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946) --- routers/api/actions/runner/utils.go | 144 ++-------------- services/actions/context.go | 161 ++++++++++++++++++ .../actions/context_test.go | 7 +- tests/integration/actions_job_test.go | 95 +++++++++++ 4 files changed, 271 insertions(+), 136 deletions(-) create mode 100644 services/actions/context.go rename routers/api/actions/runner/utils_test.go => services/actions/context_test.go (77%) diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index 539be8d889..0fd7ca5c44 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -8,14 +8,8 @@ import ( "fmt" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" - actions_module "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/actions" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" @@ -65,82 +59,16 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv } func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { - event := map[string]any{} - _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) - - // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 - // This fallback is for the old ActionRun that doesn't have the TriggerEvent field - // and should be removed in 1.22 - eventName := t.Job.Run.TriggerEvent - if eventName == "" { - eventName = t.Job.Run.Event.Event() - } - - baseRef := "" - headRef := "" - ref := t.Job.Run.Ref - sha := t.Job.Run.CommitSHA - if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { - baseRef = pullPayload.PullRequest.Base.Ref - headRef = pullPayload.PullRequest.Head.Ref - - // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request - // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, - // the ref will be the base branch. - if t.Job.Run.TriggerEvent == actions_module.GithubEventPullRequestTarget { - ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name - sha = pullPayload.PullRequest.Base.Sha - } - } - - refName := git.RefName(ref) - giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) if err != nil { log.Error("actions.CreateAuthorizationToken failed: %v", err) } - taskContext, err := structpb.NewStruct(map[string]any{ - // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context - "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. - "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. - "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. - "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. - "action_status": "", // string, For a composite action, the current result of the composite action. - "actor": t.Job.Run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. - "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": eventName, // string, The name of the event that triggered the workflow run. - "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. - "graphql_url": "", // string, The URL of the GitHub GraphQL API. - "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. - "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. - "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. - "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. - "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. - "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "repository": t.Job.Run.Repo.OwnerName + "/" + t.Job.Run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. - "repository_owner": t.Job.Run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. - "repositoryUrl": t.Job.Run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. - "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. - "run_id": fmt.Sprint(t.Job.RunID), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. - "run_number": fmt.Sprint(t.Job.Run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. - "run_attempt": fmt.Sprint(t.Job.Attempt), // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. - "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. - "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. - "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. - "token": t.Token, // string, A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the GITHUB_TOKEN secret. For more information, see "Automatic token authentication." - "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "workflow": t.Job.Run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. - "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. + gitCtx := actions.GenerateGiteaContext(t.Job.Run, t.Job) + gitCtx["token"] = t.Token + gitCtx["gitea_runtime_token"] = giteaRuntimeToken - // additional contexts - "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), - "gitea_runtime_token": giteaRuntimeToken, - }) + taskContext, err := structpb.NewStruct(gitCtx) if err != nil { log.Error("structpb.NewStruct failed: %v", err) } @@ -150,68 +78,18 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { if err := task.LoadAttributes(ctx); err != nil { - return nil, fmt.Errorf("LoadAttributes: %w", err) + return nil, fmt.Errorf("task LoadAttributes: %w", err) } - if len(task.Job.Needs) == 0 { - return nil, nil - } - needs := container.SetOf(task.Job.Needs...) - - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) + taskNeeds, err := actions.FindTaskNeeds(ctx, task.Job) if err != nil { - return nil, fmt.Errorf("FindRunJobs: %w", err) + return nil, err } - - jobIDJobs := make(map[string][]*actions_model.ActionRunJob) - for _, job := range jobs { - jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) - } - - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) - for jobID, jobsWithSameID := range jobIDJobs { - if !needs.Contains(jobID) { - continue - } - var jobOutputs map[string]string - for _, job := range jobsWithSameID { - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue - } - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - outputs := make(map[string]string, len(got)) - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - if len(jobOutputs) == 0 { - jobOutputs = outputs - } else { - jobOutputs = mergeTwoOutputs(outputs, jobOutputs) - } - } + ret := make(map[string]*runnerv1.TaskNeed, len(taskNeeds)) + for jobID, taskNeed := range taskNeeds { ret[jobID] = &runnerv1.TaskNeed{ - Outputs: jobOutputs, - Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), + Outputs: taskNeed.Outputs, + Result: runnerv1.Result(taskNeed.Result), } } - return ret, nil } - -// mergeTwoOutputs merges two outputs from two different ActionRunJobs -// Values with the same output name may be overridden. The user should ensure the output names are unique. -// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job -func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { - ret := make(map[string]string, len(o1)) - for k1, v1 := range o1 { - if len(v1) > 0 { - ret[k1] = v1 - } else { - ret[k1] = o2[k1] - } - } - return ret -} diff --git a/services/actions/context.go b/services/actions/context.go new file mode 100644 index 0000000000..be1c85522b --- /dev/null +++ b/services/actions/context.go @@ -0,0 +1,161 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" +) + +// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token +// job can be nil when generating a context for parsing workflow-level expressions +func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) map[string]any { + event := map[string]any{} + _ = json.Unmarshal([]byte(run.EventPayload), &event) + + baseRef := "" + headRef := "" + ref := run.Ref + sha := run.CommitSHA + if pullPayload, err := run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { + baseRef = pullPayload.PullRequest.Base.Ref + headRef = pullPayload.PullRequest.Head.Ref + + // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request + // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, + // the ref will be the base branch. + if run.TriggerEvent == actions_module.GithubEventPullRequestTarget { + ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name + sha = pullPayload.PullRequest.Base.Sha + } + } + + refName := git.RefName(ref) + + gitContext := map[string]any{ + // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. + "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. + "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. + "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. + "action_status": "", // string, For a composite action, the current result of the composite action. + "actor": run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. + "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. + "event_name": run.TriggerEvent, // string, The name of the event that triggered the workflow run. + "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. + "graphql_url": "", // string, The URL of the GitHub GraphQL API. + "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "job": "", // string, The job_id of the current job. + "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. + "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. + "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. + "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. + "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "repository": run.Repo.OwnerName + "/" + run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. + "repository_owner": run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. + "repositoryUrl": run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. + "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. + "run_id": "", // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. + "run_number": fmt.Sprint(run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. + "run_attempt": "", // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. + "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. + "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. + "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. + "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "workflow": run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. + "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. + + // additional contexts + "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), + } + + if job != nil { + gitContext["job"] = job.JobID + gitContext["run_id"] = fmt.Sprint(job.RunID) + gitContext["run_attempt"] = fmt.Sprint(job.Attempt) + } + + return gitContext +} + +type TaskNeed struct { + Result actions_model.Status + Outputs map[string]string +} + +// FindTaskNeeds finds the `needs` for the task by the task's job +func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*TaskNeed, error) { + if len(job.Needs) == 0 { + return nil, nil + } + needs := container.SetOf(job.Needs...) + + jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID}) + if err != nil { + return nil, fmt.Errorf("FindRunJobs: %w", err) + } + + jobIDJobs := make(map[string][]*actions_model.ActionRunJob) + for _, job := range jobs { + jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) + } + + ret := make(map[string]*TaskNeed, len(needs)) + for jobID, jobsWithSameID := range jobIDJobs { + if !needs.Contains(jobID) { + continue + } + var jobOutputs map[string]string + for _, job := range jobsWithSameID { + if job.TaskID == 0 || !job.Status.IsDone() { + // it shouldn't happen, or the job has been rerun + continue + } + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + if err != nil { + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + } + outputs := make(map[string]string, len(got)) + for _, v := range got { + outputs[v.OutputKey] = v.OutputValue + } + if len(jobOutputs) == 0 { + jobOutputs = outputs + } else { + jobOutputs = mergeTwoOutputs(outputs, jobOutputs) + } + } + ret[jobID] = &TaskNeed{ + Outputs: jobOutputs, + Result: actions_model.AggregateJobStatus(jobsWithSameID), + } + } + return ret, nil +} + +// mergeTwoOutputs merges two outputs from two different ActionRunJobs +// Values with the same output name may be overridden. The user should ensure the output names are unique. +// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job +func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { + ret := make(map[string]string, len(o1)) + for k1, v1 := range o1 { + if len(v1) > 0 { + ret[k1] = v1 + } else { + ret[k1] = o2[k1] + } + } + return ret +} diff --git a/routers/api/actions/runner/utils_test.go b/services/actions/context_test.go similarity index 77% rename from routers/api/actions/runner/utils_test.go rename to services/actions/context_test.go index c8a0a28d65..a80d2d84e3 100644 --- a/routers/api/actions/runner/utils_test.go +++ b/services/actions/context_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package runner +package actions import ( "context" @@ -14,12 +14,13 @@ import ( "github.com/stretchr/testify/require" ) -func Test_findTaskNeeds(t *testing.T) { +func TestFindTaskNeeds(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID}) - ret, err := findTaskNeeds(context.Background(), task) + ret, err := FindTaskNeeds(context.Background(), job) require.NoError(t, err) assert.Len(t, ret, 1) assert.Contains(t, ret, "job1") diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index 5af3519b93..545ab37bf6 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -4,22 +4,28 @@ package integration import ( + "context" "encoding/base64" "fmt" "net/http" "net/url" + "reflect" "testing" "time" actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" 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/setting" api "code.gitea.io/gitea/modules/structs" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJobWithNeeds(t *testing.T) { @@ -354,6 +360,95 @@ jobs: }) } +func TestActionsGiteaContext(t *testing.T) { + if !setting.Database.Type.IsSQLite3() { + t.Skip() + } + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false) + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + + // init the workflow + wfTreePath := ".gitea/workflows/pull.yml" + wfFileContent := `name: Pull Request +on: pull_request +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'test the pull' +` + opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, fmt.Sprintf("create %s", wfTreePath), wfFileContent) + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts) + // user2 creates a pull request + doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "user2/patch-1", + Message: "create user2-patch.txt", + Author: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: api.Identity{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: api.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")), + })(t) + apiPull, err := doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t) + require.NoError(t, err) + task := runner.fetchTask(t) + gtCtx := task.Context.GetFields() + actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id}) + actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID}) + actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID}) + require.NoError(t, actionRun.LoadAttributes(context.Background())) + + assert.Equal(t, user2.Name, gtCtx["actor"].GetStringValue()) + assert.Equal(t, setting.AppURL+"api/v1", gtCtx["api_url"].GetStringValue()) + assert.Equal(t, apiPull.Base.Ref, gtCtx["base_ref"].GetStringValue()) + runEvent := map[string]any{} + require.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent)) + assert.True(t, reflect.DeepEqual(gtCtx["event"].GetStructValue().AsMap(), runEvent)) + assert.Equal(t, actionRun.TriggerEvent, gtCtx["event_name"].GetStringValue()) + assert.Equal(t, apiPull.Head.Ref, gtCtx["head_ref"].GetStringValue()) + assert.Equal(t, actionRunJob.JobID, gtCtx["job"].GetStringValue()) + assert.Equal(t, actionRun.Ref, gtCtx["ref"].GetStringValue()) + assert.Equal(t, (git.RefName(actionRun.Ref)).ShortName(), gtCtx["ref_name"].GetStringValue()) + assert.False(t, gtCtx["ref_protected"].GetBoolValue()) + assert.Equal(t, (git.RefName(actionRun.Ref)).RefType(), gtCtx["ref_type"].GetStringValue()) + assert.Equal(t, actionRun.Repo.OwnerName+"/"+actionRun.Repo.Name, gtCtx["repository"].GetStringValue()) + assert.Equal(t, actionRun.Repo.OwnerName, gtCtx["repository_owner"].GetStringValue()) + assert.Equal(t, actionRun.Repo.HTMLURL(), gtCtx["repositoryUrl"].GetStringValue()) + assert.Equal(t, fmt.Sprint(actionRunJob.RunID), gtCtx["run_id"].GetStringValue()) + assert.Equal(t, fmt.Sprint(actionRun.Index), gtCtx["run_number"].GetStringValue()) + assert.Equal(t, fmt.Sprint(actionRunJob.Attempt), gtCtx["run_attempt"].GetStringValue()) + assert.Equal(t, "Actions", gtCtx["secret_source"].GetStringValue()) + assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue()) + assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue()) + assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue()) + assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue()) + token := gtCtx["token"].GetStringValue() + assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:]) + + doAPIDeleteRepository(user2APICtx)(t) + }) +} + func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ Name: repoName, From ae51a901819ad2568300bc2fc284943bb66520d5 Mon Sep 17 00:00:00 2001 From: hiifong Date: Sun, 19 Jan 2025 10:51:43 +0800 Subject: [PATCH 04/13] Fix parentCommit invalid memory address or nil pointer dereference. (#33204) When the parent Commit does not exist on gitea, an error will be reported when opening the Commit details page: invalid memory address or nil pointer dereference. ![image](https://github.com/user-attachments/assets/4c2a9802-935f-41e9-b5b9-a4f0d745f709) ![image](https://github.com/user-attachments/assets/7b0bc15e-7f5f-4d58-8d24-fee667a799fa) (cherry picked from commit b7614e2d2f2af246e33b8bd6a38ecfd1babc9ecc) --- modules/git/diff.go | 10 ++++++++-- services/gitdiff/gitdiff.go | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/git/diff.go b/modules/git/diff.go index d9f3f6dda9..8374101d2a 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -64,7 +64,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) } case RawDiffPatch: @@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 7d137fb214..65f6ac8d12 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1117,7 +1117,10 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi } else { actualBeforeCommitID := opts.BeforeCommitID if len(actualBeforeCommitID) == 0 { - parentCommit, _ := commit.Parent(0) + parentCommit, err := commit.Parent(0) + if err != nil { + return nil, err + } actualBeforeCommitID = parentCommit.ID.String() } @@ -1126,7 +1129,6 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID) opts.BeforeCommitID = actualBeforeCommitID - var err error beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID) if err != nil { return nil, err From f76fe1f16d261b9a46b2ed84f527dd2f0299fc1a Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 24 Jan 2025 13:16:30 +0100 Subject: [PATCH 05/13] fix: add non allowed domain translation - Was added in 2559c80bec27a41967b355d214253a83b9ee5dad and accidentally removed in 5a16c9d9c0fd25b25d5eaf1b0ab00f1c113e6b32. - Reworded for clarity. - Resolves #6661 --- options/locale/locale_en-US.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2c7dbe69ad..7001c1bf0f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -644,6 +644,7 @@ team_name_been_taken = The team name is already taken. team_no_units_error = Allow access to at least one repository section. email_been_used = The email address is already used. email_invalid = The email address is invalid. +email_domain_is_not_allowed = The domain of the user's email address %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Make sure you have set the email address correctly. openid_been_used = The OpenID address "%s" is already used. username_password_incorrect = Username or password is incorrect. password_complexity = Password does not pass complexity requirements: From 46e60ce966f6799205f80c0cf8a3186bd8097a61 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 24 Jan 2025 12:23:15 +0000 Subject: [PATCH 06/13] fix: load settings for valid user and email check (#6674) - The doctor commands to check the validity of existing usernames and email addresses depend on functionality that have configurable behavior depending on the values of the `[service]` settings, so load them when running the doctor command. - Resolves #6664 - No unit test due to the architecture of doctor commands. # Testing 1. Set `[service].ALLOW_DOTS_IN_USERNAMES = true`. 2. Create a user that contains a dot in their username. 3. Run the `check-user-name` doctor command and verify there's no error reported. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6674 Reviewed-by: Earl Warren Co-authored-by: Gusted Co-committed-by: Gusted --- modules/setting/service.go | 5 +++++ services/doctor/breaking.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/modules/setting/service.go b/modules/setting/service.go index 9807f33352..7a907023c4 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -140,6 +140,11 @@ func CompileEmailGlobList(sec ConfigSection, keys ...string) (globs []glob.Glob) return globs } +// LoadServiceSetting loads the service settings +func LoadServiceSetting() { + loadServiceFrom(CfgProvider) +} + func loadServiceFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("service") Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180) diff --git a/services/doctor/breaking.go b/services/doctor/breaking.go index 683ec97389..ec8433b8de 100644 --- a/services/doctor/breaking.go +++ b/services/doctor/breaking.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/validation" "xorm.io/builder" @@ -30,6 +31,8 @@ func iterateUserAccounts(ctx context.Context, each func(*user.User) error) error // addresses would be currently facing a error due to their invalid email address. // Ref: https://github.com/go-gitea/gitea/pull/19085 & https://github.com/go-gitea/gitea/pull/17688 func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error { + setting.LoadServiceSetting() + // We could use quirky SQL to get all users that start without a [a-zA-Z0-9], but that would mean // DB provider-specific SQL and only works _now_. So instead we iterate through all user accounts // and use the validation.ValidateEmail function to be future-proof. @@ -61,6 +64,8 @@ func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error { // are allowed for various reasons. This check helps with detecting users that, according // to our reserved names, don't have a valid username. func checkUserName(ctx context.Context, logger log.Logger, _ bool) error { + setting.LoadServiceSetting() + var invalidUserCount int64 if err := iterateUserAccounts(ctx, func(u *user.User) error { if err := user.IsUsableUsername(u.Name); err != nil { From 443f7d59f9606a9ec3ba331cd23d9675aa45c471 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 24 Jan 2025 16:45:46 +0000 Subject: [PATCH 07/13] chore: teach `set` module about `iter.Seq` (#6676) - Add a new `Seq` function to the `Set` type, this returns an iterator over the values. - Convert some users of the `Values` method to allow for more optimal code. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6676 Reviewed-by: Earl Warren Co-authored-by: Gusted Co-committed-by: Gusted --- cmd/web.go | 2 +- models/user/user_test.go | 4 ++-- modules/assetfs/layered.go | 8 +++----- modules/container/set.go | 11 +++++++++++ modules/container/set_test.go | 9 +++++++++ modules/util/slice.go | 8 -------- routers/web/user/package.go | 20 ++++++++++---------- services/packages/alt/repository.go | 2 +- services/release/release.go | 2 +- 9 files changed, 38 insertions(+), 28 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 3fc64f7748..787411939c 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -195,7 +195,7 @@ func serveInstalled(ctx *cli.Context) error { publicFilesSet.Remove(".well-known") publicFilesSet.Remove("assets") publicFilesSet.Remove("robots.txt") - for _, fn := range publicFilesSet.Values() { + for fn := range publicFilesSet.Seq() { log.Error("Found legacy public asset %q in CustomPath. Please move it to %s/public/assets/%s", fn, setting.CustomPath, fn) } if _, err := os.Stat(filepath.Join(setting.CustomPath, "robots.txt")); err == nil { diff --git a/models/user/user_test.go b/models/user/user_test.go index df0c3856e9..2c8c1609fd 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -715,7 +715,7 @@ func TestDisabledUserFeatures(t *testing.T) { // no features should be disabled with a plain login type assert.LessOrEqual(t, user.LoginType, auth.Plain) assert.Empty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) - for _, f := range testValues.Values() { + for f := range testValues.Seq() { assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } @@ -724,7 +724,7 @@ func TestDisabledUserFeatures(t *testing.T) { // all features should be disabled assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) - for _, f := range testValues.Values() { + for f := range testValues.Seq() { assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } } diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 9678d23ad6..9feabc3f8c 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -11,7 +11,7 @@ import ( "net/http" "os" "path/filepath" - "sort" + "slices" "time" "code.gitea.io/gitea/modules/container" @@ -143,8 +143,7 @@ func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) { } } } - files := fileSet.Values() - sort.Strings(files) + files := slices.Sorted(fileSet.Seq()) return files, nil } @@ -184,8 +183,7 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err if err := list(name); err != nil { return nil, err } - files := fileSet.Values() - sort.Strings(files) + files := slices.Sorted(fileSet.Seq()) return files, nil } diff --git a/modules/container/set.go b/modules/container/set.go index 2d654d0aee..70f837bc66 100644 --- a/modules/container/set.go +++ b/modules/container/set.go @@ -3,6 +3,11 @@ package container +import ( + "iter" + "maps" +) + type Set[T comparable] map[T]struct{} // SetOf creates a set and adds the specified elements to it. @@ -63,3 +68,9 @@ func (s Set[T]) Values() []T { } return keys } + +// Seq returns a iterator over the elements in the set. +// It returns a single-use iterator. +func (s Set[T]) Seq() iter.Seq[T] { + return maps.Keys(s) +} diff --git a/modules/container/set_test.go b/modules/container/set_test.go index 3cfbf7cc2c..e54e31a052 100644 --- a/modules/container/set_test.go +++ b/modules/container/set_test.go @@ -4,6 +4,7 @@ package container import ( + "slices" "testing" "github.com/stretchr/testify/assert" @@ -29,6 +30,14 @@ func TestSet(t *testing.T) { assert.True(t, s.Contains("key4")) assert.True(t, s.Contains("key5")) + values := s.Values() + called := 0 + for value := range s.Seq() { + called++ + assert.True(t, slices.Contains(values, value)) + } + assert.EqualValues(t, len(values), called) + s = SetOf("key6", "key7") assert.False(t, s.Contains("key1")) assert.True(t, s.Contains("key6")) diff --git a/modules/util/slice.go b/modules/util/slice.go index 9c878c24be..80c8e62f6f 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -4,7 +4,6 @@ package util import ( - "cmp" "slices" "strings" ) @@ -47,13 +46,6 @@ func SliceRemoveAll[T comparable](slice []T, target T) []T { return slices.DeleteFunc(slice, func(t T) bool { return t == target }) } -// Sorted returns the sorted slice -// Note: The parameter is sorted inline. -func Sorted[S ~[]E, E cmp.Ordered](values S) S { - slices.Sort(values) - return values -} - // TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library func ValuesOfMap[K comparable, V any](m map[K]V) []V { values := make([]V, 0, len(m)) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 707c86db7a..70ea20d388 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -6,6 +6,7 @@ package user import ( "fmt" "net/http" + "slices" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" @@ -23,7 +24,6 @@ import ( debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" packages_helper "code.gitea.io/gitea/routers/api/packages/helper" shared_user "code.gitea.io/gitea/routers/web/shared/user" @@ -200,9 +200,9 @@ func ViewPackageVersion(ctx *context.Context) { } } - ctx.Data["Branches"] = util.Sorted(branches.Values()) - ctx.Data["Repositories"] = util.Sorted(repositories.Values()) - ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + ctx.Data["Branches"] = slices.Sorted(branches.Seq()) + ctx.Data["Repositories"] = slices.Sorted(repositories.Seq()) + ctx.Data["Architectures"] = slices.Sorted(architectures.Seq()) case packages_model.TypeArch: ctx.Data["SignMail"] = fmt.Sprintf("%s@noreply.%s", ctx.Package.Owner.Name, setting.Packages.RegistryHost) groups := make(container.Set[string]) @@ -213,7 +213,7 @@ func ViewPackageVersion(ctx *context.Context) { } } } - ctx.Data["Groups"] = util.Sorted(groups.Values()) + ctx.Data["Groups"] = slices.Sorted(groups.Seq()) case packages_model.TypeDebian: distributions := make(container.Set[string]) components := make(container.Set[string]) @@ -232,9 +232,9 @@ func ViewPackageVersion(ctx *context.Context) { } } - ctx.Data["Distributions"] = util.Sorted(distributions.Values()) - ctx.Data["Components"] = util.Sorted(components.Values()) - ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + ctx.Data["Distributions"] = slices.Sorted(distributions.Seq()) + ctx.Data["Components"] = slices.Sorted(components.Seq()) + ctx.Data["Architectures"] = slices.Sorted(architectures.Seq()) case packages_model.TypeRpm, packages_model.TypeAlt: groups := make(container.Set[string]) architectures := make(container.Set[string]) @@ -250,8 +250,8 @@ func ViewPackageVersion(ctx *context.Context) { } } - ctx.Data["Groups"] = util.Sorted(groups.Values()) - ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + ctx.Data["Groups"] = slices.Sorted(groups.Seq()) + ctx.Data["Architectures"] = slices.Sorted(architectures.Seq()) } var ( diff --git a/services/packages/alt/repository.go b/services/packages/alt/repository.go index 7b7951eebb..f49c435e64 100644 --- a/services/packages/alt/repository.go +++ b/services/packages/alt/repository.go @@ -711,7 +711,7 @@ func buildRelease(ctx context.Context, pv *packages_model.PackageVersion, pfs [] architectures.Add(pd.FileMetadata.Architecture) } - for architecture := range architectures { + for architecture := range architectures.Seq() { version := time.Now().Unix() label := setting.AppName data := fmt.Sprintf(`Archive: Alt Linux Team diff --git a/services/release/release.go b/services/release/release.go index 876514beec..b52e4b124e 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -372,7 +372,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo return err } - for _, uuid := range delAttachmentUUIDs.Values() { + for uuid := range delAttachmentUUIDs.Seq() { if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil { // Even delete files failed, but the attachments has been removed from database, so we // should not return error but only record the error on logs. From 95f231612be78396aee0d12b22c892d653596ca1 Mon Sep 17 00:00:00 2001 From: Robert Wolff Date: Sat, 25 Jan 2025 08:11:37 +0000 Subject: [PATCH 08/13] fix: inline file preview for rendered files (#6572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the inline file preview for rendered files (e.g., markdown). [Here, a live issue in v11](https://v11.next.forgejo.org/mahlzahn/test-inline-file-preview/issues/1) and [the same in v7 (with even more bugs)](https://v7.next.forgejo.org/mahlzahn/test-inline-file-preview/issues/1). It fixes 1. the inline preview for possibly rendered files, when the link is specified with `?display=source`. This happens, e.g., if you are watching a (e.g., markdown) file in source and then want to link some of its lines. 2. the link to the source file inside the inline preview for possible rendered files (currently it links to the rendered version and then the `#L…` cannot point to the correct lines). This is done by always adding `?display=source` to the link. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6572 Reviewed-by: Gusted Co-authored-by: Robert Wolff Co-committed-by: Robert Wolff --- modules/markup/file_preview.go | 8 +- modules/markup/html_test.go | 103 ++++++++++++++++++ .../0b/b53b56d70d253ce75c257d3cd6334a41ef2b6c | Bin 0 -> 77 bytes .../35/75ed7948fe86ab56b0a76f796f7995222bec65 | Bin 0 -> 44 bytes .../3c/95f14e5a0ab2c5ba9ee9a47ddc261af4968043 | Bin 0 -> 90 bytes .../72/1f0ce13d83f93d431b849a554a62948b85f573 | 1 + .../72/e0a44ea5761c9055995db18019e459576b3b27 | Bin 0 -> 44 bytes .../72/e1c77b65c7baa0e848557089148833fb54705e | Bin 0 -> 90 bytes .../8b/ccd5176c25898b57da2551e076f769054e0d8e | Bin 0 -> 21 bytes .../c9/8762531dd068cd818300a5f5c7dca5da79b510 | Bin 0 -> 80 bytes .../c9/913120ed2c1e27c1d7752ecdb7a504dc7cf6be | Bin 0 -> 170 bytes .../repo/repo1_filepreview/refs/heads/master | 2 +- 12 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/0b/b53b56d70d253ce75c257d3cd6334a41ef2b6c create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/35/75ed7948fe86ab56b0a76f796f7995222bec65 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/3c/95f14e5a0ab2c5ba9ee9a47ddc261af4968043 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/72/1f0ce13d83f93d431b849a554a62948b85f573 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/72/e0a44ea5761c9055995db18019e459576b3b27 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/72/e1c77b65c7baa0e848557089148833fb54705e create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/8b/ccd5176c25898b57da2551e076f769054e0d8e create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/c9/8762531dd068cd818300a5f5c7dca5da79b510 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/c9/913120ed2c1e27c1d7752ecdb7a504dc7cf6be diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 49a5f1e8ba..3caf08f7bb 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -77,6 +77,12 @@ func newFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca commitSha := node.Data[m[4]:m[5]] filePath := node.Data[m[6]:m[7]] + urlFullSource := urlFull + if strings.HasSuffix(filePath, "?display=source") { + filePath = strings.TrimSuffix(filePath, "?display=source") + } else if Type(filePath) != "" { + urlFullSource = node.Data[m[0]:m[6]] + filePath + "?display=source#" + node.Data[m[8]:m[1]] + } hash := node.Data[m[8]:m[9]] preview.start = m[0] @@ -113,7 +119,7 @@ func newFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca titleBuffer.WriteString(" – ") } - err = html.Render(titleBuffer, createLink(urlFull, filePath, "muted")) + err = html.Render(titleBuffer, createLink(urlFullSource, filePath, "muted")) if err != nil { log.Error("failed to render filepathLink: %v", err) } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 50ea70905c..702c5a716d 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -1026,4 +1026,107 @@ func TestRender_FilePreview(t *testing.T) { localMetas, ) }) + + commitFileURL := util.URLJoin(markup.TestRepoURL, "src", "commit", "c9913120ed2c1e27c1d7752ecdb7a504dc7cf6be", "path", "to", "file.md") + + t.Run("rendered file with ?display=source", func(t *testing.T) { + testRender( + commitFileURL+"?display=source"+"#L1-L2", + `

`+ + `
`+ + `
`+ + `
`+ + `path/to/file.md`+ + `
`+ + ``+ + `Lines 1 to 2 in c991312`+ + ``+ + `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
# A`+"\n"+`
B`+"\n"+`
`+ + `
`+ + `
`+ + `

`, + localMetas, + ) + }) + + t.Run("rendered file without ?display=source", func(t *testing.T) { + testRender( + commitFileURL+"#L1-L2", + `

`+ + `
`+ + `
`+ + `
`+ + `path/to/file.md`+ + `
`+ + ``+ + `Lines 1 to 2 in c991312`+ + ``+ + `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
# A`+"\n"+`
B`+"\n"+`
`+ + `
`+ + `
`+ + `

`, + localMetas, + ) + }) + + commitFileURL = util.URLJoin(markup.TestRepoURL, "src", "commit", "190d9492934af498c3f669d6a2431dc5459e5b20", "path", "to", "file.go") + + t.Run("normal file with ?display=source", func(t *testing.T) { + testRender( + commitFileURL+"?display=source"+"#L2-L3", + `

`+ + `
`+ + `
`+ + `
`+ + `path/to/file.go`+ + `
`+ + ``+ + `Lines 2 to 3 in 190d949`+ + ``+ + `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
B`+"\n"+`
C`+"\n"+`
`+ + `
`+ + `
`+ + `

`, + localMetas, + ) + }) } diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/0b/b53b56d70d253ce75c257d3cd6334a41ef2b6c b/modules/markup/tests/repo/repo1_filepreview/objects/0b/b53b56d70d253ce75c257d3cd6334a41ef2b6c new file mode 100644 index 0000000000000000000000000000000000000000..1ab268b76c99e8c8617f0db6e89b040ef9510c65 GIT binary patch literal 77 zcmV-T0J8sh0V^p=O;s>AU@$Z=Ff%bxNXyJg)l1K3Xi>VtFH~43w>Rd!is!cjbLGn; jGm(|#rZ9A$xhkHc+Swg`OEvI8+4oFVKi)n7{P-Lc(|IE6 literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/35/75ed7948fe86ab56b0a76f796f7995222bec65 b/modules/markup/tests/repo/repo1_filepreview/objects/35/75ed7948fe86ab56b0a76f796f7995222bec65 new file mode 100644 index 0000000000000000000000000000000000000000..1493caa3dfe2d9f627884805316bb754bc739b4c GIT binary patch literal 44 zcmb)5VqnO?)H-??ybM8{~)M!?Q_e=RB0B$)E A_y7O^ literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/3c/95f14e5a0ab2c5ba9ee9a47ddc261af4968043 b/modules/markup/tests/repo/repo1_filepreview/objects/3c/95f14e5a0ab2c5ba9ee9a47ddc261af4968043 new file mode 100644 index 0000000000000000000000000000000000000000..3e9c0c0d8bf7f7aa61ae01ad1474dad2cb75d81e GIT binary patch literal 90 zcmV-g0HyzU0V^p=O;s>AVK6ZO0)>Lak_?8T2TS~xmdQ*Aof*5aLGnptc(%2=p@D&! wiHSmSW?p(us%}nZUaDS6MF~SsT~u1Vbh(84zm5YkwvSze7j9Aj0FzA}xNOQLJOBUy literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/72/1f0ce13d83f93d431b849a554a62948b85f573 b/modules/markup/tests/repo/repo1_filepreview/objects/72/1f0ce13d83f93d431b849a554a62948b85f573 new file mode 100644 index 0000000000..d781d4d248 --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/objects/72/1f0ce13d83f93d431b849a554a62948b85f573 @@ -0,0 +1 @@ +x•ŽKŠ1@]çµ$¿J¥aæz€JRÁ@w+éØsýõ®ÞâñàåÛ²´ÖÛÃè"@VL&J3%f-ÑGDÒq2>FçjBOEݹË:ÀgÃ\1¤œ¦ê¦’kÀêªEM6DÔ,Ÿ\‚âǸÞ:\6é¾OülmÈ©­;Ï­|ƒ!GäŒE‚£6Z«üzòY¥Î² ¨m¸wÙ›üÂÿi‘.x-o³ò"›úŒLÌ \ No newline at end of file diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/72/e0a44ea5761c9055995db18019e459576b3b27 b/modules/markup/tests/repo/repo1_filepreview/objects/72/e0a44ea5761c9055995db18019e459576b3b27 new file mode 100644 index 0000000000000000000000000000000000000000..7b926dc0d8324cfc92001c553b87fe03262fd3f0 GIT binary patch literal 44 zcmV+{0Mq|?0V^p=O;s?mWH2!R0)>)%2JWraVb^(8ZJx)d*4kV%_Hul$odW>PCkzCG C!V+Tu literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/72/e1c77b65c7baa0e848557089148833fb54705e b/modules/markup/tests/repo/repo1_filepreview/objects/72/e1c77b65c7baa0e848557089148833fb54705e new file mode 100644 index 0000000000000000000000000000000000000000..0bbca73af27a469407e7241c2dad9e0a2504d531 GIT binary patch literal 90 zcmV-g0HyzU0V^p=O;s>AVK6ZO0)>Lak_-mZ(zlf!|JqiEZCIXPnO`|oN&8Kzp@D&! wiHSmSW?p(us%}nZUaDS6MF~SsT~u1Vbh(84zm5YkwvSze7j9Aj0M2b5?sYFEz5oCK literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/8b/ccd5176c25898b57da2551e076f769054e0d8e b/modules/markup/tests/repo/repo1_filepreview/objects/8b/ccd5176c25898b57da2551e076f769054e0d8e new file mode 100644 index 0000000000000000000000000000000000000000..394a7bb50d7dcebdf407950c6e579b2a8ff18744 GIT binary patch literal 21 ccmbAVlXr?Ff%bxNXyJgRZ!8(O=0Lhb5%S?wX-|?mTKUGvhSI! me!P81iuBU+8CsOC@Cy~z$?c7Kuj2Xbz+CzA$V>nw>l|8&RVW<* literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/c9/913120ed2c1e27c1d7752ecdb7a504dc7cf6be b/modules/markup/tests/repo/repo1_filepreview/objects/c9/913120ed2c1e27c1d7752ecdb7a504dc7cf6be new file mode 100644 index 0000000000000000000000000000000000000000..9fc2b7c3125937c08e184007dfd05822729a0e55 GIT binary patch literal 170 zcmV;b09F5Z0gaAJ3PLdq0A2SK*$dJp{a6t35PE>TG{u5H`l?>v4<5kPz`(%B^?Ysv zkZ>`&Dv;z*o!7vYCzLR8R?X~FDT2{)^*OGsCv)SjmjPZJa}9BlDObuxY7CVs2AeRh zgJms_q(sB_alCdo%-S7n?jP*tHgwf44?eZB1(zr#au^aUt+Uq1_igAO6(RaxW%fD` YsO_Y1>-uQ=g!gIDuH|dZ3;EhgOutoAssI20 literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/refs/heads/master b/modules/markup/tests/repo/repo1_filepreview/refs/heads/master index df25bf45f0..f3d5d39dd5 100644 --- a/modules/markup/tests/repo/repo1_filepreview/refs/heads/master +++ b/modules/markup/tests/repo/repo1_filepreview/refs/heads/master @@ -1 +1 @@ -4c1aaf56bcb9f39dcf65f3f250726850aed13cd6 +c9913120ed2c1e27c1d7752ecdb7a504dc7cf6be From 5bfabc8115cb5e66ce4c8f67a19515b83c1f0a74 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Sun, 26 Jan 2025 07:37:50 +0000 Subject: [PATCH 09/13] [skip ci] chore: adjust i18n entries in CODEOWNERS (#6667) Matching non-en files would allow me to catch up with all Weblate PRs, which I sometimes completely miss. And we've got a brand new path for new strings. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6667 Reviewed-by: Otto Reviewed-by: Gusted Co-authored-by: 0ko <0ko@noreply.codeberg.org> Co-committed-by: 0ko <0ko@noreply.codeberg.org> --- CODEOWNERS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6ca34a69df..ff2a4b9fdd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -33,8 +33,9 @@ models/.* @gusted # for code that lives in here. routers/.* @gusted -# Let new strings be checked by the translation team. -options/locale/locale_en-US.ini @0ko +# Let locale changes be checked by the translation team. +options/locale/.* @0ko +options/locale_next/.* @0ko # Personal interest .*/webhook.* @oliverpool From 270a2c7fa390bdd782cb59044775032fdb63dce9 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 26 Jan 2025 13:30:00 +0000 Subject: [PATCH 10/13] chore: remove usages of `sort.Sort` (#6689) improve language stats rounding: - Add tests (I had to omit some edge cases as the current method is non-determistic in some cases, due to random order of map access). - Document the algorithm used. - Lower the amount of calculations that need to be done. - Because of the aforementioned non-determistic don't use stable sort and instead regular sort. better sorting for `RepositoryList`: - Add testing - Use `slices.Sortfunc` instead of `sort.Sort`. - Remove the methods needed for `sort.Sort`. better git tag sorter: - Use `slices.SortFunc` instead of `sort.Sort`. - Remove `tagSorter` and its related methods. - Added testing. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6689 Reviewed-by: Earl Warren Co-authored-by: Gusted Co-committed-by: Gusted --- models/repo/language_stats.go | 34 ++++++++------- models/repo/language_stats_test.go | 66 ++++++++++++++++++++++++++++++ models/repo/repo_list.go | 12 ------ modules/git/repo_tag.go | 5 ++- modules/git/repo_tag_test.go | 3 ++ modules/git/tag.go | 21 ---------- routers/web/user/home.go | 5 ++- routers/web/user/home_test.go | 2 + 8 files changed, 97 insertions(+), 51 deletions(-) create mode 100644 models/repo/language_stats_test.go diff --git a/models/repo/language_stats.go b/models/repo/language_stats.go index 0bc0f1fb40..d44fea5375 100644 --- a/models/repo/language_stats.go +++ b/models/repo/language_stats.go @@ -4,9 +4,10 @@ package repo import ( + "cmp" "context" "math" - "sort" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -67,34 +68,37 @@ func (stats LanguageStatList) getLanguagePercentages() map[string]float32 { return langPerc } -// Rounds to 1 decimal point, target should be the expected sum of percs +// Use the quota method to round the percentages to one decimal place while +// keeping the sum of the percentages at 100%. func roundByLargestRemainder(percs map[string]float32, target float32) { + // Tracks the difference between the sum of percentage and 100%. leftToDistribute := int(target * 10) - keys := make([]string, 0, len(percs)) + type key struct { + language string + remainder float64 + } + keys := make([]key, 0, len(percs)) for k, v := range percs { - percs[k] = v * 10 - floored := math.Floor(float64(percs[k])) + floored, frac := math.Modf(float64(v * 10)) + percs[k] = float32(floored) leftToDistribute -= int(floored) - keys = append(keys, k) + keys = append(keys, key{language: k, remainder: frac}) } - // Sort the keys by the largest remainder - sort.SliceStable(keys, func(i, j int) bool { - _, remainderI := math.Modf(float64(percs[keys[i]])) - _, remainderJ := math.Modf(float64(percs[keys[j]])) - return remainderI > remainderJ + // Sort the fractional part in an ascending order. + slices.SortFunc(keys, func(b, a key) int { + return cmp.Compare(a.remainder, b.remainder) }) - // Increment the values in order of largest remainder + // As long as the sum of 100% is not reached, add 0.1% percentage. for _, k := range keys { - percs[k] = float32(math.Floor(float64(percs[k]))) if leftToDistribute > 0 { - percs[k]++ + percs[k.language]++ leftToDistribute-- } - percs[k] /= 10 + percs[k.language] /= 10 } } diff --git a/models/repo/language_stats_test.go b/models/repo/language_stats_test.go new file mode 100644 index 0000000000..dcfaeee6c9 --- /dev/null +++ b/models/repo/language_stats_test.go @@ -0,0 +1,66 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package repo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLanguagePercentages(t *testing.T) { + testCases := []struct { + input LanguageStatList + output map[string]float32 + }{ + { + []*LanguageStat{{Language: "Go", Size: 500}, {Language: "Rust", Size: 501}}, + map[string]float32{ + "Go": 50.0, + "Rust": 50.0, + }, + }, + { + []*LanguageStat{{Language: "Go", Size: 10}, {Language: "Rust", Size: 91}}, + map[string]float32{ + "Go": 9.9, + "Rust": 90.1, + }, + }, + { + []*LanguageStat{{Language: "Go", Size: 1}, {Language: "Rust", Size: 2}}, + map[string]float32{ + "Go": 33.3, + "Rust": 66.7, + }, + }, + { + []*LanguageStat{{Language: "Go", Size: 1}, {Language: "Rust", Size: 2}, {Language: "Shell", Size: 3}, {Language: "C#", Size: 4}, {Language: "Zig", Size: 5}, {Language: "Coq", Size: 6}, {Language: "Haskell", Size: 7}}, + map[string]float32{ + "Go": 3.6, + "Rust": 7.1, + "Shell": 10.7, + "C#": 14.3, + "Zig": 17.9, + "Coq": 21.4, + "Haskell": 25, + }, + }, + { + []*LanguageStat{{Language: "Go", Size: 1000}, {Language: "PHP", Size: 1}, {Language: "Java", Size: 1}}, + map[string]float32{ + "Go": 99.8, + "other": 0.2, + }, + }, + { + []*LanguageStat{}, + map[string]float32{}, + }, + } + + for _, testCase := range testCases { + assert.Equal(t, testCase.output, testCase.input.getLanguagePercentages()) + } +} diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index fc51f64f6a..693f8f12af 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -36,18 +36,6 @@ const RepositoryListDefaultPageSize = 64 // RepositoryList contains a list of repositories type RepositoryList []*Repository -func (repos RepositoryList) Len() int { - return len(repos) -} - -func (repos RepositoryList) Less(i, j int) bool { - return repos[i].FullName() < repos[j].FullName() -} - -func (repos RepositoryList) Swap(i, j int) { - repos[i], repos[j] = repos[j], repos[i] -} - // ValuesRepository converts a repository map to a list // FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18 func ValuesRepository(m map[int64]*Repository) []*Repository { diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 12b0c022cb..3b48b1fb9b 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "slices" "strings" "code.gitea.io/gitea/modules/git/foreachref" @@ -153,7 +154,9 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err) } - sortTagsByTime(tags) + slices.SortFunc(tags, func(b, a *Tag) int { + return a.Tagger.When.Compare(b.Tagger.When) + }) tagsTotal := len(tags) if page != 0 { tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 1cf420ad63..a4b13bf03d 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -6,6 +6,7 @@ package git import ( "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,9 +31,11 @@ func TestRepository_GetTags(t *testing.T) { assert.EqualValues(t, "signed-tag", tags[0].Name) assert.EqualValues(t, "36f97d9a96457e2bab511db30fe2db03893ebc64", tags[0].ID.String()) assert.EqualValues(t, "tag", tags[0].Type) + assert.EqualValues(t, time.Date(2022, time.November, 13, 16, 40, 20, 0, time.FixedZone("", 3600)), tags[0].Tagger.When) assert.EqualValues(t, "test", tags[1].Name) assert.EqualValues(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[1].ID.String()) assert.EqualValues(t, "tag", tags[1].Type) + assert.EqualValues(t, time.Date(2018, time.June, 16, 20, 13, 18, 0, time.FixedZone("", -25200)), tags[1].Tagger.When) } func TestRepository_GetTag(t *testing.T) { diff --git a/modules/git/tag.go b/modules/git/tag.go index 04f50e8db8..34ce8f6fc3 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -5,7 +5,6 @@ package git import ( "bytes" - "sort" "strings" api "code.gitea.io/gitea/modules/structs" @@ -107,23 +106,3 @@ l: return tag, nil } - -type tagSorter []*Tag - -func (ts tagSorter) Len() int { - return len([]*Tag(ts)) -} - -func (ts tagSorter) Less(i, j int) bool { - return []*Tag(ts)[i].Tagger.When.After([]*Tag(ts)[j].Tagger.When) -} - -func (ts tagSorter) Swap(i, j int) { - []*Tag(ts)[i], []*Tag(ts)[j] = []*Tag(ts)[j], []*Tag(ts)[i] -} - -// sortTagsByTime -func sortTagsByTime(tags []*Tag) { - sorter := tagSorter(tags) - sort.Sort(sorter) -} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index c59dcf5c25..d67af29071 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -10,7 +10,6 @@ import ( "net/http" "regexp" "slices" - "sort" "strconv" "strings" @@ -242,7 +241,9 @@ func Milestones(ctx *context.Context) { ctx.ServerError("SearchRepositoryByCondition", err) return } - sort.Sort(showRepos) + slices.SortFunc(showRepos, func(a, b *repo_model.Repository) int { + return strings.Compare(a.FullName(), b.FullName()) + }) for i := 0; i < len(milestones); { for _, repo := range showRepos { diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index e1c8ca9a79..c09f609161 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -98,6 +98,8 @@ func TestMilestones(t *testing.T) { assert.EqualValues(t, 1, ctx.Data["Total"]) assert.Len(t, ctx.Data["Milestones"], 1) assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2 + assert.EqualValues(t, "user2/glob", ctx.Data["Repos"].(repo_model.RepositoryList)[0].FullName()) + assert.EqualValues(t, "user2/repo1", ctx.Data["Repos"].(repo_model.RepositoryList)[1].FullName()) } func TestMilestonesForSpecificRepo(t *testing.T) { From 9f3507e7260908ef3d11204913e09c8440abf71a Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Sun, 26 Jan 2025 17:04:44 +0000 Subject: [PATCH 11/13] nit(i18n): update password update instruction (#6686) Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6686 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Co-authored-by: Panagiotis "Ivory" Vasilopoulos Co-committed-by: Panagiotis "Ivory" Vasilopoulos --- options/locale/locale_en-US.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7001c1bf0f..da2457ccb7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -820,7 +820,7 @@ old_password = Current password new_password = New password retype_new_password = Confirm new password password_incorrect = The current password is incorrect. -change_password_success = Your password has been updated. Sign in using your new password from now on. +change_password_success = Your password has been updated. From now on, use your new password to sign in. password_change_disabled = Non-local users cannot update their password through the Forgejo web interface. manage_emails = Manage email addresses From a334785ac70e442af09b19c226cbf2bd898139ab Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Mon, 27 Jan 2025 14:06:17 +0100 Subject: [PATCH 12/13] chore: Update renovate to v39.136.0 --- .forgejo/workflows/renovate.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index 632fb82c19..ce9058cc77 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -25,7 +25,7 @@ jobs: runs-on: docker container: - image: data.forgejo.org/renovate/renovate:39.106.0 + image: data.forgejo.org/renovate/renovate:39.136.0 steps: - name: Load renovate repo cache diff --git a/Makefile b/Makefile index 5865262d1a..08b1874a66 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.29.0 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@39.115.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +RENOVATE_NPM_PACKAGE ?= renovate@39.136.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... From 49c5102b40d95232a9efb1075407951f09ba782f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 27 Jan 2025 16:02:25 +0000 Subject: [PATCH 13/13] Update renovate to v39.136.1 (forgejo) (#6695) Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6695 Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- .forgejo/workflows/renovate.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index ce9058cc77..f72aaa5803 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -25,7 +25,7 @@ jobs: runs-on: docker container: - image: data.forgejo.org/renovate/renovate:39.136.0 + image: data.forgejo.org/renovate/renovate:39.136.1 steps: - name: Load renovate repo cache diff --git a/Makefile b/Makefile index 08b1874a66..8ea7062f57 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.29.0 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@39.136.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +RENOVATE_NPM_PACKAGE ?= renovate@39.136.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...